SpriteKit之飞机大战

我正在参与社区游戏创意投稿大赛个人赛,详情请看:游戏创意投稿大赛

前语

记住很多年以前玩过一款很好玩的手游叫雷霆战机,那时分玩的很疯狂。那么今天咱们运用SpriteKit来完成一个简易版的雷霆战机

装置

1、iOS端装置有点难过,不像安卓直接给一个apk就好了,我这边的话上了一个TestFlight,能够下载一个TestFlightAPP,运用TestFlight装置

运用微信翻开这个地址按提示装置就能够了:

testflight.apple.com/join/YCDnN4…

翻开后大概是这样的:

SpriteKit之飞机大战

2、能够运转代码装置,demo地址在这

玩法

1、该游戏为闯关游戏,玩家拖动飞机消除敌人来获取分数
2、初始的时分玩家有3条生命,每一关会随机掉落一个血包和增加飞机的攻击力。敌人的生命值也是随着闯关级数逐级增加的,每一关都会有一个boss,打败boss会取得大量分数

下面咱们就开始一步一步的完成吧。

完成

1、新建项目,在项目里创立一个PlanScene场景

1.1、在didMove(to view: SKView)办法里设置重力加速度为0,0

override func didMove(to view: SKView) {
    super.didMove(to: view)
    //设置重力加速度
    physicsWorld.gravity = .zero
}
2、背景循环翻滚

2.1、要让背景循环翻滚的话,咱们需要创立两个一样的背景精灵,bgNode1position设置成CGPoint(x: 0, y: 0)bgNode2position设置成CGPoint(x: 0, y: size.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
}()

2.2、在override func update(_ currentTime: TimeInterval)办法里从头设置bgNode1bgNode2position

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)
    }
}

这样的话背景就能够无限翻滚起来了

SpriteKit之飞机大战

3、增加飞机并拖动飞机移动

3.1、假如想要拖动飞机,要给当时场景的view增加pan手势,因为SKSpriteNode是不能增加手势的,所以,咱们要把手势增加到当时场景的view上,然后在touchesBegan办法里判别当时接触的是不是飞机,所以咱们先界说两个变量

// 接触的是否是飞机
private var isTouchPlan: Bool = false
// 当时飞机的point
private var planPoint: CGPoint = .zero

3.2、创立飞机精灵

// 飞机
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.5)
    view.size = CGSize(width: 70, height: 54)
    view.zPosition = 2
    view.name = "plan"
    view.physicsBody = SKPhysicsBody(rectangleOf: view.size)
    //物理体是否受力
    view.physicsBody?.isDynamic = false
    //设置物理体的标识符
    view.physicsBody?.categoryBitMask = 1
    //设置可与哪一类的物理体发生磕碰
    view.physicsBody?.contactTestBitMask = 2
    return view
}()

3.3、在touchesBegan办法里边判别接触的是不是飞机

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
    }
}

3.4、给当时场景增加一个pan手势来完成飞机拖动

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: sender.view)
        // 因为SpriteKit的坐标原点在左下角,所以需要转换一下
        position = CGPoint(x: position.x, y: size.height - position.y)
        planNode.position = position
        planPoint = position
    }
}

SpriteKit之飞机大战

4、飞机发射子弹

4.1、发射子弹的话咱们创立一个定时器,在定时器办法里边生成子弹,因为咱们是关卡制,所以间隔时刻依据关卡来定,关卡越高时刻越短

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: true)
}
// 创立子弹
@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: 10)
    bulletNode.zPosition = 1
    bulletNode.name = "bullet"
    addChild(bulletNode)
    var ti = 3 - TimeInterval(leve) * 0.5
    if ti <= 0.5 {
        ti = 0.5
    }
    // 让子弹向上移动
    bulletNode.run(SKAction.moveTo(y: size.height, duration: ti)) {
        bulletNode.removeAllActions()
        bulletNode.removeFromParent()
    }
    bulletNode.physicsBody = SKPhysicsBody(rectangleOf: bulletNode.size)
    //物理体是否受力
    bulletNode.physicsBody?.isDynamic = true
    bulletNode.physicsBody?.allowsRotation = false
    bulletNode.physicsBody?.collisionBitMask = 0
    //设置物理体的标识符
    bulletNode.physicsBody?.categoryBitMask = 1
    //设置可与哪一类的物理体发生磕碰
    bulletNode.physicsBody?.contactTestBitMask = 2
}

SpriteKit之飞机大战

5、别离增加返回按钮以及显示关卡、得分、生命的精灵,代码比较简单,就不贴出来了
6、创立敌机、血包和增加攻击力包

