In the first part of the tutorial “Swing Copters – Swift Copters” we establish the base of the game. In this chapter, we are going to finish the job implementing:
At the end of the post you will have a game like this:
Lets give to our Swift Copter hero some rock & roll! We want to implement the same behaviour of the Swing Copter game: When you tap the screen, the hero change his direction and acceleration direction 180 degrees. If you tap again and again your hero will go up in a little zig-zag.
In order to handle touches in SpriteKit, we should override “touchesBegan” in our GameScene.swift:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) { if !start { nodeCopter.physicsBody?.affectedByGravity = true spriteCopter.runAction(SKAction.repeatActionForever(SKAction.animateWithTextures([SKTexture(imageNamed:"booCopter1"),SKTexture(imageNamed:"booCopter2"),SKTexture(imageNamed:"booCopter3"),SKTexture(imageNamed:"booCopter4")], timePerFrame: 0.075))) nodeCopter.physicsBody?.dynamic = true } start = true; for touch: AnyObject in touches { if gravityX > 0 { gravityX = -4 self.physicsWorld.gravity = CGVectorMake(gravityX, 0.0) self.nodeCopter.physicsBody?.applyImpulse(CGVectorMake(impulseX, impulseY)) nodeCopter.runAction(SKAction.rotateToAngle(+3.14/10, duration: 0.3))//rigth } else { gravityX = 4 self.physicsWorld.gravity = CGVectorMake(gravityX, 0.0) self.nodeCopter.physicsBody?.applyImpulse(CGVectorMake(-impulseX, impulseY)) nodeCopter.runAction(SKAction.rotateToAngle(-3.14/10, duration: 0.3))//left } } //We have to change the height of the physics bode to make it larger when the copter goes up let borderBody = SKPhysicsBody(edgeLoopFromRect: CGRectMake(-self.frame.size.width/2, -self.frame.size.height/2, self.frame.size.width, self.frame.size.height+nodeCopter.position.y)) nodeWorld.physicsBody? = borderBody nodeWorld.physicsBody?.categoryBitMask = categoryScreen } |
Here you have some explanations:
Build and Run! Play a little bit. Have you notice? There are some invisible walls at each side of the screen. However, our hero flies away at the top.
But there is a problem, right? We need the camera to follow him!
SpriteKit doesn’t have a “camera” method to center the scene on a node. They recommend us a trick. We are going to move the nodeWorld (remember, where the rest of the nodes are) in the other way of the hero when the hero moves.
How are we going to perform this? We are going to move our hero and to apply impulses to him. But, at the end of each frame, we are going move the rest of the scene the same length in the other way. SpriteKit has a method that executes every frame (in fact there are more than one) where we can do this job. (you can check Apple doc here for details)
Copy this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//game loop override func didSimulatePhysics() { // self.shouldRepositeNodes() self.centerOnNode(nodeCopter) // self.updatePoints() } //To mantain the copter in the centered at the bottom of the screen func centerOnNode(node:SKNode) { let cameraPositionInScene = node.scene?.convertPoint(node.position, fromNode: node.parent!) node.parent?.position = CGPointMake(node.parent!.position.x, node.parent!.position.y - cameraPositionInScene!.y-self.frame.size.height/3); } |
As the name of the method says: didSimulatePhysics() is the method that is fired every frame when all the physics calculations and effects have finished.
Here we are going to do some more work, but at this time, we are only calling to our method centerOnNode. Build and Launch. See the effect?
We are going to generate some enemies to stop our hero. This is an infinite scroll game…so if the player is good, there could be infinite enemies! But we want to take care of the memory of the iPhone so we will need some trick to maintain it low.
We can generate 5/10 enemies (the next ones the hero is going to meet)…and when those enemies disappear at the bottom of the screen, we could move them up again to the next position. In this way, we only generate 5/10 objects and reuse them forever.
Our enemies will have different sprites: They are composed by a bar and a hammer head at the end. One of this packs in each side of the screen. Let’s create 10 (pairs) of them.
So we have to create 10 enemies (let’s use a “for” loop), composed each one by 2×2 spriteNodes (2bar+2hammerHead), which they have their own physicalBodies to detect collisions, and that are going to be reused later.
Copy the following method, and call it at didMoveToView func.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
func startEnemies(){ for index in 1...10 { //1 let randomX:CGFloat = -(CGFloat(Int(arc4random_uniform(160)))+160) //-320 to -160 --- -160 to 0 //2 var nodeEnemy = SKNode() //3 //BARS let spriteBarLeft = SKSpriteNode(imageNamed:"enemyBarLeft") spriteBarLeft.size = CGSizeMake(spriteBarLeft.size.width/2, spriteBarLeft.size.height/2) spriteBarLeft.position = CGPointMake(randomX,0) spriteBarLeft.zPosition = 5; let borderBody:SKPhysicsBody = SKPhysicsBody(edgeLoopFromRect: CGRectMake(0, 0, spriteBarLeft.size.width, spriteBarLeft.size.height)) borderBody.dynamic = false; borderBody.categoryBitMask = categoryEnemy; borderBody.affectedByGravity = false; spriteBarLeft.name = "enemyBarLeft"; spriteBarLeft.anchorPoint = CGPointMake(0, 0) spriteBarLeft.physicsBody = borderBody; nodeEnemy.addChild(spriteBarLeft) let spriteBarRight = SKSpriteNode(imageNamed:"enemyBarRight") spriteBarRight.size = CGSizeMake(spriteBarRight.size.width/2, spriteBarRight.size.height/2) spriteBarRight.position = CGPointMake(spriteBarLeft.position.x + spriteBarLeft.size.width + ditanceBetweenBars,0) spriteBarRight.zPosition = 5; let borderBodyRight:SKPhysicsBody = SKPhysicsBody(edgeLoopFromRect: CGRectMake(0, 0, spriteBarRight.size.width, spriteBarRight.size.height)) borderBodyRight.dynamic = false; borderBodyRight.categoryBitMask = categoryEnemy borderBodyRight.affectedByGravity = false; spriteBarRight.name = "enemyBarRight"; spriteBarRight.anchorPoint = CGPointMake(0, 0) spriteBarRight.physicsBody = borderBodyRight //4 //HAMMERS let spriteSwingLeft = SKSpriteNode(imageNamed: "enemySwing") spriteSwingLeft.size = CGSizeMake(spriteSwingLeft.size.width/2, spriteSwingLeft.size.height/2) spriteSwingLeft.zPosition = 4 spriteSwingLeft.anchorPoint = CGPointMake(0.5, 1) spriteSwingLeft.position = CGPointMake(randomX+141,9) spriteSwingLeft.zRotation = -3.14/8 let spriteSwingRight = SKSpriteNode(imageNamed: "enemySwing") spriteSwingRight.size = CGSizeMake(spriteSwingRight.size.width/2, spriteSwingRight.size.height/2) spriteSwingRight.zPosition = 4 spriteSwingRight.anchorPoint = CGPointMake(0.5, 1) spriteSwingRight.position = CGPointMake(randomX+141+ditanceBetweenBars+37 ,9) spriteSwingRight.zRotation = -3.14/8 let borderBodySwings = SKPhysicsBody(edgeLoopFromRect: CGRectMake(-spriteSwingLeft.size.width/2, -spriteSwingLeft.size.height, spriteSwingLeft.size.width*0.9, 0.4*spriteSwingLeft.size.height)) borderBodySwings.dynamic = false borderBodySwings.categoryBitMask = categoryEnemy borderBodySwings.affectedByGravity = false spriteSwingLeft.name = "enemySwing" spriteSwingLeft.physicsBody = borderBodySwings let borderBodySwingsRight = SKPhysicsBody(edgeLoopFromRect: CGRectMake(-spriteSwingRight.size.width/2, -spriteSwingRight.size.height, spriteSwingRight.size.width*0.9, 0.4*spriteSwingRight.size.height)) borderBodySwingsRight.dynamic = false borderBodySwingsRight.categoryBitMask = categoryEnemy borderBodySwingsRight.affectedByGravity = false spriteSwingRight.name = "enemySwing" spriteSwingRight.physicsBody = borderBodySwingsRight //5 let actionSwing:SKAction = SKAction.sequence([SKAction.rotateByAngle(3.14/4, duration: 1),SKAction.rotateByAngle(-3.14/4, duration: 1)]) spriteSwingLeft.runAction(SKAction.repeatActionForever(actionSwing)) spriteSwingRight.runAction(SKAction.repeatActionForever(actionSwing)) //6 //Final set up nodeEnemy.addChild(spriteSwingLeft) nodeEnemy.addChild(spriteSwingRight) nodeEnemy.position = CGPointMake(0, lastYposition) nodeEnemy.addChild(spriteBarRight) nodeEnemies.addChild(nodeEnemy) //7 lastYposition += ditanceFromBarToBar } } |
If you build this, you will notice some errors. We need to add these vars on the top of our scene:
1 2 3 4 |
var lastYposition:CGFloat = 300.0 let ditanceBetweenBars:CGFloat = 175.0 let ditanceFromBarToBar:CGFloat = 300.0 let nodeEnemies = SKNode() |
Lets add nodeEnemies to our nodeWorld and call startEnemies in our didMoveToView. The method should look like this:
1 2 3 4 5 6 7 8 |
override func didMoveToView(view: SKView) { self.startWorld() self.initPhysics() self.startGround() self.startCopter() self.startEnemies() nodeWorld.addChild(nodeEnemies) } |
Now build and run it again. There should be enemies around you. But after 10 of them…nothing!...we still need to reuse those that are offscreen when the hero has passed through them.
Which place could execute code in every frame and allow us to move any node as soon as possible when it is not needed anymore? We have used it yet…didSimulatePhysiscs! Add this call and this new method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//game loop override func didSimulatePhysics() { self.shouldRepositeNodes() self.centerOnNode(nodeCopter) } //Here we reposition enemies out of the screen to the top of the sky again func shouldRepositeNodes() { let arrayEnemies:Array<SKNode> = nodeEnemies.children as Array<SKNode> for nodeEnemy:SKNode in arrayEnemies { if nodeEnemy.position.y - nodeCopter.position.y < -300.0 { nodeEnemy.position.y = lastYposition + ditanceFromBarToBar; lastYposition += ditanceFromBarToBar; } } } |
See what shouldRepositeNodes does? We get all the children nodes of nodeEnemies (every single enemy was added to this node). With go through that array an for every node, we check if it is under our hero more than 300px (out of screen at the bottom). When that happens, we are moving that enemy to the next lastYPosition+distanceFromBarToBar. An we have a “new” enemy ready to rock our hero!
Build & Run…now you should see an infinite game!
Have you trained a little bit? Okey! Now it is time for our hero to demonstrate real skills. We are going to detect the collisions.
We have set up physicalBodies everywhere: our hero, the invisible walls of the screen and the enemies (Bar and hammer head). We even specify which collisions should be detected with our hero before:
1 |
nodeCopter.physicsBody.contactTestBitMask = categoryScreen | categoryEnemy; |
So now, it is time to handle those collisions. Add these two new methods to our scene:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
func didBeginContact(contact: SKPhysicsContact!) { var firstBody:SKPhysicsBody var secondBody:SKPhysicsBody if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask { firstBody = contact.bodyA secondBody = contact.bodyB } else { firstBody = contact.bodyB secondBody = contact.bodyA } if firstBody.categoryBitMask == categoryCopter && (secondBody.categoryBitMask == categoryEnemy || secondBody.categoryBitMask == categoryScreen) { self.resetScene() } } func resetScene() { viewController?.presentGameOverScene() } |
As you imagine, didBeginContact got called when a contact between two physical bodies happen. Then, we check the categoryBitMasks. When one body has categoryCopter (the hero) and the other is an Enemy or the Screen borders…our hero should die!
In our game, we are going to transit to another scene, where we will show the user a “game over” text. But remember…our viewController is in charge of the transitions between scenes, so we call him first!
We need to do some changes in our GameViewController, but I let you check them in the final project. It is easy to make a transition between scenes, for example:
1 |
skView.presentScene(scene, transition: SKTransition.crossFadeWithDuration(0.5)) |
Here you have the full code of the tutorial:
I have added there two more things: a score node with the points and some animated clouds. But that is your homework! Check them and if you have any questions, let me know in the comments :).
I am preparing a video-course with full step by step explanations in video, and much more features for this game template. You could use it to have your own game in the Appstore in 2 days! Do you want to hear about it? Write me!
Categories: All, Apps, Development, Tutorial
Code and gitHub project updated for Xcode GM new changes (SKPhysicsBody is now optional for SKNode)
If anyone is attempting this tutorial and finding problems its because the spelling in the code is atrocious among other irritating things such as calling .addChild twice for the same SKSpriteNode – which causes it to crash.
Do not copy the code as is. Code through every line meticulously and looked out for those buggy repetitions
Hi fishdawgza!
Thanks for your warning about “atrocious and irritating” things 😀 ! (could you point them? I will fix them asap!)
Anyway, you can download the github project, it is working as I write this!
Hi,
thank you so much, I have been a iOS developer for a long time and I am developing my first sprite kit game in swift. Thanks to you I have learnt many things. But I am stuck with a very very simple project: 4 nodes in the game scene and 0 nodes in the menu scene. I have simplified a lot my sample to find out a solution. I have a ball in the game scene and its move is very smooth. When i change the scene to the menu scene then I get back to the game scene the move is laggy. Could you please look at my tiny project or just the gamescene.swift. I am willing to pay if you could help me, I have been working on it for many days and I am desperate, that would be very kind of you. Thank you so much in advance!
John