参考
原文地址:https://www.bilibili.com/read/cv15683152?spm_id_from=333.999.0.0
本篇用语雀“备份”了@张学徒(zhangxuetu)的状态机写法,在部分代码排版和注释风格上做了一定修改。并添加了原文没有的一些注释,方便查看和会意。
可以参照9.状态机初步一文,对照学习。
状态机
#============================================================
# State Machine
#============================================================
# 状态机
#============================================================
# @datetime: 2022-3-15 17:02:55
#============================================================
class_name StateMachine
extends Node
signal state_changed(last_state, current_state)
var current_state # 当前状态
var current_state_node : State # 当前状态对应的节点
var blackboard : StateBlackboard setget , get_blackboard # 状态黑板
var states := {} # 所有可用状态的字典
#============================================================
# Set/Get
#============================================================
func get_state_node_list() -> Array: # 获取所有可用状态的节点的数组
return states.values()
func get_blackboard(): # 获取状态黑板
if blackboard == null:
for child in get_children():
if child is StateBlackboard:
blackboard = child
return blackboard
func get_state_node(state) -> State: # 返回状态名称对应的节点
return states[state] as State
#============================================================
# 内置
#============================================================
func _ready():
# 找到所有子状态机记录到states字典
for child in get_children():
if child is State:
states[child.name] = child
# 如果存在至少1个状态 -- 默认进入第一个状态
if states.size() > 0:
current_state = states.keys()[0]
current_state_node = get_state_node(current_state)
current_state_node.enter(null)
else:
set_physics_process(false)
printerr("没有添加状态节点,状态机没有启动")
# 只执行当前状态的物理帧处理过程
func state_process(delta):
current_state_node.state_process(delta)
#============================================================
# 自定义
#============================================================
# 切换状态
func switch_to(state, data: Dictionary = {}):
if state != current_state: # 如果新的状态不是现在的状态
current_state_node.exit() # 判断当前状态的节点存在
emit_signal("state_changed", current_state, state)
current_state = state # 将状态记录为当前状态
current_state_node = get_state_node(current_state) # 记录状态节点为当前状态的节点
current_state_node.enter(data) # 调用当前状态的进入
状态基类
#============================================================
# State 状态基类 @datetime: 2022-3-15 17:03:14 张学徒 巽星石部分修改,用于展示
#============================================================
class_name State
extends Node
onready var state_machine = get_parent() # 获取和记录StateMachine对象
#============================================================
# Set/Get
#============================================================
# 获取状态机对象
func get_state_machine():
return state_machine
# 获取状态机的当前状态
func get_current_state() -> State:
return state_machine.get_current_state() as State
# 获取状态黑板
func get_blackboard():
return get_state_machine().get_blackboard() as StateBlackboard
#============================================================
# 核心代码
#============================================================
# 物理帧处理
func state_process(delta):
pass
# 进入状态
func enter(data):
pass
# 退出状态
func exit():
pass
# 切换状态
func switch_to(state, data: Dictionary = {}):
state_machine.switch_to(state, data)
可以看到,张学徒的代码更灵活,给予了状态基类更多的权限,比如自己切换状态,获取当前状态等等。
状态黑板类
另外张学徒还引入了一个叫做“StateBlackboard(状态黑板)”的额外类型,说是用于存储状态机的全局数据。
#============================================================
# Blackboard 状态黑板 @datetime: 2022-3-15 17:31:58
#============================================================
class_name StateBlackboard
extends Node
var data : Dictionary = {}
func _enter_tree():
get_parent().blackboard = self # 赋值状态机的blackboard属性为自己
在做的时候,我会创建对应类型的状态机、状态和对应的黑板。比如我要给 Player 节点设计状态机,我会先创建对应的 PlayerStateMachine(Player状态机)、PlayerState(Player状态基类)、PlayerBlackboard(Player状态机黑板)。 ——@张学徒(zhangxuetu)
PlayerState的推测
原文的下半部分没有贴出完整的代码,所以联系作者要了项目源码,然而,部分设计已经修改,所以类名也有所修改,所以只好贴出项目中的源码。
#============================================================
# PlayerBaseState
#============================================================
# * 这是这个状态机的基础状态类,默认添加到场景中,便于修改这个类型中的属性
# 和方法。
#============================================================
# @datetime: 2022-5-1 22:35:53
#============================================================
class_name PlayerBaseState
extends BaseExecuteState
var controller : PlayerController
var input : InputController
var attack_wrapper : AttackWrapper
var equipment_wrapper : EquipmentWrapper
var property_wrapper : PropertyWrapper
var type_layer : PlayerTypeLayer
#============================================================
# Set/Get
#============================================================
func get_host() -> Player:
return owner as Player
func get_blackboard() -> PlayerStateBlackboard:
return .get_blackboard() as PlayerStateBlackboard
func set_gravity_enabled(value: bool):
get_move_controller().gravity_enabled = value
func get_layer() -> PlayerTypeLayer:
return owner.type_layer as PlayerTypeLayer
func get_input_controller() -> InputController:
return input
func get_move_controller() -> SimplePlatformController:
return controller.move_controller
func is_moving() -> bool:
return controller.move_controller.is_moving()
func is_on_floor() -> bool:
return get_host().is_on_floor()
func is_falling():
return (
not get_host().is_on_floor()
or controller.move_controller.get_last_velocity().y > 0
)
func is_can_jump() -> bool:
return controller.move_controller.jump_enabled
func is_can_move() -> bool:
return controller.move_controller.move_enabled
func is_can_fall() -> bool:
return (get_host() as KinematicBody2D).is_on_floor()
func is_can_attack() -> bool:
return (attack_wrapper.is_enabled()
and equipment_wrapper.get_type_items("weapon").size() > 0
)
func is_on_stairs() -> bool:
return type_layer.collision.is_can_climb_stairs()
func is_on_stairs_top() -> bool:
return type_layer.collision.is_on_stairs_top()
func is_can_climb_stairs() -> bool:
return is_on_stairs() or is_on_stairs_top()
func is_can_climb_wall() -> bool:
return get_host().is_on_wall()
#============================================================
# 内置
#============================================================
func _ready():
# 等待 Player 中的 onready 节点都加载完成
yield(owner, "ready")
controller = get_layer().controller
input = get_layer().controller.input_controller
type_layer = get_host().type_layer
attack_wrapper = WrapperHelper.get_attack(owner)
equipment_wrapper = WrapperHelper.get_equipment(owner)
property_wrapper = WrapperHelper.get_property(owner)
#============================================================
# 状态方法
#============================================================
#(override)
func enter():
# printerr("进入到状态:", name)
pass
#(override)
func exit():
# printerr("退出状态:", name)
pass
#============================================================
# 自定义
#============================================================
## 移动
func input_move() -> bool:
var direction : float = Input.get_axis(input.input_left, input.input_right)
if direction and is_can_move():
controller.move(direction)
return true
return false
## 跳跃
func input_jump() -> bool:
if (Input.is_action_pressed(input.input_up)
and is_can_jump()
):
if controller.jump():
return true
return false
## 下落
func input_fall() -> bool:
if (Input.is_action_pressed(input.input_down)
and is_can_fall()
):
controller.fall()
return true
return false
## 发出攻击
func input_attack() -> bool:
if (Input.is_action_pressed(get_blackboard().input_attack)
and is_can_attack()
):
attack_wrapper.attack()
return true
return false
## 爬楼梯
func input_climb_stairs() -> bool:
var dir = Input.get_axis(input.input_up, input.input_down)
if is_can_climb_stairs():
if ((dir < 0 and not is_on_stairs_top()) # 向上移动
or dir > 0 # 向下移动
):
get_host().move_and_slide(
Vector2(0, property_wrapper.get_property("move_speed") * dir)
, Vector2.UP
)
return true
return false
## 爬墙
func input_climb_wall():
if (is_can_climb_wall()
and Input.is_action_pressed(input.input_up)
):
get_move_controller().jump(
# 爬墙速度为 jump_height 的 0.6
0.6 * property_wrapper.get_property('jump_height')
, true
)
return true
return false
#============================================================
# PlayerIdle
#============================================================
# @datetime: 2022-4-26 00:59:06
#============================================================
class_name PlayerIdle
extends PlayerBaseState
#(override)
func state_process(_arg0):
set_gravity_enabled(not is_on_stairs_top())
if input_fall():
if is_on_stairs_top():
switch_to(PlayerStateBlackboard.States.CLIMB_STARIS)
if input_jump():
switch_to(PlayerStateBlackboard.States.JUMP)
elif input_attack():
switch_to(PlayerStateBlackboard.States.ATTACK)
elif input_climb_stairs():
switch_to(PlayerStateBlackboard.States.CLIMB_STARIS)
elif input_climb_wall():
switch_to(PlayerStateBlackboard.States.CLIMB_WALL)
elif input_move():
switch_to(PlayerStateBlackboard.States.MOVE)