最近在研究Godot,做个小游戏玩玩。美术方面直接交给GEMINI负责了,但GEMINI说实话也够傻的,不愧是美国大豆宝。

游戏画面

好了,少废话吧,我们直入正题,这篇文章包括以后的文章我会记录我在学习或者项目中遇到的有趣的地方。

而这篇毫无疑问的就是和标题同样的状态机问题,老实说在cs本科期间我的408四大件其实学的都不怎么好,操作系统当初是考试前几天突击复习然后过了。

包括在操作系统课上编写的实操程序也只是我看教科书以及资料依葫芦画瓢做的假老虎罢了,好像扯远了,我们回到状态机问题上来。

我们都知道,在操作系统中,会讲进程的状态转换:新建,就绪,运行,阻塞,终止。在操作系统中会对进程的状态进行动态处理。

而在一开始,我所编写的玩家角色的控制代码是这样的(feat.Deepseek):

点击展开代码
extends CharacterBody2D

# 移动与跳跃参数
@export var move_speed: float = 50.0
@export var sprint_speed: float = 120.0
@export var jump_force: float = -300.0
@export var gravity: float = 500.0
@export var max_jump_count: int = 2

# 攻击与闪避参数
@export var attack_duration: float = 0.3        # 攻击动作持续时间(秒)
@export var dodge_duration: float = 0.2         # 闪避动作持续时间(秒)
@export var dodge_speed: float = 300.0          # 闪避冲刺速度
@export var dodge_cooldown: float = 0.5         # 闪避冷却时间(秒)

@onready var animator: AnimatedSprite2D = $AnimatedSprite2D  # 根据实际节点路径调整

# 内部状态变量
var direction: float = 0.0
var jump_count: int = 0
var is_sprinting: bool = false
var is_touching_right_wall: bool = false


# 攻击状态
var is_attacking: bool = false
var attack_timer: float = 0.0

# 闪避状态
var is_dodging: bool = false
var dodge_timer: float = 0.0
var dodge_cooldown_timer: float = 0.0
var dodge_direction: float = 0.0   # 闪避方向(1 右,-1 左)

func _physics_process(delta):
	# 更新计时器
	if attack_timer > 0:
		attack_timer -= delta
		if attack_timer <= 0:
			is_attacking = false
			attack_timer = 0.0

	if dodge_timer > 0:
		dodge_timer -= delta
		if dodge_timer <= 0:
			is_dodging = false
			dodge_timer = 0.0

	if dodge_cooldown_timer > 0:
		dodge_cooldown_timer -= delta

	# 获取输入
	direction = Input.get_axis("left", "right")
	is_sprinting = Input.is_action_pressed("sprint")

	# ---- 处理攻击输入(地面或空中均可攻击,但不能在闪避或攻击中)----
	if Input.is_action_just_pressed("attack_punch") and not is_attacking and not is_dodging:
		start_attack()

	# ---- 处理闪避输入(仅地面可用,且不在攻击/闪避中,冷却结束)----
	if Input.is_action_just_pressed("dodge")  and not is_attacking and not is_dodging and dodge_cooldown_timer <= 0:
		if is_attacking:
			is_attacking = false
			attack_timer = 0.0
	
		start_dodge()
		

	# ---- 处理水平移动(攻击或闪避时禁用常规移动)----
	if not is_attacking and not is_dodging:
		var current_speed = sprint_speed if is_sprinting else move_speed
		if direction != 0:
			velocity.x = direction * current_speed
			animator.flip_h = direction < 0
		else:
			velocity.x = 0
	else:
		# 攻击或闪避期间不响应水平移动输入
		# 闪避时速度由 start_dodge 设置,这里不再修改
		if is_attacking:
			velocity.x = 0   # 攻击时停止水平移动

	# ---- 重力与地面检测 ----
	if not is_on_floor():
		velocity.y += gravity * delta
	else:
		jump_count = 0
		if velocity.y > 0:
			velocity.y = 0

	# ---- 跳跃(攻击时仍可跳跃,闪避时禁止)----
	if Input.is_action_just_pressed("jump") and jump_count < max_jump_count and not is_dodging:
		velocity.y = jump_force
		jump_count += 1
		# 如果跳跃时正在攻击,可以选择中断攻击(此处保留攻击状态,动画会处理)

	# ---- 应用移动 ----
	move_and_slide()
	# 检测是否碰到右墙
	is_touching_right_wall = false
	if is_on_wall():
		var collision = get_last_slide_collision()
		if collision:
			var normal = collision.get_normal()
			# 右墙的法线指向左(x < 0)
			if normal.x < -0.5:  # 阈值判断,避免误判
				is_touching_right_wall = true

	# ---- 更新动画 ----
	update_animation()