6.1、创立敌机咱们仍是运用定时器

// 敌机定时器
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: true)
}
// 创立敌机
@objc private func createEnemy() {
    let enemyNode = SKSpriteNode(imageNamed: "plan01")
    // 随机敌机呈现的位置
    let pointX = CGFloat(arc4random_uniform(UInt32(size.width - 40)))
    enemyNode.position = CGPoint(x: pointX + 20, y: size.height)
    enemyNode.anchorPoint = CGPoint(x: 0.5, y: 0.5)
    enemyNode.size = CGSize(width: 40, height: 40)
    enemyNode.zPosition = 1
    enemyNode.name = "enemy"
    addChild(enemyNode)
    var ti = 6 - TimeInterval(leve) * 0.3
    if ti <= 1 {
        ti = 1
    }
    // 敌机向下移动,移动速度依据关卡来
    enemyNode.run(SKAction.moveTo(y: 0, duration: ti)) {
        enemyNode.removeAllActions()
        enemyNode.removeFromParent()
    }
    enemyNode.physicsBody = SKPhysicsBody(rectangleOf: enemyNode.size)
    //物理体是否受力
    enemyNode.physicsBody?.isDynamic = true
    enemyNode.physicsBody?.allowsRotation = false
    //设置物理体的标识符
    enemyNode.physicsBody?.categoryBitMask = 2
    //设置可与哪一类的物理体发生磕碰
    enemyNode.physicsBody?.contactTestBitMask = 1
    enemyNode.physicsBody?.collisionBitMask = 0
    enemyNode.physicsBody?.mass = CGFloat(enemyLife)
}

6.2、创立血包和增加攻击力包,这两个每个关卡别离只会呈现一次,每个关卡时长里随机呈现

private func createPotionOrAttack() {
    // 创立药水
    DispatchQueue.main.asyncAfter(deadline: .now() + TimeInterval(arc4random_uniform(5))) {
        self.createNode("plan_potion")
    }
    // 增加攻击力
    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: self.size.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: node.size)
    //物理体是否受力
    node.physicsBody?.isDynamic = true
    node.physicsBody?.allowsRotation = false
    //设置物理体的标识符
    node.physicsBody?.categoryBitMask = 2
    //设置可与哪一类的物理体发生磕碰
    node.physicsBody?.contactTestBitMask = 1
    node.physicsBody?.collisionBitMask = 0
}

SpriteKit之飞机大战

7、创立Boss与Boss发射子弹

7.1、创立boss咱们仍是运用定时器,没错仍是定时器。每个关卡多长时刻,这个boss的定时器就设置多长时刻,为了方便测试,暂时设定5s

// boss呈现定时器
private func startBossTimer() {
    bossTimer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(createBoss), userInfo: nil, repeats: true)
}
// 创立boss
@objc private func createBoss() {
    bossNode = SKSpriteNode(imageNamed: "boss01")
    bossNode?.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!)
    // 让boss左右移动
    let wait = SKAction.wait(forDuration: 2)
    let action1 = SKAction.moveTo(x: 64, duration: 1.5)
    let action2 = SKAction.moveTo(x: size.width - 64, duration: 3)
    bossNode?.run(wait) {
       self.bossNode?.run(SKAction.repeatForever(SKAction.sequence([action1, action2])))
    }
    bossNode?.physicsBody = SKPhysicsBody(rectangleOf: bossNode?.size ?? .zero)
    //物理体是否受力
    bossNode?.physicsBody?.isDynamic = true
    bossNode?.physicsBody?.allowsRotation = false
    //设置物理体的标识符
    bossNode?.physicsBody?.categoryBitMask = 2
    //设置可与哪一类的物理体发生磕碰
    bossNode?.physicsBody?.contactTestBitMask = 1
    bossNode?.physicsBody?.collisionBitMask = 0
    bossNode?.physicsBody?.mass = CGFloat(leve) * 1000
    // 生命值
    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.5),
            SKAction.moveTo(x: self.size.width - 64, duration: 3)
        ])))
    }
    // 中止敌机创立定时器
    stopEnemyTimer()
    // 中止Boss定时器
    stopBossTimer()
    // boss发射子弹
    startBossBulletTimer()
}

7.2、Boss发射子弹,这儿咱们开启一个boss发射子弹的定时器, 并在createBoss()办法里调用

