Сейчас я восстановлю структуру программы godot Hierarchical Finite State Machine

Повторим код и структуру туториала 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)

Обрабатываем текущее состояние сцены.
Создадим скрипты для соответствующих узлов:

для Idleidle.gd;

для Movemove.gd;

для Jumpjump.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) я предлагаю вам изучить самостоятельно. Спасибо за внимание.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх