I am participating in the nuggets Community game creative submission Contest. For details, please see: Game Creative Submission Contest

preface

Remember many years ago to play a very fun hand tour called Thunder fighters, at that time to play very crazy. So today we’re going to use SpriteKit to make a simple version of the Thunderbolt

The installation

1. IOS installation is a little uncomfortable, unlike Android, where you can just directly give an APK. On my side, I can download a TestFlightAPP and install it using TestFlight

Use wechat to open this address and follow the instructions to install:

Testflight.apple.com/join/YCDnN4…

It looks something like this:

2, can run code installation, demo address is here

play

2. At the beginning, the player has 3 lives, each level will randomly drop a blood pack and increase the attack power of the plane. Enemy health also increases with each level, and there is a boss in each level. Defeating the boss will earn you a lot of points

Let’s start implementing it step by step.

implementation

1. Create a new project and create one in the projectPlanScenescenario

In didMove(to view: SKView) set gravity to 0,0

Override func didMove(to view: SKView) {super.didmove (to: view) // Set physicsworld.gravity =.zero}Copy the code
2. Background loop scrolling

2.1. Create two background sprites that are the same. BgNode1 position is set to CGPoint(x: 0, y: 0) and bgNode2 position is set to CGPoint(x: 0, y: sie.height).

private lazy var bgNode1: SKSpriteNode = {
    let view = SKSpriteNode(imageNamed: "plan_bg")
    view.position = CGPoint(x: 0, y: 0)
    view.size = size
    view.anchorPoint = CGPoint(x: 0, y: 0)
    view.zPosition = 0
    view.name = "bgNode"
    return view
}()

private lazy var bgNode2: SKSpriteNode = {
    let view = SKSpriteNode(imageNamed: "plan_bg")
    view.position = CGPoint(x: 0, y: size.height)
    view.size = size
    view.anchorPoint = CGPoint(x: 0, y: 0)
    view.zPosition = 0
    view.name = "bgNode"
    return view
}()
Copy the code

Reset bgNode1 and bgNode2 positions in the Override func update(_ currentTime: TimeInterval) method

override func update(_ currentTime: TimeInterval) {
    super.update(currentTime)
    
    backgroudScrollUpdate()
}

private func backgroudScrollUpdate() {
    bgNode1.position = CGPoint(x: bgNode1.position.x, y: bgNode1.position.y - 4)
    bgNode2.position = CGPoint(x: bgNode2.position.x, y: bgNode2.position.y - 4)
    if bgNode1.position.y <= -size.height {
        bgNode1.position = CGPoint(x: 0, y: 0)
        bgNode2.position = CGPoint(x: 0, y: size.height)
    }
}
Copy the code

This way the background can scroll indefinitely

3. Add the plane and drag it to move

3.1 If we want to drag an airplane, we need to add pan gesture to the view of the current scene, because SKSpriteNode can’t add gestures, so we need to add gesture to the view of the current scene, and then determine whether the current touch is an airplane in the touchesBegan method, so we define two variables first

Private var isTouchPlan: Bool = false // Point of current aircraft private var planPoint: CGPoint =.zeroCopy the code

3.2. Create an airplane Spirit

// Private lazy var planNode: SKSpriteNode = {let view = SKSpriteNode(imageNamed: "Plan02 ") view.position = CGPoint(x: size.width / 2, y: size.height / 2) view.anchorPoint = CGPoint(x: 0.5, y: 0) view.size = CGSize(width: 70, height: 54) view.zPosition = 2 view.name = "plan" view.physicsBody = SKPhysicsBody(rectangleOf: View.physicsbody?. IsDynamic = false // Set the physical identifier, View.PhysicsBody?. CategoryBitMask = 1 View.physicsbody?. ContactTestBitMask = 2 return view}()Copy the code

3.3. In the ‘touchesBegan’ method, determine whether the object touched was an airplane

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { isTouchPlan = false guard let touch = (touches as NSSet).anyObject() as? UITouch else { return } let point = touch.location(in: self) let node = atPoint(point) switch node.name { case "plan": // isTouchPlan = true isTouchPlan = true default: break}}Copy the code

3.4. Add a PAN gesture to the current scene to drag the airplane

private func addPanGestureRecognizer(_ view: SKView) { let pan = UIPanGestureRecognizer(target: self, action: #selector(panAction(_:))) view.addGestureRecognizer(pan) } @objc private func panAction(_ sender: UIPanGestureRecognizer) { if isTouchPlan { var position = sender.location(in: Position = CGPoint(x: position.x, y: position) size.height - position.y) planNode.position = position planPoint = position } }Copy the code

The plane fired bullets

4.1 When firing bullets, we create a timer and generate bullets in the timer method. Since we are based on the level system, the interval time is determined according to the level. The higher the level, the shorter the time

Private func startBulletTimer() {var ti = 0.2 - TimeInterval(leve) * 0.02 if ti <= 0.05 {ti = 0.05} bulletTimer = Timer.scheduledTimer(timeInterval: ti, target: self, selector: #selector(createBullet), userInfo: nil, repeats: @objc private func createBullet() {let bulletNode = SKSpriteNode(imageNamed: "Plan_bullet ") bulletnode. position = planPoint bulletnode. anchorPoint = CGPoint(x: 0.5, y: 1) bulletNode.size = CGSize(width: 10, height: Bulletnode. zPosition = 1 bulletnode. name = "bullet" addChild(bulletNode) var ti = 3 - TimeInterval(leve) * 0.5 if ti <= 0.5 {ti = 0.5} // Make the bullet move bulletNode.run(skaction.moveto (y: sie.height, duration: ti)) { bulletNode.removeAllActions() bulletNode.removeFromParent() } bulletNode.physicsBody = SKPhysicsBody(rectangleOf: Bulletnode.physicsbody?. IsDynamic = true BulletNode.PhysicsBody?. AllowsRotation = false CollisionBitMask = 0 // Set the identifier for the physical body bulletNode.PhysicsBody?. CategoryBitMask = 1 // Set the type of physical body that can collide with bulletNode.physicsBody?.contactTestBitMask = 2 }Copy the code

5, respectively add back button and display level, score, life wizard, the code is relatively simple, will not post out
6. Create enemy planes, health packs, and increase attack packs

6.1. To create enemy aircraft, we still use timers

Private func startEnemyTimer() {var ti = 0.5 - TimeInterval(leve) * 0.05 if ti <= 0.1 {ti = 0.1} enemyTimer = Timer.scheduledTimer(timeInterval: ti, target: self, selector: #selector(createEnemy), userInfo: nil, repeats: @objc private func createEnemy() {let enemyNode = SKSpriteNode(imageNamed: "Plan01 ") let pointX = CGFloat(arc4random_UNIFORM (UInt32(sie.width-40))) enemyNode.position = CGPoint(x: enemyNode) PointX + 20, y: size.height) enemyNode.anchorPoint = CGPoint(x: 0.5, y: 0.5) enemyNode.size = CGSize(width: 40, height: 30) enemyNode.zPosition = 1 enemyNode.name = "enemyNode" addChild(enemyNode) var ti = 6 - TimeInterval(leve) * 0.3 if ti <= 1 {ti = 1} // EnemyNode. run(skaction.moveto (y: 0, duration:) enemyNode.run(skaction.moveto (y: 0, duration:) ti)) { enemyNode.removeAllActions() enemyNode.removeFromParent() } enemyNode.physicsBody = SKPhysicsBody(rectangleOf: Enemynode.physicsbody?. IsDynamic = true EnemyNode.physicsBody?. AllowsRotation = false CategoryBitMask = 2 EnemyNode.PhysicsBody?. ContactTestBitMask = 1 enemyNode.physicsBody?.collisionBitMask = 0 enemyNode.physicsBody?.mass = CGFloat(enemyLife) }Copy the code

6.2. Creating health packs and increasing attack packs will only appear once per level, randomly during the length of each level

Private func createPotionOrAttack () {/ / create a potion DispatchQueue. Main. AsyncAfter (deadline: .now() + TimeInterval(arc4random_Uniform (5))) {self.createNode("plan_potion")} // Increases damage DispatchQueue.main.asyncAfter(deadline: .now() + TimeInterval(arc4random_uniform(5))) { self.createNode("plan_attack") } } private func createNode(_ name: String) { let node = SKSpriteNode(imageNamed: name) let pointX = CGFloat(arc4random_uniform(UInt32(self.size.width - 40))) node.position = CGPoint(x: pointX + 20, y: Height) node.anchorPoint = CGPoint(x: 0.5, y: 0.5) node.size = CGSize(width: 40, height: 40) node.zPosition = 1 node.name = name self.addChild(node) var ti = 6 - TimeInterval(leve) * 0.3 if ti <= 1 {ti = 1} node.run(SKAction.moveTo(y: 0, duration: ti)) { node.removeAllActions() node.removeFromParent() } node.physicsBody = SKPhysicsBody(rectangleOf: IsDynamic = true Node.PhysicsBody?. AllowsRotation = false // Sets the physical identifier Node.physicsbody?. CategoryBitMask = 2 // Specifies the type of physical body that can collide with newsid. Node.physicsbody?. ContactTestBitMask = 1 node.physicsBody?.collisionBitMask = 0 }Copy the code

7. Create bosses and bosses fire bullets

We still use timers, yes, timers. The boss timer is set to last as long as each level lasts, but 5s for testing purposes

Private func startBossTimer() {bossTimer = timer.scheduledTimer (timeInterval: 5, target: self, selector: #selector(createBoss), userInfo: nil, repeats: @objc private func createBoss() {bossNode = SKSpriteNode(imageNamed: "boss01")? .position = CGPoint(x: size.width / 2, y: size.height - 50) bossNode? . AnchorPoint = CGPoint(x: 0.5, y: 1) bossNode? .size = CGSize(width: 98, height: 127) bossNode? .zPosition = 1 bossNode? .name = "boss" addChild(bossNode!) // let wait = skaction. wait(forDuration: 2) let action1 = skaction.moveto (x: 64, duration: 1) let action2 = skaction.moveto (x: sie.width-64, duration: 3) bossNode? .run(wait) { self.bossNode?.run(SKAction.repeatForever(SKAction.sequence([action1, action2]))) } bossNode?.physicsBody = SKPhysicsBody(rectangleOf: BossNode?.physicsBody?. IsDynamic = true bossNode?.PhysicsBody?. AllowsRotation = false BossNode?.PhysicsBody?. CategoryBitMask = 2 // Set the type of physical bodies that can collide with bossNode?.PhysicsBody?. ContactTestBitMask = 1 CollisionBitMask = 0 bossNode?.PhysicsBody?. Mass = CGFloat(leve) * 1000 // Health bossLife = leve * 1000 bossLifeNode = SKLabelNode(text: "\(bossLife)/\(bossLife)") bossLifeNode?.fontColor = .green bossLifeNode?.fontSize = 20 bossLifeNode?.position = bossNode?.position ?? .zero bossLifeNode?.horizontalAlignmentMode = .center bossLifeNode?.zPosition = 1 addChild(bossLifeNode!) bossLifeNode? .run(SKAction.wait(forDuration: 2)) { self.bossLifeNode?.run(SKAction.repeatForever(SKAction.sequence([ SKAction.moveTo(x: 64, duration: 1), skaction.moveto (x: self.size-width -64, duration: 3)])))} // stopEnemyTimer stopEnemyTimer() // stopBossTimer stopBossTimer() // Boss bullet startBossBulletTimer()}Copy the code

7.2, Boss firing bullets, here we start a Boss firing timer and call it in the createBoss() method

Private func startBossBulletTimer() {var ti = 1 - TimeInterval(leve) * 0.05 if ti <= 0.1 {ti = 0.1} bossBulletTimer = Timer.scheduledTimer(timeInterval: ti, target: self, selector: #selector(createBossBullet), userInfo: nil, repeats: True)} // createBossBullet @objc private func createBossBullet() {let bossBulletNode = SKSpriteNode(imageNamed: "boss_bullet") bossBulletNode.position = bossNode? .position ?? CGPoint (x: size width / 2, y: the size, height). BossBulletNode anchorPoint = CGPoint (0.5 x: y: 1) bossBulletNode.size = CGSize(width: 12, height: 25) bossBulletNode.zPosition = 1 bossBulletNode.name = "bossBullet" addChild(bossBulletNode) var ti = 6 - TimeInterval(leve) * 0.3 if ti <= 1 {ti = 1} bossBulletNode.run(skaction.moveto (y: 0, duration: ti)) { bossBulletNode.removeAllActions() bossBulletNode.removeFromParent() } bossBulletNode.physicsBody = SKPhysicsBody(rectangleOf: BossBulletNode. Size). / / whether the physical body force bossBulletNode physicsBody? (isDynamic = true bossBulletNode. PhysicsBody?. AllowsRotation = False / / set the physical object identifier bossBulletNode physicsBody? (categoryBitMask = 2 / / set can be and what kind of physical body collision bossBulletNode.physicsBody?.contactTestBitMask = 1 bossBulletNode.physicsBody?.collisionBitMask = 0 }Copy the code

The creation of the creation of good, the move of the move up, we come to achieve the collision of each node, to eliminate enemy aircraft and boss

8. Destroy enemy planes and bosses

8.1. Eliminate enemy planes and boss we use the physical engine collision to achieve, mainly using the agent method of SKPhysicsContactDelegate didBegin(_ Contact: SKPhysicsContact).

8.2. First, we set the proxy object to the physicsWorld property of the scene as the scene itself

physicsWorld.contactDelegate = self
Copy the code

8.3. Comply with SKPhysicsContactDelegate and implement didBegin(_ Contact: SKPhysicsContact)

extension PlanScene: SKPhysicsContactDelegate {
    func didBegin(_ contact: SKPhysicsContact) {
    
    }
}
Copy the code

An SKPhysicsContact object is returned in the didBegin(_ Contact: SKPhysicsContact) method

Open class SKPhysicsContact: NSObject {// bodyA open var bodyA: SKPhysicsBody {get} // bodyB open var bodyB: SKPhysicsBody {get} // In scene coordinates, the contact point between two physical bodies. Open var contactPoint: CGPoint {get} // Specifies the normal vector of the collision direction. Open var contactNormal: CGVector {get} // The strength of the two objects hitting each other in Newtons seconds. open var collisionImpulse: CGFloat { get } }Copy the code

We mainly use bodyA and bodyB in SKPhysicsContact to get the two colliding nodes. So how do we know if bodyA and bodyB are enemy planes or ours? Looking at the documentation, we will find a description of how we can set categoryBitMask(the identifier of the physical body) and contactTestBitMask(which physical body can collide with) attributes for each physical body in the scene

8.4. Throughout the game, we created physics with planes, bullets fired by planes, enemy planes, bosses, bullets fired by bosses, attack packs, and health packs

We set categoryBitMask to 1 and contactTestBitMask to 2 for aircraft and bullets fired by aircraft. We set categoryBitMask to 2 and contactTestBitMask to 1 for enemy planes, bosses, bullets fired by bosses, attack packs, and health packs.

8.5. What does bodyA and bodyB stand for respectively

extension PlanScene: SKPhysicsContactDelegate { func didBegin(_ contact: Var planeNode: SKSpriteNode? // Enemy plane, boss, bullets fired by boss, attack pack and health pack if contact.bodyA.categoryBitMask == 1 && contact.bodyB.categoryBitMask == 2 { planeNode = contact.bodyA.node as? SKSpriteNode enemyNode = contact.bodyB.node as? SKSpriteNode } else { planeNode = contact.bodyB.node as? SKSpriteNode enemyNode = contact.bodyA.node as? SKSpriteNode } } }Copy the code

Distinguish good bodyA and bodyB after what to represent respectively, do we begin to do corresponding processing

8.5. Handle node collisions

extension PlanScene: SKPhysicsContactDelegate { func didBegin(_ contact: SKPhysicsContact) { var planeNode: SKSpriteNode? var enemyNode: SKSpriteNode? if contact.bodyA.categoryBitMask == 1 && contact.bodyB.categoryBitMask == 2 { planeNode = contact.bodyA.node as? SKSpriteNode enemyNode = contact.bodyB.node as? SKSpriteNode } else { planeNode = contact.bodyB.node as? SKSpriteNode enemyNode = contact.bodyA.node as? SKSpriteNode } guard let planeNode = planeNode, Let enemyNode = enemyNode else {return} // Increases life if enemyNode.name == "plan_potion", PlaneNode. Name = = "plan" {ownLife + = 1 enemyNode. RemoveAllActions () enemyNode. RemoveFromParent () return} / / increase damage if enemyNode.name == "plan_attack", PlaneNode. Name = = "plan" {aggressivity + = 10. EnemyNode removeAllActions () enemyNode. RemoveFromParent () return} / / processing plane  switch planeNode.name { case "bullet" where (enemyNode.name ! = "bossBullet" && enemyNode.name ! = "plan_potion" && enemyNode.name ! = "plan_attack"): planeNode.removeAllActions() planeNode.removeFromParent() if enemyNode.name == "boss" { bossLife -= aggressivity bossLifeNode?.text = "\(bossLife)/\(leve*1000)" } case "plan": ownLife -= 1 if ownLife <= 0 { gameOver() planeNode.removeAllActions() planeNode.removeFromParent() } default: Break} // If the boss fired the bullet, If enemyNode.name == "bossBullet" {return} if enemyNode.name == "plan_potion" {return} if enemyNode.name == "plan_attack" { return } enemyNode.physicsBody?.mass -= CGFloat(aggressivity) if enemyNode.physicsBody?.mass ?? 0 <= CGFloat(aggressivity) { enemyNode.removeAllActions() enemyNode.removeFromParent() switch enemyNode.name { case "enemy": score += enemyLife case "boss": score += leve * 1000 leve += 1 enemyLife += leve * 10 stopBossBulletTimer() bossNode?.removeAllActions() bossNode?.removeFromParent() bossLifeNode?.removeAllActions() bossLifeNode?.removeFromParent() startBulletTimer() startEnemyTimer() startBossTimer() default: break } } } }Copy the code

Now that the basic functionality is complete, let’s add the collision effect

8.6. Add collision effects

Collision effects we useSKEmitterNodeDo it, and then display it at the node where the collision occurred. We chooseSpriteKit Particle FileTo create a target destruction effectBlast.sksAnd hitting the bossStrike.sksfileThen select the two files respectively, set the corresponding parameters, I am randomly set here not to say.

Class according to these two file names and load them at the appropriate locations

private func blast(_ position: CGPoint, fileName: String) { if let blast = SKEmitterNode(fileNamed: Blast. ZPosition = 2 blast. Position = position addChild(blast) Wait (forDuration: 0.3), skaction.run {blast. RemoveAllActions () blast. RemoveFromParent ()}]))}}Copy the code

The final effect is as shown at the top

The last

This is the end of the game. Show me what you got