Повторим код и структуру туториала Godot — Hierarchical Finite State Machine.
Сначала вспомним устройство этого примера. Я постараюсь подробно объяснить, как я его восстанавливаю. Нам поможет рисунок из предыдущего туториала.
У нас эта диаграмма будет работать немного иначе.
Как показано на рисунке, у нас будет три состояния: Idle, Move и Jump. Система построена иерархически и использует механизм наследования. Благодаря этому сигнал signal_finished может вызываться в тех местах, где происходит обработка ввода (handle_input). В свою очередь, handle_input вызывается из функции _unhandled_input, которая обращается к текущему состоянию. Текущее состояние может находиться в любом из классов, наследуемых от state.gd.
Итак, приступим.
Создадим новый проект — назовём его Hierarchical Finite State Machine Experiment.
Добавим узел CharacterBody2D.
Внутрь CharacterBody2D вложим узел Node и переименуем его в StateMachine.
Внутрь StateMachine добавим три узла Node и назовём их Idle, Move и Jump.
В CharacterBody2D также добавим узлы Sprite2D и CollisionShape2D.
В Sprite2D поместим иконку.
Для CollisionShape2D выберем форму Rectangle и растянем её под размер изображения.
По желанию можно добавить Label для отображения текущего состояния объекта.
Сохраним сцену под именем Player.tscn.
Теперь подготовим пять скриптов: state.gd, state_machine.gd, on_ground.gd, motion.gd и player_state.gd.
Начнём с state.gd — вот его содержимое.
extends Node
@warning_ignore("unused_signal")
signal finished(next_scene:String)
func _enter() -> void:
pass
func _exit() -> void:
pass
func handle_input(_input_event:InputEvent)->void:
pass
Этот код отвечает за обработку ввода и за функции начала / конца текущего состояния.
Теперь напишем код для state_machine.gd.
extends Node
@export var start_state: NodePath
signal state_changed(current_state: Node)
var states_map := {}
var _active:bool=false
var states_stack := []
var current_state:Node = null
Определяем начальное состояние — start_state.
Добавляем сигнал state_changed, который испускается при завершении состояния (кода в узле Node).
Создаём словарь states_map.
Вводим булеву переменную _active, которая указывает, готова ли сцена.
Объявляем стек состояний states_stack.
Задаём текущее состояние current_state.
Рассмотрим код подробнее.
func _enter_tree() -> void:
if start_state.is_empty():
start_state = get_child(0).get_path()
for child in get_children():
var err: bool = child.finished.connect(_change_state)
if err:
printerr(err)
initialize(start_state)
В этой функции, при инициализации сцены, мы:
Берём первый дочерний узел — Node с именем Idle.
Подключаем все сигналы finished от всех Node к функции _change_state.
Вызываем функцию initialize.
func initialize(initial_state: NodePath) -> void:
_active = true
states_stack.push_front(get_node(initial_state))
current_state = states_stack[0]
current_state.enter()
Выставляем _active = true. Кладём в стек узел Idle, запоминаем его как текущее состояние и запускаем функцию enter() (она находится в скрипте этого узла).
func _unhandled_input(input_event: InputEvent) -> void:
current_state.handle_input(input_event)
Поручаем узлу Idle обрабатывать ввод с помощью функции handle_input.
func _physics_process(delta: float) -> void:
current_state.update(delta)
Заставляем узел Idle обновляться.
func _change_state(state_name: String) -> void:
if not _active:
return
current_state.exit()
current_state = states_map[state_name]
state_changed.emit(current_state)
current_state.enter()
Выходим из текущего состояния и отправляем сигнал о переходе к следующему состоянию из стека. Затем заходим в функцию enter().
Теперь напишем код для player_state.gd
extends "res://state_machine.gd"
const PLAYER_STATE: Dictionary[StringName, StringName]= {
&"jump": &"jump",
&"idle": &"idle",
&"move": &"move",
&"walk": &"walk",
}
Словарь состояний.
Теперь рассмотрим скрипт motion.gd. Он отвечает за перемещение главного персонажа и за его направление.
func get_input_direction() -> Vector2:
return Vector2(
Input.get_axis(&"move_left", &"move_right"),
Input.get_axis(&"move_up", &"move_down")
)
Функция возвращает вектор перемещения главного персонажа (тип Vector2).
func update_look_direction(direction: Vector2) -> void:
if direction and owner.look_direction != direction:
owner.look_direction = direction
Этот код обновляет переменную look_direction в скрипте узла CharacterBody2D (пока что эта переменная не определена).
Теперь рассмотрим скрипт on_ground.gd.
func handle_input(input_event: InputEvent) -> void:
if input_event.is_action_pressed(&"jump"):
finished.emit(PLAYER_STATE.jump)
Когда игрок нажимает прыжок, срабатывает сигнал finished.
Прикрепляем к CharacterBody2D скрипт player_controller.gd. Вот весь код, который мы туда записали.
extends CharacterBody2D
var look_direction := Vector2.RIGHT
Добавим скрипт player_state_machine.gd к узлу StateMachine. В этом же узле укажем переменную StartScene и присвоим ей значение Idle.
extends "res://player_state.gd"
Скрипт player_state_machine.gd наследуется от state_machine.gd.
@onready var idle: Node = $Idle
@onready var move: Node = $Move
@onready var jump: Node = $Jump
Создадим переменные для каждого дочернего узла (Node).
func _ready() -> void:
states_map = {
PLAYER_STATE.idle: idle,
PLAYER_STATE.move: move,
PLAYER_STATE.jump: jump,
}
Создадим словарь, в который поместим все дочерние узлы (Node).
func _change_state(state_name: String) -> void:
if not _active:
return
if state_name == PLAYER_STATE.jump:
states_stack.push_front(states_map[state_name])
if state_name == PLAYER_STATE.jump and current_state == move:
jump.initialize(move.speed, move.velocity)
super._change_state(state_name)
Добавляем в стек те состояния, выполнение которых требует времени.
if state_name in [PLAYER_STATE.jump]:
states_stack.push_front(states_map[state_name])
Прыжок срабатывает только в том случае, если персонаж уже двигался и игрок нажал клавишу прыжка.
if state_name == PLAYER_STATE.jump and current_state == move:
jump.initialize(move.speed, move.velocity)
Вызываем метод _change_state() из скрипта state_machine.gd.
super._change_state(state_name)
func _unhandled_input(input_event: InputEvent) -> void:
current_state.handle_input(input_event)
Обрабатываем текущее состояние сцены.
Создадим скрипты для соответствующих узлов:
для Idle — idle.gd;
для Move — move.gd;
для Jump — jump.gd.
Начнём с idle.gd и напишем для него следующий код.
func handle_input(input_event: InputEvent) -> void:
return super.handle_input(input_event)
Этот код возвращает управление скрипту on_ground.gd, где становится возможным прыжок.
func update(_delta: float) -> void:
var input_direction: Vector2 = get_input_direction()
if input_direction:
finished.emit(PLAYER_STATE.move)
Эта функция может испускать сигнал перемещения.
Теперь рассмотрим код move.gd.
func enter() -> void:
speed = 0.0
velocity = Vector2()
var input_direction := get_input_direction()
update_look_direction(input_direction)
Она берёт вектор движения и разворачивает игрока в нужную сторону.
func handle_input(input_event: InputEvent) -> void:
return super.handle_input(input_event)
Управление переходит к скрипту прыжка, инициируя соответствующее состояние игрока.
func update(_delta: float) -> void:
var input_direction := get_input_direction()
if input_direction.is_zero_approx():
finished.emit(PLAYER_STATE.idle)
update_look_direction(input_direction)
if Input.is_action_pressed(&"run"):
speed = max_run_speed
else:
speed = max_walk_speed
var collision_info := move(speed, input_direction)
if not collision_info:
return
if speed == max_run_speed and collision_info.collider.is_in_group(&"environment"):
return
Если игрок останавливается, отправляется сигнал idle.
Если игрок нажимает кнопку «бег» (которую предварительно нужно добавить в «Проект» → «Настройки проекта» → «Список действий»), скорость игрока увеличивается. Затем вызывается функция move.
func move(p_speed: float, direction: Vector2) -> KinematicCollision2D:
owner.velocity = direction.normalized() * p_speed
owner.move_and_slide()
if owner.get_slide_collision_count() == 0:
return null
return owner.get_slide_collision(0)
В функции move объекту CharacterBody2D задаётся скорость, а также обрабатываются столкновения с помощью move_and_slide(). Это необходимо для того, чтобы в будущем можно было подбирать коробки со снаряжением.
Конец.
Мы разобрали состояния Idle и Move. Состояние Jump в иерархической конечной машине (Hierarchical Finite State Machine) я предлагаю вам изучить самостоятельно. Спасибо за внимание.





