Friday, July 31, 2009

Redlining the processor

Jetblade's written in Python. Python's a wonderful language: very easy to develop in, highly expressive without lacking power. It's not particularly fast, though. And while it doesn't matter so much if map generation takes a minute vs. 30 seconds to run, it does matter if you can only have 12 active creatures running around before your FPS starts to stumble. So this week was spent fixing things up a bit.

One of the great mottos of software development is to never optimize prematurely. Worrying about how fast your code runs when you write it for the first time will have you wasting a lot of time, probably without making any significant gains. You might spend hours optimizing your file load/save routines, only to discover that hey, you only call those once per session anyway (big hint to budding game developers: don't worry about binary file formats! It's not worth the hassle! Just use plaintext). Of course, you don't have carte blanche to just throw together any old code that gets the job done -- you want to keep your Big O at a reasonable level (as a general rule, O(n*log(n)) should be your limit). But you don't need to worry about the constant factors until you know they're causing you difficulty. And then you haul out a profiler and take a look. Don't guess which parts of your code are slow; have the profiler tell you. In Jetblade's case, the profiler is CProfile.

At the beginning of the week, the major time sucker was the Vector2D class, which does some handy vector math but is mainly there because it makes the code so much cleaner in comparison to using tuples and lists for 2D coordinates. It's such a simple class than there weren't any algorithmic problems with it, nor any obvious constant factors to get rid of. So I ditched Python.

The Vector2D module (and Range1D, and Polygon) are now written in Cython instead of Python. Cython is basically Python with static types and a little more rigour. It takes almost-Python code and compiles it into C, which is much faster. Just converting Vector2D over to Cython made mapgen 35% faster. Of course, now anyone who wants to use Jetblade needs to have Cython installed, and Cython in turn needs GCC. I'm not really happy about adding more dependencies, but faced with the clear improvements in speed, it's hard to argue. The alternative (writing the modules manually in C) would still have the GCC dependency but would also make development far more painful.

One odd bit of optimizing was discovering that a lot of time was being spent in warnings.py, which is a file that, as far as I can tell, doesn't exist on my hard drive. A little work with the profiler revealed that warnings.py was being called by Python's built-in range function, because I was calling it with floating-point numbers instead of the ints that it expected. And I was doing this for every creature in the game, every frame. Each time, it'd call warnings.py, which would then decide whether or not to print a warning. Fixing that up got me another noticeable boost.

Another thing to check for is unnecessary calculations. Why recalculate a value if it doesn't change? For example, every Block instance is a fixed bit of terrain. I need to have bounding boxes for them, but those bounding boxes don't change because the block doesn't move. So those bounding boxes can be calculated ahead of time and stored.

Finally, of course, if you can avoid calling code at all, then you won't pay the price for it. For example, if I discover that a creature has run smack into one tile of a wall, then I don't need to check any of the tiles that are in the same column as the tile the creature hit. There's either going to be no collision, or they're going to have exactly the same collision data. Skipping the somewhat-expensive collision detection algorithm even a small amount can give noticeable speedups.

At the end of all of this, Jetblade now can handle upwards of 50-60 creatures at the same time without dipping below 30FPS, which is a reasonable framerate for playing games; not great, but decent. And that's where we're going to leave it for now, since the next step would probably be to farm out collision detection to a third-party physics engine like Box2D or Chipmunk. These pure-C physics engines would be nice and fast, but they'd introduce another dependency and require reworking Jetblade's object model. So, until we find that we need even more speed, that's getting put off.

Sunday, July 19, 2009

Adding some teeth

Today the first few tentative steps towards letting our little armored soldier fight things have been made. Specifically, you can now perform an axe kick while standing on the ground:



One of the issues with making a game with melee combat is figuring out what kind of martial style you want to use. There's a wide range of real-life martial arts available, at varying ends of the flashy-practical spectrum. Sadly, most effective techniques do not look very impressive. Most games opt to end up more towards the flashy end of the spectrum, because that looks nicer, and presentation is a big part of gameplay. But attacks like that axe kick there, or a Butterfly kick, are hideously impractical in almost all real-life situations, more likely to get yourself badly hurt than to do the same to your opponent.

That said, if you want to try a game that takes a mostly-realistic approach to martial arts (disregarding the fact that the combatants can leap through the air as if they had wire harnesses on), check out Lugaru, where you will get very, very badly hurt if you try to fight everyone at once.

The plan from here on out is roughly as follows:
  • Update the animation logic to allow animations to spawn new objects on certain frames. This will allow the kick to create a new object for hitting other things; it will also allow e.g. a shooting animation to generate projectiles when the trigger is pulled, or a hive to generate new enemies, etc.
  • Set up a simple, stupid enemy object (and rework the code that handles storing dynamic objects, which currently assumes there only is one, namely the player).
  • Set up an "arena mode" for testing combat out. This would use a hardcoded map and allow you to spawn new objects to fight.
  • Add health to all and sundry, so things can be killed.
  • Get the enemies integrated into normal mapgen, so they're placed on the map and respawn when you get far enough away.

That should get us through two of the major bullet points on the road map, and that much closer to version .1.

Saturday, July 18, 2009

Let's get this show on the road

This is the Jetblade project blog. We'll be keeping this updated as notable additions are made to the project proper. That way you won't have to track the changelist to keep abreast of new developments. There's also a Livejournal feed for this blog.

It's been pretty quiet lately. Our as-yet single developer (that's me, folks) has mostly been working on some code cleanup and refactoring, getting rid of a few of the items on the To-Do List. We do have a nice new map to show you, though -- a little serendipitous randomness that showed up during some testing:

(Click for full size)


You can see the region mapping in the filled sections of the map. Prior to laying down the tree that determines the large-scale map structure, the game places a set of regions down which determine the local terrain. Of course, it's not a strict mapping; that would create abrupt changes in terrain as bits of tunnels moved into and out of different regions. Instead, the region for a given tunnel is determined by the endpoint closer to the center of the tree and by the number of tunnels since the last time the terrain changed.