轉發自:https://www.jianshu.com/p/304e84a12b91
Spritekit是iOS 7之後蘋果官方推出的2D遊戲開發框架,最近利用業餘時間認真學習了這方面的知識,並利用網上資源及教程用Swift語言仿寫了一個以前比較火的小遊戲FlappyBird。
1.準備
新建一個Project項目,模板選擇Game,語言選擇Swift,開發庫選擇SpriteKit
刪除xcode自動創建的實例文件GameScene.sks和Actions.sks,把GameViewController.swift中viewDidLoad方法中的代碼替換爲下面的代碼
if let view = self.view as! SKView? {
let scene = GameScene(size:view.bounds.size)
scene.scaleMode = .aspectFill
view.presentScene(scene)
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
刪除GameScene.swift中的代碼,留下didMove和update方法,在didMove方法中添加背景顏色,如下
override func didMove(to view: SKView) {
self.backgroundColor = SKColor(red: 81.0/255.0, green: 192.0/255.0, blue: 201.0/255.0, alpha: 1.0)
}
這時候就可以運行代碼了
導入資源文件
新建一個.atlas爲後綴的文件,放入小鳥的圖片,然後將這個文件夾拖到工程中。然後把其他圖片放在Asserts.xcasserts裏就可以了。
注:因爲當你把一類相關的貼圖圖片素材放在一個.atlas文件夾裏,編譯程序的時候Xcode會把這個文件夾裏的圖片都導入“紋理圖集”裏,相對於只用獨立的圖片文件而言,使用紋理圖集會非常顯著地提升遊戲的渲染性能。
部分xcode版本,加載.atlas中的圖片時,提示 SKTexture: Error loading image resource: "bird-01.png"
是因爲: Xcode在直接Add Files to Project 圖片文件的時候,沒有自動將其添加到編譯資源文件中,需要去項目中(Build Phases)手動添加資源文件(copy Bundle Resource)。
到此,準備工作結束~
2.佈置場景
切換到GameScene.swift類中,添加變量
/// 小鳥精靈
var bird :SKSpriteNode!
然後再在didMove()方法裏添加下面的代碼
// 地面
let groundTexture = SKTexture(imageNamed: "land")
groundTexture.filteringMode = .nearest
for i in 0..<2 + Int(self.frame.size.width / (groundTexture.size().width * 2)) {
let i = CGFloat(i)
let sprite = SKSpriteNode(texture: groundTexture)
sprite.setScale(2.0)
// SKSpriteNode的默認錨點爲(0.5,0.5)即它的中心點。
sprite.anchorPoint = CGPoint(x: 0, y: 0)
sprite.position = CGPoint(x: i * sprite.size.width, y: 0)
self.addChild(sprite)
}
// 天空
let skyTexture = SKTexture(imageNamed: "sky")
skyTexture.filteringMode = .nearest
for i in 0..<2 + Int(self.frame.size.width / (skyTexture.size().width * 2)) {
let i = CGFloat(i)
let sprite = SKSpriteNode(texture: skyTexture)
sprite.setScale(2.0)
sprite.zPosition = -20
sprite.anchorPoint = CGPoint(x: 0, y:0)
sprite.position = CGPoint(x: i * sprite.size.width, y:groundTexture.size().height * 2.0)
moving.addChild(sprite)
}
//小鳥
bird = SKSpriteNode(imageNamed: "bird-01")
bird.setScale(1.5)
bird.position = CGPoint(x: self.frame.size.width * 0.35, y: self.frame.size.height * 0.6)
addChild(bird)
運行代碼,如下圖所示:
3.精靈動起來
由於地面移動和天空移動類似,都是兩個圖片交替出現,只是速度不同,所以我們封裝一個方法,分別傳入對應的精靈及時間。如下:
//陸地及天空移動動畫
func moveGround(sprite:SKSpriteNode,timer:CGFloat) {
let moveGroupSprite = SKAction.moveBy(x: -sprite.size.width, y: 0, duration: TimeInterval(timer * sprite.size.width))
let resetGroupSprite = SKAction.moveBy(x: sprite.size.width, y: 0, duration: 0.0)
//永遠移動 組動作
let moveGroundSpritesForever = SKAction.repeatForever(SKAction.sequence([moveGroupSprite,resetGroupSprite]))
sprite.run(moveGroundSpritesForever)
}
然後在上面代碼中,添加精靈之前,就是在 self.addChild(sprite)之前,分別加入精靈動作
// 地面
self.moveGround(sprite: sprite, timer: 0.02)
//天空
self.moveGround(sprite: sprite, timer: 0.1)
之後是我們的主要對象小鳥精靈的活靈活現~
/// 小鳥飛的動畫
func birdStartFly() {
let birdTexture1 = SKTexture(imageNamed: "bird-01")
birdTexture1.filteringMode = .nearest
let birdTexture2 = SKTexture(imageNamed: "bird-02")
birdTexture2.filteringMode = .nearest
let birdTexture3 = SKTexture(imageNamed: "bird-03")
birdTexture3.filteringMode = .nearest
let anim = SKAction.animate(with: [birdTexture1,birdTexture2,birdTexture3], timePerFrame: 0.2)
bird.run(SKAction.repeatForever(anim), withKey: "fly")
}
/// 小鳥停止飛動畫
func birdStopFly() {
bird.removeAction(forKey: "fly")
}
運行效果圖如下,看小鳥是不是真的飛起來了,哈哈:
4.隨機創建管道
首先添加變量
/// 豎直管缺口
let verticalPipeGap = 150.0;
/// 向上管紋理
var pipeTextureUp:SKTexture!
/// 向下管紋理
var pipeTextureDown:SKTexture!
/// 儲存所有上下管道
var pipes:SKNode!
實現上下管道的隨機創建及消失,需要下面四個方法:
- 方法creatSpawnPipes() 創建具體某一次某一對水管的方法
- 方法startCreateRandomPipes() 開始隨機重複創建水管的動作方法
- 方法stopCreateRandomPipes() 停止創建水管的動作方法
- 方法removeAllPipesNode() 移除所有正在場景裏的水管
///創建一對水管
func creatSpawnPipes() {
// 管道紋理
pipeTextureUp = SKTexture(imageNamed: "PipeUp")
pipeTextureUp.filteringMode = .nearest
pipeTextureDown = SKTexture(imageNamed: "PipeDown")
pipeTextureDown.filteringMode = .nearest
let pipePair = SKNode()
pipePair.position = CGPoint(x: self.frame.size.width + pipeTextureUp.size().width * 2, y: 0)
// z值的節點(用於排序)。負z是”進入“屏幕,正面z是“出去”屏幕。
pipePair.zPosition = -10;
// 隨機的Y值
let height = UInt32(self.frame.size.height / 5)
let y = Double(arc4random_uniform(height) + height)
let pipeDown = SKSpriteNode(texture: pipeTextureDown)
pipeDown.setScale(2.0)
pipeDown.position = CGPoint(x: 0.0, y: y + Double(pipeDown.size.height)+verticalPipeGap)
pipePair.addChild(pipeDown)
let pipeUp = SKSpriteNode(texture: pipeTextureUp)
pipeUp.setScale(2.0)
pipeUp.position = CGPoint(x: 0.0, y: y)
pipePair.addChild(pipeUp)
// 管道移動動作
let distanceToMove = CGFloat(self.frame.size.width + 2.0*pipeTextureUp.size().width)
let movePipes = SKAction.moveBy(x: -distanceToMove, y: 0.0, duration: TimeInterval(0.01 * distanceToMove))
let removePipes = SKAction.removeFromParent()
let movePipesAndRemove = SKAction.sequence([movePipes,removePipes])
pipePair.run(movePipesAndRemove)
pipes.addChild(pipePair)
}
/// 隨機 創建
func startCreateRandomPipes() {
let spawn = SKAction.run {
self.creatSpawnPipes()
}
let delay = SKAction.wait(forDuration: TimeInterval(2.0))
let spawnThenDelay = SKAction.sequence([spawn,delay])
let spawnThenDelayForever = SKAction.repeatForever(spawnThenDelay)
self.run(spawnThenDelayForever, withKey: "createPipe")
}
///停止創建管道
func stopCreateRandomPipes() {
self.removeAction(forKey: "createPipe")
}
/// 移除所有已經存在的上下管
func removeAllPipesNode() {
pipes.removeAllChildren()
}
寫到這裏,我們已經完成了這個遊戲效果的一半了,但是細心的你會發現,點擊屏幕無效果,小鳥不受重力影響下落,小鳥與水管相撞沒反應等。。。
那麼牽扯出了下一部分,物理世界~
5.物理世界
然後再在didMove()方法裏,背景色代碼下面,添加下面的代碼
//給場景添加一個物理體,限制了遊戲範圍,確保精靈不會跑出屏幕。
self.physicsBody = SKPhysicsBody(edgeLoopFrom: self.frame)
//設置重力
self.physicsWorld.gravity = CGVector(dx: 0.0, dy: -3.0)
//物理世界的碰撞檢測代理爲場景自己
self.physicsWorld.contactDelegate = self;
然後,讓GameScene這個類遵守下面的代理協議SKPhysicsContactDelegate
協議方法待會再講,先聲明我們要用到的幾個物理體
/// 設置物理體的標示符
let birdCategory: UInt32 = 1 << 0 //1
let worldCategory: UInt32 = 1 << 1 //2
let pipeCategory: UInt32 = 1 << 2 //4
在添加地面的代碼後面加上:
// 配置陸地物理體
let ground = SKNode()
ground.position = CGPoint(x: 0, y: groundTexture.size().height)
ground.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: self.frame.size.width, height: groundTexture.size().height * 2.0))
ground.physicsBody?.isDynamic = false
//當前物理體
ground.physicsBody?.categoryBitMask = worldCategory
self.addChild(ground)
在添加小鳥的代碼後面加上:
// 配置小鳥物理體
bird.physicsBody = SKPhysicsBody(circleOfRadius: bird.size.height / 2.0)
bird.physicsBody?.allowsRotation = false
bird.physicsBody?.categoryBitMask = birdCategory
bird.physicsBody?.contactTestBitMask = worldCategory
找到creatSpawnPipes方法中,添加上下水管代碼之前加入下面的代碼內容:
pipeDown.physicsBody = SKPhysicsBody(rectangleOf: pipeDown.size)
pipeDown.physicsBody?.isDynamic = false
pipeDown.physicsBody?.categoryBitMask = pipeCategory
pipeDown.physicsBody?.contactTestBitMask = birdCategory
pipeUp.physicsBody = SKPhysicsBody(rectangleOf: pipeUp.size)
pipeUp.physicsBody?.isDynamic = false
pipeUp.physicsBody?.categoryBitMask = pipeCategory
pipeUp.physicsBody?.contactTestBitMask = birdCategory
有了物理體之後,我們會發現,運行遊戲後,小鳥會直接受重力影響,掉下來~
接下來,我們就不得不考慮,遊戲的運行狀態了
6.遊戲狀態
1.初始化狀態:小鳥在移動的背景中飛翔而未掉落,無水管出現。
2.運行中狀態:小鳥會受重力作用往下墜落,水管開始出現,點擊一次屏幕,小鳥就有會受一次上升的力。
3.已結束狀態:小鳥碰到水管或地面,遊戲結束,小鳥停止飛的動作,場景裏的水管和地面都停住不動。
在GameScene類中,定義一個枚舉來表示不同的狀態,同時增加一個遊戲狀態的變量
/// 遊戲狀態
enum GameStatus {
case idle /// 初始化
case running /// 遊戲運行中
case over /// 遊戲結束
}
/// 遊戲狀態爲初始狀態
var gameStatus:GameStatus = .idle
實現不同狀態對應的不同方法
func idleStatus() {
gameStatus = .idle
}
func runningStatus() {
gameStatus = .running
}
func overStatus() {
gameStatus = .over
}
實現點擊屏幕對應不同的狀態時,所作出的應對
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
switch gameStatus {
case .idle:
runningStatus()
break
case .running:
for _ in touches {
bird.physicsBody?.velocity = CGVector(dx: 0, dy: 0)
// 施加一個均勻作用於物理體的推力
bird.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 10))
}
break
case .over:
idleStatus()
break
}
}
小鳥:這時候,我們可以把小鳥的位置、受外力影響只爲否屬性和開始飛的方法移到初始化方法裏面
bird.position = CGPoint(x: self.frame.size.width * 0.35, y: self.frame.size.height * 0.6)
// isDynamic的作用是設置這個物理體當前是否會受到物理環境的影響,默認是true
bird.physicsBody?.isDynamic = false
self.birdStartFly()
接着,在運行中狀態,將小鳥受外力影響屬性置爲是
bird.physicsBody?.isDynamic = true
bird.physicsBody?.collisionBitMask = worldCategory | pipeCategory
然後,在結束狀態中,讓小鳥停止飛
birdStopFly()
水管:初始化方法,移除屏幕中上次可能存在的水管
removeAllPipesNode()
在運行中狀態,開始隨機生產水管
startCreateRandomPipes()
已結束狀態,停止隨機創建水管
stopCreateRandomPipes()
現在運行代碼,來看看效果怎麼樣~
我們會看到,只有初始化狀態和運行中狀態,而碰撞之後,並沒有結束,那麼下面我們就來實現一下碰撞協議~
/// SKPhysicsContact對象是包含着碰撞的兩個物理體的,分別是bodyA和bodyB
func didBegin(_ contact: SKPhysicsContact) {
if gameStatus != .running {
return
}
bird.physicsBody?.collisionBitMask = worldCategory
overStatus()
}
這時運行代碼發現,碰撞水管之後,背景及水管還在運動,我們應該讓其停止,我們首先創建變量
/// 儲存陸地、天空和水管
var moving:SKNode!
然後再在didMove()方法裏初始化變量,然後把之前的儲存水管的變量pipes也加到moving裏面
moving = SKNode()
self.addChild(moving)
pipes = SKNode()
moving.addChild(pipes)
把 self.addChild(sprite)
及self.addChild(sprite)
替換爲moving.addChild(sprite)
和moving.addChild(sprite)
在碰撞協議方法裏面,加上讓其速度爲零的屬性
moving.speed = 0
然後在初始化方法裏面,讓其速度恢復
moving.speed = 1
運行效果如下:
7.分數顯示
首先聲明一個分數變量和懶加載一個分時顯示Label
/// 分數
var score: NSInteger = 0
///分數Label
lazy var scoreLabelNode:SKLabelNode = {
let label = SKLabelNode(fontNamed: "MarkerFelt-Wide")
label.zPosition = 100
label.text = "0"
return label
}()
考慮到加分問題,肯定是在小鳥通過管道的時候,才計分+1,所以我們在管道右端加一個物理體,來實現與小鳥碰撞之後的加分。
在添加管道代碼下面添加如下:
let contactNode = SKNode()
contactNode.position = CGPoint(x: pipeDown.size.width, y: self.frame.midY)
contactNode.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: pipeUp.size.width, height: self.frame.size.height))
contactNode.physicsBody?.isDynamic = false
contactNode.physicsBody?.categoryBitMask = scoreCategory
contactNode.physicsBody?.contactTestBitMask = birdCategory
pipePair.addChild(contactNode)
然後在碰撞方法中,判斷是否相撞,通過管道,分數+1
if (contact.bodyA.categoryBitMask & scoreCategory) == scoreCategory || (contact.bodyB.categoryBitMask & scoreCategory) == scoreCategory {
score += 1
print(score)
scoreLabelNode.text = String(score)
scoreLabelNode.run(SKAction.sequence([SKAction.scale(to: 1.5, duration: TimeInterval(0.1)),SKAction.scale(to: 1.0, duration: TimeInterval(0.1))]))
}else{
moving.speed = 0
bird.physicsBody?.collisionBitMask = worldCategory
overStatus()
}
在運行中狀態,顯示分數
// 重設分數
score = 0
scoreLabelNode.text = String(score)
self.addChild(scoreLabelNode)
scoreLabelNode.position = CGPoint(x: self.frame.midX, y: 3 * self.frame.size.height / 4)
初始化狀態,移除分數labelNode
// 移除分數提示
scoreLabelNode.removeFromParent()
這是運行代碼,這款小遊戲就基本完成了
8. 優化
上面提到,我們留下兩個方法,已經用到了didMove() ,那麼update()是做什麼的吶:update()方法爲SKScene自帶的系統方法,在畫面每一幀刷新的時候就會調用一次。
在這裏,我們可以處理讓小鳥掉落的時候臉先着地(233~)
override func update(_ currentTime: TimeInterval) {
//調整頭先着地
let value = bird.physicsBody!.velocity.dy * (bird.physicsBody!.velocity.dy < 0 ? 0.003 : 0.001)
bird.zRotation = min(max(-1, value),0.5)
}
碰撞之後,添加一個背景閃光效果
func bgFlash() {
let bgFlash = SKAction.run({
self.backgroundColor = SKColor(red: 1, green: 0, blue: 0, alpha: 1.0)}
)
let bgNormal = SKAction.run({
self.backgroundColor = self.skyColor;
})
let bgFlashAndNormal = SKAction.sequence([bgFlash,SKAction.wait(forDuration: (0.05)),bgNormal,SKAction.wait(forDuration: (0.05))])
self.run(SKAction.sequence([SKAction.repeat(bgFlashAndNormal, count: 4)]), withKey: "falsh")
self.removeAction(forKey: "flash")
}
添加結束提示語:同樣懶加載一個SKLabelNode
lazy var gameOverLabel:SKLabelNode = {
let label = SKLabelNode(fontNamed: "Chalkduster")
label.text = "Game Over"
return label
}()
在結束狀態中,添加上這個提示語,同時爲了讓遊戲有個緩衝過程,我們讓屏幕2秒內不能點擊
isUserInteractionEnabled = false;
addChild(gameOverLabel)
gameOverLabel.position = CGPoint(x: self.size.width * 0.5, y: self.size.height)
//讓gameOverLabel通過一個動畫action移動到屏幕中間
let delay = SKAction.wait(forDuration: TimeInterval(1))
let move = SKAction.move(by: CGVector(dx: 0, dy: -self.size.height * 0.5), duration: 1)
gameOverLabel.run(SKAction.sequence([delay,move]), completion:{
//動畫結束 允許用戶點擊屏幕
self.isUserInteractionEnabled = true
})
在初始化狀態,移除提示語
// 移除 遊戲結束提示
gameOverLabel.removeFromParent()
OK,現在一款”飛翔的小鳥”簡易款就成型了,可以愉快的玩耍了~
升級款:
1.全新的UI視圖;
2.動聽的音樂組合;
3.計分板和引導提示圖的展示。
資料參考:
SpriteKit初探
SKScene類
AnchorPoint(錨點)
SKSpriteNode
SKLableNode
SKTextureAtlas(紋理集)
SKAction動作
SKSpriteNode拖動
SKAction常用屬性
SKPhysicsBody物理引擎
節點碰撞
SKPhysicsBody的移動和連接
iOS SpriteKit 小遊戲開發實例
FlappySwift