Sunday, August 23, 2009

Doing it by hand

Jetblade's big shtick, of course, is procedural content generation. But that's no reason to keep people from being able to make their own maps. Once the game is complete, you should be able to make your own fully-fledged world for other players to explore. And an integral part of being able to do that is the addition of a map editor.



I won't bore you with a guide to how to use the editor, because the new in-game console (courtesy of Pygame-Console) contains an overview:



In contrast to last update's major refactoring of the TerrestrialObject code, which involved a lot of work and didn't create any particularly visible changes, this time around we have many very visible changes without very much work. The actual modification of a map while the game is running just amounts to a couple of very simple function calls, so most of the work in making the map editor was at the interface level. The input processing system, for example, got refactored and is now quite easy to work with (not that it was used at all before now; previously it'd been a holdover from a previous project). All that remained was finding Pygame-Console and integrating it into Jetblade, which wasn't very hard at all.



Of course, the map editor still has a long way to go. You can't place background props or environmental effects with it yet, and there are plenty of planned game features that need to be added that it will of course have to take into account, like enemy placement, powerups, scripted events, and so on. However, the groundwork has been placed.

Friday, August 21, 2009

The FSM isn't just a spaghetti monster

I just pushed 19 changes to the Jetblade code repository. And yet the only significant difference you'll see is that there's a splash screen when you start the game up:



Now, given how long startup can take, this is an important change: you'll have something to contemplate while you wait for Cython modules to be compiled and a map to be generated; you'll be able to wonder just why our armored hero is cowering at the approach of a red four-winged flying narwhal. But it doesn't warrant 19 changes. No, most of what's changed lately is behind the scenes. I've fired the Refactoring Cannon at the code.

If you'd looked at the TerrestrialObject class before, you would have seen that it was a mess. It had a whole tangle of booleans to control what it was doing and how it could move: isGrounded, wasGrounded, justJumped, isHanging, justStoppedHanging, justStartedClimbing, shouldCrawl, isCrawling, wasCrawling, haveChangedAction. And of course, dealing with these inputs required a large tangle of if-then-else statements. You don't need Goto to make spaghetti code, and TerrestrialObject is the proof.

Fortunately, the Flying Spaghetti Monster handed me a solution to my spaghetti code in the form of a Finite State Machine. A FSM describes the behavior of some device, or creature, or program, as a set of states and transitions between states. It's a bit like a flowchart. For example, the player starts out in the Grounded state, which means that they're standing on the ground. While in this state, they can run around, fall off of ledges, jump into the air, and crawl around. Each of these is a transition to a new state. In Jetblade's case, each state is its own class, implementing the interface defined in the ObjectState class, and the states between them describe all the behaviors of a TerrestrialObject.

Now, strictly speaking, TerrestrialObject was already a FSM before the refactoring. All programs are FSMs, after all. However, formalizing the code into an explicit FSM greatly improves its cleanliness. All of those booleans I mentioned before? Gone, or else hidden within a specific state, so that the others need not worry about it. TerrestrialObject used to be 455 lines long; it's now 184 lines. And half of that is documentation, whitespace, and default values.

As a side-effect, now that transitions between states are explicit, several bits of logic that had been causing a lot of weird glitches (specifically, forcing crawls when there's no room to stand, or forcing standing when there's no room to crawl) are now much simpler and less error-prone. I won't claim that they're bug free; after all, there's always one more bug. However, debugging them just got a heck of a lot easier.

If you find yourself dreading working with a piece of code, it's probably time to refactor it. TerrestrialObject is now refactored, and quite frankly it's a joy to work with.

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.