Introduction:
状态机,用于便于管理人物的各种状态,包括人物之间状态的切换。
例如,你平常吃了睡,睡了吃就是两个状态,睡 <-> 吃。并且这两个状态之间恰好是可以相互转换的,吃饱了进入到睡的状态,睡醒了进入吃的状态。
StateMachine
使用状态机的模型,首先需要定义一个通用的状态机管理脚本。这个脚本是一个通用的脚本,便于组装到所有的角色上。
### 脚本: StateMachine.gd ###
class_name StateMachine
extends Node
var current_state: int = -1: # 开始先设置为-1专门表示起始值
set(v): # 每次为current_state 赋值的时候都会执行set(v)函数
owner.transition_state(current_state, v) # 拥有状态机脚本的角色将会执行状态的切换函数
# transition_state() 需要在角色中进行定义
current_state = v # 更新状态的值
func _ready() -> void:
await owner.ready # 开始先等待角色初始化函数执行完毕
current_state = 0 # 默认直接进入第一个状态
func _physics_process(delta: float) -> void:
while true:
var next := owner.get_next_state(current_state) as int # 每次都会调用定义的函数获取下一个状态
if current_state == next: # 如果状态不变,直接跳出
break
current_state = next # 否则就进入到下一个状态
owner.tick_physics(current_state, delta) # 执行角色的物理函数
可以发现,该函数需要角色(owner)拥有三个定义函数进行支持,下面来定义这三个函数:
### 脚本: Player.gd ###
extends CharacterBody2D
# 玩家脚本
# 首先顶一个一个状态枚举: [默认, 跑步, 跳跃, 坠落]
enum State {
IDLE,
RUNNING,
JUMP,
FALL,
}
const GROUND_STATE := [State.IDLE, State.RUNNING] # 地面状态的集合
const RUN_SPEED := 160.0
const JUMP_VELOCITY := -400.0
const FLOOR_ACCELERATION := RUN_SPEED / 0.2
const AIR_ACCELERATION := RUN_SPEED / 0.02
var default_gravity := ProjectSettings.get("physics/2d/default_gravity") as float # 默认下的重力
var is_first_tick:bool = false # 是否处于第一帧
@onready var animation_player = $AnimationPlayer
@onready var sprite_2d = $Sprite2D
@onready var coyote_timer: Timer = $"../CoyoteTimer"
@onready var jump_request_timer: Timer = $"../JumpRequestTimer"
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("jump"):
jump_request_timer.start()
if event.is_action_released("jump") and velocity.y < JUMP_VELOCITY / 2:
velocity.y = JUMP_VELOCITY / 2
func tick_physics(state: State, delta: float): # 每一帧都要执行的玩家物理逻辑函数
match state:
State.IDLE:
move(default_gravity, delta)
State.RUNNING:
move(default_gravity, delta)
State.JUMP:
move(0.0 if is_first_tick else default_gravity, delta) # 如果第一帧取消重力否则下落
State.FALL:
move(default_gravity, delta)
is_first_tick = false # 第一帧结束后取消第一帧的布尔
func move(gravity: float, delta: float) -> void:
var direction := Input.get_axis("move_left", "move_right") # 获取左右输入
var acceleration := FLOOR_ACCELERATION if is_on_floor() else AIR_ACCELERATION # 选择重力
velocity.x = move_toward(velocity.x, direction * RUN_SPEED, acceleration * delta) # 根据不同重力进行移动
velocity.y += gravity * delta # 获得重力
if not is_zero_approx(direction): # 如果有左右方向的输入
sprite_2d.flip_h = direction < 0 # 根据方向进行反转
move_and_slide() # 进行物理碰撞检测的移动
func get_next_state(state: State) ->State: # 根据输入获得下一个状态
var can_jump := is_on_floor() or coyote_timer.time_left > 0 # 在地面上或者处在coyoteTimer的时间内就允许跳跃
var should_jump := can_jump and jump_request_timer.time_left > 0 # 允许跳跃并且已经按下了跳跃键就应该跳跃
if should_jump: # 应该跳跃进入跳跃状态
return State.JUMP
############### 以下为不应该跳跃的情况 #########################
var direction := Input.get_axis("move_left", "move_right") # 获取水平输入
var is_still := is_zero_approx(direction) and is_zero_approx(velocity.x) # 如果水平静止
match state:
State.IDLE: # 在静止状态中
if not is_on_floor():
return State.FALL # 不在地面进入坠落状态
if not is_still:
return State.RUNNING # 不静止进入跑步状态
State.RUNNING: # 在跑步状态中
if is_still: # 回到静止就进入待机状态
return State.IDLE
if not is_on_floor(): # 不在地面就进入坠落状态
return State.FALL
State.JUMP: # 在跳跃状态中判断下一次状态应该的值
if velocity.y >= 0: # 如果速度变为向下,进入坠落状态
return State.FALL
State.FALL: # 在坠落状态中
if is_on_floor(): # 坠落回地面进入跑步或静止状态
return State.IDLE if is_still else State.RUNNING # 根据是否静止进入跑步或者待机状态
return state # 最后返回得到的状态
func transition_state(from: State, to: State) -> void:
if from not in GROUND_STATE and to in GROUND_STATE:
### 如果是从陆地以外的状态进入到陆地状态就停止胶囊时间CoyoteTimer ###
coyote_timer.stop()
match to: # 根据目标状态更新当前的属性
State.IDLE: # 如果要去到 IDLE 状态就切换回 IDLE 应有的动画
animation_player.play("idle")
State.RUNNING: # 如果需要进入 RUNNING 就切换到 running 的动画
animation_player.play("running")
State.JUMP: # 进入到 JUMP 切换到 jump 动画并且给一个向上的初速度
animation_player.play("jump")
velocity.y = JUMP_VELOCITY
coyote_timer.stop() # 停止胶囊时间
jump_request_timer.stop() # 停止跳跃延迟时间
State.FALL: # 需要进入坠落状态播放坠落动画
animation_player.play("fall")
if from in GROUND_STATE: # 如果是从地面状态进入到坠落状态就启动胶囊时间
coyote_timer.start()
is_first_tick = true # 设置当前是第一帧
##########################################################################
# 当进入一个状态时运行完一次后设置为是第一帧,并在物理函数中运动过一次后设置为是第一帧 #
##########################################################################
根据以上代码可以发现,如果想要运用状态机,只需要准备一个StateMachine脚本,继承自Node。
将StateMachine组装到Player类场景的节点树下,并在玩家中定义三个行为函数即可。
- transition_state(current_state, v)
- get_next_state(current_state)
- tick_physics(current_state, delta)