Keeping Up to Date

Sprite Kit games are different from traditional iOS or OS X apps in that everything you see onscreen is being continually redrawn many times a second. From the developer’s perspective, the work of updating and displaying a scene’s nodes, such as sprites or particle emitters, is mostly carried out automatically by Sprite Kit. When you’ve added a sprite into the scene, it continues to be redrawn until you remove it. Similarly, particle emitters continue to emit particles until you explicitly pause them or remove them from the scene.

To turn a static scene into something more interesting, you create and run actions on sprites. The physics simulation takes care of interactions (such as collisions) between physics bodies, and prevents sprites from overlapping. For each frame, Sprite Kit automatically updates each node’s position for changes triggered by actions or after applying the results of the physics simulation.

In Adventure, we also make programmatic adjustments to the nodes in a scene. For example, we move the hero in response to user input, and we move the camera such that the character is always visible. Whenever we move the camera, we must also adjust the layer offsets of parallax sprites. All of this programmatic update logic occurs as part of Sprite Kit’s update loop.

The Update Loop

For each frame, Sprite Kit runs through an update loop that has six key phases, as shown in Figure 4-1.

Figure 4-1  The update loop

Within these phases, there are three opportunities to make programmatic adjustments before Sprite Kit renders the frame:

  1. The start of the loop is the update: method, which is called on the scene before evaluating any actions or simulating physics.

  2. The didEvaluateActions method is called on the scene after Sprite Kit has evaluated any outstanding actions on nodes but before looking at implications from the physics simulation.

  3. The didSimulatePhysics method is called on the scene after Sprite Kit has made any adjustments from the physics simulation. This is the last overridable method call before the view renders the scene and the loop continues again.

In Adventure, we tie into this loop in two places:

  1. We implement update: in APAMultiplayerLayeredCharacterScene to move the hero based on user input. The update: method in turn calls updateWithTimeSinceLastUpdate:, which is implemented by APAAdventureScene to update the individual characters in the scene, as well as the artificial intelligence of all the enemies.

    User input and artificial intelligence are described in “Controlling the Characters.”

  2. We implement didSimulatePhysics in APAMultiplayerLayeredCharacterScene to move the camera, if necessary, based on the hero position. The APAAdventureScene class overrides this method to hide or show particle emitters based on hero position, then it updates the relative positions of the different image layers that make up parallax trees and caves.

We could have dealt with camera movement, particle emitters, and parallax updates during the initial update: phase, but each of these tasks is tied to the position of the hero. During the update: stage, a Sprite Kit node isn’t necessarily at its final position for the current frame—it could move because of actions, or because of side effects from the physics simulation. For example, if the hero was involved in a collision with another character or a wall, the hero position would change between update: and final rendering.

By delaying position-dependent work until didSimulatePhysics, we guarantee that the hero position is final for the current frame.

Moving the Camera

The Adventure scene is very large—the full background is 4096 x 4096—so only a small portion of the scene is visible at any time. The term camera relates to the concept of the visible area of the scene; there’s no explicit Sprite Kit camera object involved, but you’ll often encounter references to “moving the camera.”

In Adventure all world-related nodes, including background tiles, characters, and foliage, are children of a world node, which in turn is a child of the scene. We change the position of this top-of-tree world node within the scene to give the effect of moving a camera across the level. By contrast, the nodes that make up the HUD are children of a separate node that is a direct child of the scene rather than of the world node, so that the elements in the HUD don’t move when we “move the camera.”

We could have chosen to adjust the world node position so that the hero always appears to be at the center of the visible portion of the scene, which would mean moving the camera whenever the hero moved. Instead, we decided to define a rectangular area in which the hero could move, and to move the camera only if the hero strayed outside those bounds, as shown in Figure 4-2.

Figure 4-2  Camera edge insets

