Got a large update today. The last week I have been in the zone and have gotten a great deal done. First off, we reworked our
website a little. In my last update I said I was working on implementing plants. At that point I was still in the designing phase for them. Since then I have finished implementing them and much more.
For every feature my mantra is easily moddable but lots of control. All of the supported content up to this point has had one form of communication - engine asking for the entities components which are returned in component definition form. I struggled quite a bit while attempting to fit the plants into this format. I finally came to my senses and realized what I had to do.
All of the plant logic resides inside of python. How the plant grows, how it visually looks, how it is harvested, and even how it is planted. With this solution, any modder has unlimited control. As for the “easily” part of my mantra, I am wrapping the core plant concepts up into nice little python classes which are extremely simple to use. I have included the code below for those interested in its awesomeness.
Because of this approach I have had to go through my entire game engine and expose it to python. This is something I have been putting off because I wasn’t sure of the requirements, boost python is a little scary and I knew it was going to be quite an undertaking. With this beast finally slain, creating new content will be much easier and have even more freedom.
Here are some screenshots featuring the plants that can grow overtime and be interacted with. There is also an early version of the grass. Right now the grass just grows on soil and is destroyed when the tile underneath it is removed. Later today I plan to make grass occasionally spread to nearby areas as if seeds were dispersed.
You can harvest the trees too!
This is the Zebra tree script that most people will see and have to worry about. I hope most of this is self explanatory. We are adding the different components to the zebra tree entity and the ZebraTree class is used to override the default properties of the tree (which can be seen below). We can add bases, trunks crowns and saplings images and they will be randomly used in the appropriate places. We can also override many other properties such as the seed it produces how tall it can grow, and how fast it can grow.
Syntax Highlighted Sourcename = "Zebra Tree"
add(ModularRender())
interactable = Interactable()
add(interactable)
class ZebraTree(Tree):
trunks = [TreeImage("mods/base/plant/tree/zebra/trunk1.png", 2, 28), \
TreeImage("mods/base/plant/tree/zebra/trunk2.png", 2, 28), \
TreeImage("mods/base/plant/tree/zebra/trunk3.png", 23, 28)]
crowns = [TreeImage("mods/base/plant/tree/zebra/crown1.png", 34, 101), \
TreeImage("mods/base/plant/tree/zebra/crown2.png", 31, 98)]
saplings = [TreeImage("mods/base/plant/tree/zebra/sapling.png", 8, 33)]
seed = "mods/base/plant/tree/zebra/seed.ce"
def __init__(self, entity, organic):
super(ZebraTree, self).__init__(entity, organic)
def createZebraTree(entity, organic):
return ZebraTree(entity, organic)
add(Organic(createZebraTree))
Here is the Zebra seed script which is really simple. As with all content entity scripts we add the needed components, this time a Render, Item and Interactable components. The important part here is that we add an interaction “use” and provide the “useSeed” function which is defined in the tree script just below this one. To make this function reusable the plant the seed creates needs to be bound to the function provided - in this case we provide the path to the zebra tree.
Syntax Highlighted Sourceimport functools
name = 'Zebra Seed'
render = Render('mods/base/plant/tree/zebra/seed.png')
add(render)
item = Item()
item.stack = 999
item.delay = 200
add(item)
interactable = Interactable()
interactable.add("use", functools.partial(useSeed, "mods/base/plant/tree/zebra/zebra.ce"))
add(interactable)
Now for the meat of the tree logic. Here is the tree script which contains the Tree class and useSeed function that are used in the above scripts. As with everything this is still in the works but is most of the way there. I’m not going to go into detail in this now since there is so much going on but if you understand it then awesome. Once things start becoming finalized we’ll write some official documentation on these sorts of things.
Syntax Highlighted Sourceimport random
class TreeImage:
def __init__(self, path, center, height):
self.path = path
self.center = center
self.height = height
TreePart = enum(BASE=1, TRUNK=2, CROWN=3, SAPLING=4)
class TreeSprite:
def __init__(self, part, index):
self.index = index
self.part = part
self.sprite = Sprite()
class Tree(object):
bases = []
trunks = []
crowns = []
saplings = []
#Path to the seed content script
seed = ""
#The range of the number of seeds that will drop from the crown
seedRange = (1, 3)
wood = "mods/base/item/raw/wood.ce"
growthTimeMin = 60 * 1000 #1 minute
growthTimeMax = 5 * 60 * 1000 #5 minutes
#How much time it takes to recover from a hit
recoveryTime = 10000
#How much life each part of the tree has - how many hits it can take
partLife = 3
trunkRange = (2, 6)
def __init__(self, entity, organic):
self.organic = organic
#TODO: Is this reasonable to expect players to be network aware?
if not Game.get().network.isHost():
return
entity.interactable.add("harvest", self.onHarvest)
entity.interactable.add("removeSupport", self.onRemoveSupport)
self.addGrowthTimer()
#Start it off as a sapling
self.stage = 0
self.maxStage = random.randint(self.trunkRange[0], self.trunkRange[1])
self.sprites = [TreeSprite(TreePart.SAPLING, random.randint(0, len(self.saplings) - 1))]
self.construct()
def addGrowthTimer(self):
Game.get().timer.add(random.randint(self.growthTimeMin, self.growthTimeMax), self.grow)
def grow(self):
if self.stage == -1:
return
self.stage += 1
if self.stage is 1:
#Clear out the sapling and append the tree base and crown
self.sprites = []
if self.bases:
self.sprites.append(TreeSprite(TreePart.BASE, random.randint(0, len(self.bases) -1)))
if self.crowns:
self.sprites.append(TreeSprite(TreePart.CROWN, random.randint(0, len(self.crowns) -1)))
if self.stage >= 1:
trunk = TreeSprite(TreePart.TRUNK, random.randint(0, len(self.trunks) - 1))
if self.crowns:
self.sprites.insert(-1, trunk)
else:
self.sprites.append(trunk)
self.construct()
if self.stage is not self.maxStage:
self.addGrowthTimer()
def getImage(self, sprite):
if sprite.part is TreePart.BASE:
return self.bases[sprite.index]
elif sprite.part is TreePart.TRUNK:
return self.trunks[sprite.index]
elif sprite.part is TreePart.CROWN:
return self.crowns[sprite.index]
elif sprite.part is TreePart.SAPLING:
return self.saplings[sprite.index]
else:
raise ValueError("TreeSprite contains unknown tree part: ", sprite.part)
def construct(self):
print "Building the tree at stage:", self.stage
height = 0
sprites = []
for sprite in self.sprites:
image = self.getImage(sprite)
height -= image.height
sprite.sprite.position.x = -image.center
sprite.sprite.position.y = height
self.organic.setImage(image.path, sprite.sprite)
sprites.append(sprite.sprite)
self.organic.onConstruct(sprites)
def onHarvest(self, args):
entity = args["entity"]
sprite = entity.modularRender.getTopSprite(args["position"])
index = 0
for s in self.sprites:
if s.sprite is sprite:
break
index += 1
game = Game.get()
parent = self.organic.getParent()
if not parent:
print "Could not find parent entity for the plant."
return
damageId = "%d-%d" % (parent.id, index)
if not game.damage(damageId, 1, self.partLife, self.recoveryTime):
#return if we haven't done enough damage to the targeted tree part
return
#ignore if at the top of the tree
if index == len(self.sprites):
return
self.destroyAtIndex(index)
def destroyAtIndex(self, index):
#Create the dropped items in the world
game = Game.get()
woodId = game.mod.getContent(self.wood).id
for s in self.sprites[index:]:
image = self.getImage(s)
centerY = image.height / 2
position = s.sprite.getPosition()
position.x += image.center
position.y += centerY
woodPosition = position
woodPosition.x += random.randint(-image.center, image.center)
woodPosition.y += random.randint(-centerY, -centerY)
print "Creating dropped wood at position (%d, %d)" % (woodPosition.x, woodPosition.y)
game.dropped.create(woodId, 1, woodPosition, Vectorf(0, 8))
if s.part is TreePart.CROWN and self.seed:
seedId = game.mod.getContent(self.seed).id
for x in xrange(0, random.randint(self.seedRange[0], self.seedRange[1])):
seedPosition = position
seedPosition.x += random.randint(-image.center, image.center)
seedPosition.y += random.randint(-centerY, centerY)
print "Creating dropped seed at position (%d, %d)" % (seedPosition.x, seedPosition.y)
game.dropped.create(seedId, 1, seedPosition, Vectorf(0, 8))
#remove the targeted sprite and all of the sprites above it
self.sprites = self.sprites[:index]
if self.sprites:
#Reconstruct the graphics for the tree
self.organic.onConstruct([sprite.sprite for sprite in self.sprites])
#Set the stage to -1 signifying that it cannot grow anymore
self.stage = -1
else:
#The entire tree was destroyed. Destroy the entity.
parent = self.organic.getParent()
if parent:
parent.destroy()
def onRemoveSupport(self, args):
#First check to see if the base (or bottom trunk) is actually over the tile being changed
sprite = self.sprites[0]
spriteArea = sprite.sprite.getRect()
if not spriteArea.intersects(args["area"]):
#No overlap so this is a false alarm
return
if args["forced"]:
#The support is being forcefully removed. Destroy the entire tree
self.destroyAtIndex(0)
else:
#Cancel the request
args["canceled"] = True
def save_bad(self, stream):
import pdb
pdb.set_trace()
stream << self.stage
stream << len(self.sprites)
for sprite in self.sprites:
stream << self.part
stream << self.index
def load_bad(self, stream):
stream >> self.stage
spriteCount = 0
stream >> spriteCount
self.sprites = []
for x in xrange(0, spriteCount):
part = 0
index = 0
stream >> part
stream >> index
self.sprites.append(TreeSprite(part, index))
self.construct()
def pack_bad(self, packet):
self.save(packet)
def unpack_bad(self, packet):
self.load(packet)
def useSeed(plant, args):
#First calculate the position of the new tree and see if it is ok
position = args["position"]
ground = World.get().layer["ground"]
#If the tile clicked on is solid as well as the tile above it then do not allow seed planting
tilePos = ground.getTilePosition(position)
if ground.getTile(tilePos) is not 0 and ground.getTile(Vector(tilePos.x, tilePos.y + 1)):
return
tilePos = ground.getTileInDirection(position, Vector(0, 1))
verticalPos = tilePos.y * TileSystem.TILE_SIZE
if verticalPos > (position.y + 2 * TileSystem.TILE_SIZE):
return
position.y = verticalPos
game = Game.get()
contentId = game.mod.getContent(plant).id
tree = game.entity.create(contentId, True)
tree.modularRender.setPosition(position)
print "Created new tree at (%d, %d)" % (position.x, position.y)
World.get().layer["active"].add(tree)
args['decrement'] = 1
Lastly, I wanted to throw in the grass code which I’m happy about because I was able to write it without making any engine code changes. There is still in the works but I have the basics down. I will likely do the same for the grass that I did for the tree and have a base Grass class and allow for inheriting and changing the properties without having to worry about the logic.
Syntax Highlighted Sourcename = "Grass"
class GrassImage(object):
def __init__(self, path, height):
self.path = path
self.height = height
class GrassSprite(object):
def __init__(self, index):
self.index = index
self.sprite = Sprite()
class Grass(object):
images = [GrassImage("mods/base/plant/grass.png", 5)]
sides = []
growthTimeMin = 2 * 60 * 1000 #2 minutes
growthTimeMax = 5 * 60 * 1000 #5 minutes
spreadTimeMin = 8 * 60 * 1000 #8 minutes
spreadTimeMax = 10 * 60 * 1000 #10 minutes
def __init__(self, entity, organic):
print "Creating grass."
self.organic = organic
if not Game.get().network.isHost():
return
entity.interactable.add("removeSupport", self.onRemoveSupport)
grass = GrassSprite(random.randint(0, len(self.images) - 1))
image = self.images[grass.index]
grass.sprite.position.y = -image.height
self.organic.setImage(image.path, grass.sprite)
self.grass = [grass]
self.construct()
self.addGrowthTimer()
def addGrowthTimer(self):
Game.get().timer.add(random.randint(self.growthTimeMin, self.growthTimeMax), self.grow)
def grow(self):
self.addGrowthTimer()
world = World.get()
ground = world.layer["ground"]
if len(self.grass) >= 30:
#TODO: Check to see if the grass can spread and create a new grass entity
return
#The grass is store from left to right [leftmost:rightmost]
#First check to see if the leftmost grass can spread more left
self.growOutward(ground, 0, -1)
self.growOutward(ground, len(self.grass) - 1, 1)
self.construct()
def growOutward(self, ground, index, direction):
grassSprite = self.grass[index].sprite
position = grassSprite.getPosition()
position.x += TileSystem.TILE_SIZE * direction
position.y += TileSystem.TILE_SIZE
tilePos = ground.getTilePosition(position)
if ground.getTile(tilePos):
tile = ground.getTileComponent(tilePos)
if 'soil' in tile.getGroups() and random.randint(0, 1):
#Soil! We found it and passed the random test. Lets add it!
grass = GrassSprite(random.randint(0, len(self.images) - 1))
image = self.images[grass.index]
grass.sprite.position.x = grassSprite.position.x + (TileSystem.TILE_SIZE * direction)
grass.sprite.position.y = grassSprite.position.y
self.organic.setImage(image.path, grass.sprite)
if direction is -1:
self.grass.insert(0, grass)
else:
self.grass.append(grass)
def isTouchingTile(self, ground, tilePos):
grassTile = ground.getTilePosition(grass.sprite.getPosition())
return grassTile.x == tilePos.x and grassTile.y + 1 == tilePos.y
def onRemoveSupport(self, args):
tilePos = args["tilePos"]
ground = World.get().layer["ground"]
self.grass = [grass for grass in self.grass if not self.isTouchingTile(ground, tilePos, grass)]
if self.grass:
self.construct()
else:
#No more grass - go ahead and destroy this grass.
parent = self.organic.getParent()
if parent:
parent.destroy()
def construct(self):
self.organic.onConstruct([grass.sprite for grass in self.grass])
def createGrass(entity, organic):
return Grass(entity, organic)
add(ModularRender())
add(Interactable())
add(Organic(createGrass))
Thanks for reading through all of this! I know it is a lot. I’m interested to hear what you think of this.