Swift Game Development(Third Edition)
上QQ阅读APP看书,第一时间看更新

Laying the foundations

So far, we have learned through small bits of code, individually 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 reusable classes and actively organize our new code.

Adopting a protocol for consistency

To start, we want individual 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. Then, add the following code to your new file:

import SpriteKit

protocolGameSprite { 
var textureAtlas: SKTextureAtlas { get set } 
var initialSize: CGSize { get set } 
func onTap() 
} 

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

Organizing game objects into classes

Our old bee is working wonderfully, but we want to spawn many bees throughout the game 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. Then, add the following code:

import SpriteKit

// Create the new class Bee, inheriting from SKSpriteNode
// and adopting the GameSprite protocol: 
class Bee: SKSpriteNode, GameSprite { 
    // We will store our size, texture atlas, and animations 
    // as class wide properties. 
Var initialSize: CGSize = CGSize(width: 28, height: 24) 
var textureAtlas: SKTextureAtlas = 
SKTextureAtlas(named: "Enemies") 
Var flyAnimation = SKAction() 

    // The init function will be called when Bee is instantiated: 
init() { 
        // Call the init function on the base class (SKSpriteNode) 
        // We pass nil for the texture since we will animate the 
        // texture ourselves. 
super.init(texture: nil, color: .clear, size:  
initialSize) 
        // Create and run the flying animation: 
createAnimations() 
self.run(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"), 
textureAtlas.textureNamed("bee-fly")] 
        let flyAction = SKAction.animate(with: flyFrames, 
timePerFrame: 0.14) 
flyAnimation = SKAction.repeatForever(flyAction) 
    } 

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

    // Lastly, we are required to add this bit of boilerplate 
    // to subclass SKSpriteNode. We will need to do this any 
    // time we inherit from SKSpriteNode and use an init function 
    required init?(coder aDecoder: NSCoder) { 
super.init(coder: aDecoder) 
    } 
} 

It is now easy to spawn as many bees as we like. Switch back to GameScene.swift and add this code at the bottom of didMove:

// Add a second Bee to the scene: 
let bee2 = Bee() 
bee2.position = CGPoint(x: 325, y: 325) 
self.addChild(bee2) 
// ... and a third Bee: 
let bee3 = Bee() 
bee3.position = CGPoint(x: 200, y: 325) 
self.addChild(bee3) 

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

Organizing game objects into classes

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 need to create a new class named Ground. First, let's add the texture atlas for the ground art to our project.

Adding the ground texture to Assets.xcassets

We need to add our ground texture sprite, just as we added the bee sprites earlier. Once again, we will create a texture atlas in the Assets.xcassets file to hold our ground texture and other environmental textures we will use along the way. Follow these steps to add the ground texture to our project:

  1. Open the Assets.xcassets file in Xcode, then right-click in the left panel and select New Sprite Atlas.
  2. Change the name of the new sprite atlas from Sprites to Environment (we will use this texture atlas for all the environment textures in our game).
  3. Xcode creates a new sprite—named Sprite—inside this atlas by default. Remove it by right-clicking it and selecting Remove Selected Items.
  4. In Finder, open the asset pack you downloaded. Locate the Environment folder and select ground@2x.png and ground@3x.png.
  5. Drag and drop these two files into Xcode on top of the Environment texture atlas.

Xcode should create a new sprite named ground inside the Environment atlas. When you are done, your Assets.xcassets should look like this:

Adding the ground texture to Assets.xcassets
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. 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: "Environment") 
    // We will not use initialSize for ground, but we still need 
    // to declare it to conform to our GameSprite protocol: 
Var initialSize = CGSize.zero

    // This function tiles the ground texture across the width 
    // of the Ground node. We will call it from our GameScene. 
func createChildren() { 
        // This is one of those unique situations where we use a 
        // 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 = CGPoint(x: 0, y: 1) 

        // First, load the ground texture from the atlas: 
        let texture = textureAtlas.textureNamed("ground") 

var tileCount: CGFloat = 0 
        // We will size the tiles in their point size 
        // They are 35 points wide and 300 points tall 
        let tileSize = CGSize(width: 35, height: 300) 

        // 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 += 1 
        } 
    } 

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

