Simple Games Using SpriteKit

在ios7,苹果引入了SpriteKit,一个高性能渲染2D的框架。不像中心库(专注于画图)或中心动画(专注于动画过度),SpriteKit专注于不同领域-video games,它是苹果首次涉足ios的图形游戏编程的时代。在发布ios7盒OS X10.9(Mavericks.   2013年WWDC发布)的同时,为了写程序更为简单提供了相同的API在两个平台,尽管苹果从未像SpriteKit提供了一个框架,它有明显的相似之处是Cocos2D等各种开源库。如果你使用的是Cocos2D或类似的过去,你会感觉很熟悉。

现在创建一个工程并选择Game template名为:TextShooter 

(sks文件只是标准的归档文件,你可以用NSKeyedUnarchiver和NSKeyedArchiver类来写和读)

xcode会为你初始化一些方法例如:

 override func viewDidLoad() {
        super.viewDidLoad()
        
        if let view = self.view as! SKView? {
            // 初始化'GameScene.sks'
            if let scene = SKScene(fileNamed: "GameScene") {
                // 让缩放比例填充整个窗口Set the scale mode to scale to fit the window
                scene.scaleMode = .aspectFill
                
                // 加载这个场景(新场景取代旧场景)
                view.presentScene(scene)
            }
            //当运行时,忽视父子类的关系
            view.ignoresSiblingOrder = true
            //在右下角显示FPS的值
            view.showsFPS = true
       //在右下角显示结点(node)的个数 view.showsNodeCount = true } }

 了解完xcode自动初始化的代码,接下来我们自己手动初始化我们自己想要的,选择GameScence.swift,我们不需要didMoveToView()这个方法,现在改成:

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touch in touches {
            let location = touch.location(in: self)//获取当前位置
        }
    }

xcode自带一个GameScene.sks,里面没有我们想要的属性,所以得自己创建,需要添加属性为当前游戏等级数,生活玩家的数量,一个标志,让我们知道等级是否完成,修改GameScene.swift:    private var levelNumber: Int //等级制度    private var playerLives: Int //玩家血    private var finished = false //当前游戏是否结束    class func scene(size:CGSize, levelNumber:Int) -> GameScene {        return GameScene(size: size, levelNumber: levelNumber)}

override convenience init(size:CGSize) {
        self.init(size: size, levelNumber: 1)
}
/*
创建名为SKLabelNode类的两个实例,并选择一个字体,设置一个文本值,指定一些对齐
*/ init(size:CGSize, levelNumber:Int) { self.levelNumber
= levelNumber self.playerLives = 5 super.init(size: size) backgroundColor = SKColor.lightGray() let lives = SKLabelNode(fontNamed: "Courier")//指定字体 lives.fontSize = 16 lives.fontColor = SKColor.black() lives.name = "LivesLabel" lives.text = "Lives: (playerLives)" lives.verticalAlignmentMode = .top lives.horizontalAlignmentMode = .right lives.position = CGPoint(x: frame.size.width, y: frame.size.height) addChild(lives) let level = SKLabelNode(fontNamed: "Courier") level.fontSize = 16 level.fontColor = SKColor.black() level.name = "LevelLabel" level.text = "Level (levelNumber)" level.verticalAlignmentMode = .top level.horizontalAlignmentMode = .left level.position = CGPoint(x: 0, y: frame.height) addChild(level) } required init?(coder aDecoder: NSCoder) { levelNumber = aDecoder.decodeInteger(forKey: "level") playerLives = aDecoder.decodeInteger(forKey: "playerLives") super.init(coder: aDecoder) }
/*
required的使用规则:
required修饰符只能用于修饰类初始化方法
当子类含有异于父类的初始化方法时(初始化方法参数类型和数量异于父类),子类必须要实现父类的required初始化方法,并且也要使用required修饰符而不是override
当子类没有初始化方法时,可以不用实现父类的required初始化方法
*/
override func encode(with aCoder: NSCoder) { aCoder.encode(Int(levelNumber), forKey: "level") aCoder.encode(playerLives, forKey: "playerLives") }
/*
我们给每个label命名,是因为init(coder:)和encode(with aCoder:)方法需要,所有SpriteKit结点,包括SKScene都遵循NSCoding协议
*/

 我们配置了两个SKLabelNode,是时候让它们现身了,选择GameView.swift并添加以下代码:

override func viewDidLoad() {
        super.viewDidLoad()
        let scene = GameScene(size: view.frame.size, levelNumber: 1)
        
        //configure the view
        let skView = self.view as! SKView
        skView.showsFPS = true
        skView.showsNodeCount = true
        
        //Sprite Kit applies additional optimizations to improve rendering performance
        skView.ignoresSiblingOrder = true
        
        //Set the scale mode to scale to fit the window
        scene.scaleMode = .aspectFill
        
        skView.presentScene(scene)
    }

现在你可以顺便了解下override var prefersStatusBarHidden,return true就是状态栏隐藏,反之。现在可以运行下,正常的结果:

背景有了,接下来可以添加一些互动了,毕竟是游戏,我们先添加一个发射子弹的头部,创建Cocoa Touch class 并以SKNode为父类,命名为playerNode,添加一下代码:

import SpriteKit

class PlayerNode: SKNode {
    override init() {
        super.init()
        name = "Player (self)"
        initNodeGraph() //初始化一个结点,内容为"^"(将V旋转180度)作为发射子弹的头部
        
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    private func initNodeGraph() {
        let label = SKLabelNode(fontNamed: "Courier") //指定字体
        label.fontColor = SKColor.blue
        label.fontSize = 40
        label.text = "v"
        label.zRotation = CGFloat(Double.pi) //绕z轴旋转180度
        label.name = "label"
        self.addChild(label)
    }
}

跟刚才添加level,lives一样,在GameScene,swift里实例化(实现)playerNode:

在addChild(level)后面加上这两行:

        playerNode.position = CGPoint(x: frame.midX, y: frame.height * 0.1)

        addChild(playerNode)

现在运行你就会看到如下场景:

现在我们来讨论如何用手指来移动它,这边插个题外话,在web前端里面坐标轴是已左上角为基准,但在SpriteKit我测试了下,添加一个结点并设置position(x:0,y:0),效果如下图所示:

所以这里的坐标是以左下角为基准。

我们假设当手指在屏幕下方的0.2部分(以下)滑动的时候就是有意要让发射器移动,下面这段代码就是这个意思:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touch in touches {
            let location = touch.location(in: self)
            if location.y < frame.height * 0.2 {
                let target = CGPoint(x: location.x, y: playerNode.position.y)
                playerNode.moveToward(target) //移动到手指当前位置
            }
        }
    }

并且在playerNode类添加:

    func moveToward(_ location: CGPoint) {
        removeAction(forKey: "movement")
        let distance = pointDistance(position, location) //计算当前位置和发射器位置的直线距离
        let screenWidth = UIScreen.main.bounds.size.width 
        let duration = TimeInterval(2 * distance/screenWidth) //转换为时间间隔专用的单位,例如:毫秒
        run(SKAction.move(to: location, duration: duration),
            withKey:"movement")//duration:指定移动过程需要的时间,可以自己指定
}

可以发现上面的pointDistance()并没有定义,这边可以创建一个swift文件专门放置计算点或向量之类的算法,我的算法代码如下:

import UIKit

// Takes a CGVector and a CGFLoat.
// 返回一个新向量(旧向量的x,y分量乘以参数CGPoint)
func vectorMultiply(_ v: CGVector, _ m: CGFloat) -> CGVector {
    return CGVector(dx: v.dx * m, dy: v.dy * m)
}
// Takes two CGPoints.
// Returns a CGVector representing a direction from p1 to p2.

func vectorBetweenPoints(_ p1: CGPoint, _ p2: CGPoint) -> CGVector {
    return CGVector(dx: p2.x - p1.x, dy: p2.y - p1.y)
}
// Takes a CGVector.
// Returns a CGFloat containing the length of the vector, calculated using
// Pythagoras' theorem.
//√(x^2+y^2)
func vectorLength(_ v: CGVector) -> CGFloat {
    return CGFloat(sqrtf(powf(Float(v.dx), 2) + powf(Float(v.dy), 2)))
}
// Takes two CGPoints. Returns a CGFloat containing the distance between them,
// calculated with Pythagoras' theorem.
//√(x^2+y^2)
func pointDistance(_ p1: CGPoint, _ p2: CGPoint) -> CGFloat {
    return CGFloat(
        sqrtf(powf(Float(p2.x - p1.x), 2) + powf(Float(p2.y - p1.y), 2)))
}

现在运行可以用手指轻触屏幕下方可以移动发射器了(当然在屏幕下方的0.2部分),并且移动速度也还不错,但在移动的过程中这个发射器什么都不会做,我们可以给它添加一些动作,翻转什么的:

func moveToward(_ location: CGPoint)
    {
        removeAction(forKey: "movement")
        
        let distance = pointDistance(position, location)
        let screenWidth = UIScreen.main.bounds.size.width
        let duration = TimeInterval(2 * distance/screenWidth) //转换为时间间隔专用的单位,例如:毫秒
        run(SKAction.move(to: location, duration: duration),
            withKey:"movement")                               //duration:指定移动过程需要的时间,可以自己指定
        
        
        let wobbleTime = 0.3
        let halfWobbleTime = wobbleTime/2
        let wobbling = SKAction.sequence([
            SKAction.scaleX(to: 0.2, duration: halfWobbleTime),
            SKAction.scaleX(to: 1.0, duration: halfWobbleTime)
            ])//接收一个action队列(数组)
        let wobbleCount = Int(duration/wobbleTime)
        //当duration大于wobbleTime时才会大于1,才会执行,所以当距离比较近的时候是不会旋转的,这个可以自由发挥
        run(SKAction.repeat(wobbling, count: wobbleCount), withKey: "wobbling")
    
    }

现在运行的效果就比较好看一点,现在改添加一些敌人了,创建一个父类为SKNode,命名为:EnemyNode,并添加以下代码:

import SpriteKit

class EnemyNode: SKNode {
        override init() {
            super.init()
            name = "Enemy (self)"
            initNodeGraph()
        }
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
        }
        private func initNodeGraph() {
            let topRow = SKLabelNode(fontNamed: "Courier-Bold")
            topRow.fontColor = SKColor.brown
            topRow.fontSize = 20
            topRow.text = "x x"
            topRow.position = CGPoint(x: -20, y: 15)//为了不显示在屏幕所以设定-20
            addChild(topRow)
            
            let middleRow = SKLabelNode(fontNamed: "Courier-Bold")
            middleRow.fontColor = SKColor.brown
            middleRow.fontSize = 20
       middleRow.position = CGPoint(x: -20, y: 0)
middleRow.text = "x" addChild(middleRow) let bottomRow = SKLabelNode(fontNamed: "Courier-Bold") bottomRow.fontColor = SKColor.brown bottomRow.fontSize = 20 bottomRow.text = "x x" bottomRow.position = CGPoint(x: -20, y: -15) addChild(bottomRow)
       //三个SKLabelNode构成一个敌人
        }
}

跟刚才一样在GameScene.swift加载该结点,并为这个结点设置一个函数随机生成x,y坐标来来生成敌人:

在addChild(playerNode)后面添加

spawnEnemies()//随机生成敌人

addChild(enemies)

private func spawnEnemies() {                      
        let count = Int(log(Float(levelNumber))) + levelNumber
        for _ in 0..<count {
            let enemy = EnemyNode()
            let size = frame.size;
            let x = arc4random_uniform(UInt32(size.width * 0.8))
                + UInt32(size.width * 0.1)                       //随机生成x坐标,范围0.1屏幕宽度~0.8屏幕宽度
            let y = arc4random_uniform(UInt32(size.height * 0.5))
                + UInt32(size.height * 0.5)                      //随机生成y坐标,范围0.5屏幕高度~0.5屏幕高度
            enemy.position = CGPoint(x: CGFloat(x), y: CGFloat(y))
            enemies.addChild(enemy)
        }
    }

发射器有了,敌人也有了,现在该弄子弹了,创建一个BulletNode继承于SKNode:

//
//  BulletNode.swift
//  otherGame
//
//  Created by 陈金伙 on 2017/4/8.
//  Copyright © 2017年 cjh. All rights reserved.
//

import SpriteKit
class BulletNode: SKNode {
    var thrust:CGVector = CGVector(dx: 0, dy: 0)
    
    override init() {
        super.init()
        let dot = SKLabelNode(fontNamed: "Courier")
        dot.fontColor = SKColor.black
        dot.fontSize = 40
        dot.text = "."
        addChild(dot)
        let body = SKPhysicsBody(circleOfRadius: 1)
        body.isDynamic = true
        body.categoryBitMask = PlayerMissileCategory //用于定义物理主体所属的类别
        body.contactTestBitMask = EnemyCategory      //一个掩码,定义哪些类别的物体引起与这个物理体的交集通
        body.collisionBitMask = EnemyCategory        //定义哪些类别的物理机构可以与这个物理体碰撞
        body.fieldBitMask = GravityFieldCategory     //定义哪些类别的物理领域可以施加力量在这个物理机构
        body.mass = 0.01                             //以千克为单位的物体
        physicsBody = body
        name = "Bullet (self)"
        
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        let dx = aDecoder.decodeFloat(forKey: "thrustX")
        let dy = aDecoder.decodeFloat(forKey: "thrustY")
        thrust = CGVector(dx: CGFloat(dx), dy: CGFloat(dy))
    }
    
    override func encode(with aCoder: NSCoder) {
        super.encode(with: aCoder)
        aCoder.encode(Float(thrust.dx), forKey: "thrustX")
        aCoder.encode(Float(thrust.dy), forKey: "thrustY")
    }
    
    class func bullet(from start: CGPoint, toward destination: CGPoint) -> BulletNode {
        let bullet = BulletNode()
        bullet.position = start
        let movement = vectorBetweenPoints(start, destination)      //差的向量
        let magnitude = vectorLength(movement)                      //两点之间的距离
        let scaledMovement = vectorMultiply(movement, 1/magnitude)  //缩放向量
        let thrustMagnitude = CGFloat(100.0)
        bullet.thrust = vectorMultiply(scaledMovement, thrustMagnitude)//扩大向量,无论屏幕多大都能发射到
        bullet.run(SKAction.playSoundFileNamed("shoot.wav",
                                               waitForCompletion: false))
        return bulle
    }
    
    func applyRecurringForce() {
        physicsBody!.applyForce(thrust)                             //对物理体的重心施加力量,如果没有这个函数,发射的子弹会向下滑
    }


}

同理的向GameScene.swift添加

private let playerBullets = SKNode()

addChild(playerbullets)

并在

touchesBegan()中添加else(默认不移动发射器就是发射子弹)

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touch in touches {
            let location = touch.location(in: self)
            if location.y < frame.height * 0.2 {
                let target = CGPoint(x: location.x, y: playerNode.position.y)
                playerNode.moveToward(target) //移动到手指当前位置
            }
            else {                            //没有移动发射器就默认发射子弹
                let bullet = BulletNode.bullet(from: playerNode.position, toward: location)//从发射器当前位置发射到手指的位置
                playerBullets.addChild(bullet)
            }
        }
    }

