Published by Addison-Wesley Professional
ISBN-10: 0-321-69942-4
ISBN-13: 978-0-321-69942-8 One of the most important elements of a game is the game loop. It is the heartbeat that keeps the game ticking. Every game has a series of tasks it must perform on a regular basis, as follows:
- Update the game state
- Update the position of game entities
- Update the AI of game entities
- Process user input, such as touches or the accelerometer
- Play background music and sounds This may sound like a lot, especially for a complex game that has many game elements, and it can take some time to process each of these stages. For this reason, the game loop not only makes sure that these steps take place, but it also ensures that the game runs at a constant speed.
This chapter shows you how to build the game loop for Sir Lamorak's Quest. We take the OpenGL ES template app created in Chapter 3, "The Journey Begins," and make changes that implement the game loop and the general structure needed to easily extend the game in later chapters.
Timing Is Everything
Let's start out by looking at some pseudocode for a simple game loop, as shown in Listing 4.1.
Listing 4.1??A Simple Game Loop
BOOL gameRunning = true; while(gameRunning) { updateGame; renderGame; } This example game loop will continuously update the game and render to the screen until gameRunning is false.
Although this code does work, it has a serious flaw: It does not take time into account. On slower hardware, the game runs slowly, and on fast hardware, the game runs faster. If a game runs too fast on fast hardware and too slow on slow hardware, your user's experience with the game will be disappointing. There will either be too much lag in the game play, or the user won't be able to keep up with what's going on in the game.
This is why timing needs to be handled within your game loop. This was not such a problem for games written back in the 1980s, because the speed of the hardware on which games were written was known, and games would only run on specific hardware for which they were designed. Today, it is possible to run a game on many different types of hardware, as is the case with the iPhone. For example, the following list sorts the devices (from slowest to fastest) that run the iOS:
- iPhone (first generation)
- iPod Touch 1G
- iPhone 3G
- iPod Touch 2G
- iPhone 3GS/iPod Touch 3G
- iPad/iPhone 4 As a game developer, you need to make sure that the speed of your game is consistent. It's not a good idea to have a game on the iPhone 3GS running so fast that the player can't keep up with the action, or so slow on an iPhone 3G that the player can make a cup of tea before the next game frame is rendered.
There are two common components used to measure a game loop's speed, as follows:
- Frames Per Second (FPS):FPS relates to how many times a game scene is rendered to the screen per second. The maximum for the iPhone is 60 FPS, as that is the screen's maximum refresh rate. In Listing 4.1, this relates to how many times the renderGame method is called.
- Update speed:This is the frequency at which the game entities are updated. In Listing 4.1, this relates to how many times the updateGame method is called.
Collision Detection
Timing is important for a number of reasons--not only the overall game experience, but, maybe more importantly, for functions such as collision detection. Identifying when objects in your game collide with each other is really important and is a basic game mechanic we need to use. In Sir Lamorak's Quest, having the player able to walk through walls is not a great idea, and having the player's sword pass through baddies with no effect is going to frustrate the player and keep them from playing the game.
Collision detection is normally done as part of the game's update function. Each entity has their AI and position updated as play progresses. Those positions are checked to see if it has collided with anything. For example, Sir Lamorak could walk into (or collide with) a wall, or a ghost could collide with an axe. As you can imagine, the distance a game entity moves between each of these checks is important. If the entity moves too far during each game update, it may pass through another object before the next check for a collision.
Having entities move at a constant speed during each game update can help to reduce the chances of a collision being missed. There is, however, always a chance that a small, fast-moving entity could pass through another object or entity unless collision checks are implemented that don't rely solely on an entities current position, but also its projected path. Collision detection is discussed in greater detail in Chapter 15, "Collision Detection," which is where we implement it into the game.
The Game Loop
The game loop is a key, if not the key, element in a game. I spent considerable time tweaking the game loop for Sir Lamorak's Quest, and it was something that I revisited a number of times, even after I thought I had what was needed.
There are so many different approaches to game loops. These range from the extremely simple closed loops you saw earlier in Listing 4.1, up to multithreaded loops that handle things such as path finding and complex AI on different threads. I found that I actually started off with the very simple approach that then became more complex as the game developed (and as I ran into issues).
We will not review some of the different game loops that I tried through the development of Sir Lamorak's Quest. Instead, we'll focus on the game loop used in the game, rather than diving down some rabbit hole we'll never end up using.
Frame-Based
The easiest type of game loop is called a frame-based loop. This is where the game is updated and rendered once per game cycle, and is the result of the simple game loop shown in Listing 4.1. It is quick and easy to implement, which was great when I first started the game, but it does have issues.
The first of these issues is that the speed of the game is directly linked to the frame rate of the device on which the game is running--the faster the hardware, the faster the game; the slower the hardware, the slower the game. Although we are writing a game for a very small family of devices, there are differences in speed between them that this approach would highlight.
Figure 4.1 shows how frames are rendered more quickly on fast hardware and more slowly on slower hardware. I suppose that could be obvious, but it can often be overlooked when you start writing games, leaving the player open to a variable playing experience. Also remember that each of these frames is performing a single render and update cycle.
Figure 4.1: Entity following a curved path on slow and fast hardware. A time-based variable interval loop is similar to the frame-based approach, but it also calculates the elapsed time. This calculation is used to work out the milliseconds (delta) that have passed since the last game cycle (frame). This delta value is used during the update element of the game loop, allowing entities to move at a consistent speed regardless of the hardware's speed.
For example, if you wanted an entity to move at 1 unit per second, you would use the following calculation:
position.x += 1.0f * delta; Although this gets over the problem of the game running at different speeds based on the speed of the hardware (and therefore the frame rate), it introduces other problems. While most of the time the delta could be relatively small and constant, it doesn't take much to upset things, causing the delta value to increase with some very unwanted side effects. For example, if a text message arrived on the iPhone while the user was playing, it could cause the game's frame rate to slow down. You could also see significantly larger delta values causing problems with elements, such as collision detection.
Each game cycle causes an entity in Figure 4.2 to move around the arc. As you can see in the diagram, with small deltas, the entity eventually hits the object and the necessary action can be taken.
Figure 4.2: Frames using a small delta value. However, if the delta value were increased, the situation shown in Figure 4.3 could arise. Here, the entity is moving at a constant distance, but the reduced frame rates (and, therefore, increased delta) has caused what should have been a collision with the object to be missed.
Figure 4.3: Frames using a large delta. Don't worry--there is a reasonably easy solution, and that is to use a time-based, fixed interval system.
Time-Based, Fixed Interval
The key to this method is that the game's state is updated a variable number of times per game cycle using a fixed interval. This provides a constant game speed, as did the time-based variable interval method, but it removes issues such as the collision problem described in the previous section.
You'll remember that the previous methods tied the game's update to the number of frames. This time, the game's state could be updated more times than it is rendered, as shown in Figure 4.4. We are still passing a delta value to the game entities, but it's a fixed value that is pre-calculated rather than the variable delta that was being used before (thus, the term fixed interval).
Figure 4.4: Variable numbers of updates per frame with a single render. This system causes the number of game updates to be fewer when the frame rate is high, but it also increases the number of game updates when the frame rate is low. This increase in the number of game updates when the game slows down means that the distance each frame travels is constant. The benefit is that you are not losing the chance to spot a collision by jumping a large amount in a single frame.
Getting Started
The project that accompanies this chapter already contains the game loop and other changes we are going to run through in the remainder of this chapter. You should now open the project CH04_SLQTSOR. We run through the changes and additions to the project since Chapter 3.
Note - This project should be compiled against version 3.1 or higher of the iPhone SDK. The CADisplayLink function used in this example is only available in version 3.1 of the iPhone SDK. If you compile this project using iPhone SDK 3.0 or less, it still works, but you will need to use NSTimer rather than CADisplayLink. Using iPhone SDK 3.0 or less will also generate warnings, as shown in Figure 4.5.
Figure 4.5: Errors generated in EAGLView.m when compiling against iPhone SDK version 3.0 or lower. When you open the CH04_SLQTSOR project in Xcode, you see a number of new groups and classes in the Groups & Files pane on the left that have been added to the project since Chapter 3, including the following:
- Group Headers: This group holds global header files that are used throughout the project.
- Abstract Classes: Any abstract classes that are created are kept in this group. In CH04_SLQTSOR, it contains the AbstractScene class.
- Game Controller: The game controller is a singleton class used to control the state of the game. We will see how this class is used and built later in this chapter.
- Game Scenes: Each game scene we create (for example, the main menu or main game) will have its own class. These classes are kept together in this group. Let's start with the changes made to the EAGLView class.
Inside the EAGLView Class
The first change to EAGLView.h, inside the Classes group, is the addition of a forward declaration for the GameController class. This class does not exist yet, but we will create it soon:
@class GameController; Inside the interface declaration, the following ivars have been added:
CFTimeInterval lastTime; GameController *sharedGameController; These instance variables will be used to store the last time the game loop ran and point to an instance of the GameController class, which we create later. No more changes are needed to the header file. Save your changes, and let's move on to the implementation file.
Inside the EAGLView.m File
In Xcode, select EAGLView.m and move to the initWithCoder: method. The changes in here center around the creation of the renderer instance. In the previous version, an instance of ES2Renderer was created. If this failed, an instance of ES1Renderer was created instead. We are only going to use OpenGL ES 1.1 in Sir Lamorak's Quest, so we don't need to bother with ES2Renderer.
Because we are not using ES2Renderer, the ES2Renderer.h and .m files have been removed from the project. The Shaders group and its contents have also been removed.
There is also an extra line that has been added to the end of the initWithCoder method, as shown here:
sharedGameController = [GameController sharedGameController]; The next change is the actual code for the game loop. We are going to have EAGLView running the game loop and delegating the rendering and state updates to the ES1Renderer instance called renderer. Just beneath the initWithCoder: method, you can see the game loop[sup]1[/sup] code, as shown in Listing 4.2.
Listing 4.2??EAGLView gameLoop: Method
#define MAXIMUM_FRAME_RATE 45 #define MINIMUM_FRAME_RATE 15 #define UPDATE_INTERVAL (1.0 / MAXIMUM_FRAME_RATE) #define MAX_CYCLES_PER_FRAME (MAXIMUM_FRAME_RATE / MINIMUM_FRAME_RATE) - (void)gameLoop { static double lastFrameTime = 0.0f; static double cyclesLeftOver = 0.0f; double currentTime; double updateIterations; currentTime = CACurrentMediaTime(); updateIterations = ((currentTime - lastFrameTime) + cyclesLeftOver); if(updateIterations > (MAX_CYCLES_PER_FRAME * UPDATE_INTERVAL)) updateIterations = (MAX_CYCLES_PER_FRAME * UPDATE_INTERVAL); while (updateIterations >= UPDATE_INTERVAL) { updateIterations -= UPDATE_INTERVAL; [sharedGameController updateCurrentSceneWithDelta:UPDATE_INTERVAL]; } cyclesLeftOver = updateIterations; lastFrameTime = currentTime; [self drawView:nil]; } When the game loop is called from either CADisplayLink or NSTimer, it first obtains the current time using CACurrentMediaTime(). This should be used instead of CFAbsoluteTimeGetCurrent() because CACurrentMediaTime() is synced with the time on the mobile network if you are using an iPhone. Changes to the time on the network would cause hiccups in game play, so Apple recommends that you use CACurrentMediaTime().
Next, we calculate the number of updates that should be carried out during this frame and then cap the number of update cycles so we can meet the minimum frame rate.
The MAXIMUM_FRAME_RATE constant determines the frequency of update cycles, and MINIMUM_FRAME_RATE is used to constrain the number of update cycles per frame.
Capping the number of updates per frame causes the game to slow down should the hardware slow down while running a background task. When the background task has finished running, the game returns to normal speed.
Using a variable time-based approach in this situation would cause the game to skip updates with a larger delta value. The approach to use depends on the game being implemented, but skipping a large number of updates while the player has no ability to provide input can cause issues, such as the player walking into a baddie without the chance of walking around or attacking them.
I tried to come up with a scientific approach to calculating the maximum and minimum frame rate values, but in the end, it really was simple trial and error. As Sir Lamorak's Quest developed and the scenes became more complex, I ended up tweaking these values to get the responsiveness I wanted while making sure the CPU wasn't overloaded.
The next while loop then performs as many updates as are necessary based on updateIterations calculated earlier. updateIterations is not an actual count of the updates to be done, but an interval value that we use later:
while (updateIterations >= UPDATE_INTERVAL) { updateIterations -= UPDATE_INTERVAL; [sharedGameController updateCurrentSceneWithDelta:UPDATE_INTERVAL]; } This loops around reducing the interval in updateIterations by the fixed UPDATE_INTERVAL value and updates the games state. Once updateIterations is less than the UPDATE_INTERVAL, the loop finishes, and we load any fractions of an update left in updateIterations into cyclesLeftOver. This means we don't lose fractions of an update cycle that we can accumulate and use later.
With all the updates completed, we then render the scene:
[self drawView:nil]; The CADisplayLink or NSTimer now calls the game loop until the player quits or the battery runs out (which it could do, given how much they will be enjoying the game!).
This is not a complex game loop, although it may take a while to get your head around the calculations being done. I found that moving to this game loop reduced the CPU usage on Sir Lamorak's Quest quite significantly and really smoothed out the game.
The final changes to EAGLView are within the startAnimation method. To get things ready for the first time we run through the gameLoop, we need to set the lastTime ivar. Because the gameLoop will not be called until the animation has started, we need to add the following line to the startAnimation method beneath the animating = TRUE statement:
lastTime = CFAbsoluteTimeGetCurrent(); The selectors used when setting up the CADisplayLink and NSTimer also need to be changed. The new selector name should be gameLoop instead of drawView.
Having finished with the changes inside EAGLView, we need to check out the changes to ES1Renderer. This class is responsible for setting up the OpenGL ES context and buffers, as noted in Chapter 3. However, we are going to extend ES1Renderer slightly so it sets up the OpenGL ES state we need for the game and renders the currently active scene.
ES1Renderer Class
When you look inside the ES1Renderer.h file, you see a forward declaration to the GameController class, which is in the next section, and an ivar that points to the GameController instance. The rest of the header file is unchanged.
In Xcode, open the ES1Renderer.m file. The GameController.h file is imported, followed by an interface declaration, as shown here:
@interface ES1Renderer (Private) // Initialize OpenGL - (void)initOpenGL; @end This interface declaration specifies a category of Private and is being used to define a method that is internal to this implementation. (I normally create an interface declaration such as this inside my implementations so I can then declare ivars and methods that are going to be private to this class.) There is only one method declared, initOpenGL, which is responsible for setting up the OpenGL ES states when an instance of this class is created.
Although Objective-C doesn't officially support private methods or ivars, this is a common approach used to define methods and ivars that should be treated as private.
The next change comes at the end of the init method:
sharedGameController = [GameController sharedGameController]; This is pointing the sharedGameController ivar to an instance of the GameController class. GameController is implemented as a singleton. This is a design pattern, meaning there can be only one instance of the class. It exposes a class method called sharedGameController that returns a reference to an instance of GameController. You don't have to worry if an instance has already been created or not because that is all taken care of inside the GameController class itself.
The next change is within the render method, shown in Listing 4.3. This is where the template initially inserted drawing code for moving the colored square. We will see the code used to draw the square again, but it's not going to be in this method. If you recall, this method is called by the game loop and needs to call the render code for the currently active scene.
Listing 4.3??EAGLView render Method
- (void) render { glClear(GL_COLOR_BUFFER_BIT); [sharedGameController renderCurrentScene]; [context presentRenderbuffer:GL_RENDERBUFFER_OES]; } First of all, glClear(GL_COLOR_BUFFER_BIT) clears the color buffer and then clears the screen, making it ready for the next scene to be rendered. For the rendering, the game controller is asked to render the currently active scene. This is how the render message is passed from the game loop to ES1Renderer, and then onto the game controller and eventually the render method inside the currently active game scene. Figure 4.6 shows how a game scene fits into the other classes we are reviewing.
Figure 4.6: Class relationships. The last line in this method presents the render buffer to the screen. If you remember, this is where the image that has been built in the render buffer by the OpenGL ES drawing commands is actually displayed on the screen.
Having looked at the changes to the render method, we'll move on to the resizeFromLayer: method. If you recall, the resizeFromLayer: method was responsible for completing the OpenGL ES configuration by assigning the renderbuffer created to the context (EAGLContext) for storage of the rendered image. It also populated the backingWidth and backingHeight ivars with the dimensions of the renderbuffer.
The following line of code has been added to this method that calls the initOpenGL method:
[self initOpenGL]; If this looks familiar, that's because this method was placed inside the interface declaration (described earlier) as a private category. As the resizeFromLayer method assigns the render buffer to the context and finishes up the core setup of OpenGL ES, it makes sense to place this OpenGL ES configuration activity in this method so we can set up the different OpenGL ES states needed for the game.
Now move to the bottom of the implementation and look at the initOpenGL method. This method sets up a number of key OpenGL ES states that we will be using throughout the game.
If you move to the bottom of the implementation, you can see the following implementation declaration:
@implementation ES1Renderer (Private) You can tell this is related to the interface declaration at the top of the file because it's using the same category name in brackets. There is only one method declared in this implementation: initOpenGL.
At the start of the method, a message is output to the log using the SLQLOG macro defined in the Global.h header file. The next two lines should be familiar, as they were covered in Chapter 3. We are switching to the GL_PROJECTION matrix and then loading the identity matrix, which resets any transformations that have been made to that matrix.
The next line is new and something we have not seen before:
glOrthof(0, backingWidth, 0, backingHeight, -1, 1); This command describes a transformation that produces an orthographic or parallel projection. We have set the matrix mode to GL_PROJECTION, so this command will perform a transformation on the projection matrix. The previous function sets up an orthographic projection--in other words, a projection that does not involve perspective (it's just a flat image).
Note - I could go on now about orthographic and perspective projection, but I won't. It's enough to know for our purposes that glOrthof is defining the clipping planes for width, height, and depth. This has the effect of making a single OpenGL ES unit equal to a single pixel in this implementation because we are using the width and height of the screen as the parameters.As mentioned earlier, OpenGL ES uses its own units (that is, a single OpenGL ES unit by default does not equal a single pixel). This gives you a great deal of flexibility, as you can define how things scale as they're rendered to the screen. For Sir Lamorak's Quest, we don't need anything that complex, so the previous function--which results in a unit equal to a pixel--is all we need.
Configuring the View Port
It is not common to make many changes to the GL_PROJECTION matrix apart from when initially setting up the projection.
As we are setting up the projection side of things, this is a good place to also configure the view port:
glViewport(0, 0, backingWidth , backingHeight); The Viewport function specifies the dimensions and the orientation of the 2D window into which we are rendering. The first two parameters specify the coordinates of the bottom-left corner, followed by the width and height of the window in pixels. For the width and height, we are using the dimensions from the renderbuffer.
With the projections side set up, we then move onto setting up the GL_MODELVIEW matrix. This is the matrix that normally gets the most attention as it handles the transformations applied to the game's models or sprites, such as rotation, scaling, and translation. As noted in Chapter 3, once the matrix mode has been switched to GL_MODELVIEW, the identity matrix is loaded so it can apply the transformations.
glMatrixMode(GL_MODELVIEW); glLoadIdentity(); Next, we set the color to be used when we clear the screen and also disable depth testing. Because we are working in 2D and not using the concept of depth (that is, the z-axis), we don't need OpenGL ES to apply any tests to pixels to see if they are in front of or behind other pixels. Disabling depth testing in 2D games can really help improve performance on the iPhone.
Not using the depth buffer means that we have to manage z-indexing ourselves (that is, the scene needs to be rendered from back to front so objects at the back of the scene appear behind those at the front):
glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glDisable(GL_DEPTH_TEST); We finish up the OpenGL ES configuration by enabling more OpenGL ES functions. You may remember that OpenGL ES is a state machine. You enable or disable a specific state, and it stays that way until you change it back. We have done exactly that when disabling depth testing, which now stays disabled until we explicitly enable it again:
glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_COLOR_ARRAY); The preceding states are used to tell OpenGL ES that we are going to be providing an array of vertices and an array of colors to be used when rendering to the screen. Other client states will be described and used later in Chapter 5, "Image Rendering."
That's it for the configuration of OpenGL ES. A lot of the OpenGL ES configuration should have looked familiar to you. A number of the functions in there were present in the render code from the OpenGL ES template. In the template, the states were set each time we rendered. Although this is fine, it isn't necessary to do that unless you are using state changes to achieve specific effects.
Tip - Keep the number of state changes being made within the game loop to a minimum, as some, such as switching textures, can be expensive in terms of performance. Is really is worth creating your own state machine that stores the states set in OpenGL ES. These can then be checked locally to see if they need to change i.e. there is no point in setting them if the values are the same.That completes all the changes that have been made to the ES1Renderer class. We have added a pointer to the game controller, so rendering can be delegated to an instance of that class. We have also added some core OpenGL ES configuration to the class as well, making it responsible for all OpenGL ES setup that gives us a single place to go when we need to change that setup.
Game Scenes and the Game Controller
Having looked at the changes that were needed within EAGLView and ES1Renderer, we need to now look at the game controller and game scenes. Because we have the game loop in place, we need to introduce new classes that will handle the introduction of other game elements. The game elements I'm talking about are the different scenes used in Sir Lamorak's Quest, such as the following:
- The main menu: This is where players are taken when they first launch Sir Lamorak's Quest. The main menu provides the player with options to change the game settings, view credits, or start the game.
- The game itself: This is where the (game) action takes place. The idea is that a game scene is responsible for its own rendering and game logic updates. This helps to break up the game into manageable chunks. I've seen entire games containing multiple scenes coded in a single class. For me, this is just too confusing, and creating a separate class for each scene just seemed logical.
In addition to the game scenes, we need to create a game controller. We have already seen the game controller mentioned in the EAGLView and ES1Renderer classes, so let's run through what it does.
Creating the Game Controller
Figure 4.6 shows the relationship between the classes (that is, EAGLView, ES1Renderer, and GameController), and the game scene classes.
The GameController Class
If we are going to have a number of scenes, and each scene is going to be responsible for its rendering and logic, we are going to need a simple way of managing these scenes and identifying which scene is active. Inside the game loop, we will be calling the game update and render methods on a regular basis. And because there will be multiple scenes, we need to know which of those scenes is currently active so the update and render methods are called on the right one. Remember from looking at EAGLView that inside the game loop, we were using the following code to update the game:
[sharedGameController updateCurrentSceneWithDelta:UPDATE_INTERVAL]; This line calls a method inside an instance of GameController. We are not telling the game controller anything about the scene that should be rendered as are expecting the GameController to already know.
Note - One important aspect of the game controller is that it is a singleton class. We don't want to have multiple game controllers within a single game, each with their own view of the game's state, current scene, and so forth.Inside the Game Controller group, you find the GameController.h and GameController.m files. Open GameController.h and we'll run through it.
Although it may sound complicated to make a class a singleton, it is well-documented within Apple's Objective-C documentation. To make this even easier, we use the SynthesizeSingleton macro created by Matt Gallagher.[sup]2[/sup] Matt's macro enables you to turn a class into a singleton class simply by adding a line of code to your header and another to your implementation.
At the top of the GameController.h file, add the following import statement to bring in this macro:
#import "SynthesizeSingleton.h"Note - I won't run through how the macro works, because all you need can be found on Matt's website. For now, just download the macro from his site and import the SynthesizeSingleton.h file into the project.Next is another forward declaration to AbstractScene, which is a class we will be looking at very shortly. This is followed by an interface declaration that shows this class is inheriting from NSObject and implements the UIAccelerometerDelegate protocol:
@interface GameController : NSObject UIAccelerometerDelegate is used to define this class as the delegate for accelerometer events, and it supports the methods necessary to handle events from the accelerometer.
Within the interface declaration, we have just a couple of ivars to add. The first is as follows:
NSDictionary *gameScenes; This dictionary will hold all the game scenes in Sir Lamorak's Quest. I decided to use a dictionary because it allows me to associate a key to each scene, making it easier to retrieve a particular scene:
AbstractScene *currentScene; As you will see later, AbstractScene is an abstract class used to store the ivars and methods common between the different game scenes. Abstract classes don't get used to create class instances themselves. Instead, they are inherited by other classes that override methods which provide class-specific logic.
Note - Objective-C does not enforce abstract classes in the same way as Java or C++. It's really up to the developer to understand that the class is meant to be abstract, and therefore subclassed--thus placing Abstract at the beginning of the class name.This works well for our game scenes, as each scene will have its own logic and rendering code, but it will have ivars and methods, such as updateSceneWithDelta and renderScene, that all scenes need to have. We run through the AbstractScene class in a moment.
After the interface declaration, the next step is to create a single property for the currentScene ivar. This makes it so currentScene can be both read and updated from other classes.
Creating the Singleton
So far, this looks just like any other class. Now let's add two extra lines of code to make this a singleton class:
+ (GameController *)sharedGameController; This is a class method identified by the + at the beginning of the method declaration. Because this is going to be a singleton class, this is important. We use this method to get a pointer to the one and only instance of this class that will exist in the code, which is why the return type from this method is GameController.
Next, we have two more method declarations; the first is as follows:
- (void)updateCurrentSceneWithDelta:(float)aDelta; This method is responsible for asking the current scene to update its logic passing in the delta calculated within the game loop. The next method is responsible for asking the current scene to render:
- (void)renderCurrentScene; Now that the header is complete, open GameController.m so we can examine the implementation.
Inside GameController.m
To start, you can see that the implementation is importing a number of header files:
#import "GameController.h" #import "GameScene.h" #import "Common.h" GameScene is a new class; it inherits from the AbstractScene class. Because we will be initializing the scenes for our game in this class, each scene we created will need to be imported. Common.h just contains the DEBUG constant at the moment, but more will be added later.
Next, an interface is declared with a category of Private. This just notes that the methods defined in this interface are private and should not be called externally to the class. Objective-C does not enforce this, although there really is no concept of a private method or ivar in Objective-C:
@interface GameController (Private) - (void)initGame; @end As you can see from this code, we are using initGame to initialize game scenes.
Next is the implementation declaration for GameController. This is a standard declaration followed by a synthesize statement for currentScene, so the necessary getters and setters are created. The next line is added to turn this class into a singleton class:
SYNTHESIZE_SINGLETON_FOR_CLASS(GameController); The macro defined within the SynthesizeSingleton.h file adds all the code necessary to convert a class into a singleton. If you look inside the SynthesizeSingleton.h file, you see the code that gets inserted into the class when the project is compiled.
Notice that this class also has an init method. The init is used when the initial instance of this class is created. The formal approach to getting an instance of this class is to call the method sharedGameController, as we defined in the header file. This returns a pointer to an instance of the class. If it's the first time that method has been called, it creates a new instance of this class and the init method is called.
Tip - The name of the class method defined in the header is important; it should be shared, followed by the name of the class (for example, sharedClassName). The class name is passed to the macro in the implementation, and it is used to create the sharedClassName method.If an instance already exists, a pointer to that current instance will be returned instead, thus only ever allowing a single instance of this class to exist. If you tried to create an instance of this class using alloc and init, you will again be given a pointer to the class that already exists. The code introduced by the synthesize macro will stop a second instance from being created.
initGame is called within the init method and sets up the dictionary of scenes, as well as the currentScene.
If you move to the bottom of the file, you see the implementation for the private methods.
Inside the initGame method, we are writing a message to the console, before moving on to set up the dictionary. It's good practice to make sure that all these debug messages are removed from your code before you create a release version. The next line creates a new instance of one of the game scenes, called GameScene:
AbstractScene *scene = [[GameScene alloc] init]; As you can see, GameScene inherits from AbstractScene. This means we can define *scene as that type. This enables you to treat all game scenes as an AbstractScene. If a game scene implements its own methods or properties that we need to access, we can cast from AbstractScene to the actual class the scene is an instance of, as you will see later.
Now that we have an instance of GameScene, we can add it to the dictionary:
[gameScenes setValue:scene forKey:@"game"]; This creates an entry in the dictionary that points to the scene instance and gives it a key of game. Notice that the next line releases scene:
[scene release]; Adding scene to the dictionary increases its retain count by one, so releasing it now reduces its retain count from two to one. When the dictionary is released or the object is asked to release again, the retain count on the object will drop to zero and the object's dealloc method will be called. If we didn't ask scene to release after adding it to the dictionary, it would not be released from memory when dictionary was released without another release call, which we may not realize we need to do. This is a standard approach for managing memory in Objective-C.
The last action of the method is to set the currentScene. This is a simple lookup in the gameScenes dictionary for the key game, which we used when adding the game scene to the dictionary. As additional game scenes are added later, we will add them to the dictionary with the following method:
currentScene = [gameScenes objectForKey:@"game"]; We only have a few more methods left to run through in the GameController class. Next up is the updateCurrentSceneWithDelta: method, shown here:
- (void)updateCurrentSceneWithDelta:(float)aDelta { [currentScene updateSceneWithDelta:aDelta]; } This takes the delta value calculated within the game loop and calls the updateSceneWithDelta method inside the currentScene. Remember that we have set the currentScene to point to an object in the gameScene dictionary. These objects should all inherit from AbstractScene and therefore support the update method.
The same approach is taken with the render method, shown here:
-(void)renderCurrentScene { [currentScene renderScene]; } The final method to review is accelerometer:didAccelerate, shown here:
- (void)accelerometer:(UIAccelerometer *)accelerometer didAcceler- ate:(UIAcceleration *)acceleration { } This delegate method needs to be implemented because the class uses the UIAccelerometerDelegate protocol. When the accelerometer is switched on, this method is passed UIAcceleration objects that can be used to find out how the iPhone is being moved. This can then be used to perform actions or control the player inside the game. We aren't using this method in Sir Lamorak's Quest, but it's useful to understand how this information could be obtained. More information on user input can be found in Chapter 12, "User Input."
AbstractScene Class
AbstractScene was mentioned earlier, and as the name implies, it is an abstract class. All the game scenes we need to create will inherit from this class.
Open AbstractScene.h in the Abstract Classes group, and we'll take a look.
The header starts off by importing the OpenGL ES header files. This allows any class that inherits from AbstractScene.h to access those headers as well. The class itself inherits from NSObject, which means it can support operations such as alloc and init.
A number of ivars are defined within the interface declaration. Again, the ivars will be available to all classes that inherit from this class. The idea is to place useful and reusable ivars in this abstract class so they can be used by other game scenes. The ivars you will find here include the following:
- screenBounds: Stores the dimensions of the screen as a CGRect.
- sceneState: Stores the state of the scene. Later, we create a number of different scene states that can be used to track what a scene is doing (for example, transitioning in, transitioning out, idle, and running).
- sceneAlpha: Stores the alpha value to be used when rendering to the screen. Being able to fade everything in and out would be cool, so storing an overall sceneAlpha value that we can use when rendering enables us to do this.
- nextSceneKey: A string that holds the key to the next scene. If the GameController receives a request to transition out, the next scene specified in this ivar will become the current scene.
- sceneFadeSpeed: Stores the speed at which the scene fades in and out. After the interface declaration, two more properties are defined, as follows:
@property (nonatomic, assign) uint sceneState; @property (nonatomic, assign) GLfloat sceneAlpha; These simply provide getter and setter access to the sceneState and sceneAlpha ivars.
Next, a number of methods are defined to support the game scenes, including the update and render methods we have already discussed:
- (void)updateSceneWithDelta:(float)aDelta; - (void)renderScene; There are also a few new methods, too. The first relates to touch events. The EAGLView class responds to touch events as it inherits from UIView, and these need to be passed to the currently active game scene. The active scene uses this touch information to work out what the player is doing. The following touch methods are used to accept the touch information from EAGLView and allow the game scene to act upon it:
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event view:(UIView*)aView; - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event view:(UIView*)aView; - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event view:(UIView*)aView; - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event view:(UIView*)aView; The next method declared is similar to the touch methods. Just as touches are fed to each game scene, accelerometer events also need to be fed down in the same way. We have already seen that GameController is the target for accelerometer events; therefore, GameController needs to pass down accelerometer event information to the current game scene:
- (void)updateWithAccelerometer:(UIAcceleration*)aAcceleration; That's it for the header file. Now let's move to the implementation file by opening AbstractScene.m.
You may be surprised by what you find in the implementation file. When I said earlier that the abstract class doesn't do anything, I really meant it. Apart from setting up the synthesizers for the two properties we declared, it just contains empty methods.
The idea is that the game scene that inherits from this class will override these methods to provide the real functionality.
That being the case, let's jump straight to the final class to review in this chapter: the GameScene class.
GameScene Class
The GameScene class is responsible for implementing the game logic and rendering code for the scene. As described earlier, a game scene can be anything from the main menu to the actual game. Each scene is responsible for how it reacts to user input and what it displays onscreen.
For the moment, we have created a single game scene that we will use for testing the structure of Sir Lamorak's Quest. You find the GameScene.h file inside the Game Scenes group in Xcode. When you open this file, you see that all we have defined is an ivar, called transY. We have no need to define anything else at the moment because the methods were defined within the AbstractScene class we are inheriting from.
Tip - When you inherit in this way, you need to make sure the header of the class you are inheriting from is imported in the interface declaration file (the .h file).Because there is not much happening in the header file, open the GameScene.m file. This is where the magic takes place. All the logic for rendering something to the screen can be found in the GameScene.m file.
To keep things simple at this stage, we are simply implementing a moving box, just like you saw in Chapter 3 (refer to Figure 3.4). You may recall from the previous chapter that the logic to move the box, and the box rendering code itself, were all held within the render method. This has now been split up inside GameScene.
The updateSceneWithDelta method is called a variable number of times within the game loop. Within that method, we have defined the transY ivar, which increases within the updateSceneWithDelta method:
- (void)updateSceneWithDelta:(float)aDelta { transY += 0.075f; } When the updating has finished, the game loop will render the scene. This render request is passed to the GameController, which then asks the currently active scene to render. That request ends with the next method, renderScene.
The renderScene method is where the code to actually render something to the screen is held. As mentioned earlier, we are just mimicking the moving box example from the previous chapter, so the first declaration within this method is to set up the vertices for the box:
static const GLfloat squareVertices[] = { 50, 50, 250, 50, 50, 250, 250, 250, };Note - Do you notice anything different between the data used in this declaration and the one used in the previous project? Don't worry if you can't spot it; it's not immediately obvious.This behavior uses the OpenGL ES configuration we defined earlier in the initOpenGL method (located inside the ES1Renderer class). We configured the orthographic projection and view port. This now causes OpenGL ES to render using pixel coordinates, rather than defining the vertices for the square.
The vertex positions in the previous example were defined using values that ranged from -1.0 to 1.0. This time, the values are much bigger.
Going forward, this will make our lives much easier, as we can more easily position items on the screen and work out how large they will be.
Having defined the vertices for the square, we can define the colors to be used within the square. This is exactly the same as in the previous example.
Next, we perform a translation that moves the point at which the square is rendered. As before, we are not changing the vertices of the square, but instead moving the drawing origin in relation to where the rendering takes place:
glTranslatef(0.0f, (GLfloat)(sinf(transY)/0.15f), 0.0f); Once the translation has finished, we point the OpenGL ES vertex pointer to the squareVertices array and the color pointer to the squareColors array:
glVertexPointer(2, GL_FLOAT, 0, squareVertices); glColorPointer(4, GL_UNSIGNED_BYTE, 0, squareColors); If you have a photographic memory, you may notice that there are a couple of lines missing from this section of code that were present in the previous example. When we last configured the vertex and color pointers, we enabled a couple of client states in OpenGL ES, which told OpenGL ES that we wanted it to use vertex and color arrays. There is no need to do that this time because we have already enabled those client states inside ES1Renderer when we performed the initOpenGL method. Remember that OpenGL ES is a state machine, and it remembers those settings until they have been explicitly changed.
Having pointed OpenGL ES at the vertices and colors, it's now time to render to the screen:
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); At this point, our lovely colored box is rendered, but it isn't on the screen just yet. When this method is finished, it passes control back to the GameController and then up to the render method in EAGLView, whose next task is to present the renderbuffer to the screen. It is at that point that you actually see the square in the display.
Summary
We have created a number of new classes that give us a structure on which we can build going forward. We now have the ability to create any number of different game scenes, each of which will perform its own logic and rendering, all being controlled by the GameController class.
We use this structure throughout this book and build upon it to create Sir Lamorak's Quest.
In the next chapter, we go more in-depth on how to render images to the screen. This involves looking at OpenGL ES in greater detail and creating a number of classes that make the creation, configuration, and rendering of images in Sir Lamorak's Quest much easier.
Exercises
If you run the project for this chapter, you see the colored box moving up and down the screen. If you want the project to do more, try making some changes to the project, such as the following:
- Create a new game scene called TriangleScene and change the rendering code so that it draws a triangle rather than a square.
- Hint - Rather than drawing two triangles that make a square, which GL_TRIANGLE_STRIP is for, you only need a single triangle; GL_TRIANGLES is great for that. Remember, a triangle only has three points, not four.
- After you create your new class, initialize it in the GameController initGame method and add it to the dictionary with a key.
- Hint - Don't forget to make your new scene the current scene.
If you get stuck, you can open the CH04_SLQTSOR_EXERCISE project file to see what you need to do.
Footnotes
- The game loop code used is based on a tutorial by Alex Diener at http://sacredsoftware.net/tutorials/Animation/TimeBasedAnimation.xhtml. SynthesizeSingleton; see http://cocoawithlove.com/2008/11/singletons-appdelegates-and-top-level.html.
(C) Copyright Pearson Education. All rights reserved.
Reprinted with permission