Game Development with Swift
上QQ阅读APP看书,第一时间看更新

Laying the foundation

So far, we have learned through small bits of code, inpidually added to the GameScene class. The intricacy of our application is about to increase. To build a complex game world, we will need to construct re-usable classes and actively organize our new code.

Following protocol

To start, we want inpidual classes for each of our game objects (a bee class, a player penguin class, a power-up class, and so on). Furthermore, we want all of our game object classes to share a consistent set of properties and methods. We can enforce this commonality by creating a protocol, or a blueprint for our game classes. The protocol does not provide any functionality on its own, but each class that adopts the protocol must follow its specifications exactly before Xcode can compile the project. Protocols are very similar to interfaces, if you are from a Java or C# background.

Add a new file to your project (right-click in the project navigator and choose New File, then Swift File) and name it GameSprite.swift. Then add the following code to your new file:

import SpriteKit

protocol GameSprite {
    var textureAtlas: SKTextureAtlas { get set }
    func spawn(parentNode: SKNode, position: CGPoint, size: 
        CGSize)
    func onTap()
}

Now, any class that adopts the GameSprite protocol must implement a textureAtlas property, a spawn function, and an onTap function. We can safely assume that the game objects provide these implementations when we work with them in our code.

Reinventing the bee

Our old bee is working wonderfully, but we want to spawn many bees throughout the world. We will create a Bee class, inheriting from SKSpriteNode, so we can cleanly stamp as many bees to the world as we please.

It is a common convention to separate each class into its own file. Add a new Swift file to your project and name it Bee.swift. Then, add this code:

import SpriteKit

// Create the new class Bee, inheriting from SKSpriteNode
// and adopting the GameSprite protocol:
class Bee: SKSpriteNode, GameSprite {
    // We will store our texture atlas and bee animations as
    // class wide properties.
    var textureAtlas:SKTextureAtlas = 
        SKTextureAtlas(named:"bee.atlas")
    var flyAnimation = SKAction()

    // The spawn function will be used to place the bee into
    // the world. Note how we set a default value for the size
    // parameter, since we already know the size of a bee
    func spawn(parentNode:SKNode, position: CGPoint, size: CGSize 
        = CGSize(width: 28, height: 24)) {
        parentNode.addChild(self)
        createAnimations()
        self.size = size
        self.position = position
        self.runAction(flyAnimation)
    }

    // Our bee only implements one texture based animation.
    // But some classes may be more complicated,
    // So we break out the animation building into this function:
    func createAnimations() {
        let flyFrames:[SKTexture] = 
            [textureAtlas.textureNamed("bee.png"), 
            textureAtlas.textureNamed("bee_fly.png")]
        let flyAction = SKAction.animateWithTextures(flyFrames, 
            timePerFrame: 0.14)
        flyAnimation = SKAction.repeatActionForever(flyAction)
    }

    // onTap is not wired up yet, but we have to implement this
    // function to adhere to our protocol.
    // We will explore touch events in the next chapter.
    func onTap() {}
}

It is now easy to spawn as many bees as we like. Switch back to GameScene.swift, and add this code in didMoveToView:

// Create three new instances of the Bee class:
let bee2 = Bee()
let bee3 = Bee()
let bee4 = Bee()
// Use our spawn function to place the bees into the world:
bee2.spawn(world, position: CGPoint(x: 325, y: 325))
bee3.spawn(world, position: CGPoint(x: 200, y: 325))
bee4.spawn(world, position: CGPoint(x: 50, y: 200))

Run the project. Bees, bees everywhere! Our original bee is flying back and forth through a swarm. Your simulator should look like this:

Depending on how you look at it, you may perceive that the new bees are moving and the original bee is still. We need to add a point of reference. Next, we will add the ground.

The icy tundra

We will add some ground at the bottom of the screen to serve as a constraint for player positioning and as a reference point for movement. We will create a new class named Ground. First, let us add the texture atlas for the ground art to our project.

Another way to add assets

We will use a different method of adding files to Xcode. Follow these steps to add the new artwork:

  1. In Finder, navigate to the asset pack you downloaded in Chapter 2, Sprites, Camera, Actions!, and then to the Environment folder.
  2. You learned to create a texture atlas earlier, for our bee. I have already created texture atlases for the rest of the art we use in this game. Locate the ground.atlas folder.
  3. Drag and drop this folder into the project manager in Xcode, under the project folder, as seen in this screenshot:
  4. In the dialog box, make sure your settings match the following screenshot, and then click Finish:

