本节主要了解状态机是什么, 以及在游戏开发中的使用
为什么需要状态机
如果我们想表达一个复杂的状态转换流程, 一般会绘制流程图, 比如给之前的敌人添加"充能"和"攻击"两种状态, 整个状态变换的过程可能如下
┌─────────┐ ┌─────────┐
│ spawn ├─────►│ normal ├────┐
└─────────┘ └─▲──┬────┘ │
│ │ │
│ │ │
┌───────────┘ │ ┌──▼──────┐
│ attack ◄────┼──────┤ charge │
└──────┬──┘ │ └──┬──────┘
│ │ │
│ ┌──▼───┐ │
└────► died ◄─────┘
└──────┘
在之前的实现中, 我使用了一个is_spawning标志位, 来表达状态, 并且多处判断该标志位, 来进行不同状态下的逻辑处理
如果是上面这种状态流程, 需要增加的标志位会增加好几个, 并且依据标志位来判断的逻辑会变得异常复杂, 这是一个非常好的使用状态机的情形
使用状态机有以下好处:
- 解耦各种状态下的运行逻辑, 每个状态只关心该状态下的逻辑
- 每个状态具有自己的生命周期, 有明确的 "进入" - "运行" - "退出" 生命事件
- 状态机的切换比较简单, 如果使用标志位, 则需要同时管理多个标志位的状态, 容易出错
- 同一时刻只有一个状态, 避免出现使用标志位可能出现的"多状态"重叠, 逻辑混乱
游戏开发中的状态机
在Godot中我们一般使用基于Node节点的状态机, 方便管理
先看看基础状态State的实现, 非常简单: 继承于Node方便放入场景节点树, 表达生命周期的三个函数, 以及一个状态转换的信号(供StateMachine监听切换状态)
class_name State
extends Node
@warning_ignore("unused_signal")
signal transitioned(next: String)
## 当进入状态时调用
func enter() -> void:
pass
## 该状态的更新调用
func update() -> void:
pass
# 退出状态时调用
func exit() -> void:
pass
再看看状态机StateMachine的实现
class_name StateMachine
extends Node
signal state_changed(old: String, new: String)
@export var initial_state: String
# 当前状态, 空字符串表示没有状态
var current_state: String = String():
get:
return current_state
set(value):
_state_transition(value)
var states: Dictionary[String,State] = {}
func _ready() -> void:
for child in get_children():
if child is State:
states[child.name.to_lower()] = child
child.transitioned.connect(_on_state_transition)
if initial_state:
_state_transition(initial_state)
func _process(_delta: float) -> void:
if current_state:
states[current_state].update()
func _state_transition(next: String) -> void:
if next not in states:
push_error("Transition to a non-exists state: %s" % next)
return
if current_state == next:
push_error("Transition to a same state: %s" % next)
return
if current_state in states:
states[current_state].exit()
var old_state = current_state
current_state = next
states[current_state].enter()
state_changed.emit(old_state, current_state)
func _on_state_transition(next: String) -> void:
current_state = next
整体也不复杂, ready时遍历所有子节点, 将State类型的子节点纳入管理中, 在process中执行当前状态的update生命函数, 处理State之间的切换, 以及State的enter/exit生命函数执行
整个StateMachine实现不到50行, 但是能极大解耦复杂状态转换, 对于简单的敌人逻辑相当好用
本节对状态机做一个整体介绍, 下次就使用这个状态机改进敌人, 让敌人实现最开始流程图中的状态转换