Reader beware: this post is part of the older "Objective-C era" on Cocoa with Love. I don't keep these articles up-to-date so the code may be broken or superceded by newer APIs. There's some good information but there's also some opinions I no longer endorse – keep a skeptical mind. Read "A new era for Cocoa with Love" for more.
How would you write an arcade-style 2D game in CoreAnimation? I'll show you how to write a resolution independent, high-speed, model-view-controller designed, Asteroids-style arcade game using CoreAnimation as the screen renderer. In this third of four parts, I add the logic for the game and game objects and present the finished code for the project.
From renderer to game
In the previous post, I presented a rendering system using CoreAnimation layers (
CALayer). To complete the game, we now need to add the game logic.
For this Asteroids-style game, the game logic will be very simple but will need to include the following components:
- User control of the ship
- Shooting and shot/asteroid collision detection and destruction.
- Player/asteroid collision and a finite number of player "lives"
- Level logic, so that a new, harder level is started when the previous one is cleared.
With the game logic added, the code for the project will be complete.
A screenshot of the game with shots and asteroid destruction.
Most games treat keyboard input in a different manner to applications. Instead of reading
keyDown: messages, including the
isARepeat messages (whose delay and frequency are specified in the System Preferences), games simply want the state of specific keys at a given time. e.g. "Is the 'thrust' key pressed?"
To provide the game with what it wants, I created a subclass of
NSView to use as the
contentView of the window. The sole purpose of this view subclass is to read key event messages passed to the window. When
keyDown: messages are received, boolean values on the
GameData object are set. When the
keyUp: message is received, the boolean values are cleared. In this way, we store the keyboard state so we can read it in an asynchronous manner whenever it is needed.
The implementation of the
keyDown: method follows.
Notice that Apple provide constants for higher level keys (like the arrow keys) but you are expected to handle ASCII codes (like the Escape key) yourself. You can get these from the ASCII manual page (
man ascii in a terminal window). Be careful to avoid the octal set — I have no idea how it earned first position in this file.
To respond to these new keyboard flags, I will create a subclass of
GameObject (the generic class representing an object in the game) named
PlayerObject to handle the player's ship. The real purpose of this subclass will be to add game-logic related behaviors. The primary location for adding behaviors will be an override of the parent class'
updateWithTimeInterval: that supplements the default behavior with player-specific movement and interaction.
Here is the code to handle the "thrust" key (the up arrow):
This code is some basic trigonometry to add the ship's current
trajectory vector to the new thrust vector, limiting the ship's maximum speed. The actual
y of the ship is modified by invoking the
super implementation (which applies this
Model-view-controller design note
This approach to key control is not typical of an application. In an application, a user-interface controller chooses the target object and sends the keyboard control directly to that object. In this game, the user-interface sets keyboard state in a game-accessible location and game objects choose whether to incorporate that state into their own. This behavior is good for a game because it allows the game objects to choose their own interaction logic but games represent a special case in this regard.
The other key aspect of user control in the game is the ability to fire shots.
Once again, I will use a
GameObject subclass to handle the shots. These
ShotObjects will be created in the
updateWithTimeInterval: method, since they will need to incorporate speed and trajectory information from the
The following code fires a shot when the user presses the 'shoot' key (spacebar).
initWithX:y: line creates the
addGameObject:forKey: part adds it to the game.
GameObjects are all stored by a single key in this game but I use the
keyForShotAtIndex: to create this key from a prefix ("shot") and a suffix (the shot's index) so that I can extract information about the object from its key.
This is a lazy approach to organizing game objects and any larger game would want to store its objects in a more metadata rich storage system. Better organizations might include:
- metadata methods on each game object so you can query each object about what it is
- additionally storing game objects in dictionaries keyed by type or other essential category information
- storing game objects in richly indexed system like an SQLite database (which is then accessible by any column)
setGameDataObject:forKey: line sets the number of shots that the player has fired in the
GameData's generic object storage. This will allow us to limit the number of shots fired by the player to
The next code is some more trigonometry to apply the ship's speed and trajectory to the shot. Then finally the
shotCooldown value is set. This is a simple counter (decremented on each
updateWithTimeInterval:) which will prevent another shot being fired while it is greater than zero (allowing us to set the minimum time between shots).
Last week's code contained a very simple main update loop that called the
updateWithInterval: method on all
GameObjects. This time, we expand the update loop to the following:
We time the duration between updates ourselves, even though we ask the
NSTimer to invoke us every 0.03 seconds. This allows us to keep the animation as smooth as possible even if the timer is delayed or frames start taking longer than 0.03 seconds to complete.
After this, we iterate over all the
GameObjects twice: once to process collisions and once to update positions. The reason for this separation is to ensure that all of the game objects are at the same moment in time when we process them for collisions — if we processed collisions during the update of positions then everything already updated for position would be 1 frame ahead of objects not yet processed when collisions are tested.
I chose to perform collisions before the update so that the collision applies to what the user can currently see (rather than what they will see after CoreAnimation updates).
Performing collisions is very simple — especially in this game since I'm only performing bounding box collisions. The collision code in
ShotObject which tests for collisions between shots and asteroids follows.
Once again, I'm using the prefix metadata in my game object keys to select asteroids for collisions.
The collision code lives on the
This code creates a bounding rectangle from the
GameObject specified by the
testObjectKey and collides it with all objects stored by keys starting with
prefix that aren't the
This code returns the whole set of colliding objects — overkill since the game only ever uses one at a time — but functional and easily fast enough for our purposes.
Tying it all together
We can control the ship, shoot and hit asteroids. There's a bit more that's in the game that you can see if you want:
- The shots animate through 5 frames and expire after a fixed amount of time. I chose to store the current animation frame in the game data and handle the animation there (instead of in the view like the asteroid animation was in the last post). I wanted to show that animation state could be part of the game data if it is data-related (this animation isn't really data-related so I have some regrets).
- The asteroids start at random locations in a ring around the player for each level and spawn 3 smaller asteroids when shot, each of which travels in a random direction with a random rotation.
Instead of dwelling on that, I'll jump to the last part of the game logic: the game (including lives) and the levels.
I want the following behaviors in the game:
- The player begins with 3 lives at level 1.
- Each level starts with a "Prepare for level X..." message for a few seconds before displaying the contents of the level and starting normal play.
- Every time the player loses a life (collides with an asteroid) the game displays an "X lives remaining..." message for a few seconds before positioning the player in the middle of the screen and starting normal play again.
- When the number of lives hits zero, a "Game Over" message is displayed and the game is stopped.
The data containing the number of lives and the current level are just objects in the
gameData dictionary. Creating a new level creates (3 + levelNumber) asteroids and places the player in the center. The messages are just a string, set using a specific
GAME_DATA_MESSAGE_KEY, also on the
gameData dictionary. These values are displayed by using bindings to connect them to ordinary
NSTextFields in the window.
More interesting is that the game needs to change its underlying behavior when the player dies or a new level begins.
The solution I chose is to maintain two different update loops for the two basic behaviors. The
updateLevel: method I've already shown is the "normal" update method. A second update method named
readyCountdown: will be used to do nothing (not update the
GameObjects) for a few seconds while a message is displayed, before returning to the regular update method.
endGame methods, the game now has the ability to transition between different gameplay states.
With the window and design from the first part, the rendering and layers from the second part and now the game logic, the game is complete. Download it, build it, play it and be underwhelmed by its simplicity.
In the final part, I'll present analysis of CoreAnimation's utility as a game rendering engine. I'll look at some features of CoreAnimation that I didn't use, including features that impacted negatively on performance. I'll also look at how CoreAnimation performance changes as the number of render objects is increased.
An Asteroids-style game in CoreAnimation, Part Two.
An Asteroids-style game in CoreAnimation, Part Four.