Blog Archives

Apocalypse Bunny, lessons learned.

Working on Apocalypse Bunny was quite fun.  The ambition was low, which makes the goal reachable and the progress quick.  Also I got to touch many pieces of the software so there was no time to be bored.  I even had to do some pixel art \o/.

Sure there’s no sound, and yes the world map doesn’t make sense, and of course the gameplay is limited.  Don’t care.

It’s time to go back to Infiniworld, the big project.  I learned some valuable lessons thanks to Apocalypse Bunny.

Let’s split our code in packages and modules as early as we can.

I know the zen of Python: “Flat is better than nested”, and I agree.  Python is not java, and theses periods don’t come for free.  The thing is that we do not need to have a flat code to have a flat interface.  To the user, it does not matter that the constant infiniworld.NATURE_GRASS is actually a shortcut for infiniworld.models.tiles.NATURE_GRASS.

The more files we have, the less code they contain.  The less code they contain, the less reasons we have to modify them.  The less we modify, the less we break.  It also helps us keeping our list of import statements short, which helps keeping track of dependencies.

I started by keeping all my modules in the root package but they grew very big.  I got entangled in that mess and had to reorganize all the code in smaller pieces.  That’s not a problem at all for Python, and not a problem for the user either if we make our __init__.py files properly, but that’s a problem for git or every version control system: I lost all my history.  Git accepts that we move or rename a file, but it cannot handle the fact that one file became five or ten.  There’s a big discontinuity in the tree now :(.

So if you know in advance that you will have to split your module, don’t write a module but write a package instead.

We need to make room for the special effects.

Until Apocalypse Bunny, all the EntityViews used to corresponding to an EntityModel.  Apocalypse Bunny changed that with what I called “special effects”.  The expanding circle drawn when we blast a psy-wave does NOT exist in the model, this is a purely visual artifact created only by the view.  Same goes for the blood left on the floor when a creature dies, it’s only visual special effects.

A special effect has a position, a rendering method, a sprite, and listens to events like any other EntityView, but it does not have any corresponding entity in the model.  It is so similar to an EntityView that I actually inherited the special effect classes from EntityView.  I will reconsider the names here because that’s going to be confusing.

Because of the similarities, we want to process special effects like we process the views corresponding to real entities.  So we want to put them in the same list/dictionary/set/group than the ‘real’ ones.  But because of the difference, we need a special identifier that no real entity is using.  I decided that since the entity models were having positive integers as identifiers, I would use negative integers for the special effects.  And thanks to this tiny trick, I got all the special effect code working for free with absolutely no effort at all.

Disappearing entities break everything.

When a fox dies, or when a carrot is picked up, their entity disappears.  That crashes the entire game.  Imagine: the carrots are picked up when we collide with them.  It means that the physics engine is in the middle of its iteration doing collision response when we pick up the carrot and it disappears: RuntimeError: dictionary changed size during iteration.

So you think: “okay, let’s not destroy the carrot entity now, instead let’s schedule it from destruction by posting a DestroyEntityRequest which will be processed when the physics has finished running”.  Think again, because you end up with the following possibility: you touch the carrot, and your friend touch the carrot, so you both pick it up and then it’s scheduled for destruction twice.  It may not crash (although I did ask the event manager to crash when something is unregistered twice) but it’s buggy: only one of you should get the carrot.

So we need:  to remove the dead entity immediately, but also to keep it around, alive.  At the same time.  Schrödinger would have loved this.

Imagine if the wave function had collapsed the other way :(.

Solution: add a boolean variable on our entity saying “I (don’t) exist”. Since the entity stays in memory the dictionaries don’t splode. And since we can tell that the entity doesn’t exist, we can skip it in the physics step. We must really get rid of the entity at some point though, so after setting this variable to False, we post a DestroyEntityRequest.  Neat!

Now, the same is true for appearing entities, except that I do not see any reason to create new entities during a physics iteration. Yet. I’ll think about it tonight.

Not all entities are tangible.

Carrots are supposed to represent small things on the floor.  They should not be obstacles, they should not be pushable.  Sure they won’t block the bunny since the bunny picks them up when she touches them, but they should not be an obstacle for the foxes either.  So we want an entity that detects collisions (so that they detect that a bunny touched them) but does not trigger any collision response code from the physics.

Easy: just add a “I’m (not) solid” boolean variable on the entity body and use it well.  Neat, again!

I tried for fun to make the bunny non solid: she crosses the walls and everything, awesome!  Now we can have ghosts \o/.

We need to pause the physics.

The physics engine should not run all the time: we may be on the title screen, or paused, or maybe there is not even a world yet.  It was not easy to add a pause function to my game loop because of the way I wrote it, so a lot of code has changed in there.  Surprisingly, the code got cleaner ô_O.

Partition space for entities.

My collision code for entity vs entity was brute force: test every entity against every other.  This O(n2) algorithm is of course very poor, but I didn’t know how poor, so I chose not to optimize it early.  Well, after having seen how the performance was crashing once I was reaching 50 entities (I’m talking about 5 FPS here), I HAD to optimize.  I did not use any quadtree or cool stuff like that yet, I just cut my map in chunks of 8×8 tiles and query entities by chunks.  8 is a quite random number, I could probably reduce it to 4, 2 or maybe 1, but that creates more chunks containing nothing and therefore more overhead when updating or searching the chunk map.  I need to test to find a good value.  In any case, as soon as we don’t look at the entire map, we’re fine.

That’s all…

At least all I can think of right now.

The engine source files changed quite a lot ; there is no point in merging the two branches.  I will branch out of Apocalypse Bunny and remove the code related to this specific game (it’s all in the bunny package!) to have my new codebase for infiniworld.  I’ll be doing some refactoring in the next days.  Then, new fun stuff!

Advertisements