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:
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:
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!
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.
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
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:
Conceptually drawing a sprite involves:
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.
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:
Having finished the project:
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.
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.
(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:
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:
These rules govern jump blocks:
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 →
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 →
Hover for diagram →
Others are more loop-like, with some restrictions on movement and a feeling of defined paths:
Hover for diagram →
Hover for diagram →
Hover for diagram →
Hover for diagram →
Other floors are extremely linear, with the feel of a "beginning" and "end".
Hover for diagram →
Hover for diagram →
Hover for diagram →
Hover for diagram →
Hover for diagram →
Hover for diagram →
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.
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:
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.
|Player directly to the right||0%||50%||0%||0%||50%|
|Player directly to the left||0%||50%||0%||0%||50%|
|Player toward the left||75%||17%||8%||0%||0%|
|Player toward the right||75%||8%||17%||0%||0%|
|Player directly behind||75%||13%||13%||0%||0%|
|Player behind but not directly||75%||25%||0%||0%||0%|
|Player behind but not directly||75%||0%||25%||0%||0%|
|Player toward the left||0%||67%||33%||0%||0%|
|Player toward the right||0%||33%||67%||0%||0%|
|Player directly behind||0%||33%||33%||33%||0%|
|Player directly in front||0%||50%||50%||0%||0%|
|Player behind, but also off to either side by at least 45 degrees||0%||50%||0%||50%||0%|
|Player behind, but also off to either side by at least 45 degrees||0%||0%||50%||50%||0%|
|Player narrowly in front||0%||75%||0%||25%||0%|
|Player narrowly in front||0%||0%||75%||25%||0%|
To summarize the above rules in prose form:
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.
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.
As a non-composer, I faced a dilemma of what to do about music. My options, as I saw them:
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.
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.
Have fun playing the game! And programmers, good luck with your apps!← Mach Block Is Available in the App Store