Autómatas no arcade

Cando estamos a xogar a un videoxogo notamos como hai personaxes ou outros elementos que se comportan de modo distinto segundo o que estea a pasar. Un inimigo pode estar seguindo un camiño e correr tras do xogador cando o ve, o propio personaxe do xogador pode subirse a un vehículo e comezar a conducilo.

Para calquera elemento do xogo hai unha serie de procesos que deben acontecer en cada imaxe debuxada na pantalla. Para un inimigo, por exemplo, debe de actualizarse a animación sexa de andar ou correr, os sons dos seus pasos, o obxecto de físicas que dita como se comportará ao empurrar unha porta e moitos outros.

Cando o estado actual da entidade cambia, o comportamento de todos os sistemas cos que interactúa debe mudar á vez. A maneira trivial de levar isto a cabo sería que cada sistema tivese unha serie de condicionais e todos dependesen dunha mesma variable:

enum Estado {ANDAR, PERSEGUIR, CONDUCIR}
var estado_actual: Estado = Estado.ANDAR

func _input(event: InputEvent):
  if estado_actual == Estado.CONDUCIR:
    # modificar aceleración do auto
  else:
    # modificar posición do xogador

func _process(delta):
  if estado_actual == Estado.CONDUCIR:
    # xestionar animación do vehículo
  elif estado_actual == Estado.ANDAR
    # animación de andar, movemento lento
  else:
    # animación de correr, movemento rápido

func _physics_process(delta):
  # igual que arriba, decidir segundo "estado_actual"

Xa no exemplo de arriba, no que non hai código real que realize nada, xa vemos o incómodo que podería resultar. A maiores, é habitual que para cada estado haxa unha serie de variables que non son necesarias nas outras, o que faría a maraña aínda máis complexa.

Por último, tomando un dos exemplos do xogo Metarcade, que faríamos se un dos personaxes fose en cadeira de rodas? Quizais engadir un “if” máis nos casos de correr e andar. Ou facer unha subclase que en estes estados en concreto rectificase algunha decisión da clase pai?

Autómatas

A pesar do xeral que soa o título, neste artigo só imos ver un tipo de autómata en concreto a máquina de estados. Deste xeito, non sería o nodo actual o que executaría os comandos para cada estado, senón que só xestionaría en que “Estado” se encontra, e delegaría noutros nodos para executar cada caso concreto.

Para isto, comezaríamos por definir un script base para cada estado:

class_name State
extends Node

func input(event: InputEvent):
	pass

func process(delta):
	pass

func physics_process(delta):
	pass

func enter():
	pass

func exit():
	pass

Aquí proporcionamos a calquera estado unha base que tomar para implementar todas as funcionalidades que precise. Queda un script moito máis curto e fácil de ler, no que só as variables necesarias estarán dispoñibles. Aparecen aquí dúas novas funcións que non estaban presentes no exemplo inicial, enter() e exit(). Serán moi cómodas para definir transicións entre estados ou para iniciar e parar procesos que só teñen que ver con un deles.

Agora que sabemos como modelar un estado, quedaría por definir como xestionar o actual ou o cambio entre eles.

class_name StateMachine
extends Node

@export var initial_state: State;

var cur_state: State = null;

func _ready():
  set_state(initial_state)

func set_state(new_state: State) :
  if cur_state != null:
    cur_state.exit()
  cur_state = new_state
  cur_state.enter()

func _input(event: InputEvent):
  cur_state.input(event)

func _process(delta):
  var next_state = cur_state.process(delta)
  if next_state != null:
    set_state(next_state)

func _physics_process(delta):
  cur_state.physics_process(delta)

As funcións _input e _physics_process soamente delegan no estado actual o seu comportamento, mentres que _process permite ao estado en execución pedir un cambio a outro diferente.

Unha vez definidos unha serie de estados, a xerarquía queda como na seguinte imaxe.

A modo de remate, antes de comezar cos exemplos no noso xogo, gustaríame recomendar o capítulo “State” no libro Game Programming Patterns.

Máquinas de estados en Metarcade

Hai moitas máquinas de estado en Metarcade, pero vou mostrar o caso máis claro, o dos NPC. Como se pode ver na seguinte imaxe, os NPC poden, entre outras variables, un diálogo (Dialogue Resource) e un camiño a seguir (Path to Follow).

Como é de esperar, no caso de existir un diálogo a personaxe entrará no estado de conversa cando o xogador o invoque. No caso de non haber ningún camiño definido de antemán, o estado por defecto será estático, cunha animación acorde. Para os NPC que teñen un camiño definido, estes seguirano indefinidamente mentres non sexan parados polo xogador.

A máquina de estados básica é polo tanto como se mostra na seguinte imaxe. O script raíz da escea NPC configúraa na carga segundo os parámetros expostos anteriormente.

Dos anteriores, quizais o Walking sexa o máis interesante. A maneira de que un nodo en Godot siga un camiño predeterminado é definir unha curva de tipo Path3D, que é a que exportamos arriba e será estática, e crear un obxecto de tipo PathFollow3D que é o que se irá actualizando a medida que o nodo avanza pola curva.

Cando o nodo entra no estado Walking, debe buscar o punto da curva asignada que está máis preto ao punto actual para inicializar o obxecto PathFollow3D. Deste modo o comportamento será máis natural. Xa que estamos definindo un estado a función enter é a idónea para encargarse disto.

var path_follow: PathFollow3D

func enter():
  path_follow = PathFollow3D.new()
  var offset_point =     curve_path.curve.get_closest_offset(character.position)
  path_follow.progress = offset_point

Unha vez todo está preparado, a función process pode encargarse do movemento.

func process(delta):
  path_follow.progress += speed * delta;
  var next_position = path_follow.position;
  character.set_position(next_pos)
  character.rotation = path_follow.rotation
  character.play_animation(walk_animation)

Para unha implementación básica isto sería suficiente. Queda bastante claro que o uso de estados foi a decisión correcta pola facilidade que temos para xestionar o comezo da marcha ou ao ver como a variable path_follow está contida aquí sen afectar a outros estados.

Personalización do comportamento

Por suposto o caso xeral exposto non aplicará a todos os NPC e complicalo moito para ter en conta cada caso específico sería moi mal deseño.

Un exemplo co que tivemos que lidar foi o dun personaxe que bloqueaba o paso no caso de que o xogador non cumprise unha serie de criterios e se movese cando estes fosen satisfeitos.

Para isto, unha vez colocado na escena adecuada, activamos a opción “editable children” de Godot. Isto permite engadir novos nodos e modificar certas variables dos existentes na escena importada. Así, creamos o nodo SelectState e o definimos como o nodo usado inicialmente na máquina de estados.

Este é un nodo moi sinxelo que expón dúas variables de tipo State e simplemente transiciona a unha ou outra dependendo dunha condición.


Grazas por ler este post! Xunto co anterior, Usos de @export en Godot, recollen a charla que demos na última xuntanza en Vigo. Nos post seguintes tratarei de dar algunha nova sobre o que estamos a facer no xogo e non explicacións tan densas sobre a ferramenta.

Comentarios

2 respostas a “Autómatas no arcade”

  1. Avatar de jsmtux

    @metarcade_bitacora novo artigo no blog do noso xogo Metarcade!
    Vai sobre o uso de máquinas de estado en #godot e como as aplico no xogo.

Deixa unha resposta

O teu enderezo electrónico non se publicará Os campos obrigatorios están marcados con *