游戲邏輯框架
和上一個游戲不同,這次用中文編寫代碼筐咧,可以讓我這個初學(xué)者鸯旁,更好的理解框架邏輯的組成方式。首先在GameViewController.swift
中創(chuàng)建場景量蕊,只用到兩個方法羡亩,一個定義了場景的基本參數(shù),并傳入SKView
危融;另一個是隱藏手機頂部狀態(tài)欄畏铆。
class GameViewController: UIViewController {
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if let sk視圖 = self.view as? SKView {
if sk視圖.scene == nil {
// 創(chuàng)建場景
let 長寬比 = sk視圖.bounds.size.height / sk視圖.bounds.size.width
let 場景 = GameScene(size:CGSize(width: 320, height: 320 * 長寬比))
sk視圖.showsFPS = true
sk視圖.showsNodeCount = true //顯示場景中節(jié)點數(shù)量,也就是元素數(shù)量
k視圖.showsPhysics = false //顯示物理模型的輪廓
sk視圖.ignoresSiblingOrder = true //忽略加入場景的元素的先后順序
場景.scaleMode = .aspectFill //等比咧縮放
sk視圖.presentScene(場景) //加入視圖
}
}
}
override func prefersHomeIndicatorAutoHidden() -> Bool { //手機頂部的狀態(tài)欄是否隱藏吉殃?
return true
}
}
后來我發(fā)現(xiàn)辞居,最新的Xcode9.0在系統(tǒng)提供的方法中,好像已經(jīng)基本預(yù)設(shè)好了蛋勺。在GameScene.swift
中也預(yù)設(shè)了很多方法瓦灶,不過,還是先全部刪除了抱完,自己慢慢手打一遍贼陶。好了,剩下的代碼會全部在GameScene.swift
中完成巧娱。所有的執(zhí)行代碼都在一個類(class)內(nèi)實現(xiàn)碉怔,執(zhí)行的默認(rèn)代理SKScene
和后加入的SKPhysicsContactDelegate
(物理碰撞代理):
class GameScene: SKScene, SKPhysicsContactDelegate { }
在GameScenc
類里面,在缺省的幾個方法下面:override func didMove
(程序啟動時)禁添、override func touchesBegan
(點擊屏幕時)撮胧、override func update
(程序運行時),要放入相應(yīng)的執(zhí)行方法來實現(xiàn)老翘。
1.在程序啟動時芹啥,需要調(diào)用切換主菜單()
方法:
let 世界單位 = SKNode()
override func didMove(to view: SKView) {
//關(guān)掉重力
physicsWorld.gravity = CGVector(dx: 0, dy: 0)
//設(shè)置碰撞代理
physicsWorld.contactDelegate = self
addChild(世界單位)
切換到主菜單()
}
而在切換主菜單()
中繼續(xù)調(diào)用其它幾個方法:
func 切換到主菜單() {
當(dāng)前的游戲狀態(tài) = .主菜單
設(shè)置背景()
設(shè)置前景()
設(shè)置主菜單()
}
2.在點擊屏幕時鞠绰,用到了switch判斷語句渤滞,在設(shè)定的當(dāng)前游戲狀態(tài)中冀自,分別執(zhí)行不同的動作抬驴,很強大簡潔:
var 當(dāng)前的游戲狀態(tài): 游戲狀態(tài) = .游戲
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let 點擊 = touches.first else {
return
}
let 點擊位置 = 點擊.location(in: self)
switch 當(dāng)前的游戲狀態(tài) {
case .主菜單:
if 點擊位置.y < size.height * 0.15 {
去學(xué)習(xí)()
} else if 點擊位置.x < size.width/2 {
切換到教程狀態(tài)()
} else {
去評價()
}
break
case .教程:
切換到游戲狀態(tài)()
break
case .游戲:
主角飛一下()
break
case .跌落:
break
case .顯示分?jǐn)?shù):
break
case .結(jié)束:
切換到新游戲()
break
}
}
就是在不同狀態(tài)下,當(dāng)你點擊屏幕傀履,你希望能發(fā)生的所有動作疏虫,用switch
判斷語句還能添加更多的狀態(tài)。
3.在程序運行過程中啤呼,同樣也用了switch
:
var 上一次更新時間: TimeInterval = 0
var dt: TimeInterval = 0
override func update(_ 當(dāng)前時間: TimeInterval) {
if 上一次更新時間 > 0 {
dt = 當(dāng)前時間 - 上一次更新時間
} else {
dt = 0
}
上一次更新時間 = 當(dāng)前時間
switch 當(dāng)前的游戲狀態(tài) {
case .主菜單:
break
case .教程:
break
case .游戲:
更新主角()
更新前景()
撞擊障礙物檢查()
撞擊地面檢查()
更新得分()
break
case .跌落:
更新主角()
撞擊地面檢查()
break
case .顯示分?jǐn)?shù):
break
case .結(jié)束:
break
}
}
這就是游戲的大框架卧秘,里面調(diào)用的所有的方法,同樣寫在class
大類中官扣,但在class
之外翅敌,先要定義二個enum
(枚舉)和一個struck
(結(jié)構(gòu)體):
enum 圖層: CGFloat {
case 背景
case 障礙物
case 前景
case 游戲角色
case UI
}
enum 游戲狀態(tài) {
case 主菜單
case 教程
case 游戲
case 跌落
case 顯示分?jǐn)?shù)
case 結(jié)束
}
struct 物理層 {
static let 無: UInt32 = 0 //0二進制
static let 游戲角色: UInt32 = 0b1 //1
static let 障礙物: UInt32 = 0b10 //2
static let 地面: UInt32 = 0b100 //4
}
圖層
枚舉里,系統(tǒng)默認(rèn)由小到大區(qū)分前后順序惕蹄,象ps中的圖層一樣蚯涮,背景在最下面,上面是障礙物卖陵、前景和游戲角色遭顶。定義好了就可以在下面的方法中給背景z坐標(biāo)賦值:
背景.zPosition = 圖層.背景.rawValue
4.然后在class
類中需要定義的常量和變量,用于給方法中參數(shù)賦值泪蔫,當(dāng)然棒旗,也可以在方法中定義,但集中寫在一起撩荣,方便閱讀和修改數(shù)值铣揉。
let k前景地面數(shù) = 2
let k地面移動速度:CGFloat = -150.0
let k重力: CGFloat = -1000.0
let k上沖速度: CGFloat = 300.0
var 速度 = CGPoint.zero
let k底部障礙最小乘數(shù): CGFloat = 0.1
let k底部障礙最大乘數(shù): CGFloat = 0.6
let k缺口乘數(shù): CGFloat = 4.0
let k首次生成障礙延遲: TimeInterval = 1.75
let k每次重生障礙延遲: TimeInterval = 1.5
let k動畫延遲 = 0.3
let k頂部留白: CGFloat = 20.0
let k字體名字 = "AmericanTypewriter-Bold"
var 得分標(biāo)簽: SKLabelNode!
var 當(dāng)前分?jǐn)?shù) = 0
var 撞擊了地面 = false
var 撞擊了障礙物 = false
var 當(dāng)前的游戲狀態(tài): 游戲狀態(tài) = .游戲
let 世界單位 = SKNode()
var 游戲區(qū)域起始點: CGFloat = 0
var 游戲區(qū)域的高度: CGFloat = 0
let 主角 = SKSpriteNode(imageNamed: "Bird0")
var 上一次更新時間: TimeInterval = 0
var dt: TimeInterval = 0
// 創(chuàng)建音效
let 拍打的音效 = SKAction.playSoundFileNamed("flapping.wav", waitForCompletion: false)
let 撞擊地面的音效 = SKAction.playSoundFileNamed("hitGround.wav", waitForCompletion: false)
let 摔倒的音效 = SKAction.playSoundFileNamed("whack.wav", waitForCompletion: false)
let 下落的音效 = SKAction.playSoundFileNamed("falling.wav", waitForCompletion: false)
let 得分的音效 = SKAction.playSoundFileNamed("coin.wav", waitForCompletion: false)
let 乒的音效 = SKAction.playSoundFileNamed("pop.wav", waitForCompletion: false)
let 叮的音效 = SKAction.playSoundFileNamed("ding.wav", waitForCompletion: false)
比如,在點擊屏幕時餐曹,游戲狀態(tài)下調(diào)用的讓主角飛一下()
方法逛拱,用到了變量:var 速度 = CGPoint.zero
、常量:let k上沖速度: CGFloat = 300.0
和常量:let 拍打的音效 =...
:
func 主角飛一下() {
速度 = CGPoint(x: 0, y: k上沖速度)
run(拍打的音效)
}
剩下的工作流程就是添加場景元素台猴,讓場景循環(huán)移動朽合,造成游戲主角在向前飛行的視覺假象,下面是關(guān)于不斷生成障礙物的三個方法饱狂,第一個先創(chuàng)建障礙物并設(shè)置物理屬性曹步,第二個是在場景里生成障礙,位置嗡官、間距箭窜,第三個是讓障礙無限重生:
func 創(chuàng)建障礙物(圖片名: String) -> SKSpriteNode{
let 障礙物 = SKSpriteNode(imageNamed: 圖片名)
障礙物.zPosition = 圖層.障礙物.rawValue
障礙物.userData = NSMutableDictionary() //初始化用戶數(shù)據(jù)
障礙物.physicsBody = SKPhysicsBody(rectangleOf: 障礙物.size)
障礙物.physicsBody?.categoryBitMask = 物理層.障礙物
障礙物.physicsBody?.collisionBitMask = 0 //關(guān)閉系統(tǒng)提供的碰撞處理
障礙物.physicsBody?.contactTestBitMask = 物理層.游戲角色 //打開碰撞檢測
return 障礙物
}
func 生成障礙() {
let 底部障礙 = 創(chuàng)建障礙物(圖片名: "CactusBottom")
let 起始X坐標(biāo) = size.width + 底部障礙.size.width/2
let Y坐標(biāo)最小值 = (游戲區(qū)域起始點 - 底部障礙.size.height/2) + 游戲區(qū)域的高度 * k底部障礙最小乘數(shù)
let Y坐標(biāo)最大值 = (游戲區(qū)域起始點 - 底部障礙.size.height/2) + 游戲區(qū)域的高度 * k底部障礙最大乘數(shù)
底部障礙.position = CGPoint(x: 起始X坐標(biāo), y: CGFloat.random(min: Y坐標(biāo)最小值, max: Y坐標(biāo)最大值))
底部障礙.name = "底部障礙"
世界單位.addChild(底部障礙)
let 頂部障礙 = 創(chuàng)建障礙物(圖片名: "CactusTop")
頂部障礙.zRotation = CGFloat(180).degreesToRadians()
頂部障礙.position = CGPoint(x: 起始X坐標(biāo), y: 底部障礙.position.y + 底部障礙.size.height/2 + 頂部障礙.size.height/2 + 主角.size.height * k缺口乘數(shù))
頂部障礙.name = "頂部障礙"
世界單位.addChild(頂部障礙)
let X軸移動距離 = -(size.width + 底部障礙.size.width)
let 移動持續(xù)時間 = X軸移動距離 / k地面移動速度
let 移動的動作隊列 = SKAction.sequence([
SKAction.moveBy(x: X軸移動距離, y: 0, duration: TimeInterval(移動持續(xù)時間)),
SKAction.removeFromParent()
])
頂部障礙.run(移動的動作隊列)
底部障礙.run(移動的動作隊列)
}
func 無限重生障礙() {
let 首次延遲 = SKAction.wait(forDuration: k首次生成障礙延遲)
let 重生障礙 = SKAction.run(生成障礙)
let 每次重生間隔 = SKAction.wait(forDuration: k每次重生障礙延遲)
let 重生的動作隊列 = SKAction.sequence([重生障礙, 每次重生間隔])
let 無限重生 = SKAction.repeatForever(重生的動作隊列)
let 總的動作隊列 = SKAction.sequence([首次延遲, 無限重生])
run(總的動作隊列, withKey: "重生")
}
在第二個生成障礙的方法中,先放置底部障礙衍腥,它的y坐標(biāo)需要隨機產(chǎn)生底部障礙.position = CGPoint(x: 起始X坐標(biāo), y: CGFloat.random(min: Y坐標(biāo)最小值, max: Y坐標(biāo)最大值))
磺樱,這段代碼用到教程中事先寫好的模版代碼,因為教程的編譯是swift2.0版的婆咸,在swift4.0下大量報錯竹捉,我就先把模版刪除了,結(jié)果尚骄,這段random(min: Y坐標(biāo)最小值, max: Y坐標(biāo)最大值)
不出意外的報錯块差,提示沒有.random
的方法,在網(wǎng)上查了半天倔丈,才突然想起被刪除的模版憨闰,又只有尷尬的找回模版,慢慢的修改了80多個版本升級后的報錯需五,才找到這段代碼:
// Returns a random floating point number in the range min...max, inclusive.
public static func random(min: CGFloat, max: CGFloat) -> CGFloat {
assert(min < max)
return CGFloat.random() * (max - min) + min
}
目的是將隨機結(jié)果轉(zhuǎn)換成CGFloat鹉动。好了,成功的隨機生成了障礙宏邮。
后面頂部障礙.zRotation = CGFloat(180).degreesToRadians()
還用到了一段解決將圖形旋轉(zhuǎn)180的方法泽示,也是模版提供的:
//Converts an angle in degrees to radians.
public func degreesToRadians() -> CGFloat {
return π * self / 180.0
}
網(wǎng)上也有其它旋轉(zhuǎn)圖片的方法,好像這個比較簡單蜜氨,可以直接輸入角度參數(shù)械筛。
關(guān)于分?jǐn)?shù)的存儲,提供一段固定代碼飒炎,用于寫入磁盤埋哟,這樣程序重啟后也能保存最高分:
func 最高分() -> Int {
return UserDefaults.standard.integer(forKey: "最高分")
}
func 設(shè)置最高分(最高分: Int) {
UserDefaults.standard.set(最高分, forKey: "最高分")
UserDefaults.standard.synchronize()
}
還有一個很方便移除元素的方法,比如:在加載了教程的一些元素在場景中郎汪,之后要開始游戲定欧,就要移除教程元素,可以先給所有教程元素都命名為“教程”怒竿,然后在移除的時候砍鸠,通過全局匹配名字,同時移除耕驰。
func 設(shè)置教程() {
let 教程 = SKSpriteNode(imageNamed: "Tutorial")
教程.position = CGPoint(x: size.width * 0.5, y: 游戲區(qū)域的高度 * 0.4 + 游戲區(qū)域起始點)
教程.name = "教程"
教程.zPosition = 圖層.UI.rawValue
世界單位.addChild(教程)
let 準(zhǔn)備 = SKSpriteNode(imageNamed: "Ready")
準(zhǔn)備.position = CGPoint(x: size.width * 0.5, y: 游戲區(qū)域的高度 * 0.7 + 游戲區(qū)域起始點)
準(zhǔn)備.name = "教程"
準(zhǔn)備.zPosition = 圖層.UI.rawValue
世界單位.addChild(準(zhǔn)備)
}
func 切換到游戲狀態(tài)() {
當(dāng)前的游戲狀態(tài) = .游戲
世界單位.enumerateChildNodes(withName: "教程", using: {匹配單位, _ in
匹配單位.run(SKAction.sequence([
SKAction.fadeOut(withDuration: 0.05),
SKAction.removeFromParent()
]))})
無限重生障礙()
主角飛一下()
}
世界單位.enumerateChildNodes(withName: "教程", using: {匹配單位, _ in 匹配單位.run(SKAction.sequence([ SKAction.fadeOut(withDuration: 0.05), SKAction.removeFromParent()]))})
......好長啊爷辱,這是一個block,找到“教程”這個匹配單位朦肘,就執(zhí)行動作:淡出fadeOut(0.05秒)饭弓,從父視圖中移除。
最后媒抠,有個實現(xiàn)圖片在屏幕上閃爍的動畫效果弟断,其實就是讓如片先放大1.02,再縮小0.98:
// 學(xué)習(xí)按鈕的動畫
let 放大動畫 = SKAction.scale(to: 1.02, duration: 0.75)
放大動畫.timingMode = .easeInEaseOut
let 縮小動畫 = SKAction.scale(to: 0.98, duration: 0.75)
縮小動畫.timingMode = .easeInEaseOut
學(xué)習(xí).run(SKAction.repeatForever(SKAction.sequence([
放大動畫,縮小動畫
])))
學(xué)習(xí)過程還真充滿樂趣趴生,每次實現(xiàn)一個效果阀趴,解決一個問題昏翰,總是令人開心,“加油吧刘急,少年棚菊!”(my son's pet phrase)