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:
- In Finder, navigate to the asset pack you downloaded in Chapter 2, Sprites, Camera, Actions!, and then to the
Environment
folder. - 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. - Drag and drop this folder into the project manager in Xcode, under the project folder, as seen in this screenshot:
- 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:
- Open
GameScene.swift
and add a new property to theGameScene
class to create an instance of theGround
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()
- 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:
- In
GameScene.swift
, near the top, remove the line that creates abee
constant in theGameScene
class. Instead, we want to instantiate an instance ofPlayer
. Add the new line:let player = Player()
. - Completely delete the
addTheFlyingBee
function. - In
didMoveToView
, remove the line that callsaddTheFlyingBee
. - In
didMoveToView
, at the bottom, add a new line to spawn the player:player.spawn(world, position: CGPoint(x: 150, y: 250))
- Further down, in
didSimulatePhysics
, replace the references to the bee with references toplayer
. Recall that we created thedidSimulatePhysics
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: