Bitácora de desenvolvemento (devblog)

  • Autómatas no arcade

    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.

  • Usos de @export en Godot

    Usos de @export en Godot

    Ademais de compartir novas do desenvolvemento de Metarcade, neste blog pretendo compartir cousas que aprendín e continúo a aprender sobre o uso de Godot.

    Quizais o uso de @export para notar que unha variable poda ser modificada dende o editor de maneira visual. De entrada pode valer para equipos grandes nos compañeiros que non programan podan modificar o comportamento do xogo, pero ten moitas outras capacidades para deseñar a arquitectura dun videoxogo.

    En todos os casos o recomendado é consultar a documentación de Godot, pero en resumo, podemos exportar variables do seguitne modo:

    @export var velocidade: int = 5
    @export var outro_nodo: Node

    Como no exemplo anterior, na maior parte dos casos necesítase definir o tipo de variable e, opcionalmente, incluír un valor por defecto.

    Definir relacións entre nodos

    Un uso sinxelo para esta funcionalidade que me axudou enormemente a estruturar o noso proxecto foi usar @export para definir relacións entre distintos nodos.

    Metarcade, como calquera xogo non trivial, ten diferentes “estados de xogo” como explorar, xogar ás máquinas ou dialogar con outros NPC. No noso caso temos definidos os estados como nodos simples que comparten un pai na xerarquía. Como cada estado está asociado a unha interface de usuario distinta, ao principio modeláramolo como na seguinte imaxe:

    Diría que se hai un manual de boas prácticas de Godot, mesturar deste xeito na xerarquía nodos de distinto tipo estaría desaconsellado en letras grandes e vermellas. No caso dos nodos de control (en verde) que teñen propiedades semellantes, como neste caso que todos ocupan a mesma porción de pantalla, o mellor é que todos herden do mesmo nodo de control, para así só definir estas propiedades comúns unha vez.

    Deste xeito a xerarquía ten máis sentido, pero a conexión entre o estado “Exploring” e os controis “walking_controls” non me resultaba doado de modelar. De entrada trato de non usar variables globais (autoload en Godot) e facer as conexións nun script do nodo pai do seguinte modo:

    $StateMachine/Exploring.control_node = $gui_main/walking_controls

    Non só é engorroso senón que é moi fraxil ao estragarse xa só se os nodos cambian de nome.

    Ata que me percatei de que podía facilmente conectar os diferentes nodos con variables exportadas:

    @export var state_controls: Control = null;

    Deste xeito, basta con arrastrar cada un dos nodos á propiedade state_controls do estado.

    Tipos complexos

    Unha maneira que temos de simplificar o traballo de deseño dos niveis é utilizando variables exportadas en nodos xenéricos como os das máquinas de arcade.

    Hai un único nodo que define a máquina e logo exporta variables como o modelo 3D específico, a cor da luz que emite ou a descrición e nome para mostrar nos menús do xogo.

    Para isto, no canto de exportar todas as variables por separado, creei un tipo de datos novo que as contén e se ve do seguinte modo no editor:

    Non só axuda a organizar as propiedades no menú (que tamén se podería facer con @export_subgroup), senón que me permite enviar a outros nodos a información en conxunto. Un exemplo é o menú que aparece antes de xogar, que indica título, descrición e unha imaxe coa máquina.

    Para facer isto basta con crear un novo script, definir un class_name e incluír todas as variables necesarias. Como consello, no canto de herdar de Node recomendaría usar Resource como a clase base, xa que non se utilizará ningunha funcionalidade que non a conteña. Hai máis información sobre esto último na documentación.

    No meu caso, e para o exemplo das máquinas de arcade, o script final quedou así:

    extends Resource
    class_name ArcadeMachineData
    @export var machine_name: String = "Machine Name"
    @export_multiline var machine_desc: String = "Empty description"
    @export var machine_difficulty: int = 10
    @export var machine_mesh: PackedScene = null
    @export var machine_color: Color = Color.WHITE

    É importante destacar deste código que tódalas variables teñen un valor por defecto. Isto pode ocorrer unha función _init ou, como enriba, na declaración das variables. De existir algunha variábel sen valor por defecto, a pesar de ser un recurso totalmente válido, o editor de Godot non será capaz de mostralo na interface.

    Modificar a escena dende un script

    Nalgúns casos vai ser cómodo que os cambios que introducimos a través dun @export se vexan reflexados na escena no momento de cambialos. Para isto podemos marcar o noso script como @tool, como explica a documentación.

    Deste xeito, se a cor dunha fonte de luz ven definida por unha variable exportada e a cambiamos, poderemos velo directamente na escena mentres a estamos a editar.

    Existe un pequeno problema á hora de usar isto para instanciar novas escenas. Cando queremos instanciar unha escena nova, a pesar de facelo na función _ready, os cambios non serán reflectidos directamente no editor, para iso haberá que modificar a instancia do seguinte xeito:

    node.owner = get_tree().edited_scene_root

    No meu caso, o código final quedaría así:

    @export var data: ArcadeMachineData = ArcadeMachineData.new()
    
    const MESH_NODE_NAME = "arcade-machine"
    
    func _ready():
      if Engine.is_editor_hint():
      instantiate_configured_mesh()
    
    func instantiate_configured_mesh():
      var existing_machine = get_node(MESH_NODE_NAME)
      if existing_machine:
        existing_machine.queue_free()
      var instanced_mesh = data.machine_mesh.instantiate()
      add_child(instanced_mesh)
      instanced_mesh.name = MESH_NODE_NAME
      instanced_mesh.owner = get_tree().edited_scene_root

    E deste xeito podo facilmente configurar cómo se verá a escena final.


    E ata aquí o que podo dicir de @export, invítovos a comentar calquera pregunta que teñades, ou ideas sobre como mellorar o meu uso de Godot. Se queredes probar o xogo hai unha pequena demo do que levamos feito ata o de agora en jsmtux.itch.io/metarcade

  • Comezando Metarcade

    Comezando Metarcade

    Metarcade é un xogo nacido da primeira xuntanza de Godoteires, na Coruña. Eu levaba tempo con ganas de aprender un pouco máis de Godot, polo que fixen un pequeno xogo RPG de exemplo e din unha pequena charla sobre el. Está publicado en https://github.com/jsmtux/July_RPG.

    Lucía (Ferro) ía vir comigo igual, así que a liei para facer unha charla tamén. Xa que ela é escritora tiña bastante que compartir sobre facer historias. Ademais de que xa levaba tempo a investigar sobre como a interactividade deste medio podía axudar a transmitir ideas. Quedaralle tamén unha charla moi chula https://www.youtube.com/watch?v=P6rBRN3mnVY.

    Logo de tan exitoso encontro, decidimos facer un proxecto un pouco máis serio xuntos. Ao non dedicarnos a facer videoxogos, o plan que trazamos para o desenvolvemento debía adaptarse ao noso tempo libre.

    Aínda que me podo manexar con Gimp, Inkscape ou Blender e fixen algun proxecto con eles, como as portadas dos libros de Lucía, non me vexo capaz de xerar todos os gráficos para un videoxogo completo. Xa que decidiramos facer algo en 3D, visitamos kenney.nl para ver que había dispoñible. Chamounos a atención un dos últimos, sobre un salón de arcade con diferentes máquinas. Facer un xogo nun salón de arcade ten de vantaxe que podemos usar moitos dos outros assets para as distintas salas temáticas.

    Queriamos que a parte principal do xogo fose a narrativa, artellada a través de conversas cos diferentes personaxes do salón de arcade. A historia da protagonista comeza cando visita o salón por vez primeira buscando a unha amiga súa, que tardará en atopar. O resto de persoas do arcade si a coñecen, pero nuns casos non saberán moito e noutros non conversarán coa protagonista por non ser suficientemente boa xogando ás máquinas.

    Polo momento decidimos que as máquinas ás que se xoga durante o xogo non sexan interactivas, senón que o resultado veña dado polo nivel ou diferentes habilidades que esta irá obtendo. Este é un punto de continua confrontación entre ambos, xa que ela prefire engadir algún tipo de interacción mentres o xogo descorre e eu que unha vez comezada a partida non se interactúe máis antes que unha mecánica chorras. Xa veremos.

    O noso obxectivo inmediato era ter algo que presentar na seguinte xuntanza de Godoteires. Dende principios de ano eu dispuxen de moito tempo, polo que logo de discutir a idea xeral do que queriamos facer comecei programando a base tecnolóxica. A idea era ter o traballo o máis avanzado posible para que cando Lucía tivese a historia e conversas rematadas fose o máis rápido posible integrar todo o contido.

    Non foi ata un par de días antes da xuntanza cando estivo a maior parte do contido rematado. É certo que algún día estivemos a traballar ata media noite, pero semella que toda a preparación que fixeramos pagou a pena e puidemos integrar todos os personaxes a tempo. Do que queríamos nun principio para esta primeira demo faltaron algunhas mecánicas extra que tiñamos ideadas e facer diferentes videoxogos para cada máquina de arcade, reutilizando un mesmo con diferentes graos de dificultade.

    A demo do xogo podedes descargala para Android a través de https://jsmtux.itch.io/metarcade, e todo feedback será apreciado! Os videos das charlas subiranse á web de A industriosa, pero espero nas próximas semanas facer unha entrada sobre a miña. E en xeral de vez en cando subir un post a este blog sobre como continúa o desenvolvemento.