现在我们考虑当子弹飞出屏幕时可以让它消失(从内存中撤销)在GameScene的update()添加:

override func update(_ currentTime: TimeInterval) {
        updatebullets()
    }
    private func updatebullets() {
        var bulletsToRemove:[BulletNode] = []
        for bullet in playerBullets.children as! [BulletNode] {
            
            if !frame.contains(bullet.position) {
                // 当子弹离开屏幕时放入一个数组
                bulletsToRemove.append(bullet)
                continue
            }
            // 对物理体的重心施加力量
            bullet.applyRecurringForce()
        }
        playerBullets.removeChildren(in: bulletsToRemove)
    }

 现在的子弹遇到敌人并没有攻击性,因为我们只给BulletNode指定物理性质,现在该轮到PlayerNode,EnemyNode了,选择EnemyNode,并添加以下代码:

 private func initPhysicsBody() {
            let body = SKPhysicsBody(rectangleOf: CGSize( 40, height: 40))
            body.affectedByGravity = false
            body.categoryBitMask = EnemyCategory
            body.contactTestBitMask = PlayerCategory | EnemyCategory  //发射器和子弹可以与之碰撞
            body.mass = 0.2           //本身重量
            body.angularDamping = 0   //阻力
            body.linearDamping = 0
            body.fieldBitMask = 0
            physicsBody = body
        }

