Simple Games Using SpriteKit

时间:2022-06-14 15:07:07

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

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

Simple Games Using SpriteKit(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: )
}
/*
创建名为SKLabelNode类的两个实例,并选择一个字体,设置一个文本值,指定一些对齐
*/
init(size:CGSize, levelNumber:Int) {
self.levelNumber = levelNumber
self.playerLives =
super.init(size: size)
backgroundColor = SKColor.lightGray()
let lives = SKLabelNode(fontNamed: "Courier")//指定字体
lives.fontSize =
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 =
level.fontColor = SKColor.black()
level.name = "LevelLabel"
level.text = "Level \(levelNumber)"
level.verticalAlignmentMode = .top
level.horizontalAlignmentMode = .left
level.position = CGPoint(x: , 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就是状态栏隐藏,反之。现在可以运行下,正常的结果:

Simple Games Using SpriteKit

背景有了,接下来可以添加一些互动了,毕竟是游戏,我们先添加一个发射子弹的头部,创建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 =
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)

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

Simple Games Using SpriteKit

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

Simple Games Using SpriteKit

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

我们假设当手指在屏幕下方的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( * 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), ) + powf(Float(v.dy), )))
}
// 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), ) + powf(Float(p2.y - p1.y), )))
}

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

func moveToward(_ location: CGPoint)
{
removeAction(forKey: "movement") let distance = pointDistance(position, location)
let screenWidth = UIScreen.main.bounds.size.width
let duration = TimeInterval( * distance/screenWidth) //转换为时间间隔专用的单位,例如:毫秒
run(SKAction.move(to: location, duration: duration),
withKey:"movement") //duration:指定移动过程需要的时间,可以自己指定 let wobbleTime = 0.3
let halfWobbleTime = wobbleTime/
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 =
topRow.text = "x x"
topRow.position = CGPoint(x: -20, y: )//为了不显示在屏幕所以设定-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 =
bottomRow.text = "x x"
bottomRow.position = CGPoint(x: -20, y: -)
addChild(bottomRow)
       //三个SKLabelNode构成一个敌人
        }
}

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

在addChild(playerNode)后面添加

spawnEnemies()//随机生成敌人

addChild(enemies)

private func spawnEnemies() {                      
let count = Int(log(Float(levelNumber))) + levelNumber
for _ in ..<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: , dy: ) override init() {
super.init()
let dot = SKLabelNode(fontNamed: "Courier")
dot.fontColor = SKColor.black
dot.fontSize =
dot.text = "."
addChild(dot)
let body = SKPhysicsBody(circleOfRadius: )
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, /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(width: , height: ))
body.affectedByGravity = false
body.categoryBitMask = EnemyCategory
body.contactTestBitMask = PlayerCategory | EnemyCategory //发射器和子弹可以与之碰撞
body.mass = 0.2 //本身重量
body.angularDamping = //阻力
body.linearDamping =
body.fieldBitMask =
physicsBody = body
}

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

 private func initPhysicsBody() {
let body = SKPhysicsBody(rectangleOf: CGSize(width: , height: ))
body.affectedByGravity = false
body.categoryBitMask = PlayerCategory
body.contactTestBitMask = EnemyCategory
body.collisionBitMask =
body.fieldBitMask =
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 =
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 + ) //等级不断增加
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属性进行调整,这边是我的(随便调的,*发挥):

Simple Games Using SpriteKit

同理再创建一个命名为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 -=
} //What do we do with the attacker and the attackee?
attackee.receiveAttacker(attacker, contact: contact)//扩展类的方法,下面有给
playerBullets.removeChildren(in: [attacker])
enemies.removeChildren(in: [attacker]) }
}

Simple Games Using SpriteKit

四种大小分别代表不同的种类,在我们给他们的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 = //默认为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 =
text.position = CGPoint(x: frame.size.width/, y: frame.size.height/)
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 = //当生命值为0时,爆炸变的更大
explosion.position = playerNode.position
scene!.addChild(explosion)
playerNode.removeFromParent() let transition = SKTransition.doorsOpenVertical(withDuration: )
let gameOver = GameOverScene(size: frame.size)
view!.presentScene(gameOver, transition: transition)
} private func checkForGame() -> Bool { //添加到update(),实时监测
if playerLives == {
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