参考

本节参考自GDQuest的《Godot中的有限状态机》一文。
有关状态机的解释也可以参考:https://gpp.tkchu.me/
未命名.jpg

场景结构

image.png

  1. # Player节点的代码 -- 提供基础的控制参数和全局变量
  2. extends KinematicBody2D
  3. class_name Player
  4. export var speed = 60.0 # 速度 像素/秒
  5. export var gravity = 5000 # 重力加速度
  6. export var jump_force = 3000.0 # 单次跳跃高度
  7. var velocity = Vector2.ZERO # 速度向量

状态转换示意图

9.状态机初步 - 图3

9.状态机初步 - 图4

  1. class_name StateMachine
  2. extends Node
  3. signal changed(state_name)
  4. export var initial_state := NodePath() # 通过属性面板设置初始状态,平台跳跃游戏的玩家初始在空中,所以指定Air
  5. onready var state: State = get_node(initial_state) # 当前状态(节点)
  6. func _ready() -> void:
  7. # 因为StateMachine是它的owner也就是根节点Player的子节点
  8. # 所以StateMachine先就绪,Player要等到所有节点就绪后才就绪
  9. yield(owner, "ready") # 等待根节点就绪
  10. # 遍历StateMachine的子节点,将各个子节点代码中的state_machine设置为自己
  11. for child in get_children():
  12. child.state_machine = self as StateMachine
  13. # 调用状态的进入函数
  14. state.enter()
  15. # 输入、帧处理、物理帧处理统一调用当前状态中的对应函数
  16. func _unhandled_input(event: InputEvent) -> void:
  17. state.handle_input(event)
  18. func _process(delta: float) -> void:
  19. state.update(delta)
  20. func _physics_process(delta: float) -> void:
  21. state.physics_update(delta)
  22. # 改变状态
  23. func change_to(target_state_name: String, msg: Dictionary = {}) -> void:
  24. if not has_node(target_state_name): # 不存在对应名称的子节点,也就是不存在对应的状态
  25. return
  26. state.exit() # 调用旧状态的退出
  27. state = get_node(target_state_name) # 赋值和记录新的状态
  28. state.enter(msg) # 调用新状态进入方法,并传递消息
  29. $"../Label".text = target_state_name # 这一句自己加的,用于显示状态名称
  30. emit_signal("changed", state.name) # 触发信号,表明状态已经切换完成

状态基类State

  1. # 状态基类,供具体的状态继承和重写
  2. class_name State
  3. extends Node
  4. var state_machine = null # 状态机对象,由StateMachine.gd内部初始化时自动赋值给具体的状态
  5. # 进入状态
  6. func enter(_msg := {}) -> void:
  7. pass
  8. # _unhandled_input -- 事件处理
  9. func handle_input(_event: InputEvent) -> void:
  10. pass
  11. # _process -- 帧处理
  12. func update(_delta: float) -> void:
  13. pass
  14. # _physics_process -- 物理帧处理
  15. func physics_update(_delta: float) -> void:
  16. pass
  17. # 退出状态
  18. func exit() -> void:
  19. pass

中间状态类PlayerState

  1. # 中间类,用于方便在状态中将owner写成Player,并获得代码提示
  2. class_name PlayerState
  3. extends State
  4. var player: Player
  5. func _ready() -> void:
  6. yield(owner, "ready") # 等待根节点Player就绪
  7. player = owner as Player # 将根节点保存为变量
  8. assert(player != null)

通过让具体的状态继承PlayerState,而不是State。可以将owner改为player,并且此时脚本编辑器中也支持代码提示了。语义上也更加清晰了。
另外之所以不直接写入State,是因为后续可能会给其他比如“敌人”也设定状态机,这是,State就无法复用了。

具体的状态

  1. # 状态:静止
  2. extends PlayerState
  3. func enter(_msg := {}) -> void:
  4. player.velocity = Vector2.ZERO
  5. func update(delta: float) -> void:
  6. # 跳跃
  7. if Input.is_action_just_pressed("ui_accept"):
  8. state_machine.change_to("Air", {do_jump = true})
  9. # 跑步
  10. elif Input.is_action_pressed("ui_left") or Input.is_action_pressed("ui_right"):
  11. state_machine.change_to("Run")
  1. # 状态:在移动
  2. extends PlayerState
  3. func enter(_msg := {}) -> void:
  4. player.velocity = Vector2.ZERO
  5. func update(delta: float) -> void:
  6. # 左右移动
  7. player.velocity.x = Input.get_axis("ui_left","ui_right") * player.speed
  8. player.velocity = player.move_and_slide(player.velocity,Vector2.UP)
  9. # 跳跃
  10. if Input.is_action_just_pressed("ui_accept"):
  11. state_machine.change_to("Air", {do_jump = true})
  12. # 静止
  13. elif is_equal_approx(player.dir.x, 0.0):
  14. state_machine.change_to("Idle")
  1. # 状态:在空中
  2. extends PlayerState
  3. func enter(_msg := {}) -> void:
  4. player.velocity = Vector2.ZERO
  5. # 执行跳跃
  6. if _msg.has("do_jump"):
  7. print(player.jump_force)
  8. player.velocity.y = -player.jump_force
  9. player.velocity = player.move_and_slide(player.velocity,Vector2.UP)
  10. func update(delta: float) -> void:
  11. player.velocity = Vector2.ZERO
  12. # 左右移动
  13. player.velocity.x = Input.get_axis("ui_left","ui_right") * player.speed
  14. player.velocity = player.move_and_slide(player.velocity,Vector2.UP)
  15. # 实现下落
  16. player.velocity.y += player.gravity * delta
  17. player.velocity = player.move_and_slide(player.velocity,Vector2.UP)
  18. # 在地面
  19. if player.is_on_floor():
  20. if is_equal_approx(player.velocity.x, 0.0):
  21. state_machine.change_to("Idle")
  22. else:
  23. state_machine.change_to("Run")