We use the didSimulatePhysics method in APAMultiplayerLayeredCharacterScene to change the position of the world node only if the hero is within kMinHeroToEdgeDistance (256) points of the edge of the current visible area.

  1. Adventure: APAMultiplayerLayeredCharacterScene.m -didSimulatePhysics
  2. - (void)didSimulatePhysics {
  3. APAHeroCharacter *defaultHero = self.defaultPlayer.hero;
  4. if (defaultHero) {
  5. CGPoint heroPosition = defaultHero.position;
  6. CGPoint worldPos = self.world.position;
  7. CGFloat yCoordinate = worldPos.y + heroPosition.y;
  8. if (yCoordinate < kMinHeroToEdgeDistance) {
  9. worldPos.y = worldPos.y - yCoordinate + kMinHeroToEdgeDistance;
  10. self.worldMovedForUpdate = YES;
  11. } else if (yCoordinate > (self.frame.size.height - kMinHeroToEdgeDistance)) {
  12. worldPos.y = worldPos.y + self.frame.size.height - yCoordinate - kMinHeroToEdgeDistance;
  13. self.worldMovedForUpdate = YES;
  14. }
  15. ... (Repeat for horizontal axis)
  16. self.world.position = worldPos;
  17. }
  18. [self performSelector:@selector(clearWorldMoved) withObject:nil afterDelay:0.0f];
  19. }
3

If there are multiple players, the camera always follows the default player’s hero.

18

Using performSelector:withObject:afterDelay: with the special delay value of 0.0 means that the selector call occurs after the current pass through the run loop. Using this value ensures that the subclass implementation of didSimulatePhysics will occur before the worldMovedForUpdate property is reset to NO.

We set the worldMovedForUpdate property to YES whenever the camera is moved. The APAAdventure implementation of didSimulatePhysics checks worldMovedForUpdate to determine whether we need to update parallax sprites, or remove any particle emitters that are no longer visible.

Creating the Parallax Effect

Adventure is created from two-dimensional graphical elements. To give the illusion of a three-dimensional world, we built some of the sprites (trees and goblin caves) from multiple image layers; when the camera moves to follow the hero, we adjust the positions of these layers at different rates to simulate depth using a technique known as parallax scrolling.

To understand parallax using a realworld example, imagine that you are a passenger in a moving car, looking through the windows at the buildings and trees alongside the road. As the car moves, the trees closest to you appear to travel through your field of vision much faster than do buildings in the far distance, as shown in Figure 4-3.

Figure 4-3  Real-world parallax

We mimic this effect in Adventure by moving the top layers of a tree or goblin cave a greater distance relative to the center of the camera than the lower layers. Parallax support is provided by the APAParallaxSprite class, from which all characters and trees inherit. Most of the character subclasses set the usesParallaxEffect to NO, but the APATree and APACave classes both set the property to YES.

Whenever we initialize a sprite for parallax, we use the designated initializer for APAParallaxSprite, initWithSprites:usingOffset:, which takes an array of sprite nodes and adds them as children of the container sprite at decreasing z-positions.

  1. Adventure: APAParallaxSprite.m -initWithSprites:usingOffset:
  2. - (id)initWithSprites:(NSArray *)sprites usingOffset:(CGFloat)offset {
  3. self = [super init];
  4. if (self) {
  5. _usesParallaxEffect = YES;
  6. CGFloat zOffset = 1.0f / (CGFloat)[sprites count];
  7. CGFloat ourZPosition = self.zPosition;
  8. NSUInteger childNumber = 0;
  9. for (SKNode *node in sprites) {
  10. node.zPosition = ourZPosition + (zOffset + (zOffset * childNumber));
  11. [self addChild:node];
  12. childNumber++;
  13. }
  14. _parallaxOffset = offset;
  15. }
  16. return self;
  17. }
10

The higher the z-position value, the earlier in the drawing order the sprite is drawn.

We create shared APATree instances when loading assets in APAAdventureScene, and supply the child sprite nodes for the different layers.

  1. Adventure: APAAdventureScene.m +loadSceneAssets
  2. + (void)loadSceneAssets {
  3. SKTextureAtlas *atlas = [SKTextureAtlas atlasNamed:@"Environment"];
  4. ...
  5. sSharedSmallTree = [[APATree alloc] initWithSprites:@[
  6. [SKSpriteNode spriteNodeWithTexture: [atlas textureNamed:@"small_tree_base.png"]],
  7. [SKSpriteNode spriteNodeWithTexture: [atlas textureNamed:@"small_tree_middle.png"]],
  8. [SKSpriteNode spriteNodeWithTexture: [atlas textureNamed:@"small_tree_top.png"]]]
  9. usingOffset:25.0f];
  10. ...
  11. }