并在init()里添加刚才我们加的initPhysicsBody(),同理,在PlayerNode添加以下代码:

 private func initPhysicsBody() {
        let body = SKPhysicsBody(rectangleOf: CGSize( 20, height: 20))
        body.affectedByGravity = false
        body.categoryBitMask = PlayerCategory
        body.contactTestBitMask = EnemyCategory
        body.collisionBitMask = 0
        body.fieldBitMask = 0
        physicsBody = body
}

并在init()添加initPhysicsBody(),现在可以运行试试看效果,当你把屏幕唯一的敌人打掉之后,你就应该想到下一步该设定升级了,当我们把敌人打出屏幕时,也应该像子弹那样从内存中移除,在GameScene添加以下代码:

private func updateEnemies() {
        var enemiesToRemove:[EnemyNode] = []
        for node in enemies.children as! [EnemyNode] {
            if !frame.contains(node.position) {
                enemiesToRemove.append(node)
                continue
            }
        }
        enemies.removeChildren(in: enemiesToRemove)
    }

并更新update()函数的内容:

 override func update(_ currentTime: TimeInterval) {
        if finished {
            return
        }
        updatebullets()
        updateEnemies()
        checkForNextlevel()
    }
    private func checkForNextlevel() {  //查看是否还有敌人存活
        if enemies.children.isEmpty {
            goToNextLevel()
        }
    }
    
    private func goToNextLevel() {     //进入下一级
        finished = true
        
        let label = SKLabelNode(fontNamed: "Courier")
        label.text = "Level Complete!"
        label.fontColor = SKColor.blue
        label.fontSize = 32
        label.position = CGPoint(x: frame.size.width * 0.5, y: frame.size.height * 0.5)
        addChild(label)
        
        let nextLevel = GameScene(size: frame.size, levelNumber: levelNumber + 1) //等级不断增加
        nextLevel.playerLives = playerLives                       //生命值不变
        view!.presentScene(nextLevel, transition: SKTransition.flipHorizontal(withDuration: 1.0))
    }

