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 and many contain code that no longer works or is superceded by newer APIs. Many others contain out-of-date information or offer advice and opinions I no longer endorse. 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 second of four parts, I'll create basic objects in the game and their corresponding CoreAnimation Layers on screen.
We have a window
In the previous post I proposed an Asteroids-style game in CoreAnimation and explained the basic design of the Quartzeroids2 game
I showed the code to construct and scale the window. It has a blue gradient background and can switch between fullscreen and windowed modes.
Now, we need to put something in that window.
Objects placed on screen in this part.
Drawing in the window is done using CoreAnimation layers. If you looked closely at how the window background was drawn in Part One, you'll notice that it was a
CALayer, drawn using a single image:
Since this single image is a PDF made from vector (not bitmapped) components, this means that the layer can be drawn at any resolution without aliasing effects from resizing. In fact, the background PDF isn't even the right aspect ratio and Cocoa happily reshapes it for us. The added processing time to render a PDF, relative to bitmap, doesn't really matter since CoreAnimation only renders the CALayer once, then reuses the existing texture.
Game Objects and Layers
Placing a game-related object on screen will require two different components: the
GameObject and the
GameObject is the version of the object as handled in the
GameData. Since the game logic is responsible for deciding the size, positioning, speed, trajectory and in some cases the image and angle of rotation of the object, these properties will all be properties of the
GameObjects are held by the
GameData object. It tracks all of the
GameObjects in a dictionary, so all
GameObjects can be accessed at any time by their unique key in the
The biggest quirk about how I decided to implement the
GameObjectis that it is totally resolution independent. All coordinates and sizes are measured in units where
1.0is the height of the game window. So the coordinates
(0.5 * GAME_ASPECT, 0.5)and
(GAME_ASPECT, 1.0)are the bottom-left corner, center and top-right corners of the screen respectively (
GAME_ASPECTis the window aspect ratio: width of the window divided by the height).
GameObject being just a long list of Objective-C properties, most of the code in
GameObject exists to set, modify or update those properties. The biggest common "update" that needs to be performed is to move the object according to its speed and trajectory and "wrap" the object if it goes off the edge of the screen:
This method returns "NO" to indicate that it was not deleted during the update. This won't be used until next week when we add more of the game logic.
The objects are allowed to exceed the edge of the bounds by
GAME_OBJECT_BOUNDARY_EXCESS. This ensures that they don't feel like they disappeared with a tiny portion still onscreen.
GameObjectLayer is a subclass of
ImageLayer, using that class' code to render a single image to a
GameObjectLayer contains a key that identifies its corresponding
GameObject in the
GameData. It observes the
gameObjects dictionary for changes on that key and when any of the observed
GameObject properties change, the
GameObjectLayer will update itself accordingly.
The result is that the only substantial work required in the
GameObjectLayer is updating itself when a change in the
GameObject is observed.
observeValueForKeyPath:ofObject:change:context: method is smart enough to realize when the
GameObject changes to
NSNull (i.e. is deleted) and autodeletes.
Notice that the
GameObjectLayer is not resolution independent, so the
GameObject coordinates are multiplied through by the
gameHeight to convert them to coordinates in the layer hierarchy.
Controller code to bind them
The final element required to make
GameObjectLayers work together is controller code to construct the
GameObjectLayer for each
GameObject as it appears.
I chose to do this by making the
GameData send a
GameObjectNewNotification every time a new
GameObject is added to the
gameObjects dictionary. The GameController observes this notification with the following method:
Transactions are disabled so the layer doesn't fade in, it appears immediately.
Letting the view reinterpret the data
If you ran the program with the above code, the asteroid would not look like the slightly soccerball texture shown in the screenshot at the top and it would not spin. The asteroid would be a smooth gradient circle. This is because the the asteroid shown in the screenshot is made of two components: the non-rotating "asteroid-back" which provides a consistent lightsource-like effect and the "asteroid-front" which is a spinning second layer on top of the back layer.
GameData only contains the basic bounds information, which is mapped onto the "asteroid-back" by default. How can we add the second spinning layer for the purposes of display?
We could add the second layer as another object in the game but since I want this layer for the purposes of display (it has no real game-logic impact), I decided to handle it a different way.
[CATransaction commit]; line in the previous code sample, I include the code:
So I look to see if the new
GameObject is added to the
gameObjects dictionary using a key that starts with
GAME_ASTEROID_KEY_BASE. If it does, then I create a second layer that tracks the same underlying
GameObject. This second layer is an
AsteroidFrontLayer instead of the generic
AsteroidFrontLayer class is a subclass of
GameObjectLayer that overrides the
imageName to be "asteroid-front" and applies a rotation to the layer on each update.
You can download the Quartzeroids2 Part 2 project (225kB) which demonstrates the classes presented in this post.
The project for this part shows the
AsteroidFrontLayer in a simple, non-interactive display. To show everything on screen, the
GameData class contains a
newGame method which constructs some sample objects and then starts a timer running to call the
updateWithTimeInterval: methods repeatedly.
Now that we can draw our objects on screen, Part 3 will add user-interaction and game logic.