Home Games Blog About

The Making of Mach Block

Posted on by Jeff

Table of Contents

Introduction

Getting a finished game into the App Store took at least four times as long as I originally anticipated. When I first planned to write iPhone apps, I felt a twinge of regret that if only I had started right when the App Store launched (or even in the beta program that started a few months prior to launch) in 2008, I could have easily released an app and made a fortune on the early sales rush. As my first game, Mach Block, approached completion, any and all such regret evaporated, as I saw just how much work and polish must have gone into the early winners in the App Store—many of these created by experienced game developers. As somebody writing my first game, I would have never stood a chance to get an app out in time to make an early splash.

Approaching the project as a developer (and not an artist, composer, or marketer), I naturally focused most of my brainstorming and planning attention on developing the core gameplay engine for my game idea—and thinking that once I had developed that, then "all" I had to do was create some "simple" graphics, sound effects, menus, help, and release it! In reality, the core gameplay engine was only a portion of the code (less than half)—and writing the code was only a small portion of the entire project.

Breakdown of lines of code by functionality:

Pie chart for lines of code

Roughly 30% of the "core gameplay and engine" code was rewritten or added after the first complete iteration, to resolve usability and playability issues discovered only during playtesting. "Core gameplay and engine" means the barebones needed just to play the game—level 1 begins on starting the app, no sound, no music, no on-screen indicators except number of lives, no score, an unchanging solid color background, no ending (a "kill screen" level (impossible to complete) was used early in development so there was no path to exit the game code), no pause, and no save/restore.

Breakdown of major tasks by human time spent:

Pie chart for time spent

Multiply the "code" wedge by the "core gameplay and engine" wedge in the previous chart, and you get 11%. It's easy for a first-time solo iPhone game developer to fall into the trap of thinking primarily about the 11% and thinking that the rest is just a "little more". (This trap is even more pronounced when remembering that this 11% figure also includes the rewritten and additional code based on playtesting, after the first iteration was deemed code complete.)

I learned why most successful games in the App Store, even by small independent developers without a larger team, have at least two authors—not just one. Two, or three, or four people can specialize in their areas of expertise. With one person, the burden of everything falls on them, and constantly switching gears is a distraction and a drain on energy.

Despite my first app taking much longer than anticipated, I would easily do it again. As a long-time video game player, finally implementing my own game was an extremely rewarding experience, even if my first app doesn't make its money's worth in the time spent creating it. And it will be easier the second time.

Non-programmers may wish to skip ahead in this article to the Level Design section, where gameplay discussion begins, which may be of interest to any player. Programmers (and aspiring programmers) may wish to keep reading!

Developing for the iPhone

As somebody new to development on Apple platforms, I had a warming-up period to become familiar with the development basics. Despite occasional frustrations, I found developing on iOS to be a pretty pleasant experience.

