Building the World

There’s only one level in Adventure, but it’s filled with a variety of sprites, including trees, caves that spawn goblins, burning torches, and a boss character. At app launch, we load all the shared resources needed by the scene, and then build the world by adding in the background tiles, sprites, and collision walls. Figure 3-1 shows an outline of this process.

Figure 3-1  Building the world

As soon as the assets are loaded, we create an instance of the scene. During scene initialization, we create and set initial positions for all the nodes needed for the level. Finally, when initialization is complete, we add the scene into an SKView, at which point the view renders the content to the screen.

Adventure Loads Assets Asynchronously

There are a large number of texture atlas and particle emitter files used by the Adventure scene. Rather than load all these assets each time they’re needed, or even lazily just when they’re first needed, Adventure uses an asynchronous loading mechanism to load everything once, when the game launches. This avoids problems during gameplay, such as interruptions or dropped frames caused by reading files from disk.

Creating the Scene

For the OS X target, the APAAppDelegateOSX object is responsible for loading the scene at launch. In the iOS version, this is the responsibility of the APAViewController object’s viewWillAppear: method, but the code is essentially the same.

We load the assets asynchronously; this means we can display the game logo and a spinning progress indicator while the files load from disk. When loading is complete, we use a completion handler to create an instance of the scene and add it to the SKView instance in the app’s user interface.

  1. Adventure: APAAppDelegateOSX.m -applicationDidFinishLaunching:
  2. - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
  3. ... (Show the progress indicator)
  4. [APAAdventureScene loadSceneAssetsWithCompletionHandler:^{
  5. APAAdventureScene *scene = [APAAdventureScene sceneWithSize:CGSizeMake(1024, 768)];
  6. [self.skView presentScene:scene];
  7. ... (Hide the progress indicator)
  8. ... (Show the Warrior/Archer buttons)
  9. [scene configureGameControllers];
  10. }];
  11. ... (Show debug info)
  12. }
9

The completion handler calls configureGameControllers to check for any configured game controller devices, as described in “Supporting External Game Controllers.”

Loading Scene Assets

The loadSceneAssetsWithCompletionHandler: method is implemented by the APAMultiplayerLayeredCharacterSceneclass. It uses dispatch_async() to call the loadSceneAssets method in the background; when asset loading is complete, it calls the completion handler back on the main thread.

The loadSceneAssets method is overridden by APAAdventureScene to load scene-specific assets, including preconfigured particle emitters.

  1. Adventure: APAAdventureScene.m +loadSceneAssets
  2. + (void)loadSceneAssets {
  3. sSharedProjectileSparkEmitter = [SKEmitterNode apa_emitterNodeWithEmitterNamed:@"ProjectileSplat"];
  4. ... (Load other emitters and sprites)
  5. [self loadWorldTiles];
  6. [APACave loadSharedAssets];
  7. ... (Load other characters' assets)
  8. }
3

Each emitter node is loaded using apa_emitterNodeWithEmitterNamed:, a helper method implemented in a category on SKEmitterNode to read in the node from an archived file on disk.

After loading the particle emitters, the method calls loadWorldTiles. The scene has a very large, square background (4096 x 4096 pixels). Rather than ask the GPU to load this entire image when we only ever need to draw a small portion of the background, we divided the texture into a grid of 32 x 32 tiles, as shown in Figure 3-2, resulting in 1024 individual tiles.

Figure 3-2  Dividing the background image

We could have kept these tiles as separate files, but this would have meant loading 1024 files from disk, and resulted in a large number of GPU render calls as each tile would have had to be drawn separately. Instead, we store the images in a Tiles.atlas folder in the project. At compile time, Xcode uses this folder to build a texture atlas, splitting the 1024 tiles across 5 images, shown in Figure 3-3. As well as minimizing the number of files we have to load, this also means that the visible portion of the background is drawn with only one or two GPU render calls.

Figure 3-3  The five background tiles texture atlas images

The loadWorldTiles method creates an SKSpriteNode instance for each image and sets the relative position so that the tiles are laid out correctly when added to the world later, during scene initialization. After the tiles are loaded, the loadSharedSceneAssets method calls the loadSharedAssets method on each character class used by the scene.

Loading Shared Character Assets

Most of the character classes in Adventure are instantiated multiple times within the scene—there are lots of goblins and caves, for example, and as many heroes as there are players. To minimize the memory footprint, each class of characters shares a set of assets. For Adventure, we assumed that these characters would be used by any additional scenes that you might add to the game, so the assets are loaded once and stay in memory as long as the game is running.

Each character class’s loadSharedAssets method uses a dispatch_once block to ensure that the code is executed only once. The APABoss class implements this method to load the animation frames used to animate the level boss when idle, walking, attacking, getting hit, or dying; it also loads an Xcode-created particle emitter and flash colorize action to show damage when a hero projectile hits the boss.

  1. Adventure: APABoss.m +loadSharedAssets
  2. + (void)loadSharedAssets {
  3. [super loadSharedAssets];
  4. static dispatch_once_t onceToken;
  5. dispatch_once(&onceToken, ^{
  6. sSharedIdleAnimationFrames = APALoadFramesFromAtlas(@"Boss_Idle", @"boss_idle", kBossIdleFrames);
  7. ... (Load other animation frames)
  8. sSharedDamageEmitter = [SKEmitterNode apa_emitterNodeWithEmitterNamed:@"BossDamage"];
  9. sSharedDamageAction = [SKAction sequence:@[
  10. [SKAction colorizeWithColor:[SKColor whiteColor] colorBlendFactor:1.0 duration:0.0],
  11. [SKAction waitForDuration:0.5],
  12. [SKAction colorizeWithColorBlendFactor:0.0 duration:0.1]]];
  13. }
  14. }
7

The APALoadFramesFromAtlas() function is defined in APAGraphicsUtilities.m. It loads a texture atlas from disk, extracts the named and numbered textures, and returns an array of SKTexture instances.

Most shared assets in Adventure classes are stored in static variables, with a simple accessor method to return the value.

  1. Adventure: APABoss.m -damageEmitter
  2. static SKEmitterNode *sSharedDamageEmitter = nil;
  3. - (SKEmitterNode *)damageEmitter {
  4. return sSharedDamageEmitter;
  5. }
2

A static variable at file scope means that the same variable is shared between all instances of the class, but cannot be accessed outside this file.

After the asset is loaded, it stays in memory for use by any future instance of the class.

The other character classes implement the loadSharedAssets method in much the same way as APABoss to load their animation frames, emitters, and shared actions.

Building the Scene

After all the assets are loaded, the app delegate creates an instance of the APAAdventureScene class. This class inherits from APAMultiplayerLayeredCharacterScene, which is responsible for configuring the environment of any scene within Adventure.

If you look in the initWithSize: method for APAMultiplayerLayeredCharacterScene, you’ll see that it initializes an array to keep track of the players.

  1. Adventure: APAMultiplayerLayeredCharacterScene.m -initWithSize:
  2. - (instancetype)initWithSize:(CGSize)size {
  3. self = [super initWithSize:size];
  4. if (self) {
  5. _players = [[NSMutableArray alloc] initWithCapacity:kNumPlayers];
  6. _defaultPlayer = [[APAPlayer alloc] init];
  7. [(NSMutableArray *)_players addObject:_defaultPlayer];
  8. for (int i = 1; i < kNumPlayers; i++) {
  9. [(NSMutableArray *)_players addObject:[NSNull null]];
  10. }
6

One APAPlayer instance is created to represent the defaultPlayer.

9

The NSNull instances indicate spaces for future players who might join.

10

Adventure supports up to 4 players (kNumPlayers is 4), so the array ends up with one APAPlayer and three NSNull instances.

Next, the method builds a skeleton node tree for the scene. This tree represents the basic layers within the world. Each layer has a different zPosition, which affects the drawing order of its nodes within the scene. The different layers are described by the APAWorldLayer enum.

  1. Adventure: APAMultiplayerLayeredCharacterScene.h
  2. typedef enum : uint8_t {
  3. APAWorldLayerGround = 0,
  4. APAWorldLayerBelowCharacter,
  5. APAWorldLayerCharacter,
  6. APAWorldLayerAboveCharacter,
  7. APAWorldLayerTop,
  8. kWorldLayerCount
  9. } APAWorldLayer;

Every time a node is added to the scene, either while building the world or later in the game, the node is added into a specific layer using the addNode:atWorldLayer: method. This ensures that the background tiles are always the first to be drawn, followed by anything that should be above ground but below the characters, followed by the characters, and so on.

Because every layer within the scene is contained within the top-of-tree world node, the initWithSize: method next creates an array of world layers and adds them as children of the world node, and then adds the world as a child of the scene.

  1. Adventure: APAMultiplayerLayeredCharacterScene.m -initWithSize:
  2. _world = [[SKNode alloc] init];
  3. [_world setName:@"world"];
  4. _layers = [NSMutableArray arrayWithCapacity:kWorldLayerCount];
  5. for (int i = 0; i < kWorldLayerCount; i++) {
  6. SKNode *layer = [[SKNode alloc] init];
  7. layer.zPosition = i - kWorldLayerCount;
  8. [_world addChild:layer];
  9. [(NSMutableArray *)_layers addObject:layer];
  10. }
  11. [self addChild:_world];

The final part of the initWithSize: method sets up the Heads-Up-Display and activates the portion of the HUD for the default player. The HUD nodes are added into a separate child of the scene, outside the world node. This means that when we change the position of the world node so that the camera follows the hero, the HUD nodes remain static at the top of the view.

Constructing the Adventure Scene

After APAMultiplayerLayeredCharacterScene is configured, initialization falls through to APAAdventureScene.

The APAdventureScene class’s initWithSize: method sets up the various arrays used to track different elements within the scene, and then creates the level and tree map data structures.

  1. Adventure: APAAdventureScene.m -initWithSize:
  2. - (id)initWithSize:(CGSize)size {
  3. self = [super initWithSize:size];
  4. if (self) {
  5. _heroes = [[NSMutableArray alloc] init];
  6. ... (Create the other arrays)
  7. _levelMap = APACreateDataMap(@"map_collision.png");
  8. _treeMap = APACreateDataMap(@"map_foliage.png");

The level and tree maps are used later, when we start adding sprites into the world. The initWithSize: method continues by calling buildWorld to start the world building process. This method then uses the centerWorldOnPosition: method to set the position of the world so that the camera is centered on the spawn point where the hero will appear when the user starts the game.

  1. Adventure: APAAdventureScene.m -initWithSize:
  2. [self buildWorld];
  3. CGPoint startPosition = self.defaultSpawnPoint;
  4. [self centerWorldOnPosition:startPosition];
  5. }
  6. return self;
  7. }

Building the World

The buildWorld method configures basic physics simulation settings, and then adds sprites and sets the start positions for characters within the level:

  1. Adventure: APAAdventureScene.m -buildWorld
  2. - (void)buildWorld {
  3. self.physicsWorld.gravity = CGVectorMake(0.0f, 0.0f);
  4. self.physicsWorld.contactDelegate = self;
  5. [self addBackgroundTiles];
  6. [self addSpawnPoints];
  7. [self addTrees];
  8. [self addCollisionWalls];
  9. }
3

The Sprite Kit physics engine simulates gravity for games where, for example, characters run and jump over obstacles. But, Adventure is a game where you look vertically down from above, so there’s no need for gravity. We set the gravity vector to {0.0f, 0.0f} to indicate no gravity.

4

Adventure does use the Sprite Kit physics engine to handle collisions automatically so that, for example, a character can’t walk through a wall, or walk over the top of cave. We set the contactDelegate to the scene to receive callbacks when certain collisions occur; these are covered in detail in “Handling Collisions.”

The addBackgroundTiles method just walks through the preloaded tile nodes and adds them to the world; the rest of the methods are discussed in the sections that follow.

Reading the Level Map

To make it easy to configure starting positions for the different characters in the game, we created a level map, shown in Figure 3-4. This map is a .png file that uses four pixel colors to indicate different items in the game:

  • A transparent pixel represents the location of the level boss.

  • A red pixel indicates a wall.

  • A green pixel indicates a goblin cave.

  • A blue pixel indicates the starting location of the hero.

This strategy makes it easy for a level designer to focus on game artwork without having to worry about issues such as exact screen dimensions.

Figure 3-4  The level data map

The addSpawnPoints method uses the _levelMap that was loaded earlier by calling the APACreateDataMap() function from initWithSize:.

APACreateDataMap() is defined in APAGraphicsUtilities.m. It loads and draws the image into an ARGB bitmap context. If you trace through the APACreateDataMap() function, you’ll see that it calls APACreateARGBBitmapContext(), which is also defined in APAGraphicsUtilities.m, to create a context that has 8 bits per component.

  1. Adventure: APAGraphicsUtilities.m APACreateARGBBitmapContext()
  2. context = CGBitmapContextCreate(bitmapData,
  3. pixelsWide,
  4. pixelsHigh,
  5. 8, // bits per component
  6. bitmapBytesPerRow,
  7. colorSpace,
  8. (CGBitmapInfo)kCGImageAlphaPremultipliedFirst);

If you look in APAGraphicsUtilities.h, you’ll see that the APADataMap structure is defined using four uint8_t fields:

  1. Adventure: APAGraphicsUtilities.h
  2. typedef struct {
  3. uint8_t bossLocation, wall, goblinCaveLocation, heroSpawnLocation;
  4. } APADataMap;

The “4 x 8-bits per pixel” data in the ARGB context translate directly into the four fields in the APADataMap structure.

The APACreateDataMap() function just returns a pointer to raw data, but the APAAdventureScene class accesses that data by treating it as a C array of APADataMap structures. This means that the code is easier to read because we’re referring to .goblinCaveLocation rather than looking at raw bytes for “the green channel.”

To translate pixels in the level map into scene coordinates, the addSpawnPoints method cycles through the APADataMap structures using nested for loops. Each pass through the loop checks a pixel’s APADataMap to see whether anything should be created at the current location.

  1. Adventure: APAAdventureScene.m -addSpawnPoints
  2. - (void)addSpawnPoints {
  3. for (int y = 0; y < kLevelMapSize; y++) {
  4. for (int x = 0; x < kLevelMapSize; x++) {
  5. CGPoint location = CGPointMake(x, y);
  6. APADataMap spot = [self queryLevelMap:location];
  7. CGPoint worldPoint = [self convertLevelMapPointToWorldPoint:location];
  8. if (spot.boss <= 200) {
  9. ... (Create a boss at this location)
  10. }
  11. if (spot.goblinCaveLocation >= 200) {
  12. ... (Create a goblin cave at this location)
  13. }
  14. if (spot.heroSpawnLocation >= 200) {
  15. ... (Set the hero spawn point)
  16. }
  17. }
  18. }
  19. }
3

The kLevelMapSize constant is 256—the height and width of the level map image. Each of these pixels translates into a coordinate within the scene, through a series of helper methods that convert between the different coordinate systems.

9

The boss spawn location is represented by the alpha channel. If you look very closely at the map in Figure 3-4, you’ll see one transparent pixel at the end of the maze, which is why the code checks whether the alpha value is less than 200.

12

Goblin caves are represented by green pixels in the collision map. Any pixel with a green color value of 200 or more translates into an APACave instance in the game.

15

The hero spawn location is represented by a value greater than 200 in the blue channel of the pixel.

18

(The red channel is used to indicate the walls of the maze, as described in “Adding the Collision Walls”)

Reading the Tree Map

The locations of the trees in the scene are specified in a foliage map, shown in Figure 3-5.

Figure 3-5  The foliage data map

This map is again translated into data using the APACreateDataMap() function, but this time APAAdventureScene interprets the data as an array of APATreeMap structures.

In the foliage map, the alpha and blue channels are unused, but the red and green channels are used as follows:

  • The red channel indicates a small tree.

  • The green channel indicates a large tree.

  1. Adventure: APAGraphicsUtilities.h
  2. typedef struct {
  3. uint8_t unusedA, bigTreeLocation, smallTreeLocation, unusedB;
  4. } APATreeMap;

Trees in Adventure are built from multiple image layers, which move at different rates to exhibit a parallax effect as the camera moves across the world. This effect is described in more detail in “Creating the Parallax Effect.”

Adding the Collision Walls

The addCollisionWalls method uses the data encoded in the red channel of each pixel in the level map to determine where to place the walls in the maze. The code in this method is rather dense, and so you might find it somewhat unintuitive if you’re not used to raw C bit manipulation.

  1. Adventure: APAAdventureScene.m -addCollisionWalls
  2. - (void)addCollisionWalls {
  3. unsigned char *filled = alloca(kLevelMapSize * kLevelMapSize);
  4. memset(filled, 0, kLevelMapSize * kLevelMapSize);
  5. ...
3

The alloca() call allocates a block of memory large enough to hold the pixels in the image map.

4

This block is then filled with zeros.

The addCollisionWalls method continues by doing what looks like the same thing twice—there are two long blocks of code involving nested for loops. The first of these loops is responsible for creating horizontal walls; the second handles the vertical walls.

Each loop walks through all the pixels in the collision map, looking for groups of consecutive pixels whose wall (red) channel value is greater than 200. Each consecutive block is then turned into a collision wall using the addCollisionWallAtWorldPoint:withWidth:height: method, which adds a basic sprite with configured, rectangular physics body.

  1. Adventure: APAAdventureScene.m -addCollisionWallAtWorldPoint:withWidth:height:
  2. - (void)addCollisionWallAtWorldPoint:(CGPoint)worldPoint withWidth:(CGFloat)width height:(CGFloat)height {
  3. CGRect rect = CGRectMake(0, 0, width, height);
  4. SKNode *wallNode = [SKNode node];
  5. wallNode.position = CGPointMake(worldPoint.x + rect.size.width * 0.5, worldPoint.y - rect.size.height * 0.5);
  6. wallNode.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:rect.size];
  7. wallNode.physicsBody.dynamic = NO;
  8. wallNode.physicsBody.categoryBitMask = APAColliderTypeWall;
  9. [self addNode:wallNode atWorldLayer:APAWorldLayerGround];
  10. }
3

The physics body is a simple, nondynamic, rectangular body. When other sprites in the game collide with the wall, they are automatically prevented from trespassing inside the rectangular area, giving the illusion of walking through a maze of walls.

9

The categoryBitMask for a wall is APAColliderTypeWall. We use this value to set whether physics bodies of one type collide with physics bodies of another type; collisions are covered in more detail in “Handling Collisions.”

As soon as the walls are added, the world is complete!