2018-01-08

游戲邏輯框架

和上一個游戲不同,這次用中文編寫代碼筐咧,可以讓我這個初學(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)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市叔汁,隨后出現(xiàn)的幾起案子统求,更是在濱河造成了極大的恐慌,老刑警劉巖据块,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件码邻,死亡現(xiàn)場離奇詭異,居然都是意外死亡另假,警方通過查閱死者的電腦和手機像屋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來浪谴,“玉大人开睡,你說我怎么就攤上這事」冻埽” “怎么了篇恒?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長凶杖。 經(jīng)常有香客問我胁艰,道長,這世上最難降的妖魔是什么智蝠? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任腾么,我火速辦了婚禮,結(jié)果婚禮上杈湾,老公的妹妹穿的比我還像新娘解虱。我一直安慰自己,他們只是感情好漆撞,可當(dāng)我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布殴泰。 她就那樣靜靜地躺著,像睡著了一般浮驳。 火紅的嫁衣襯著肌膚如雪悍汛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天至会,我揣著相機與錄音离咐,去河邊找鬼。 笑死奉件,一個胖子當(dāng)著我的面吹牛宵蛀,可吹牛的內(nèi)容都是我干的昆著。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼糖埋,長吁一口氣:“原來是場噩夢啊……” “哼宣吱!你這毒婦竟也來了窃这?” 一聲冷哼從身側(cè)響起瞳别,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎杭攻,沒想到半個月后祟敛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡兆解,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年馆铁,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片锅睛。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡埠巨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出现拒,到底是詐尸還是另有隱情辣垒,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布印蔬,位于F島的核電站勋桶,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏侥猬。R本人自食惡果不足惜例驹,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望退唠。 院中可真熱鬧鹃锈,春花似錦、人聲如沸瞧预。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽松蒜。三九已至扔茅,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間秸苗,已是汗流浹背召娜。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留惊楼,地道東北人玖瘸。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓秸讹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親雅倒。 傳聞我的和親對象是個殘疾皇子璃诀,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容