iPhone development uses Cocoa (Apple's development framework) built on Objective-C (a language), using Xcode as the IDE (development tool)—all similar to developing for the Mac. Objective-C is an object-oriented language, but without automatic memory managament (it uses pointers). In this specific respect, it is closer to C++ than Java or C#. Objective-C is a superset of C, so Objective-C programs can contain portions written in straight C.

The Xcode suite includes an iOS simulator that runs iOS apps under development. The simulator can act as either an iPhone or iPad, using the mouse pointer to simulate touch. The orientation can be switched, and in iPhone mode, either standard or retina resolution can be used. There is a limited form of multitouch support (you can simulate a two-touch "pinch" about a center point by holding Option), and you can exit the app and access Settings and a few other built-in apps, and the multitasking UI. The simulator does not emulate the hardware, but only provides a compatible environment (hence the name); when you run on the simulator, your app is compiled to x86 rather than ARM. Despite this, it has nearly 100% compatibility; I only once observed differences from the actual device, and this was only a single minor difference in graphics rendering which did not affect program logic.

Xcode provides a modern debugger which works identically between the simulator and device. You can use breakpoints or observe variables right on the device while tethered over USB. When you debug, Xcode installs the app if there were any changes (takes about 20 seconds), attaches the debugger, and you're off! Having dealt with the nightmare of "remote debugging" in Visual Studio, the fact that debugging on the iPhone is easy and works flawlessly 95% of the time was an impressive and pleasant surprise and one of the great things about developing for the iOS platform.

Graphics on iOS

Having no prior professional game development experience, drawing graphics was a lower-level process than I had guessed.

Apple provides several frameworks for app user interfaces, but the only one suitable for graphics-intensive games is OpenGL. Before starting, I casually speculated that there might be a method for something like "draw 2D sprite", taking as parameters the x and y location on screen, and perhaps other options like a scale, rotation, or stretch factor. Maybe there would be options to define motion, or move a sprite, right? Control its velocity or acceleration?

I realized that what I had in mind was a game engine, not a graphics engine. OpenGL is a graphics engine, and as I learned its methodology, it became clear that OpenGL was a platform on which game engines are built, not games. OpenGL does not manage your sprites. It doesn't even manage your polygons. Rather, OpenGL draws triangles, processed through a pipeline of matrix transformations (including but not limited to world and camera transformations). OpenGL operates at the level of filling triangles to the graphics buffer, and has no concept of multiple frames, or even the order in 3D or 2D space of different polygons (instead only looking at the order they were drawn in code). Each frame is a separate universe to OpenGL, rendered anew on a blank screen. Optimizations are limited to the scope of a frame—for example, the graphics engine may determine that an earlier drawn polygon is completely covered by a later polygon in the same frame and save GPU time by not rendering the earlier polygon at all. (All of this discussion doesn't even look at shaders, which I didn't use in Mach Block.)

Uploading a texture to the GPU memory is a performance hit, but even changing the active texture already in memory is a hit (though less so). Thus, it's standard practice in game development to create a texture atlas, which is a single image containing the layout of as many sprites or textures as will fit in the image. The texture atlas is actually one "texture" as far as the GPU is concerned. In the case of Mach Block, all of the graphics fit in a single 1024x1024 image, with room to spare. (I originally sized it to 512x512, which is why most of the graphics are in the upper left quadrant.) This means that the active texture never needs to change.

Mach Block texture atlas

Mach Block texture atlas

Warning: Full image (follow link to view) contains slight spoilers.

There are 560 sprites in Mach Block (a few of them unused).

Drawing a rectangular sprite is performed by drawing two triangles. A series of triangles can be drawn in one OpenGL command by using a triangle strip data structure. A triangle strip is a series of contiguous triangles, each sharing one of its sides with the previous in the series. Thus, each subsequent triangle requires only one additional vertex, and the number of vertices with n triangles in a strip is 2 + n.

The vertices in a triangle strip in a rectangle look like:

triangle strip

Triangle strip

Conceptually drawing a sprite involves:

  1. Binding the texture to render (can be skipped if it was the last one bound);
  2. Creating the triangle strip to define the subset of the texture to render from;
  3. Creating the triangle strip to define the subset of the current render space (for example, the screen, in a 2D game) to render to;
  4. Calling the command to draw the triangle array.

I created a sprite drawing method to abstract this, accepting as arguments the identifier of the sprite to draw and the coordinates on screen to draw at. The method generates the triangle strip vertices from the texture atlas index data. This index data includes a "center point" of each sprite, so that, for example, various frames of a sprite animation can be different sizes, but be drawn to the same coordinates; the center point of all in-game characters (player and enemies) is at the characters' feet regardless of the dimensions of the sprite so that the game engine code need not be special-cased depending on the specifics of each character image.

When rendering a texture in OpenGL, the scaling mode can be controlled for when the sprite is being upscaled or downscaled, and also for when the positioning of the texture is not perfectly aligned with pixel boundaries. In almost all cases in Mach Block, the textures are being rendered at 1:1 size (when played on a non-retina iPhone or iPod touch). However, there are many places where a texture is not aligned to the pixel grid. The game coordinates in Mach Block are much finer than the screen coordinates. Most in-game sprites are rendered using the "nearest neighbor" scaling method, which, when used on a texture at its original size, merely aligns it to the pixel grid. However, during the scrolling between levels, all game sprites are rendered using a filtered scaling method to prevent jumpiness. (To see the difference, look carefully at the buildings and mountains when they scroll, vs. the sun, clouds, and stars: The buildings and mountains use nearest neighbor, but the celestial objects use filtered scaling, because the filtering would cause aliasing in the lights of the buildings which looked worse than the jumpiness. It's a subtle difference, but you can tell if you're paying attention.) On retina iPhones and iPod touches, and the iPad, filtered scaling is used for all the upscaling as this looks much superior to nearest neighbor (this would be pixel doubling on a retina display). Contrary to popular belief, retina iPhones and iPod touches do not use pixel doubling by default, even for apps not designed for the retina display. They use a much smoother scaling method.

Development Design Decisions (and Mistakes)

When I started developing Mach Block, being new to developing on iOS, I made certain development decisions, some of which, in retrospect, were errors.

I wanted to release my first app somewhat quickly and thus reduce the "risk"—the risk that I would be delayed by learning unnecessary complexities in a new platform, that I would use a framework that few people were using and thus be unable to find help online if I got stuck, or that I would be stuck troubleshooting code that depended on subtleties in underlying frameworks that I was unfamiliar with.

Thus, I decided to:

  1. Use mostly C-style procedural code and struct-based data structures, rather than object oriented techniques and Objective-C classes;
  2. Not use an existing game development or graphics framework; and
  3. Build and maintain my texture atlas, and its index, by hand (I wouldn't need more than a handful of graphics, right?).

Having finished the project:

  1. Eschewing OO was definitely a mistake. Objective-C's OO would have been welcome in many parts of the code, where I clumsily used pointers and nested structs instead, and iterated over fixed-length arrays instead of using more sophisticated collection classes.
  2. I am not sure if shunning existing game engine frameworks was a "mistake", but I could hardly have gone wrong using cocos2D, which I now realize is quite common in iPhone development. I would have had no problem finding all the help I needed for it.
  3. It was a big mistake to build the texture atlas by hand. The number of sprites I needed grew at least 4-fold, and assembling and maintaining the texture atlas was a huge time sink. Worse, this also discouraged me from making graphics changes if they would change the size of the sprite, because this meant updating both the atlas layout and my index table.

Lesson: Make the investment to learn and use the tools that the community has made! Embrace, don't fear, Cocoa and Objective-C!

An example of where OO would have been perfect is the actor handling. In the Mach Block code, an "actor" is any moving entity in the gameplay. This includes the player, enemies, the projectiles of the snout, and the comet weapon. An "enemy" is any actor which harms the player on contact, and whose presense prevents the game from advancing to the next level. There are multiple kinds of enemies. This lends itself to the classic OO inheritance pattern: A beetle "is a(n)" enemy. An enemy "is a(n)" actor.

Instead of classes, I used a struct for each type of entity. When one entity was an instance of a more general category of entity, the struct contained, as a field, another struct of type of the more general category. For example, the player struct contained an actor struct, and the beetle struct contained the enemy struct. (In a further mistake, each type of enemy, such as the beetle, bat, etc. directly contained the actor struct as well as the enemy struct, rather than what I should have done: the beetle, bat, etc. containing the enemy struct, which in turn would contain the actor struct. By the time I realized this design mistake, which led to unnecessarily complex code where pointers to both types of structs needed to be passed around rather than just one, enough code had been written and tested to make it not worth fixing.)

You can certainly make a working game using purely procedural programming techniques as this game (and countless games in history) has proven, and it makes no direct difference to the player in the end. But in my misguided effort to reduce "risk" by avoiding delving into Objective-C's OO programming (all the more silly since I've used OO in Java and C#), the code clarity, complexity, size, and maintainability suffered, to no benefit. A mistake I will make sure to correct in my future apps.

Finally, for my future apps, even if I work alone, I plan to use version control (most likely git). The benefits, even for single-person projects, are well-known.

Game Loop

Like many games, Mach Block uses a typical "game loop". The game runs internally at 60 ticks per second, regardless of the frame rate—which can vary due to hardware, the complexity of the action at any time, or even other processes running on the device (although in most circumstances the game maintains 60 frames per second).

The game engine is primarily split into two regions of code. One region only processes game logic (advances the gameplay by one sixtieth of a second per iteration), without any graphics logic. The other region only draws a frame, without modifying the game state. Most of the time, one frame is drawn after each gameplay tick. But if the framerate is lower, then multiple gameplay ticks may occur between a drawn frame. The same frame could even be drawn more than once and the gameplay logic code would be none the wiser (this doesn't happen except in certain edge cases).

There are only a few exceptions to the game logic/graphics independence. One such case is that the player's invincibility flicker is synchronized with the actual frame rate, because it displays visible and invisible for alternating frames. If it weren't synchronized, then if the frame rate slowed down to 30fps for some reason, then the sprite would be drawn either always on, or always off, depending on a 50/50 chance. Instead, the flicker slows down with the frame rate.

The game loop processes nearly every moment of the app (except for fading out the splash screen company logo), including menus (title, help, scores, etc.) and the pause screen. Just as there are variables tracking the state of gameplay, such as the locations of the player and enemies, the number of lives, the score, and the current level; there are other variables tracking the current state of the menu, just as if it were a game, such as which menu screen is displayed, the progress of the transition from one menu screen to another, and which help page is currently displayed.

Level Design

(Non-programmers, you can resume reading here!)

Before delving into level design, let's review the game movement rules, which imply the principles of Mach Block's level design:

The player walks around on a rectangular grid of blocks with an isometric view (consider it a top-down view for purposes of this article). The blocks are floating in space, and each position in the grid may or may not occupy a block. So separate block platforms float in space. Example from floor 3:

Floor 3

Tapping anywhere on the same platform that the player is standing on (even out of direct line of sight, or around corners) causes the player to automatically navigate the shortest route to that square. Tapping on a different platform doesn't do anything, unless the block that was tapped is a jump block.

The player travels to different platforms by dashing to colored "jump blocks". There are two types of jump blocks:

  • Green blocks (one-way)
  • Blue blocks (two-way) (these may appear purple in screen shots, but are blue on the device, due to differences in color between displays)

These rules govern jump blocks:

  • Dash to a jump block (when legal to do so) by tapping it.
  • All jump blocks can only be dashed to when the player is standing orthogonal to them (directly up, down, left or right). (The player can dash horizontally or vertically, but not diagonally.)
  • Green jump blocks can be dashed to from any square. This means that a green block establishes a "one-way" path, because the block that the player was previously standing is probably not a jump block.
  • Blue jump blocks (purple in screen shots) can be dashed to only when the player is standing on another blue block. This means that a blue block establishes a "two-way" path, because the player can always jump right back to the blue block that they were just standing on.

Enemies are positioned on various platforms in each level. The player can only attack enemies that are on the same platform, so the player needs to access most or all of the platforms in each level to defeat all the enemies.

These diagrams of floor 3 illustrate the rules, and demonstrate how the layout defines the movement through the level:

Hover for diagram →

On a square with a red dot, follow the paths to see the valid jump blocks to dash to.

The gameplay style in Mach Block changes quite a bit from floor to floor based on the layout. Merely two types of jump blocks provide the building blocks for a huge variety of mazes.

Some floors are very "open", with simple grid-like jump block layouts that allow quickly moving freely anywhere in the level:

Hover for diagram →

Floor 7

Hover for diagram →

Floor 17

Others are more loop-like, with some restrictions on movement and a feeling of defined paths:

Hover for diagram →

Floor 6

Hover for diagram →

Floor 20

Hover for diagram →

Floor 23

Hover for diagram →

Floor 32

Other floors are extremely linear, with the feel of a "beginning" and "end".

Hover for diagram →

Floor 24

Hover for diagram →

Floor 35

Floor 35 contains a 4-block-wide moving platform along the bottom edge.

Hover for diagram →

Floor 42

Floor 42 contains a 2-block-tall moving platform near the left edge.

Hover for diagram →

Floor 43

Floor 43 contains a 1-block moving platform on the right edge as the only way to access the upper-right platform.

Hover for diagram →

Floor 49 – Route map

A snout enemy starts on each of the five larger platforms.

Hover for diagram →

Floor 49 – Graph

Floor 49 is the most complex level in the game, and took by far the longest to design.

Every floor in the game is categorized as either "symmetrical" or "linear". The way you can tell which is which is that in two-player mode, the starting squares for the first and second players are close to each other for "linear" floors, but opposite each other for "symmetrical" floors.

When designing the levels, I found that the best and most fun floors are those with a single theme, or possibly two themes. Floors where I attempted to mix and match as many elements as possible turned out dull and boring (which is counterintuitive). Simple is better, and less is more. (Most of these dull floors were removed during development.) Similarly, levels tended to feel the most exciting if they had only one or two enemy types (rather than all four).

As far as I know, based on extensive playtesting, every level is "fair", meaning: There is a straightforward strategy for safely completing each level. Though on some of the late levels, such as 47 or 49, such "safe" strategies may not be obvious.

Enemy Movement Logic

Is the enemy movement purely random? Or deterministic?

The answer is some of both.

Enemies which can walk (which means, all of them except the bat) always walk from the center of one square to the center of an adjacent square. This means that enemies, like the player, can only walk horizontally or vertically, but never diagonally relative to the block they are on. If the adjacent square is no longer present (for example, because a block which can move has just started to move away), then the enemy will U-turn when it reaches the edge and go back to the center of the same square.

When an enemy reaches the center of any square, it makes a decision to do one of the following:

  • Keep going straight
  • Turn left
  • Turn right
  • U-turn

This decision incorporates certain rules based on which blocks adjacent to the enemy are present or not, the position of the player, as well as a random factor.

If the enemy movement were purely random, without regard to nearby blocks or the player position, it would feel strange—the player would experience a mixture of feeling disconcerted and unthreatened. Enemies should chase the player. On the other hand, if the enemies determinstically homed in on the player without relent, the game would feel too stressful and unnatural. Yes, the enemies should chase the player, but they're bug-like creatures—they're not too smart. The complexity mixed with randomness makes it feel like the enemies are gradually approaching you, but have a "personality".

All the enemies use the same movement logic (in addition to the spider's jumping ability and the snout's firing attack). It is described by the following table. If two players are present, the location of the player which is closer, by a bird's eye measure, is solely used. Described player positions are relative to the direction that the particular enemy is facing.

Block in frontBlock to the leftBlock to the rightBlock behindPlayer positionChance of going straightChance of turning leftChance of turning rightChance of U-turningChance of continuing in table
YesOptionalYesOptionalPlayer directly to the right0%50%0%0%50%
YesYesOptionalOptionalPlayer directly to the left0%50%0%0%50%
YesYesYesOptionalPlayer toward the left75%17%8%0%0%
YesYesYesOptionalPlayer toward the right75%8%17%0%0%
YesYesYesOptionalPlayer directly behind75%13%13%0%0%
YesYesNoOptionalPlayer behind but not directly75%25%0%0%0%
YesNoYesOptionalPlayer behind but not directly75%0%25%0%0%
NoYesYesOptionalPlayer toward the left0%67%33%0%0%
NoYesYesOptionalPlayer toward the right0%33%67%0%0%
NoYesYesOptionalPlayer directly behind0%33%33%33%0%
NoYesYesOptionalPlayer directly in front0%50%50%0%0%
NoNoNoYesNot considered0%0%0%100%0%
NoNoYesNoNot considered0%0%100%0%0%
NoYesNoNoNot considered0%100%0%0%0%
NoYesNoOptionalPlayer behind, but also off to either side by at least 45 degrees0%50%0%50%0%
NoNoYesOptionalPlayer behind, but also off to either side by at least 45 degrees0%0%50%50%0%
NoYesNoOptionalPlayer narrowly in front0%75%0%25%0%
NoNoYesOptionalPlayer narrowly in front0%0%75%25%0%
NoNoNoNoNot considered0%25%25%25%0%
OptionalOptionalOptionalOptionalNot considered100%0%0%0%0%

To summarize the above rules in prose form:

  • Enemy decisions based on player position only look at the relative bird's eye position and the presence or absense of the immediately adjacent blocks—no "navigation" logic is used. For example, a player directly behind an enemy on the same platform is treated the same as if there is a gap separating them.
  • Enemies tend to walk straight ahead most of the time.
  • Enemies tend to turn toward the player when they can, but don't always do so, and sometimes even turn toward the opposite direction.
  • Enemies shy away from U-turning, but will do so at dead ends, sometimes at corners, and occasionally at an edge if the player is directly behind.

The spider can jump over gaps that are one block wide (a distance of two blocks). It will only jump if there is a gap. Every time the spider crosses the center of a block, it decides whether to jump. If the spider is standing next to a gap across from another block, it will jump in that direction with a probability of 1 in 3 if it will bring it closer to the player (only the nearer player is considered in a two-player game). It will jump in that direction with a probability of 1 in 16 if it will move it farther (or an equivalent distance) from the player. The spider will pause before jumping unless it was already moving in that same direction; in that case, it will jump without delay.

The snout can fire a projectile in any direction. Every time the snout crosses the center of a block, it decides whether to fire a projectile. It will only fire in the orthogonal direction that is most toward the player (in a two-player mode, you guessed it—only the player that is closer is considered), unless the player is dashing or respawning, in which case, the direction is random. The probability of firing is: 1 / (n + 1), where n is the number of spaces in the closest cardinal direction away from the player, and the probability will not exceed 1 in 4, nor will it be less than 1 in 16. (For example, if the snout is 12 spaces away from the player horizontally and one space away vertically, that is a distance of n = 1 in the formula.) Also, the snout will not fire more than once in two seconds, regardless of the probability rules. The snout always pauses before firing, regardless of any factors.

When the spider lands from a jump, and after the snout fires, they use the same formula in determining their first direction of motion: Of all the directions with an adjacent block, if there is more than one, move in the direction that is most toward the player 2/3 of the time, and randomly pick one of the remaining directions 1/3 of the time. If it is standing on a solitary block, move randomly.

Sound

There are 108 sounds in Mach Block. More than you would have guessed? The main reason the number is so large is that four of the unique sounds are duplicated 16 times, pitch-adjusted. These are used for the increasing crescendos of the chain and combo bonuses, and for the increasing pitch of the "target" sound when you attack multiple enemies in one drag (plus the "thump" sound when you do so while dashing; you can only hear this sound when you use headphones due to the low frequency).

Nearly all the sounds were synthesized from scratch in Adobe Audition (better known as its former name Cool Edit, even though it was acquired and renamed 8 years ago), making heavy use of just a few transforms. Each sound starts from a base of either white noise, or a generated tone. The generated tones sometimes use a triangle wave, or harmonics, or modulation, or all of these. Then, the base effect is processed through EQ, fades, an envelope (for example, an attack/decay/sustain/release curve), or a combination. Finally, many sound effects actually consist of multiple components, mixed in the multitrack editor. Only occasionally were any other filters used.

If I were doing a larger amount of sound synthesis, I might have used the sound synthesis engine, SuperCollider. For this project, the amount of sound I needed to create was small enough that it may not have been worth the time to set up and re-learn SuperCollider (I used it heavily in 2005 for classes at UW DXARTS). Also, I liberally used the array of filter presets in Adobe Audition that I didn't have the necessary audio knowledge to create on my own.

The relative volume levels of individual sound effects changes depending on whether headphones or the built-in speaker are being used. Each sound effect has both a headphone, and a speaker, volume attached to it, derived from experimentation. This is primarily because the built-in speaker renders very little bass.

iOS provides several interfaces for sound playback, but the best suited for sound effects in games is OpenAL. (AVAudioPlayer, by contrast, would briefly pause for a couple of frames at the start of any sound effect, so was not an option.) I used the free software Finch library as an easier interface to OpenAL, than OpenAL directly.

Music

As a non-composer, I faced a dilemma of what to do about music. My options, as I saw them:

  • No music
  • Painstakingly compose just a little bit of music, perhaps as short cues instead of full background music
  • Use public-domain classical music
  • Hire somebody to write music (I was wary of this for my first game project)
  • …and then there was this program called "GarageBand" I remember hearing about... that's right, I have a Mac, so I have it!

Yep, GarageBand saved the day. I "composed" 15 different background tunes, of 30-60 seconds each, using the built-in Apple Loops in GarageBand. This means that some game players will recognize pieces of the music from elsewhere, either from GarageBand directly, or from other games and projects which did the same thing I did. But I thought that was much better than having no music! I assigned the 15 tunes to the 50 levels, often matching each tune to the level's theming. Although some tunes are used on a number of levels, a few are used on only one level each, particularly later in the game. The final level has the longest and most dramatic music of all, used only on that level.

Although GarageBand may appear at a quick glance to be a "toy" application, useful only for novices making loop-based tracks or simple instrument recordings, it actually is far more powerful. For example, most Apple Loops are not mere recordings, but editable melodies applied to a synthesis pipeline for the particular instrument used in that loop. The instrument is defined either from a preset resembling a real-world instrument, or a synthesized effect; it is then passed through a series of filters similar to what one could use in a destructive waveform editor (like Adobe Audition). Except in GarageBand, it's all non-destructive. GarageBand actually might have been a better tool to create many of the sound effects than Audition was (if I had known about its capabilities earlier).

In contrast to sound effects, I used AVAudioPlayer to play music. The slight delay when the music changes between levels is acceptable, and only AVAudioPlayer—not OpenAL—supports music encoded in a compressed file format (AAC or MP3). It hardware-accelerates the decoding instead of using the CPU, for battery savings.

Drawing the Graphics

The graphics were built in Photoshop. Photoshop is a powerful and versatile editor which has paid for itself many times over. But sometimes what I really would have liked is a graphics synthesis language—especially an editor or IDE with a real-time viewer as I modify the graphics source code. The reason is that when I was finished with some sprite or piece of art, I'd often want to go back and change something in an early step—and unless I had memorized, written down, or was willing to re-create the steps (which I had sometimes performed months ago), it was difficult and extremely time-consuming to make some of these changes. And unlike for sound, I did spend enough time on the graphics—an enormous amount of time—to justify learning a synthesis platform.

I made one of the four enemies a bat because it was fewer frames to draw (2 instead of 12). Yes, I'm serious.

Miscellaneous Tidbits

  • The buildings in the background are positioned on a grid, and randomly placed each game. Instead of individually placing each building one at a time, they're placed in pre-defined patterns of 1, 2, 3, or 4, in a 3x3 grid, so they appear to be clustered more realistically.
  • In the first iteration of the game engine, the gamefield size was 8x13 blocks (it is currently 9x14). The one additional block in each direction helped immensely; the original demo levels were re-tooled from 8x13 to 9x14 and felt far more spacious; additionally, many of the level concepts in the final game wouldn't work with even one less block in height or would need to be drastically simplified.
  • In two-player mode, the left/right stereo panning changes to a mode in which sounds pertaining to one player go to one speaker, and sounds pertaining to the other player go to the other speaker. Sounds which can't be associated with just one player play in the middle.
  • During development, the game was far harder than it is now. The enemy counts in each level were much higher, and you could only continue at every 10th level (rather than every 5th). You can infer this latter change by looking at the buttons in the linked texture atlas image (in the Graphics on iOS section).
  • The code to render the score "gets"—the on-screen indicators when you earn points, including the chain and combo bonus counters and the "extra life" indicator—is quite complex. Notice how they never overlap, even if many appear simultaneously, and they appear in esthetically pleasing arrangements? Here's why: The size of each "get" is individually computed, taking into account whether a chain or combo bonus is displayed, and whether an extra life indicator is displayed (and even how many—yes, it's possible in certain rare cases, with the right types of bonus multipliers, to get more than one extra life at a time). Additionally, the locations of each on-screen "get" is tracked until it disappears. Then, every position on screen (in increments of 8 pixels both horizontally and vertically) is attempted, and given a "score" based on its distance from the optimal position (as well as two alternate optimal positions). The further the distance, the higher the score, and an overlap with any other score "get" greatly raises the score. The optimal position is based on the direction that the player is facing—it's above the player unless the player is facing up, and the alternates are the other directions besides the one in which the player is moving (to minimize the chance that the score "get" will appear on top of where the player is about to walk). Finally, the position with the lowest score is chosen. In other words, the score positioning algorithm uses minmax! Simpler algorithms were considered, but only this algorithm looked esthetically natural.

Have fun playing the game! And programmers, good luck with your apps!

Share
This entry was posted in Uncategorized. Bookmark the permalink.