iT邦幫忙

2021 iThome 鐵人賽

DAY 16
0
Mobile Development

從零開始的8-bit迷宮探險!Swift SpriteKit 遊戲開發實戰系列 第 16

從零開始的8-bit迷宮探險【Level 16】丞相,起風了!遠方飄來烏雲怪物了

「萬事俱備,只欠東風了」Rain 對著 Storm 說。
「居然有人敢入侵我們的家園!」Lightning 說到激動處還不時冒出電流。
「我看這傢伙走路有點慢,我們快去把他趕走吧!」Snow 回憶著撞見山姆時的情境。
午後的黑森林,要變天了。

今日目標

  • 新增怪物的移動方法,讓他們能在迷宮中隨機移動
  • 播放各方向的怪物序列動畫

PS. 這裡是開發 iOS 手機遊戲的系列文,如果還沒看過之前 劇情 文章的朋友,歡迎先點這邊回顧唷!


站在巨人的肩膀上

先複習一下,目前我們的主角-山姆,已經可以在迷宮中移動了,想更詳細了解,可以點擊:【Level 14】讓主角奔跑吧!Running Sam

我們已經在共用的角色類別 GameCharacter 寫好可以幫助製作移動跟動畫的有:

  • 方向列舉:Direction
  • 位置及移動資訊
  • 判斷可行方向的方法:getValidDirection()
  • 判斷是否為牆壁的方法:isWall(dir: Direction)
  • 移動協定:Move
  • 播放角色序列動畫:playAnimation(imageName: String, num: Int, repeatAni: Bool = true)

實作移動方法

回到我們的壞天氣怪物類別 (Weather),讓它遵循 Move protocol,並且實作方法

  • startMove:
    • 這邊我們遵循 Move,不過移動方向由方法內決定,可以忽略帶進來的 direction 參數
    • 我們透過 getValidDirection() 取得當前可以前進的方向 validDirections
    • 透過 randomElement() 取得任一個隨機的可行方向,因得到的結果是 Optional 型別,所以這邊運用了 guard 的功能,當沒有取得時直接 return
    • 接著將得到的方向寫入 self.direction
    • 給予 isMoving 是否正在移動的值
    • 像先前的主角一樣,使用 SKAction.moveBy 播放移動動畫,xy 帶入移動的向量,duration 帶入移動間隔秒數。最後透過 .run 執行動畫,當動畫完成時,執行 endMove 方法
    • 設定該方向的格子 x、y
  • endMove:動畫完成時,執行 startMove 方法,會讓移動動畫持續進行
  • Weather.swift
class Weather: GameCharacter, Move {
    ...
    func startMove(direction : Direction) {
        // 取得可行方向
        let validDirections: [Direction] = self.getValidDirection()
        
        // 隨機取得一個可行方向
        guard let randomDirection = validDirections.randomElement() else {
            return
        }
        self.direction = randomDirection
        
        // 是否正在移動中
        self.isMoving = self.direction != Direction.NONE
        
        // 播放移動動畫
        let animation = SKAction.moveBy(x: self.moveX[self.direction]!, y: self.moveY[self.direction]!, duration: self.moveInterval)
        self.node.run(animation, completion: endMove)
        
        // 設定格子
        self.setGridXY(direction: self.direction)
    }
    func endMove() {
        self.startMove(direction: self.direction)
    }
}

讓怪物移動吧

在遊戲場景的 didMove,建立好怪物之後,讓每一個怪物都呼叫移動方法 startMove
就可以看到怪物在地圖內自動移動了

  • GameScene.swift
class GameScene: SKScene {
    ...
    override func didMove(to view: SKView) {
        ...
        for weather in self.weathers {
            weather.startMove(direction: .NONE)
        }
    }
}

目前成果

https://imgur.com/hwRr6y6.gif

修正移動

目前怪物可以隨機移動了,可是看起來有一直來回走動的感覺
為了修正這個問題,我們希望能讓怪物一直往前走,或者是轉個彎再走,避開回頭走的方向
所以在取得方向的時候,稍微修正一下:

  • 新增一個變數 newDirection,先暫時將隨機取得的方向存進變數中
  • 判斷當前方向是否剛好與新的方向 newDirection 相反
  • 當不是反方向的時候,或是原本的方向已經不能走的時候,才更新 self.direction
  • Weather.swift
class Weather: GameCharacter, Move {
    ...
    func startMove(direction : Direction) {
        var newDirection: Direction = .NONE
        
        // 取得可行方向
        let validDirections: [Direction] = self.getValidDirection()
        
        // 隨機取得一個可行方向
        guard let randomDirection = validDirections.randomElement() else {
            return
        }
        newDirection = randomDirection
        
        // 判斷是否回頭走
        let isBack = self.direction == .LEFT && newDirection == .RIGHT ||
            self.direction == .RIGHT && newDirection == .LEFT ||
            self.direction == .UP && newDirection == .DOWN ||
            self.direction == .DOWN && newDirection == .UP
            
        // 不是回頭路 或 原方向撞牆,才更新方向
        if !isBack || !validDirections.contains(self.direction) {
            self.direction = newDirection
        }
        
        ...
    }
}

修正後的成果

https://imgur.com/TUroUek.gif

穿梭地圖

最後,讓怪物也能透過左右的通道穿梭地圖吧!
這邊的方式跟製作主角移動時一樣
在播放移動動畫之前,先判斷走到特地位置時,就將怪物瞬間移動到地圖另一邊

  • Weather.swift
