Press "Enter" to skip to content

Creating Levels in Pencil Adventure with Swift: An Excerpt from Learning Swift

admin

rocketborder3rev

There are many ways to create a level for your game. For example, you can make a custom level editor and store only the node type and position in each level. You can also consider using a third party level editor, such as the Tiled Map Editor, which is a great tool for tile mapping. The best part is that it is free! So check it out if you want to build a tile map game.

You can read the full source code of our Pencil Adventure game here on Github.

In our Pencil Adventure project, we use the hybrid way to create levels. Each level was designed in Xcode’s level editor with texture names and position definition in the game. The physics definition for each item was added to the sprite node when the game scene’s loaded.

swiftfig1

In addition, we defined the background items in the level editor for a parallax effect. We hope you’ve been following along with the code in the previous chapters. In this chapter, you will start with creating a new level for the game to make it fun. Now, let’s get started by using the level editor in Xcode.

Level editor

In our game, we used Xcode’s level editor to create each level. It’s easy to update the position of the objects and give you direct visual feedback in the level design. It is versatile enough to allow specifying a physical definition, such as collision category, physics body, and so on. The level will be saved as a binary file with a sks name extension. Let’s walk through the following few steps to create a level.

  • Create Sprite Kit scene file
  • Add SKSpriteNode to scene
  • Edit sprite property
  • Add More sprites to scene
  • Edit physics property through level editor
  • Edit physics property through game scene

Go ahead and open Xcode to continue with the Pencil Adventure project. First, create a new level in the project. Go to Xcode and create a file. When the file type dialog window shows up, select the Resources group under iOS from the left panel, then select SpriteKit Scene, and click Next.

swiftfig2

Save the scene file as 1.sks in the project. Note, sks is the file name extension for SpriteKit Scene file.

swiftfig3

Ok, you now have a blank scene in front of you to create a new level. Notice on Xcode’s right side, there is the SKNode inspector panel on the top right corner. That’s where you can edit the details about each Node’s property. For example, the texture, size, position:

swiftfig4

Go to the object library located at the bottom right and drag a SKSpriteNode to the scene in the center.

swiftfig5

Cool, you just added a shelf to the scene by simply dragging a SKSpriteNode to the scene. Update the texture name with the shelf image. You will notice the size will be automatically updated to the texture size. You can drag the object when it’s selected to change the position in the level.

swiftfig6

Now you can try to add a Sharpener and a box in the scene. Move these objects around to give Steve some space to jump forward. You can actually copy the selected sprite node and paste it in the scene. That may help when you plan to create a longer level.

swiftfig7

Now go ahead and design a level you like with the assets in the game. When you are done with this first level, run the game and play it!

Hmm, well, Steve runs but the game does not have a solid feedback when Steve runs into the power-up: the sharpener. Remember you defined the category for shelf and sharpener in the game scene? You need to define the physics body on each object individually. You can actually edit the physics body property in the level editor directly. Let’s take a look at how to edit the physics definitions next.

swiftfig8

As shown in the image above, in the physics definition section, you can choose the bounding rectangle as body type, and deselect the dynamic option to make the object static. Next, you can update the collision mask and category mask to allow Steve to have correct collisions with the shelf. Select a shelf in the scene and update the Category Mask to 2, the Collision Mask to 1 and leave the Field Mask to 0.(We don’t use Field Mask in the game.)

These steps seems fine if you plan to create a short level, but in Pencil Adventure it seems difficult to keep track of the physics definition inside the level editor for every object. Ideally, we would like to have a subclass of SKSpriteNode for power-up, so we can custom the physics defination inside the class. However, at the time of writing, Xcode’s level editor doesn’t support adding subclass SKSpriteNode yet. Let’s hope this feature will be available in the future release.

In our Pencil Adventure game, we actually use the level as the visual level design tool to layout the position for each sprite node in the game scene. Each object’s physics definition is added by code when the scene’s loaded in the game. In the next section, let’s add this code change to the GameScene.swift file.

Physics definition