// boss发射子弹定时器
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)
}
// 创立boss Bullet
@objc private func createBossBullet() {
    let bossBulletNode = SKSpriteNode(imageNamed: "boss_bullet")
    bossBulletNode.position = bossNode?.position ?? CGPoint(x: size.width / 2, y: size.height)
    bossBulletNode.anchorPoint = CGPoint(x: 0.5, 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)
    //物理体是否受力
    bossBulletNode.physicsBody?.isDynamic = true
    bossBulletNode.physicsBody?.allowsRotation = false
    //设置物理体的标识符
    bossBulletNode.physicsBody?.categoryBitMask = 2
    //设置可与哪一类的物理体发生磕碰
    bossBulletNode.physicsBody?.contactTestBitMask = 1
    bossBulletNode.physicsBody?.collisionBitMask = 0
}

SpriteKit之飞机大战

该创立的创立好了,该动的动起来了,下面咱们来完成各个节点的磕碰,来消除敌机与boss

8、消除敌机与boss

8.1、消除敌机与boss咱们就运用物理引擎的磕碰来完成,首要是运用SKPhysicsContactDelegate中的didBegin(_ contact: SKPhysicsContact)这个署理办法完成的。

8.2、首先,咱们要对场景的physicsWorld特点设置署理目标为场景自身

physicsWorld.contactDelegate = self

8.3、遵守SKPhysicsContactDelegate并完成didBegin(_ contact: SKPhysicsContact)办法

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

didBegin(_ contact: SKPhysicsContact)办法中返回了一个SKPhysicsContact目标

open class SKPhysicsContact : NSObject {
    // 磕碰体A
  open var bodyA: SKPhysicsBody { get }
    // 磕碰体B
  open var bodyB: SKPhysicsBody { get }
    // 在场景坐标中,两个物理体之间的接触点。
  open var contactPoint: CGPoint { get }
    // 指定磕碰方向的法向量。
  open var contactNormal: CGVector { get }
    // 这两个物体在牛顿秒内彼此撞击的强度。
  open var collisionImpulse: CGFloat { get }
}

咱们首要运用SKPhysicsContact中的bodyAbodyB来获取两个磕碰的节点。那么咱们怎么知道bodyAbodyB是敌机仍是咱们飞机呢?检查文档咱们会发现有这么一段描述,咱们能够通过给场景中的每个物理体,设置categoryBitMask(物理体的标识符)contactTestBitMask(标记可与哪一类的物理体发生磕碰)特点

SpriteKit之飞机大战

8.4、在整个游戏中,咱们创立了有飞机机发射的子弹敌机bossboss发射的子弹攻击力包血包这几个物理体

其间飞机飞机发射的子弹咱们设置categoryBitMask1contactTestBitMask2
那么对应的敌机bossboss发射的子弹攻击力包血包咱们设置categoryBitMask2contactTestBitMask1

8.5、判别出bodyAbodyB别离代表什么

extension PlanScene: SKPhysicsContactDelegate {
  func didBegin(_ contact: SKPhysicsContact) {
        // 飞机、机发射的子弹
        var planeNode: SKSpriteNode?
        // 敌机、boss、boss发射的子弹、攻击力包和血包
    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
    }
    }
}

区分好bodyAbodyB别离代表什么之后,咱们就开始做相应处理

8.5、处理节点磕碰

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 }
        // 增加生命
        if enemyNode.name == "plan_potion", planeNode.name == "plan" {
            ownLife += 1
            enemyNode.removeAllActions()
            enemyNode.removeFromParent()
            return
        }
        // 增加攻击力
        if enemyNode.name == "plan_attack", planeNode.name == "plan" {
            aggressivity += 10
            enemyNode.removeAllActions()
            enemyNode.removeFromParent()
            return
        }
        // 处理飞机
        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
        }
        // 假如是boss发射的子弹,则不做处理
        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
            }
        }
    }
}

SpriteKit之飞机大战

基本功能已经完成了,下面咱们就来增加磕碰作用

8.6、增加磕碰作用
磕碰作用咱们运用SKEmitterNode来做,然后在磕碰的节点处显示出来。咱们挑选SpriteKit Particle File来新建一个目标销毁时的作用Blast.sks和击打boss发生的作用Strike.sks文件

SpriteKit之飞机大战
然后别离选中两个文件,设置相应参数,我这边是随意设置的就不多说了。
依据这两个文件名称来初始化,并在相应的位置加载

private func blast(_ position: CGPoint, fileName: String) {
    if let blast = SKEmitterNode(fileNamed: fileName) {
        blast.zPosition = 2
        blast.position = position
        addChild(blast)
        // 0.3秒后消失
        blast.run(SKAction.sequence([
            SKAction.wait(forDuration: 0.3),
            SKAction.run {
                blast.removeAllActions()
                blast.removeFromParent()
            }
        ]))
    }
}

最后作用就入顶部展现的那样

最后

整个游戏到此就结束了,快来秀出你的战绩吧