class Weather: GameCharacter, Move {
    ...
    func startMove(direction : Direction) {
        ...
        // 左右穿梭
        if ((gridX == gridMapping.leftPass1.x && gridY == gridMapping.leftPass1.y && direction == .LEFT) || (gridX == gridMapping.leftPass2.x && gridY == gridMapping.leftPass2.y && direction == .LEFT) || (gridX == gridMapping.rightPass1.x && gridY == gridMapping.rightPass1.y && direction == .RIGHT)) {
            self.gridX = direction == .LEFT ? gridMapping.rightPass1.x + 1 : gridMapping.leftPass1.x - 1
            self.gridY = direction == .LEFT ? gridMapping.rightPass1.y : [gridMapping.leftPass1.y, gridMapping.leftPass2.y].randomElement()!
            self.node.position = CGPoint(x: (gridX * gridWH) + (gridWH/2), y: -gridY * gridWH - (gridWH/2))
        }
        
        // 播放移動動畫
        let animation = SKAction.moveBy(x: self.moveX[self.direction]!, y: self.moveY[self.direction]!, duration: self.moveInterval)
        self.node.run(animation, completion: endMove)
        
        ...
    }
}

穿梭效果

https://imgur.com/X9EQDdO.gif


播放怪物對應方向的動畫

目前的怪物只有正面一個方向的序列圖,我們將他優化,把各個方向都做出來吧!

準備怪物各方向的圖片

https://imgur.com/Izixl4M.png
https://imgur.com/bPrmrCe.png
https://imgur.com/MgHGBXB.png
https://imgur.com/iAsqBx3.png
https://imgur.com/PtIiQv7.png
https://imgur.com/AWXq1bz.png
https://imgur.com/vKn8GbU.png
https://imgur.com/fJ4VST2.png

將圖片命名好:

  • rain
    • 左:rain_left_1、rain_left_2
    • 右:rain_right_1、rain_right_2
    • 上:rain_up_1、rain_up_2
    • 下:rain_down_1、rain_down_2
  • storm
    • 左:storm_left_1、storm_left_2
    • 右:storm_right_1、storm_right_2
    • 上:storm_up_1、storm_up_2
    • 下:storm_down_1、storm_down_2
  • lightning
    • 左:lightning_left_1、lightning_left_2
    • 右:lightning_right_1、lightning_right_2
    • 上:lightning_up_1、lightning_up_2
    • 下:lightning_down_1、lightning_down_2
  • snow
    • 左:snow_left_1、snow_left_2
    • 右:snow_right_1、snow_right_2
    • 上:snow_up_1、snow_up_2
    • 下:snow_down_1、snow_down_2

調整模式列舉

因為怪物有不同種類的移動模式,我們預計在不同模式會顯示不同的樣貌
因此在加各方向的圖片時,我們事先規劃,在 Mode 加上 getImage 來取得對應移動模式之下,對應怪物的圖片名稱
目前我們先以預設 .ATTACK 模式來製作,其他模式未來會再說明

  • Weather.swift
enum Mode {
    case ATTACK // 追擊主角
    case ESCAPE // 逃離主角
    case PLAY // 聚集到湖邊玩耍
    case REBIRTH // 回到出生點重生
    
    func getImage(role: Role) -> String {
        switch self {
        case .ATTACK, .PLAY:
            switch role {
            case .RAIN:
                return "rain"
            case .STORM:
                return "storm"
            case .LIGHTNING:
                return "lightning"
            case .SNOW:
                return "snow"
            default:
                return ""
            }
        case .ESCAPE:
            return "cloud"
        case .REBIRTH:
            return "water"
        }
    }
}

祕技:這邊比較特別的是運用了在列舉 (enum) 裡使用方法 (function) 的技巧,可以結合 switch case,分別讓各個 case 回傳不同的結果,只需要加上 switch self,就可以方便地對自身的 case 做條件判斷並回傳對應的值。

播放對應方向的移動動畫

最後,來寫播放動畫的程式碼吧!

  • 在一進到 startMove 方法後,就先將當下的移動方向暫存到 tempDirection
  • 在取得新的方向後,將新的方向與暫存方向做個判斷,如果方向改變的話,就播放對應方向的移動動畫 playAnimation
  • imageName 用其屬性來組成:
    • 使用剛剛新增的 self.mode.getImage(role: self.role),取得怪物種類名稱
    • 使用 self.direction.rawValue,取得當前方向
    • 例如:當前角色是 Rain,且方向是朝左移動,則會取得 rain_left
  • num:2張序列圖
  • Weather.swift
class Weather: GameCharacter, Move {
    ...
    func startMove(direction : Direction) {
        let tempDirection: Direction = self.direction
        ...
        self.isMoving = self.direction != Direction.NONE
        
        // 改變怪物圖片
        if tempDirection != self.direction {
            self.playAnimation(imageName: "\(self.mode.getImage(role: self.role))_\(self.direction.rawValue)", num: 2)
        }
        
        // 左右穿梭
        ...
    }
}

動畫成果

https://imgur.com/0HJGD84.gif


今日小結

目前怪物已經可以自動偵測可以前進的方向,並且隨機移動囉!
但這樣還不夠聰明~/images/emoticon/emoticon15.gif
明天將會介紹如何讓怪物朝著主角的位置移動!


上一篇
從零開始的8-bit迷宮探險【Level 15】迷人的反派角色-製作怪物
下一篇
從零開始的8-bit迷宮探險【Level 17】稻草人也想要智慧大腦,給怪物一點靈魂跟一點點個性
系列文
從零開始的8-bit迷宮探險!Swift SpriteKit 遊戲開發實戰30

尚未有邦友留言

立即登入留言