这个游戏里的每个结点都是模拟现实物理的,所以我们还要考虑当我们用子弹打到第一个敌人时,敌人会被打飞,被打飞的过程中或许会碰撞到另一个敌人又或许会随重力下滑,掉落到发射器上,即玩家生命值减一操作,这就需要委托了,因为委托事件里面有contact事件对当前阶段很好用,添加

class GameScene: SKScene, SKPhysicsContactDelegate {

并在init(size:CGSize, levelNumber:Int) { 添加

physicsWorld.gravity = CGVector(dx: 0, dy: -1) //设置重力向下

physicsWorld.contactDelegate = self

这边我们可以先想想碰撞的特效xcode提供自带的文件,创建SpriteKit partical file命名MissleExplosion,并在inspector属性进行调整,这边是我的(随便调的,自由发挥):

同理再创建一个命名为EnemyExplosion,并在inspector属性进行调整(自由发挥)

选择GameScene添加以下代码:

func didBegin(_ contact: SKPhysicsContact) {
        if contact.bodyA.categoryBitMask == contact.bodyB.categoryBitMask {
            //一样的种类
            let nodeA = contact.bodyA.node!
            let nodeB = contact.bodyB.node!
        }
        else {
            var attacker: SKNode
            var attackee: SKNode
            
            if contact.bodyA.categoryBitMask > contact.bodyB.categoryBitMask {//种类的大小,下面有给图说明
                // A attack B
                attacker = contact.bodyA.node!
                attackee = contact.bodyB.node!
                
            }
            else {
                //B attack A
                attacker = contact.bodyB.node!
                attackee = contact.bodyA.node!
            }
            if attackee is PlayerNode {
                playerLives -= 1
            }
            
            //What do we do with the attacker and the attackee?
            attackee.receiveAttacker(attacker, contact: contact)//扩展类的方法,下面有给
            playerBullets.removeChildren(in: [attacker])
            enemies.removeChildren(in: [attacker])
            
        }
    }

四种大小分别代表不同的种类,在我们给他们的physicBody初始化时就有给他们指定,接下来扩展SKnode类,为什么要扩展SKNode?,因为在SpriteKit每个对象都是一个结点,所以扩展SKNode,可以对敌人,发射器,子弹都好操作,新建一个swift file命名SKNode+Extra并添加以下代码:

import SpriteKit

extension SKNode {
    func receiveAttacker(_ attacker: SKNode, contact: SKPhysicsContact)
    {
        // Default implementation does nothing
        
        physicsBody!.affectedByGravity = true
        let force = vectorMultiply(attacker.physicsBody!.velocity, contact.collisionImpulse)
        
        let myContact = scene!.convert(contact.contactPoint, to: self)
        physicsBody!.applyForce(force, at: myContact)
        
        let path  = Bundle.main.path(forResource: "MissileExplosion", ofType: "sks")
        
        let explosion = NSKeyedUnarchiver.unarchiveObject(withFile: path!)
            as! SKEmitterNode
        explosion.numParticlesToEmit = 20           //默认为0,无限粒子,这边指定20颗粒子
        explosion.position = contact.contactPoint  //在子弹击中的部位出现粒子
        scene!.addChild(explosion)
    }
    
    func friendlyBumpFrom(_ node: SKNode) {
        // Default implementation does nothing
        physicsBody!.affectedByGravity = true
        
    }
}

现在运行你会发现一切都良好,就是尽管敌人掉落到发射器上,玩家的血是没有扣的,我记得明明有添加

if attackee is PlayerNode {

                playerLives -= 1

            }

 可是不起作用,其实是有起作用的,不信你可以调试下在后台输出playerLives,只是没有实时更新到界面,

    private var playerLives: Int {

        didSet {

            let lives = childNode(withName: "LivesLabel") as! SKLabelNode

            lives.text = "Lives: (playerLives)"

        }

    }

更改私有属性变成属性观察者,一旦playerlives有变化就执行didSet里面的代码,现在可以了,但是生命值会一直减,没有尽头的,就像是无敌模式,是时候给这个游戏来个收尾了,

创建cocoa touch class命名GameOverScene,并以SKScene为父类,添加以下代码:

import SpriteKit

class GameOverScene: SKScene {
    override init(size: CGSize) {
        super.init(size: size)
        backgroundColor = SKColor.purple
        let text = SKLabelNode(fontNamed: "Courier")
        text.text = "Game Over"
        text.fontColor = SKColor.white
        text.fontSize = 50
        text.position = CGPoint(x: frame.size.width/2, y: frame.size.height/2)
        addChild(text)
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
}

这就是结束界面,在GameScene里面来实现它:

private func triggerGameOve() {
        finished = true
        
        let path = Bundle.main.path(forResource:"EnemyExplosion",
                                    ofType: "sks")
        let explosion = NSKeyedUnarchiver.unarchiveObject(withFile: path!)
            as! SKEmitterNode
        explosion.numParticlesToEmit = 200      //当生命值为0时,爆炸变的更大
        explosion.position = playerNode.position
        scene!.addChild(explosion)
        playerNode.removeFromParent()
        
        let transition = SKTransition.doorsOpenVertical(withDuration: 1)
        let gameOver = GameOverScene(size: frame.size)
        view!.presentScene(gameOver, transition: transition)
    }
    
    private func checkForGame() -> Bool { //添加到update(),实时监测
        if playerLives == 0 {
            triggerGameOve()
            return true
        }
        return false
    }

    override func update(_ currentTime: TimeInterval) {
        if finished {
            return
        }
        updatebullets()
        updateEnemies()
        if (!checkForGame()) {
            checkForNextlevel()
        }
        
    }

现在基本可以完了,要是想要美观的话可以搞个开始界面,这边就不搞了。

 要下载全部的代码请到我的github库:https://github.com/TypeInfos/SpriteKit-Game

原文地址:https://www.cnblogs.com/doudoublog/p/6679514.html