前言
之前在本站已经写过一篇有限状态机的博客,最近又看到一种新的实现方法,撰以记.
结构
本状态机使用继承的方法,首先创建一个角色类和一个状态机脚本(状态机类的代码),在角色类上添加状态机类节点.这里的代码是所有使用状态机的角色的统一规范.
角色类
在 Character
类中有所有角色最基本的节点,对于碰撞体积和动画精灵都不做定义和修改,因为这里相当于一个抽象父类(Java中的概念),用来定义规范和接口(主打一个封装).
FininateStateMachine – 状态机
状态机中的脚本:
extends Node
class_name FininateStateMachine
var states: Dictionary = {}
var previous_state: int = -1
var state = -1:
set = set_state # 设置变量并读取变量
@onready var parent: Character = get_parent() # 获取使用状态机的角色
@onready var animation_player: AnimationPlayer = parent.get_node("AnimationPlayer") # 获取角色下的动画播放器
func _physics_process(delta: float) -> void:
if (state != -1): # 当状态有效时执行
_state_logic(delta) # 状态的逻辑
var transition:int = _get_transition() # 检测是否需要切换状态,默认-1不转换
if (transition != -1):
set_state(transition) # 将状态设置成新的状态
func _get_transition() -> int:
# 检测是否需要切换状态,在子类中实现
return -1
func _state_logic(_delta: float) -> void:
# 状态的主要逻辑
pass
func _add_state(new_state: String) -> void:
# 在状态机字典中添加一种新的状态
states[new_state] = states.size()
func _enter_state(_previous_state: int, _new_state: int) -> void:
# 从旧的状态进入到新的状态时执行
pass
func _exit_state(_state_exited: int) -> void:
# 离开状态时执行
pass
func set_state(new_state: int) -> void:
# 将状态设置成新的状态时执行
_exit_state(state)
previous_state = state
state = new_state
_enter_state(previous_state, state)
状态机中定义了一些管理状态的接口规范.
Character – 角色类
角色类中只定义了最基本的角色们的规范.
@icon("res://AssetsPack/heroes/knight/knight_idle_anim_f0.png")
extends CharacterBody2D
class_name Character
@onready var animated_sprite: AnimatedSprite2D = $AnimatedSprite2D
const FRICTION:float = .15
@export var acceleration: int = 40
@export var max_speed: int = 10
var speed: int = 0
var move_direction: Vector2 = Vector2.ZERO
func _physics_process(_delta: float) -> void:
move_and_slide() # 移动
velocity = lerp(velocity, Vector2.ZERO, FRICTION) # 应用摩擦力
func move() -> void:
move_direction = move_direction.normalized() # 将向量长度变为1
speed += acceleration
speed = clampf(speed, -max_speed, max_speed)
velocity += speed * move_direction
Player – 玩家类
我们利用 Character
类创建一个新的继承场景,重命名为 Player
作为我们玩家的类,接触继承后自带的状态机和 CharacterBody2d
上默认的父脚本,并重新创建两个脚本, 让他们分别继承状态机父类和角色的父类,例如我专门为玩家创建了一个文件夹并将玩家节点下的所有脚本以及场景放在其中,这里的状态机脚本就是继承得来的,我们在复写父类的空方法体即可.
player.gd – 玩家脚本
玩家脚本继承自父类角色类脚本,其实所有的游戏中的角色都应该继承自父类角色脚本,这样方便使用统一的接口.
extends Character
func _process(delta: float) -> void:
var mouse_direction: Vector2 = (get_global_mouse_position() - global_position).normalized() # 得到一个指向鼠标的单位向量
# 默认朝向右边,flip后朝向左边
if (mouse_direction.x > 0):
animated_sprite.flip_h = false
else:
animated_sprite.flip_h = true
func get_input() -> void:
move_direction = Vector2.ZERO
if (Input.is_action_pressed("down")):
move_direction += Vector2.DOWN
if (Input.is_action_pressed("up")):
move_direction += Vector2.UP
if (Input.is_action_pressed("left")):
move_direction += Vector2.LEFT
if (Input.is_action_pressed("right")):
move_direction += Vector2.RIGHT
玩家状态机
玩家的状态机应该专门有自己的状态机逻辑,所以我们为玩家类单独创建了一个状态机节点.
extends FininateStateMachine
func _init() -> void:
_add_state("idle")
_add_state("move")
func _ready() -> void:
set_state(states.idle)
func _state_logic(_delta: float) -> void:
parent.get_input()
parent.move()
func _get_transition() -> int:
match state:
states.idle:
if (parent.velocity.length() > 10):
return states.move
states.move:
if (parent.velocity.length() < 10):
return states.idle
return -1
func _enter_state(_previous_state: int, _new_state: int) -> void:
match state:
states.idle:
animation_player.play("idle")
states.move:
animation_player.play("move")
func _exit_state(_state_exited: int) -> void:
# 离开状态时执行
pass
到这里就实现了玩家的状态机和玩家的主要逻辑,运行可以发现玩家可以正常移动和播放动画.