We initialize the goblin caves using the APACave designated initializer, initAtPosition:, which initializes the cave in the same way, using child sprites with the cave_base and cave_top textures.

Each time we add a parallax sprite to the world in APAAdventureScene, we add it to an array of parallaxSprites.

Updating Parallax Offsets

We update the offsets of parallax sprites each time through the update loop. Because the offset changes based on the camera position, and because the camera position depends on the hero position, the APAAdventureScene class updates parallax offsets in didSimulatePhysics, after the APAMultiplayerLayeredCharacterScene class implementation of the method has adjusted the world position.

  1. Adventure: APAAdventureScene.m -didSimulatePhysics
  2. - (void)didSimulatePhysics {
  3. [super didSimulatePhysics];
  4. ... (Get the position of the default hero, or hero spawn point)
  5. if (!self.worldMovedForUpdate) {
  6. return;
  7. }
  8. ...
  9. for (APAParallaxSprite *sprite in self.parallaxSprites) {
  10. if (APADistanceBetweenPoints(sprite.position, position) >= 1024) {
  11. continue;
  12. }
  13. [sprite updateOffset];
  14. }
  15. }
5

We need to update offsets only if the world node has moved.

10

We need to update only the offsets of sprites that are currently visible, or soon to be visible. Therefore we check the distance between the sprite and the hero; if no hero is visible, we use the hero spawn point—the starting point in the level.

The updateOffset method is implemented by the APAParallaxSprite class to adjust the offsets of each child sprite node relative to the center of the screen.

  1. Adventure: APAParallaxSprite.m -updateOffset
  2. - (void)updateOffset {
  3. SKScene *scene = self.scene;
  4. SKNode *parent = self.parent;
  5. ... (Return early if parallax is disabled)
  6. CGPoint scenePos = [scene convertPoint:self.position fromNode:parent];
  7. CGFloat offsetX = (-1.0f + (2.0 * (scenePos.x / scene.size.width)));
  8. CGFloat offsetY = (-1.0f + (2.0 * (scenePos.y / scene.size.height)));
  9. CGFloat delta = self.parallaxOffset / (CGFloat)self.children.count;
  10. int childNumber = 0;
  11. for (SKNode *node in self.children) {
  12. node.position = CGPointMake(offsetX*delta*childNumber, offsetY*delta*childNumber);
  13. childNumber++;
  14. }
  15. }
8

We bias the offset directions to the (–1.0, 1.0) range relative to the center of the screen.

14

Each child node is offset by an increasing amount; the top child is therefore offset the most.

Keeping the Game Running Smoothly

To keep everything looking smooth to the user, the optimal redraw rate is 60 frames per second. This means that the entire update loop for each frame must take less than 16.7 milliseconds. In addition to optimizing the code that we wrote for update: and didSimulatePhysics, we adopted various strategies to try to minimize the amount of time it takes Sprite Kit to update everything in the scene.

Emitters are one of the most performance-intensive aspects of a Sprite Kit game, so we used the Xcode particle emitter editor to minimize the number of particles onscreen at any time while still achieving the desired effect. Each time through the update loop, we also pause any emitters that aren’t visible:

  1. Adventure: APAAdventureScene.m -didSimulatePhysics
  2. - (void)didSimulatePhysics {
  3. [super didSimluatePhysics];
  4. ...
  5. if (!self.worldMovedForUpdate) {
  6. return;
  7. }
  8. for (SKEmitterNode *particles in self.particleSystems) {
  9. BOOL particlesAreVisible = APADistanceBetweenPoints(particles.position, position) < 1024;
  10. if (!particlesAreVisible && !particles.paused) {
  11. particles.paused = YES;
  12. } else if (particlesAreVisible && particles.paused) {
  13. particles.paused = NO;
  14. }
  15. }
  16. ...
  17. }
5

Much of the work in didSimulatePhysics needs to be done only if the camera moved, so we check the worldMovedForUpdate property (set by APAMultiplayerLayeredCharacterScene) and return early if the world node didn’t move.