从零开始的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
明天将会介绍如何让怪物朝着主角的位置移动!


<<:  Day 12. 生命周期 - Lifecycle Hooks

>>:  Day 10 - SELECT INTO !

[Day12]PHP 可变函数及回传值

PHP函数 函数返回值return 值通过使用可选的返回语句返回。可以返回包括数组和对象的任意类型。...

[Day06 - UI/UX] 建立 APP Design Guideline

在开始画设计稿之前,我们要先确认一下我们的字级以及主色以及辅色等等。这边我套用去年自制的 Desig...

Day-19 承载游戏梦想的南蛮黑船 XBOX 再启航

微软也做游戏机!这种在现在听来理所当然的事情、发生在 20 年前、其实还是让人感到挺震惊的。而当时诞...

[Day 27] 从零开始学Python - 科学绘图Matplotlib:画着你,画不出你的骨骼

注:本文同步刊载在Medium,若习惯Medium的话亦可去那边看呦! 今天我们要来介绍的是Matp...

Day 16 - SNMP、Banner Grabbing 与 Firewall Rules

出於书本 Chapter 8. Network Infrastructure SNMP 扫描 什麽是...