Perfect – you should see the ground texture atlas in the project navigator.

Adding the Ground class

Next, we will add the code for the ground. Add a new Swift file to your project and name it Ground.swift. Use the following code:

import SpriteKit

// A new class, inheriting from SKSpriteNode and
// adhering to the GameSprite protocol.
class Ground: SKSpriteNode, GameSprite {
    var textureAtlas:SKTextureAtlas = 
        SKTextureAtlas(named:"ground.atlas")
    // Create an optional property named groundTexture to store 
    // the current ground texture:
    var groundTexture:SKTexture?

    func spawn(parentNode:SKNode, position:CGPoint, size:CGSize) {
        parentNode.addChild(self)
        self.size = size
        self.position = position
        // This is one of those unique situations where we use
        // non-default anchor point. By positioning the ground by
        // its top left corner, we can place it just slightly
        // above the bottom of the screen, on any of screen size.
        self.anchorPoint = CGPointMake(0, 1)

        // Default to the ice texture:
        if groundTexture == nil {
            groundTexture = textureAtlas.textureNamed("ice-
                tile.png");
        }

        // We will create child nodes to repeat the texture.
        createChildren()
    }

    // Build child nodes to repeat the ground texture
    func createChildren() {
        // First, make sure we have a groundTexture value:
        if let texture = groundTexture {
            var tileCount:CGFloat = 0
            let textureSize = texture.size()
            // We will size the tiles at half the size
            // of their texture for retina sharpness:
            let tileSize = CGSize(width: textureSize.width / 2, 
                height: textureSize.height / 2)

            // Build nodes until we cover the entire Ground width
            while tileCount * tileSize.width < self.size.width {
                let tileNode = SKSpriteNode(texture: texture)
                tileNode.size = tileSize
                tileNode.position.x = tileCount * tileSize.width
                // Position child nodes by their upper left corner
                tileNode.anchorPoint = CGPoint(x: 0, y: 1)
                // Add the child texture to the ground node:
                self.addChild(tileNode)

                tileCount++
            }
        }
    }

    // Implement onTap to adhere to the protocol:
    func onTap() {}
}
Tiling a texture

Why do we need the createChildren function? SpriteKit does not support a built-in method to repeat a texture over the size of a node. Instead, we create children nodes for each texture tile and append them across the width of the parent. Performance is not an issue; as long as we attach the children to one parent, and the textures all come from the same texture atlas, SpriteKit handles them with one draw call.

Running wire to the ground

We have added the ground art to the project and created the Ground class. The final step is to create an instance of Ground in our scene. Follow these steps to wire-up the ground:

  1. Open GameScene.swift and add a new property to the GameScene class to create an instance of the Ground class. You can place this underneath the line that instantiates the world node (the new code is in bold):
    let world = SKNode()
    let ground = Ground()
    
  2. Locate the didMoveToView function. Add the following code at the bottom, underneath our bee spawning lines:
    // size and position the ground based on the screen size.
    // Position X: Negative one screen width.
    // Position Y: 100 above the bottom (remember the ground's top
    // left anchor point).
    let groundPosition = CGPoint(x: -self.size.width, y: 100)
    // Width: 3x the width of the screen.
    // Height: 0. Our child nodes will provide the height.
    let groundSize = CGSize(width: self.size.width * 3, height: 0)
    // Spawn the ground!
    ground.spawn(world, position: groundPosition, size: groundSize)

Run the project. You will see the icy tundra appear underneath our bees. This small change goes a long way towards creating the feeling that our central bee is moving through space. Your simulator should look like this:

A wild penguin appears!

There is one more class to build before we start our physics lesson: the Player class! It is time to replace our moving bee with a node designated as the player.

First, we will add the texture atlas for our penguin art. By now, you are familiar with adding files through the project navigator. Add the Pierre art as you did previously with the ground assets. I named Pierre's texture atlas pierre.atlas. You can find it in the asset pack, inside the Pierre folder.

Once you add Pierre's texture atlas to the project, you can create the Player class. Add a new Swift file to your project and name it Player.swift. Then add this code:

import SpriteKit

class Player : SKSpriteNode, GameSprite {
    var textureAtlas:SKTextureAtlas = 
        SKTextureAtlas(named:"pierre.atlas")
    // Pierre has multiple animations. Right now we will
    // create an animation for flying up, and one for going down:
    var flyAnimation = SKAction()
    var soarAnimation = SKAction()