You designed a level and added a shelf, sharpener and box in the level editor with the custom position, name and texture. Next, you can set the correct physics definition to each object when the level is loaded in the game scene. In this dynamic loading process, you will define sprite’s physics body based on the name defined in the level editor.

Go ahead and add the following code changes to the GameScene.swift file.

private func ensurePhysicsBody(sprite: SKSpriteNode, 
useTextureAlpha: Bool = true) -> Bool { //#1
     // Make sure our sprite has a physicsBody
     if sprite.physicsBody == .None {    //#2
           // First, try to create a physicsBody from the 
texture alpha
           if useTextureAlpha && sprite.texture != .None {
                sprite.physicsBody = SKPhysicsBody(texture: 
sprite.texture, alphaThreshold: 0.9, size: sprite.size)
                }
                // Next, try to create a physicsBody 
from the sprite's smallest
                // bounding rectangle
                if sprite.physicsBody == .None {
                      NSLog("*** Falling back to rectangle 
for sprite: \(sprite.name)")
                      sprite.physicsBody = SKPhysicsBody
(rectangleOfSize: sprite.frame.size)
                }
			
                // If we still don't have a physicsBody, 
just move on to the next one
                if sprite.physicsBody == nil {
                      NSLog("*** Falling back to no 
physicsBody for sprite: \(sprite.name)")
                      return false
                 }
                 // Default these to no collisions/contacts
                 sprite.physicsBody?.categoryBitMask = 0
                 sprite.physicsBody?.collisionBitMask = 0
                 sprite.physicsBody?.contactTestBitMask = 0
            }
            // Defaults for the physics body
            sprite.physicsBody?.dynamic = false
            return true
      }

First let’s take a look at #1 the definition of this function.

ensurePhysicsBody(sprite: SKSpriteNode, useTextureAlpha: Bool = true) -> Bool

This function takes two parameters: sprite and useTextureAlpha. -> Bool indicates there is a return value for this function and it’s a Bool value.

#2 has a new value in Swift: .None

That’s because physicsBody is an optional property in the sprite node class. When the optional value is not defined, in this example, it means that the object does not have a physics body defined.

sprite.physicsBody == .None

We are using this condition to make sure this code won’t impact any existing physicsBody definition in the sprite node.

In the above code listing, you created a function to define the physicsBody property for a given sprite node, and the return value is Bool. If the physics body is not defined, you will create the physics body from the bounding rectangle of the sprite node. By default, the object will be static and won’t move.

Ok, now with function ensurePhysicsBody(sprite: SKSpriteNode, useTextureAlpha: Bool = true) -> Bool defined, you can call it inside the sprites enumeration. The setupAccessories() function in the following code listing:

private func setupAccessories() {
   for child in self.children as [SKNode] { 	  //#1
      if var sprite = child as? SKSpriteNode {    //#2
         if sprite.name == .None {
            continue
         }
         sprite.zPosition = levelItemZPosition   //#3 
         if let spriteSpec = sprite.name {       //#3 
            var components = spriteSpec
.componentsSeparatedByCharactersInSet(NSCharacterSet
(charactersInString: "|"))
            if (components.count <= 1) {     //#4 
            if ensurePhysicsBody(sprite) {
              sprite.physicsBody?.categoryBitMask 
|= levelItemCategory
              sprite.physicsBody?.collisionBitMask 
|= heroCategory
            }
              continue
         }
         sprite.name = components[0]        //#5 
         components.removeAtIndex(0)        //#5 
         for accessory in components {		//#6 
           switch accessory      {  //#7
           case "background":       //#8
	     sprite.zPosition = BackgroundZPosition
	
	    case "finish":           //#9
	     if ensurePhysicsBody(sprite, 
useTextureAlpha: false) {
	     sprite.physicsBody?.categoryBitMask 
|= finishCategory
	     sprite.physicsBody?.collisionBitMask 
|= heroCategory
		       }
	    sprite.alpha = 0
							
            case "powerup":         //#10
             if ensurePhysicsBody(sprite) {
             sprite.physicsBody?.categoryBitMask 
|= powerupCategory
             sprite.physicsBody?.contactTestBitMask 
|= heroCategory
		 }

        case "death":           //#11
          if ensurePhysicsBody(sprite) {
	     sprite.physicsBody?.categoryBitMask 
|= deathtrapCategory
	     sprite.physicsBody?.collisionBitMask 
|= heroCategory
		}
							
        default:                //#12
	   if ensurePhysicsBody(sprite) {
	     sprite.physicsBody?.categoryBitMask 
|= levelItemCategory
	     sprite.physicsBody?.collisionBitMask 
|= heroCategory
            }
          }
        }
      }
    }
  }
}

As mentioned in the last section, the Xcode level editor doesn’t allow for any custom data to be added to a node or the subclass SKSpriteNode. So we’ve used the name field to add additional information in the form of a sprite specification. We format the name with a special character | to separate node name and the type: (sprite node name)|(accessory type). Therefore, a sprite named picture|background would represent a sprite named picture that is a background accessory type. We then remove the accessory type from the name and setup that accessory type appropriately.

You started for-loop in #1 to go through every node in the scene and defined #2 if condition to make sure the current child is a spirte node.

In #3 you set the sprite’s zPosition and check the sprite name for additional information if the accessory type is defined with |(accessory type).

Move on to #4, if there’s no accessory defined in the sprite specification, you can assume that’s just a normal level item. Next you add the physics body definition to the sprite by calling the function defined earlier and continue with the loop.

Next in #5, you replaced the sprite’s name with the accessory type in the sprite specification so you can apply the appropriate properties to this sprite in #6 in the for-in loop.

Ok now let’s spend a bit time looking at the switch case loop in #7. You’ve defined categoryBitMask and collisionBitMask in Chapter 5 for the game scene:

let heroCategory: UInt32 = 1 << 0
let groundCategory: UInt32 = 1 << 1
let levelCategory: UInt32 = 1 << 2
let powerupCategory: UInt32 = 1 << 3
let deathtrapCategory: UInt32 = 1 << 4
let finishCategory: UInt32 = 1 << 5

You may notice deathtrapCategory is new here. We added this object to the game as TNT boxes in the indoor world and propane tanks in the outdoor world. When Mr Steve runs into the deathtrap objects, the game is over.

Now you can assign the correct categoryBitMask and collisionBitMask when a matching sprite type’s found. For example, #8 handles the background objects. All of the objects in the background will be further away and they have no physics body in the game scene, so you simply assign the sprite’s zPosition as BackgroundZPosition. The BackgroundZPosition is defined as a constant in the game scene:

let BackgroundZPosition: CGFloat = -10

This will add more depth to the game visual effect. In the next section, we will spend more time covering scrolling with parallax effect.

#9 handles the finish line. In the level editor, the finish line’s created with a name box|finish. The finish line is a tall skinny rectangle box with categoryBitMask equals to finishCategory, and collisionBitMask equals to heroCategory. We want to hide this object so the alpha’s 0.

#10 handles the power up. Similar to the finish line, power up is added with a name sharpener|powerup in the level editor, so we want to assign the correct categoryBitMask and collisionBitMask to the sprite.

#11 handles the death trap, the sprite node name is “box|death” in the level editor.

#12 takes care of the rest of level items, the brick wall, the stone, the shelf, the stool and etc.They all share the same categoryBitMask.

Now let’s call the function setupAccessories() when the game scene’s loaded.

        public override func didMoveToView(view: SKView) {
	super.didMoveToView(view)
        physicsWorld.gravity = CGVector(dx: 0.0, dy: -9.8)
        physicsWorld.contactDelegate = self
        // setup physics defination on every sprite
	setupAccessories()
		...

That’s a long list of code changes. Now with the physics defined on the objects in the level, you can play the game and take a break from all the hard work you’ve put into this project, taking an adventure with Mr Steve.

Create more levels

Now that you have the tools to make a level, give it a try and create some interesting and challenging levels. You may also fork this project on github and share your levels with all of the readers. https://github.com/jocelynlih/SwiftGameBook

flameborder_learningswift