Why do we need the createChildren function? This is one method of tiling textures. We can create a child node 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 cam node (the new code is in bold):
            let cam = SKCameraNode() 
    let ground = Ground()
  2. Locate the didMove function. Add the following code at the bottom, underneath our bee-spawning lines:
            // Position the ground based on the screen size. 
            // Position X: Negative one screen width. 
            // Position Y: 150 above the bottom (remember the top 
            // left anchor point). 
    ground.position = CGPoint(x: -self.size.width * 2, y: 150) 
            // Set the ground width to 3x the width of the scene 
            // The height can be 0, our child nodes will create the height 
    ground.size = CGSize(width: self.size.width * 6, height: 0) 
            // Run the ground's createChildren function to build 
            // the child texture tiles: 
    ground.createChildren() 
            // Add the ground node to the scene: 
    self.addChild(ground) 

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

Running wire to the ground

Adding the player's character

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 sprite atlas for our penguin art. By now, you should be familiar with adding new sprite atlases and sprites to the Assets.xcassets file. Follow these steps to add the flying penguin art to your project:

  1. Create a new sprite atlas named Pierre in Assets.xcassets by right-clicking in the left panel and selecting New Sprite Atlas.
  2. Locate the Pierre folder in your downloaded asset bundle. Drag and drop all of the .png files from this folder onto the Pierre atlas in Xcode.
  3. Your Assets.xcassets file should now look like this:
    Adding the player's character
  4. Now that you have Pierre Penguin's textures in a texture atlas, 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 initialSize = CGSize(width: 64, height: 64) 
    var textureAtlas: SKTextureAtlas = 
    SKTextureAtlas(named: "Pierre") 
                // Pierre has multiple animations. Right now, we will 
                // create one animation for flying up, 
                // and one for going down: 
    Var flyAnimation = SKAction() 
    Var soarAnimation = SKAction() 
    
    init() { 
                    // Call the init function on the 
                    // base class (SKSpriteNode) 
    super.init(texture: nil, color: .clear, size:  
    initialSize) 
    
    createAnimations() 
                    // If we run an action with a key, "flapAnimation", 
                    // we can later reference that 
                    // key to remove the action. 
    self.run(flyAnimation, withKey: "flapAnimation") 
                } 
    
    func createAnimations() { 
                    let rotateUpAction = 
    SKAction.rotate(toAngle: 0, duration: 0.475) 
    rotateUpAction.timingMode = .easeOut
                    let rotateDownAction = SKAction.rotate(toAngle: -1, 
                        duration: 0.8) 
    rotateDownAction.timingMode = .easeIn
    
                    // Create the flying animation: 
                    let flyFrames: [SKTexture] = [ 
    textureAtlas.textureNamed("pierre-flying-1"), 
    textureAtlas.textureNamed("pierre-flying-2"), 
    textureAtlas.textureNamed("pierre-flying-3"), 
    textureAtlas.textureNamed("pierre-flying-4"), 
    textureAtlas.textureNamed("pierre-flying-3"), 
    textureAtlas.textureNamed("pierre-flying-2") 
                    ] 
                    let flyAction = SKAction.animate(with: flyFrames, 
    timePerFrame: 0.03) 
                    // Group together the flying animation with rotation: 
    flyAnimation = SKAction.group([ 
    SKAction.repeatForever(flyAction), 
    rotateUpAction
                    ]) 
    
                    // Create the soaring animation, 
                    // just one frame for now: 
                    let soarFrames: [SKTexture] = 
                    [textureAtlas.textureNamed("pierre-flying-1")] 
                    let soarAction = SKAction.animate(with: soarFrames, 
    timePerFrame: 1) 
                    // Group the soaring animation with the rotation down: 
    soarAnimation = SKAction.group([ 
    SKAction.repeatForever(soarAction), 
    rotateDownAction
                    ]) 
                } 
    
                // Implement onTap to conform to the GameSprite protocol: 
    func onTap() {} 
    
                // Satisfy the NSCoder required init: 
                required init?(coder aDecoder: NSCoder) { 
    super.init(coder: aDecoder) 
                } 
            } 

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 initiate an instance of Player. Add the new line let player = Player().
  2. Completely delete the addTheFlyingBee function.
  3. Also, remove the addBackground function for now.
  4. In didMove, remove the line that calls addTheFlyingBee.
  5. In didMove, at the bottom, add the following new code to position and add the player:
            // Position the player: 
    player.position = CGPoint(x: 150, y: 250) 
            // Add the player node to the scene: 
    self.addChild(player) 
  6. Further down, in didSimulatePhysics, replace the reference to the bee with a reference to the player. The new line will read: self.camera!.position =player.position. 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 that our 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.