    func spawn(parentNode:SKNode, position: CGPoint,
        size:CGSize = CGSize(width: 64, height: 64)) {
        parentNode.addChild(self)
        createAnimations()
        self.size = size
        self.position = position
        // If we run an action with a key, "flapAnimation",
        // we can later reference that key to remove the action.
        self.runAction(flyAnimation, withKey: "flapAnimation")
    }

    func createAnimations() {
        let rotateUpAction = SKAction.rotateToAngle(0, duration: 
            0.475)
        rotateUpAction.timingMode = .EaseOut
        let rotateDownAction = SKAction.rotateToAngle(-1, 
            duration: 0.8)
        rotateDownAction.timingMode = .EaseIn

        // Create the flying animation:
        let flyFrames:[SKTexture] = [
            textureAtlas.textureNamed("pierre-flying-1.png"),
            textureAtlas.textureNamed("pierre-flying-2.png"),
            textureAtlas.textureNamed("pierre-flying-3.png"),
            textureAtlas.textureNamed("pierre-flying-4.png"),
            textureAtlas.textureNamed("pierre-flying-3.png"),
            textureAtlas.textureNamed("pierre-flying-2.png")
        ]
        let flyAction = SKAction.animateWithTextures(flyFrames, 
            timePerFrame: 0.03)
        // Group together the flying animation frames with a 
        // rotation up:
        flyAnimation = SKAction.group([
            SKAction.repeatActionForever(flyAction),
            rotateUpAction
        ])

        // Create the soaring animation, just one frame for now:
        let soarFrames:[SKTexture] = 
            [textureAtlas.textureNamed("pierre-flying-1.png")]
        let soarAction = SKAction.animateWithTextures(soarFrames, 
            timePerFrame: 1)
        // Group the soaring animation with the rotation down:
        soarAnimation = SKAction.group([
            SKAction.repeatActionForever(soarAction),
            rotateDownAction
        ])
    }

    func onTap() {}
}

Great! Before we continue, we need to replace our original bee with an instance of the new Player class we just created. Follow these steps to replace the bee:

  1. In GameScene.swift, near the top, remove the line that creates a bee constant in the GameScene class. Instead, we want to instantiate an instance of Player. Add the new line: let player = Player().
  2. Completely delete the addTheFlyingBee function.
  3. In didMoveToView, remove the line that calls addTheFlyingBee.
  4. In didMoveToView, at the bottom, add a new line to spawn the player:
    player.spawn(world, position: CGPoint(x: 150, y: 250))
  5. Further down, in didSimulatePhysics, replace the references to the bee with references to player. Recall that we created the didSimulatePhysics function in Chapter 2, Sprites, Camera, Actions!, when we centered the camera on one node.

We have successfully transformed the original bee into a penguin. Before we move on, we will make sure your GameScene class includes all of the changes we have made so far in this chapter. After that, we will begin to explore the physics system.

Renovating the GameScene class

We have made quite a few changes to our project. Luckily, this is the last major overhaul of the previous animation code. Moving forward, we will use the terrific structure we built in this chapter. At this point, your GameScene.swift file should look something like this:

class GameScene: SKScene {
    let world = SKNode()
    let player = Player()
    let ground = Ground()

    override func didMoveToView(view: SKView) {
        // Set a sky-blue background color:
        self.backgroundColor = UIColor(red: 0.4, green: 0.6, blue: 
            0.95, alpha: 1.0)

        // Add the world node as a child of the scene:
        self.addChild(world)

        // Spawn our physics bees:
        let bee2 = Bee()
        let bee3 = Bee()
        let bee4 = Bee()
        bee2.spawn(world, position: CGPoint(x: 325, y: 325))
        bee3.spawn(world, position: CGPoint(x: 200, y: 325))
        bee4.spawn(world, position: CGPoint(x: 50, y: 200))

        // Spawn the ground:
        let groundPosition = CGPoint(x: -self.size.width, y: 30)
        let groundSize = CGSize(width: self.size.width * 3, 
            height: 0)
        ground.spawn(world, position: groundPosition, size: 
            groundSize)

        // Spawn the player:
        player.spawn(world, position: CGPoint(x: 150, y: 250))
    }

    override func didSimulatePhysics() {
        let worldXPos = -(player.position.x * world.xScale – 
            (self.size.width / 2))
        let worldYPos = -(player.position.y * world.yScale – 
            (self.size.height / 2))
        world.position = CGPoint(x: worldXPos, y: worldYPos)
    }
}

Run the project. You will see our new penguin hovering near the bees. Great work; we are now ready to explore the physics system with all of our new nodes. Your simulator should look something like this screenshot: