Checking the Flixel web site for new code drops from Saltsman is routine at this point. When committing my code back to Perforce, I've found it easier to separate the "updating my project to the latest version of Flixel" from the "adding new functionality" stage. So first I check out the Flixel directory, then I check out any individual code files that need to be edited to accommodate changes in Flixel, if any. It's a bad idea to commit any code that results in a broken build. It's more time-consuming than simply checking out the entire project, updating Flixel, and checking it all back in, but I'm trying to train myself in good team-coding habits. However, I haven't decided if I should also be checking out the compiled runtime directory when I'm updating Flixel, since every new version of Flixel would necessarily change that too, but Perforce has a habit of yelling at me when I do that.
Any opinions would be welcome!
The next step in Tim's tutorial is the addition of enemies. This involves creating an "Enemy" class, which is much like the "Player" class, only instead of keyhandling functions, Tim lays out the logic for basic AI. Jump if the Player is higher, turn towards the player ninja, and move. There's a lot of code to limit bunny-hopping: the enemy must be on the ground and two seconds must pass between enemy jumps. So now I have a new enemy sprite, but... where's the code to actually insert them into the level?
There's a step missing from the tutorial, it seems. Re-using some of the code for inserting the player sprite into the sprite layer, a second green ninja successfully appears, following my player ninja like a little puppy dog. At this point, there's no code to handle damage, so we quickly become best friends. My hastily-created level design turns out to be a pretty tricky obstacle course for the enemy, requiring pixel-perfect jumps that the AI wasn't designed to handle. I spend a couple of minutes trying to coax it to higher platforms, impressed how Tim's small set of rules created fairly entertaining enemy behavior.
Satisfied, I move on to adding ninja throwing stars. Time to put the puppy dog to sleep. At the end of every chapter in Tim's tutorial, he displays the entire code listing of all the classes that changed as a secondary check. That's where I actually find his implementation for adding enemies to the sprite layer. Instead of adding a single object, he creates an array for enemies and just pushes one into the array for now. This is a nice, flexible approach, that anticipates hordes of ninja enemies for the future. This bodes well for my player ninja, due to the Law of Inverse Ninjitsu.
Ninja stars are basically a static image, but Tim demonstrates Flixel's ability to rotate sprites here. Tim loads the ninja star image pretty much the same way he loaded previous animated characters into memory, but Saltsman has actually provided an easy way to do it by passing the image reference as a parameter to the sprite constructor. There's something to be said about consistency for readability, but I'm keeping a mental note to try out the constructor parameter in the future, just for kicks.
'Tis but a flesh wound!
Tim also adds code to handle damage here. The ninja stars are capable of altering the health variable of enemy ninja sprites, and the enemy ninjas themselves get to hurt the player ninja by bumping into them. The Player class keeps track of the last time the player ninja received damage with a variable called _hurt_counter, so there's a couple seconds of temporary invulnerability while the player flees to safety. Collisions, however, are handled in the PlayState class (basically the main game loop) so PlayState needs to know _hurt_counter to figure out if it should take damage. Turns out _hurt_counter is private to Player, so I need to change it to public. Just for kicks, I try a different approach, doing the collision test in Player instead of exposing private class variables, and I get the same behavior. For the sake of keeping up with Tim's tutorial, though, I revert back to his version.
The ninja stars also give Tim the opportunity to show off FlxEmitters for particle effects. When the stars hit a surface, they throw out a shower of white sparks. They look good and I notice a slight motion-blur effect on the particles, but that might be due to my LCD screen than in code. No worries, I'll take it! As for the spinny rotation of the ninja stars, that doesn't look too hot due to aliasing from the chunky resolution of the game. I crank up the angular velocity of the ninja stars so they look more deadly, at least. (Re: Storm Shadow in G.I. Joe.)
I've highlighted a couple of quirky observations about Flixel code after the jump. It's easier to use actual class names to make my point, but it's definitely less comfortable to read. I'll try to be less jargony in my next post.
Consistent timing with different framerates
The FlxG.elapsed method returns the number of seconds since the last frame, typically a fraction. When computing anything in a game that changes over time (e.g. enemy or player position), FlxG.elapsed gives you a means of producing consistent results across low-framerate and high-framerate circumstances. For example, Distance = Speed x Time, so if you multiply enemy velocity with FlxG.elapsed, you'll get the proper positional offset of the enemy for the next frame no matter how fast or slow your rig is running. However, unless Flixel does some amazing lazy evaluation tricks, I'm assuming that FlxG.elapsed will only tell you the time elapsed between past frames. It can't predict how long your current frame is going to take to render, so if you call some processor-intensive function intermittently (e.g. pathfinding), it's an estimate at best. But it's probably a reasonable estimate that scales linearly with processor power.
Tim actually explains this much better here.
Collision function names in Flixel are platformy
The FlxSprite class (which both Player and Enemy are inheriting) has functions called hitFloor, hitCeiling, and hitWall. Remember how Flixel level maps are basically comma-delimited strings of integers? When you use that data to create a FlxTileMap, you get to specify an integer called collideIndex. When FlxSprites touch tiles that had an equal or higher integer than collideIndex, they call their own FlxSprite.hitFloor, hitCeiling, and hitWall functions, depending on which side of the tile they collided with. The fact that "Floor", "Ceiling" and "Wall" are the actual function names tells you something about the assumptions of Flixel. These function names make good enough mnemonic sense, but shmup or puzzle game coders (for instance) may have to do constant vocabulary-remapping in their heads if they want to use Flixel for their base classes.
This is particularly interesting to me in light of Jason Begy's blog post earlier this year about jump mechanics and how it ties into the evolution of 2D representation in games.