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:
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:
- Open the
Assets.xcassets
file in Xcode, then right-click in the left panel and select New Sprite Atlas. - Change the name of the new sprite atlas from
Sprites
toEnvironment
(we will use this texture atlas for all the environment textures in our game). - Xcode creates a new sprite—named
Sprite
—inside this atlas by default. Remove it by right-clicking it and selecting Remove Selected Items. - In Finder, open the asset pack you downloaded. Locate the
Environment
folder and selectground@2x.png
andground@3x.png
. - 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 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:
- 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 thecam
node (the new code is in bold):let cam = SKCameraNode() let ground = Ground()
- 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:
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:
- Create a new sprite atlas named
Pierre
inAssets.xcassets
by right-clicking in the left panel and selecting New Sprite Atlas. - Locate the
Pierre
folder in your downloaded asset bundle. Drag and drop all of the.png
files from this folder onto thePierre
atlas in Xcode. - Your
Assets.xcassets
file should now look like this: - 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 itPlayer.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:
- In
GameScene.swift
, near the top, remove the line that creates abee
constant in theGameScene
class. Instead, we want to initiate an instance ofPlayer
. Add the new linelet player = Player()
. - Completely delete the
addTheFlyingBee
function. - Also, remove the
addBackground
function for now. - In
didMove
, remove the line that callsaddTheFlyingBee
. - In
didMove
, at the bottom, add the following new code to position and add theplayer
:// Position the player: player.position = CGPoint(x: 150, y: 250) // Add the player node to the scene: self.addChild(player)
- Further down, in
didSimulatePhysics
, replace the reference to thebee
with a reference to theplayer
. The new line will read:self.camera!.position =player.position
. 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 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.