func start_attack():
	is_attacking = true
	attack_timer = attack_duration
	# 可选:立即设置攻击动画(由 update_animation 处理)

func start_dodge():
	is_dodging = true
	dodge_timer = dodge_duration
	dodge_cooldown_timer = dodge_cooldown

	# 确定闪避方向:优先使用当前输入方向,否则使用角色面向方向
	if direction != 0:
		dodge_direction = direction
	else:
		# 如果 flip_h 为 true(朝左),则闪避向左,否则向右
		dodge_direction = -1.0 if animator.flip_h else 1.0

	# 立即设置水平速度进行冲刺
	velocity.x = dodge_direction * dodge_speed

func update_animation():
	# 动画优先级:闪避 > 攻击 > 空中 > 地面移动 > 待机
	if is_dodging:
		animator.play("dodge")
	elif is_attacking:
		animator.play("attack_punch")
	elif not is_on_floor():
		if velocity.y < 0:
			animator.play("jump")
		else:
			animator.play("fall")
	else:
		if direction != 0:
			if is_sprinting:
				animator.play("sprint")
			else:
				animator.play("run")
		else:
			animator.play("idle")

可以看到,每次切换不同动作时本质就是靠if-else来回切,现在虽然我们的动作少,但如果往后我们想要增加新动作就会使我们的代码变成if-else地狱。再往后代码将变得完全不可读,不说别人了,要是我自己看到这些玩意儿都想紫砂了。

这个时候我们就可以用到我们之前所说的状态机的思想了,我们可以把不同的动作看成不同的状态(当然不只限于动作)。我们将其分为状态后,让其在状态之间互相转换,降低了代码的耦合性。

光说有些抽象,我们可以来看看图:

if-else版本

我想不论什么人看到这一串东西都应该直接微距了,再往上面加东西最后只会变成一团毛线球。

状态机版本

我们可以发现状态机版本的图就非常清晰,我们只需要考虑在什么时候转换到什么状态就行了,同时状态和状态之间也可以互相转换。

虽然仔细想想某种层面睐说状态机也只不过是if-else,但将一个原本复杂的问题简单化也算工程学的奇迹了。

接着我们来看代码:

点击展开代码
extends CharacterBody2D

# -------------------- 导出参数 --------------------
@export var move_speed: float = 50.0
@export var sprint_speed: float = 150.0
@export var jump_force: float = -300.0
@export var gravity: float = 500.0
@export var max_jump_count: int = 2

@export var attack_duration: float = 0.3
@export var dodge_duration: float = 0.2
@export var dodge_speed: float = 500.0
@export var dodge_cooldown: float = 0.5

@onready var animator: AnimatedSprite2D = $AnimatedSprite2D

# -------------------- 状态定义 --------------------
enum State {
	IDLE,
	RUN,
	SPRINT,
	JUMP,
	FALL,
	ATTACK,
	DODGE
}

var state: State = State.IDLE

# 对外暴露的布尔标志(兼容旧代码)
var is_attacking: bool = false
var is_dodging: bool = false

# -------------------- 内部变量 --------------------
var direction: float = 0.0
var jump_count: int = 0
var is_sprinting: bool = false
var is_touching_right_wall: bool = false

var attack_timer: float = 0.0
var dodge_timer: float = 0.0
var dodge_cooldown_timer: float = 0.0
var dodge_direction: float = 0.0

# -------------------- 生命周期 --------------------
func _physics_process(delta):
	# 更新冷却
	if dodge_cooldown_timer > 0:
		dodge_cooldown_timer -= delta

	# 输入
	direction = Input.get_axis("left", "right")
	is_sprinting = Input.is_action_pressed("sprint")

	# ---- 统一重力与地面重置 ----
	if not is_on_floor():
		velocity.y += gravity * delta
	else:
		jump_count = 0
		if velocity.y > 0:
			velocity.y = 0

	# ---- 统一跳跃输入(闪避状态下禁止) ----
	if state != State.DODGE and Input.is_action_just_pressed("jump") and jump_count < max_jump_count:
		velocity.y = jump_force
		jump_count += 1
		if state != State.ATTACK:
			change_state(State.JUMP)

	# ---- 状态分发 ----
	match state:
		State.IDLE:
			_state_idle(delta)
		State.RUN:
			_state_run(delta)
		State.SPRINT:
			_state_sprint(delta)
		State.JUMP:
			_state_jump(delta)
		State.FALL:
			_state_fall(delta)
		State.ATTACK:
			_state_attack(delta)
		State.DODGE:
			_state_dodge(delta)

	# ---- 移动 ----
	move_and_slide()

	# ---- 墙面检测(保留) ----
	is_touching_right_wall = false
	if is_on_wall():
		var collision = get_last_slide_collision()
		if collision:
			var normal = collision.get_normal()
			if normal.x < -0.5:
				is_touching_right_wall = true

# -------------------- 状态切换(同步标志) --------------------
func change_state(new_state: State):
	state = new_state
	is_attacking = (state == State.ATTACK)
	is_dodging   = (state == State.DODGE)

# -------------------- 各状态处理(仅水平移动与动画) --------------------
func _state_idle(delta):
	if direction != 0:
		var speed = sprint_speed if is_sprinting else move_speed
		velocity.x = direction * speed
		animator.flip_h = direction < 0
		change_state(State.SPRINT if is_sprinting else State.RUN)
	else:
		velocity.x = 0
		animator.play("idle")

func _state_run(delta):
	if direction == 0:
		change_state(State.IDLE)
		return
	if is_sprinting:
		change_state(State.SPRINT)
		return
	velocity.x = direction * move_speed
	animator.flip_h = direction < 0
	animator.play("run")

func _state_sprint(delta):
	if direction == 0 or not is_sprinting:
		change_state(State.IDLE if direction == 0 else State.RUN)
		return
	velocity.x = direction * sprint_speed
	animator.flip_h = direction < 0
	animator.play("sprint")

func _state_jump(delta):
	if direction != 0:
		var speed = sprint_speed if is_sprinting else move_speed
		velocity.x = direction * speed
		animator.flip_h = direction < 0
	else:
		velocity.x = 0
	animator.play("jump")
	if velocity.y >= 0:
		change_state(State.FALL)

func _state_fall(delta):
	if direction != 0:
		var speed = sprint_speed if is_sprinting else move_speed
		velocity.x = direction * speed
		animator.flip_h = direction < 0
	else:
		velocity.x = 0
	animator.play("fall")
	if is_on_floor():
		change_state(State.IDLE)

func _state_attack(delta):
	velocity.x = 0
	animator.play("attack_punch")
	attack_timer -= delta
	if attack_timer <= 0:
		if is_on_floor():
			change_state(State.IDLE)
		else:
			change_state(State.FALL)

func _state_dodge(delta):
	# 闪避时水平速度由 start_dodge 设置,不响应方向键
	animator.play("dodge")
	dodge_timer -= delta
	if dodge_timer <= 0:
		if is_on_floor():
			change_state(State.IDLE)
		else:
			change_state(State.FALL)

# -------------------- 输入事件(攻击与闪避) --------------------
func _input(event):
	# 攻击
	if event.is_action_pressed("attack_punch") and state != State.ATTACK and state != State.DODGE:
		start_attack()

	# 闪避(现可在空中使用)  ★★★ 修改点:移除了 is_on_floor() 检查 ★★★
	if event.is_action_pressed("dodge") and state != State.ATTACK and state != State.DODGE and dodge_cooldown_timer <= 0:
		start_dodge()

# -------------------- 启动动作 --------------------
func start_attack():
	attack_timer = attack_duration
	change_state(State.ATTACK)

func start_dodge():
	dodge_timer = dodge_duration
	dodge_cooldown_timer = dodge_cooldown

	if direction != 0:
		dodge_direction = direction
	else:
		dodge_direction = -1.0 if animator.flip_h else 1.0

	velocity.x = dodge_direction * dodge_speed
	change_state(State.DODGE)

最核心的函数莫过于change_state(new_state: State)用来管控状态的切换,而其他一系列相应的状态函数也十分简单,只需要考虑自己的事情以及何时切换就行了,以后想要增加新功能便也只需要在最开始的枚举中添加并编写相应的状态函数就行了。

ps:也许你会看到一些奇怪的变量和语句,那和玩家角色无关,是我用来整活做摄像机的效果的。