Tu primer juego

Sinopsis

Este tutorial te guiará en la creación de tu primer proyecto en Godot. Aprenderás cómo funciona el editor, estructurar un proyecto y construir un juego 2D.

Nota

Este proyecto es una introducción al motor Godot. Se asume que el lector posee alguna experiencia previa en programación. Si estás empezando desde cero a programar, debes empezar por aquí : Scripting.

El juego se llama «Dodge the Creeps!». El personaje debe moverse y esquivar los enemigos tanto tiempo como sea posible. Aquí hay una vista previa del resultado final:

../../_images/dodge_preview.gif

¿Por qué 2D? Los juegos 3D son mucho más complejos que los 2D. Debes permanecer en 2D hasta que hayas adquirido un conocimiento suficiente del proceso de desarrollo de un videojuego.

Configuración del proyecto

Ejecuta Godot y crea un proyecto nuevo. Luego descarga las imágenes y sonidos que se utilizarán para hacer el juego de dodge_assets.zip. Descomprime los archivos en la carpeta del proyecto.

Nota

Para este tutorial, asumiremos que ya estás familiarizado con el editor. Si no te leiste Escenas y nodos, este es el momento de hacerlo para saber cómo configurar un proyecto y utilizar el editor.

Este juego utiliza el modo vertical (portrait), así que tendremos que ajustar el tamaño de la ventana. Haz clic en Proyecto -> Ajustes del proyecto -> Display -> Window y colocar «Width» en 480 y «Height» en 720.

Organizando el proyecto

En este proyecto, haremos 3 escenas independientes: Player (jugador), Mob (enemigo), y HUD (visualización del estado del juego), los que se combinarán en la escena Main (principal). En un proyecto grande, puede ser útil crear carpetas que contengan las escenas y sus scripts, pero para este juego relativamente pequeño se pueden grabar las escenas y scripts en la carpeta raíz, normalmente referida como res://. Se pueden ver las carpetas en el Panel de Sistema de Archivos, en la parte superior izquierda:

../../_images/filesystem_dock.png

Escena del jugador

La primer escena que haremos define el objeto Player. Uno de los beneficios de crear una escena del jugador aparte es que se puede probar de manera separada, aún antes de haber creado otras partes del juego.

Estructura de nodos

Para comenzar, haz clic en el botón «Añadir/Crear un nuevo nodo» y agrega un nodo Area2D a la escena.

../../_images/add_node.png

Con Area2D podemos detectar objetos que se superponen o entran en contacto con el jugador. Cambia su nombre a Player haciendo clic en el nombre del nodo. Este es el nodo raíz de la escena. Podemos añadir nodos adicionales al player para añadir funcionalidad.

Antes de que agreguemos un hijo al nodo Player, debemos asegurarnos de no moverlos o escalarlos accidentalmente al hacer clic en ellos. Selecciona el nodo y haz clic en el icono a la derecha del candado; su descripción dice «Hace que los hijos de un nodo no sean seleccionables».

../../_images/lock_children.png

Guarda la escena. Haz clic en Escenas -> Guardar escena, o presiona Ctrl+S en Windows/Linux o Command+S en Mac.

Nota

Para este proyecto, seguiremos las convenciones de nomenclatura de Godot.

  • GDScript: Clases (nodos) usan PascalCase, variables y funciones utilizan snake_case, y las constantes TODO_MAYUSCULAS (Lee Guía de estilo de GDScript).
  • C#: Las clases, las variables de exportación y los métodos usan PascalCase, los campos privados usan _camelCase, y las variables y parámetros locales usan camelCase (Ver Guía de estilo de C#). Ten cuidado de escribir los nombres de los métodos de manera precisa al conectar señales.

Animación del sprite

Haz clic en el nodo Player y añade un nodo AnimatedSprite como hijo. El AnimatedSprite se encargará de la apariencia y las animaciones de nuestro jugador. Observa que hay un símbolo de advertencia junto al nodo. Un AnimatedSprite requiere un recurso SpriteFrames, compuesto por una lista de las animaciones que puede mostrar. Para crear uno, busca la propiedad Frames en el Inspector y haz clic en «[empty]» -> «New SpriteFrames». Esto debería abrir automáticamente el panel SpriteFrames.

../../_images/spriteframes_panel.png

A la izquierda hay una lista de animaciones. Haz clic en la opción «default» y renómbrala a «right». Luego haz clic en el botón «Añadir» para crear una segunda animación llamada «up». Arrastra las dos imágenes para cada animación a la sección «Cuadros de animación» del panel:

../../_images/spriteframes_panel2.png

Las imágenes del jugador son un poco grandes para la ventana del juego, así que tenemos que reducirlas. Haz clic en el nodo AnimatedSprite y establece la propiedad Scale en (0.5, 0.5). Puedes encontrarla en el Inspector bajo el título Node2D.

../../_images/player_scale.png

Finalmente, añade un CollisionShape2D como hijo de Player. Esto determinará el «hitbox» del jugador, o los límites de su área de colisión. Para este personaje, un nodo CapsuleShape2D sería el más indicado ya que se ajusta mejor a la imagen, así que junto a «Shape» en el Inspector, haz clic en «<null>» -> «New CapsuleShape2D». Ahora usa los dos manejadores de tamaño y redimensiona la forma de colisión para cubrir el sprite:

../../_images/player_coll_shape.png

Cuando hayas terminado, tu escena Player debería verse así:

../../_images/player_scene_nodes.png

Movimiento del jugador

Ahora necesitamos añadir alguna funcionalidad que no podemos obtener de un nodo integrado, así que añadiremos un script. Haz clic en el nodo Player y en el botón «Añadir Script»:

../../_images/add_script_button.png

En la ventana de configuración del script, puedes dejar los ajustes por defecto. Simplemente haz clic en «Crear»:

Nota

Si quieres crear un script C# u otro lenguaje, selecciona el lenguaje en el menú desplegable «Lenguaje » antes de pulsar crear.

../../_images/attach_node_window.png

Nota

Si esta es la primera vez que te enfrentas con GDScript, por favor, lee Scripting antes de continuar.

Comienza declarando las variables miembro que este objeto necesitará:

extends Area2D

export var speed = 400  # How fast the player will move (pixels/sec).
var screen_size  # Size of the game window.
public class Player : Area2D
{
    [Export]
    public int Speed = 400; // How fast the player will move (pixels/sec).

    private Vector2 _screenSize; // Size of the game window.
}

Utilizar la palabra clave export en la primera variable speed nos permite asignar su valor desde el Inspector. Esto puede ser útil para valores que quieres ajustar como ajustas las propiedades de los nodos comunes. Haz clic en el nodo Player y verás que en la sección «Script Variables» del Inspector aparece la nueva propiedad. Recuerda que si cambias el valor aquí, sobrescribirá el valor asignado en el script al ejecutar el juego.

Advertencia

Si estás usando C#, necesitas (re)construir los archivos assembly del proyecto cada vez que quieras tener visibles las variables exportadas o las señales. Esta construcción puede ser lanzada manualmente pulsando en la palabra «Mono» en la parte inferior de la ventana de edición para revelar el Panel Mono, y luego haciendo clic en el botón «Construir Proyecto».

../../_images/export_variable.png

La función _ready() se llama cuando un nodo entra en la escena, este es un buen momento para averiguar el tamaño de la ventana de juego:

func _ready():
    screen_size = get_viewport_rect().size
public override void _Ready()
{
    _screenSize = GetViewport().GetSize();
}

Ahora podemos utilizar la función _process() para definir lo que hará el jugador. _process() se llama en cada frame, así que la usaremos para actualizar elementos del juego que esperamos que cambien a menudo. Para el jugador haremos lo siguiente:

  • Comprobar entradas.
  • Moverse en la dirección dada.
  • Reproducir la animación apropiada.

Primero, necesitamos comprobar entradas - ¿está el jugador presionando una tecla? Para este juego, tenemos 4 entradas de dirección para comprobar. Las acciones de entrada (Input actions) se definen en Ajustes del Proyecto, dentro de la pestaña Mapas de Entrada. Puedes definir eventos personalizados y asignarlos a diferentes claves, eventos del ratón u otras entradas. Para esta demo, usaremos los eventos por defecto que se asignan a las flechas del teclado.

Puedes detectar si una tecla se está presionando usando Input.is_action_pressed(), lo que devuelve true si está presionada o false en caso contrario.

func _process(delta):
    var velocity = Vector2()  # The player's movement vector.
    if Input.is_action_pressed("ui_right"):
        velocity.x += 1
    if Input.is_action_pressed("ui_left"):
        velocity.x -= 1
    if Input.is_action_pressed("ui_down"):
        velocity.y += 1
    if Input.is_action_pressed("ui_up"):
        velocity.y -= 1
    if velocity.length() > 0:
        velocity = velocity.normalized() * speed
        $AnimatedSprite.play()
    else:
        $AnimatedSprite.stop()
public override void _Process(float delta)
{
    var velocity = new Vector2(); // The player's movement vector.

    if (Input.IsActionPressed("ui_right"))
    {
        velocity.x += 1;
    }

    if (Input.IsActionPressed("ui_left"))
    {
        velocity.x -= 1;
    }

    if (Input.IsActionPressed("ui_down"))
    {
        velocity.y += 1;
    }

    if (Input.IsActionPressed("ui_up"))
    {
        velocity.y -= 1;
    }

    var animatedSprite = GetNode<AnimatedSprite>("AnimatedSprite");

    if (velocity.Length() > 0)
    {
        velocity = velocity.Normalized() * Speed;
        animatedSprite.Play();
    }
    else
    {
        animatedSprite.Stop();
    }
}

Comenzaremos colocando velocity en (0,0) - por defecto, el jugador no se debería estar moviendo. Luego revisamos cada entrada y sumamos o restamos de velocity para obtener la dirección. Por ejemplo, si mantienes right (derecha) y down (abajo) al mismo tiempo, el resultado del vector velocity será (1,1). En este caso, como estamos agregando un movimiento horizontal y vertical, el jugador podrá moverse más rápido que si solo se estuviese moviendo horizontalmente.

Podemos prevenir eso normalizando velocity, lo que significa que su longitud será 1, y multiplicando por la velocidad total deseada. Esto hará que el movimiento diagonal no sea más rápido.

Truco

Si no has usado antes cálculo vectorial, o necesitas un repaso, puedes ver una explicación de la utilización de vectores en Godot en Matemáticas vectoriales. Es bueno saberlo pero no será necesario para el resto de este tutorial.

También revisaremos si el jugador (player) se está moviendo así podemos comenzar o detener la animación de AnimatedSprite.

Truco

En GDScript, $ retorna el nodo acorde a la ruta relativa al nodo actual, o null si no se encuentra el nodo. Como AnimatedSprite es hijo del nodo actual, podemos utilizar $AnimatedSprite.

$ es una abreviatura de``get_node()``. En el código anterior, $AnimatedSprite.play() es lo mismo que get_node("AnimatedSprite").play().

Now that we have a movement direction, we can update the player’s position. We can also use clamp() to prevent it from leaving the screen. Clamping a value means restricting it to a given range. Add the following to the bottom of the _process function:

position += velocity * delta
position.x = clamp(position.x, 0, screen_size.x)
position.y = clamp(position.y, 0, screen_size.y)
Position += velocity * delta;
Position = new Vector2(
    x: Mathf.Clamp(Position.x, 0, _screenSize.x),
    y: Mathf.Clamp(Position.y, 0, _screenSize.y)
);

Truco

The delta parameter in the _process() function refers to the frame length - the amount of time that the previous frame took to complete. Using this value ensures that your movement will remain consistent even if the frame rate changes.

Haz clic en «Reproducir escena» (F6) y confirma que puedes mover el player por la pantalla en todas las direcciones. La salida de la consola que se abre al reproducir la escena puede cerrarse haciendo clic en Output (que debe estar resaltado en azul) en la parte inferior izquierda del Panel Inferior.

Advertencia

Si ves un error en el panel «Depurador» que se refiere a «null instance», probablemente signifique que se ha escrito mal un nombre. Los nombres de nodos son sensibles a mayúsculas y $NombreDeNodo o get_node("NombreDeNodo") debe coincidir con el nombre que ves en la escena.

Seleccionar animaciones

Ahora que Player se puede mover, necesitamos cambiar la animación, que AnimatedSprite ejecutará, basadonos en la dirección. Tenemos una animación «right» (derecha), que será volteada (flip) horizontalmente usando la propiedad flip_h para movimiento hacia la izquierda, y una animación «up» (arriba) que usará flip_v para movimiento hacia abajo. Coloquemos este código al final de nuestra función _process():

if velocity.x != 0:
    $AnimatedSprite.animation = "right"
    $AnimatedSprite.flip_v = false
    # See the note below about boolean assignment
    $AnimatedSprite.flip_h = velocity.x < 0
elif velocity.y != 0:
    $AnimatedSprite.animation = "up"
    $AnimatedSprite.flip_v = velocity.y > 0
if (velocity.x != 0)
{
    animatedSprite.Animation = "right";
    // See the note below about boolean assignment
    animatedSprite.FlipH = velocity.x < 0;
    animatedSprite.FlipV = false;
}
else if(velocity.y != 0)
{
    animatedSprite.Animation = "up";
    animatedSprite.FlipV = velocity.y > 0;
}

Nota

Las asignaciones booleanas en el código de arriba son una abreviatura para los programadores. Considere este código versus la asignación booleana abreviada de arriba:

if velocity.x < 0:
    $AnimatedSprite.flip_h = true
else:
    $AnimatedSprite.flip_h = false
if velocity.x < 0:
    animatedSprite.FlipH = true
else:
    animatedSprite.FlipH = false

Reproduce la escena de nuevo y revisa que las animaciones sean correctas para cada una de las direcciones. Cuando estés seguro de que el movimiento funciona correctamente, agrega esta línea a _ready() para que el jugador esté oculto cuando comience el juego:

hide()
Hide();

Preparacion para las colisiones

Queremos detectar cuando un enemigo alcanza a Player, pero no hemos creado ningún enemigo todavía. Eso está bien, porque vamos a utilizar la funcionalidad de señales (signal) de Godot para hacer que funcione.

Añade lo siguiente en la parte superior del script, después de extends Area2d:

signal hit
// Don't forget to rebuild the project so the editor knows about the new signal.

[Signal]
public delegate void Hit();

Esto define una señal personalizada llamada «hit» que nuestro player emitirá (enviará) cuando colisione con un enemigo. Usaremos Area2D para detectar la colisión. Selecciona el nodo Player y haz clic en la pestaña «Nodos» junto a la pestaña Inspector para ver la lista de señales que puede emitir el player:

../../_images/player_signals.png

Observa que nuestra señal personalizada de «hit» también está ahí! Como nuestros enemigos van a ser nodos RigidBody2D, necesitamos la señal body_entered( Object body ); esta se emitirá cuando un cuerpo entre en contacto con el player. Haz clic en «Conectar…» y luego en «Conectar» de nuevo en la ventana » Conectar señal «. No necesitamos cambiar ninguna de estas configuraciones - Godot creará automáticamente una función en el script de su player. Esta función se llamará cada vez que se emita la señal - que manipula la señal.

Truco

Cuando conectamos una señal, en lugar de hacer que Godot cree una función, puedes dar el nombre de una función existente a la que quieras vincular a la señal.

Agrega este código a la función:

func _on_Player_body_entered(body):
    hide()  # Player disappears after being hit.
    emit_signal("hit")
    $CollisionShape2D.set_deferred("disabled", true)
public void OnPlayerBodyEntered(PhysicsBody2D body)
{
    Hide(); // Player disappears after being hit.
    EmitSignal("Hit");
    GetNode<CollisionShape2D>("CollisionShape2D").SetDeferred("disabled", true);
}

Cada vez que un enemigo golpea al jugador, la señal será emitida. Necesitamos deshabilitar la colisión del jugador para que no activemos la señal de hit más de una vez.

Nota

Desactivar la forma de colisión del nodo Area puede causar un error si ocurre en medio del procesado de colisiones del motor. El uso de set_deferred() nos permite hacer que Godot espere para desactivar la forma hasta que sea seguro hacerlo.

La última parte de nuestro Player es agregar una función que llamaremos para reiniciarlo cuando comenzamos un juego nuevo.

func start(pos):
    position = pos
    show()
    $CollisionShape2D.disabled = false
public void Start(Vector2 pos)
{
    Position = pos;
    Show();
    GetNode<CollisionShape2D>("CollisionShape2D").Disabled = false;
}

Escena del enemigo

Es momento de hacer los enemigos que nuestro jugador tendrá que evitar. Su comportamiento no será muy complejo: los enemigos aparecerán al azar en los bordes de la pantalla y se moverán en una dirección al azar en línea recta, luego desaparecerán cuando salgan de la pantalla.

Haremos esto en una escena Mob, la que podremos instanciar para crear cualquier número independiente de enemigos en el juego.

Configuración del Nodo

Haz clic en Escena -> Nueva escena y crearemos el Mob.

La escena Mob usará los siguientes nodos:

  • RigidBody2D (llamado Mob)
    • AnimatedSprite
    • CollisionShape2D
    • VisibilityNotifier2D (llamado Visibility)

No olvides marcar los hijos para que no se puedan seleccionar, como hiciste con la escena Player.

En las propiedades de RigidBody2D, ajustar Gravity Scale a 0, así los enemigos no caerán hacia abajo. Además, dentro de la sección PhysicsBody2D, haz clic en la propiedad Mask y desmarca el primer cuadro, para asegurarse que los enemigos no colisionen entre sí.

../../_images/set_collision_mask.png

Configura el AnimatedSprite como hiciste con el jugador. Esta vez, tenemos 3 animaciones: fly, swim, y walk. Marca la propiedad Playing en el Inspector como «Activado» y ajusta la configuración «Velocidad FPS» como se muestra debajo. Seleccionaremos una de esas animaciones al azar así los enemigos tendrán alguna variedad.

../../_images/mob_animations.gif

fly debe estar en 3 FPS, mientras que swim y walk en 4 FPS.

Como las imágenes del jugador, la de los enemigos deberán escalarse. Ajusta la propiedad Scale de AnimatedSprite a (0.75, 0.75).

Como en la escena Player, añade una CapsuleShape2D para las colisiones. Para alinear la forma de colisión con la imágen, deberás ajustar la propiedad Rotation Degrees a 90 en la sección Node2D.

Script del enemigo

Añade un script al nodo Mob y luego añade las siguientes variables miembro:

extends RigidBody2D

export var min_speed = 150  # Minimum speed range.
export var max_speed = 250  # Maximum speed range.
var mob_types = ["walk", "swim", "fly"]
public class Mob : RigidBody2D
{
    // Don't forget to rebuild the project so the editor knows about the new export variables.

    [Export]
    public int MinSpeed = 150; // Minimum speed range.

    [Export]
    public int MaxSpeed = 250; // Maximum speed range.

    private String[] _mobTypes = {"walk", "swim", "fly"};
}

Cuando creamos un «mob», tomaremos un número al azar entre min_speed y max_speed para decidir cuán rápido se debe mover cada uno (sería aburrido si todos se moviesen igual). Asignaremos 150 y 250 en el Inspector. También tendremos un arreglo que contendrá las tres animaciones, el cual usaremos para tomar una al azar. Asegúrate de que los nombres en el script son los mismos que en SpriteFrames.

Ahora veamos el resto del script. En _ready() seleccionaremos al azar una de las tres animaciones:

func _ready():
    $AnimatedSprite.animation = mob_types[randi() % mob_types.size()]
// C# doesn't implement GDScript's random methods, so we use 'System.Random' as an alternative.
static private Random _random = new Random();

public override void _Ready()
{
    GetNode<AnimatedSprite>("AnimatedSprite").Animation = _mobTypes[_random.Next(0, _mobTypes.Length)];
}

Nota

Tienes que usar randomize() si quieres que la secuencia de números al «azar» sea diferente cada vez que se ejecute la escena. Usaremos randomize() en la escena Main, así que no lo necesitaremos aquí. randi() % n es la forma estándar de obtener un número entre 0 y n-1.

La última pieza es hacer que los enemigos se borren a sí mismos cuando abandonan la pantalla. Conecte la señal screen_exited() del nodo Visibility y agregue este código:

func _on_Visibility_screen_exited():
    queue_free()
public void OnVisibilityScreenExited()
{
    QueueFree();
}

Esto completa la escena Mob.

Escena principal

Ahora es momento de juntar todo. Crea una nueva escena y agrega un Node llamado Main. Haz clic en el botón «Instanciar» y selecciona la escena grabada Player.tscn.

../../_images/instance_scene.png

Nota

Mira Instanciar para aprender más sobre instanciación.

Ahora agrega los siguientes nodos como hijos de Main, y nómbralos como se muestra (los valores son en segundos):

  • Timer (llamado MobTimer) - para controlar cuán seguido aparecerán los enemigos
  • Timer (llamado ScoreTimer) - para incrementar el puntaje cada segundo
  • Timer (llamado StartTimer) - para crear una retardo antes de comenzar
  • Position2D (llamado StartPosition) - para indicar la posición inicial del jugador

Ajustar la propiedad Wait Time (tiempo de espera) de cada nodo Timer como se indica:

  • MobTimer: 0.5
  • ScoreTimer: 1
  • StartTimer: 2

Además, marca la propiedad One Shot (disparo único) como «Activado» y ajusta la propiedad Position del nodo StartPosition en (250,450).

Agregando enemigos

El nodo Main generará los nuevos enemigos, y queremos que aparezcan en una ubicación al azar en el borde de la pantalla. Agregue un nodo Path2D llamado MobPath como hijo de Main. Cuando seleccione Path2D, verá que aparecen nuevos botones en la parte de arriba del editor:

../../_images/path2d_buttons.png

Selecciona el botón del medio («Añadir Punto») y dibuja el camino haciendo clic para agregar los puntos en las esquinas como se muestra. Para tener los puntos ajustados a la rejilla, asegúrate de tener marcado «Ajustar a cuadrícula». Esta opción se encuentra dentro de las «Opciones de fijado» a la izquierda del «candado», aparece como tres puntos verticales.

../../_images/draw_path2d.gif

Importante

Dibuja la ruta en el sentido de las agujas del reloj ¡o tus enemigos apuntarán hacia afuera en lugar de hacia adentro!

Después de colocar el punto 4, haz clic en el botón «Cerrar Curva» y la curva se completará.

Ahora que el camino está definido, agrega un nodo PathFollow2D como hijo de MobPath y nómbralo MobSpawnLocation. Este nodo rotará automáticamente y seguirá el camino mientras se mueve, así podemos usarlo para seleccionar una posición al azar y una dirección a lo largo del camino.

Script Principal

Agrega un script a Main. Al principio del script usaremos export (PackedScene) para permitirnos elegir la escena de enemigos que queremos instanciar.

extends Node

export (PackedScene) var Mob
var score

func _ready():
    randomize()
public class Main : Node
{
    // Don't forget to rebuild the project so the editor knows about the new export variable.

    [Export]
    public PackedScene Mob;

    private int _score;

    // We use 'System.Random' as an alternative to GDScript's random methods.
    private Random _random = new Random();

    public override void _Ready()
    {
    }

    // We'll use this later because C# doesn't support GDScript's randi().
    private float RandRange(float min, float max)
    {
        return (float)_random.NextDouble() * (max - min) + min;
    }
}

Arrastra Mob.tscn``del panel "Sistema de Archivos" y suéltalo en la propiedad ``Mob dentro de la sección Variables de Script del nodo Main.

Luego, haz clic en Player y conecta la señal hit. Haremos una nueva función llamada game_over, la que ejecutará lo que debe suceder cuando el juego termina. Escribe «game_over» en el campo «Method In Node» en la parte inferior de la ventana «Confirmar decisión» de las señales. Agrega el siguiente código, así como una función new_game para configurar todo para un nuevo juego:

func game_over():
    $ScoreTimer.stop()
    $MobTimer.stop()

func new_game():
    score = 0
    $Player.start($StartPosition.position)
    $StartTimer.start()
public void GameOver()
{
    GetNode<Timer>("MobTimer").Stop();
    GetNode<Timer>("ScoreTimer").Stop();
}

public void NewGame()
{
    _score = 0;

    var player = GetNode<Player>("Player");
    var startPosition = GetNode<Position2D>("StartPosition");
    player.Start(startPosition.Position);

    GetNode<Timer>("StartTimer").Start();
}

Ahora conecta la señal timeout() de cada uno de los nodos Timer (StartTimer, ScoreTimer y MobTimer) al script de Main. StartTimer iniciará los otros dos temporizadores. ScoreTimer incrementará la puntuación en 1.

func _on_StartTimer_timeout():
    $MobTimer.start()
    $ScoreTimer.start()

func _on_ScoreTimer_timeout():
    score += 1
public void OnStartTimerTimeout()
{
    GetNode<Timer>("MobTimer").Start();
    GetNode<Timer>("ScoreTimer").Start();
}

public void OnScoreTimerTimeout()
{
    _score++;
}

En _on_MobTimer_timeout(), crearemos una instancia del enemigo, tomaremos una ubicación aleatoria a lo largo del Path2D, y luego pondermos al enemigo en movimiento. El nodo PathFollow2D rotará automáticamente mientras recorre el camino, así que usaremos eso para seleccionar la dirección y la posición del enemigo.

Observa que añadiremos la nueva instancia a la escena usando add_child().

Ahora haz clic en MobTimer en la ventana de la escena, luego ve a la ventana del inspector, cambia a la vista de nodos, después haz clic en timeout() y conecta la señal.

Añade el siguiente código:

func _on_MobTimer_timeout():
    # Choose a random location on Path2D.
    $MobPath/MobSpawnLocation.set_offset(randi())
    # Create a Mob instance and add it to the scene.
    var mob = Mob.instance()
    add_child(mob)
    # Set the mob's direction perpendicular to the path direction.
    var direction = $MobPath/MobSpawnLocation.rotation + PI / 2
    # Set the mob's position to a random location.
    mob.position = $MobPath/MobSpawnLocation.position
    # Add some randomness to the direction.
    direction += rand_range(-PI / 4, PI / 4)
    mob.rotation = direction
    # Set the velocity (speed & direction).
    mob.linear_velocity = Vector2(rand_range(mob.min_speed, mob.max_speed), 0)
    mob.linear_velocity = mob.linear_velocity.rotated(direction)
public void OnMobTimerTimeout()
{
    // Choose a random location on Path2D.
    var mobSpawnLocation = GetNode<PathFollow2D>("MobPath/MobSpawnLocation");
    mobSpawnLocation.SetOffset(_random.Next());

    // Create a Mob instance and add it to the scene.
    var mobInstance = (RigidBody2D)Mob.Instance();
    AddChild(mobInstance);

    // Set the mob's direction perpendicular to the path direction.
    float direction = mobSpawnLocation.Rotation + Mathf.Pi / 2;

    // Set the mob's position to a random location.
    mobInstance.Position = mobSpawnLocation.Position;

    // Add some randomness to the direction.
    direction += RandRange(-Mathf.Pi / 4, Mathf.Pi / 4);
    mobInstance.Rotation = direction;

    // Choose the velocity.
    mobInstance.SetLinearVelocity(new Vector2(RandRange(150f, 250f), 0).Rotated(direction));
}

Importante

En funciones que requieren ángulos, GDScript usa radianes, no grados. Si prefieres utilizar grados, utiliza deg2rad() y rad2deg() para convertir entre ambas.

HUD

La pieza final de nuestro juego es una UI (interfaz de usuario): una interfaz para mostrar cosas como la puntuación, el mensaje de «juego terminado» y un botón de reinicio. Crea una nueva escena, agrega un nodo CanvasLayer llamado HUD. «HUD» viene de «heads-up display» y es la serie de elementos informativos que aparecen encima de la vista de juego.

El nodo CanvasLayer nos permitirá colocar elementos de nuestra UI en una capa por encima del juego, así la información que mostrará no estará cubierta por ningún elemento como el jugador o los enemigos.

El HUD mostrará la siguiente información:

  • Puntuación, cambiado por ScoreTimer.
  • Un mensaje como «Game Over» o «Get Ready!»
  • Un botón «Start» para comenzar el juego.

El nodo básico para elementos de UI es Control. Para crear nuestra UI, usaremos dos tipos de nodos Control: Label y Button.

Cree los siguientes hijos del nodo HUD:

  • Label llamado ScoreLabel.
  • Label llamado MessageLabel.
  • Button llamado StartButton.
  • Timer llamado MessageTimer.

Haz clic en ScoreLabel y escribe un número en el campo Text del inspector. La fuente por defecto para los nodos Control es pequeña y no escala bien. En los recursos del juego se incluye una fuente llamada «Xolonium-Regular.ttf». Para usar esta fuente, haz lo siguiente para cada uno de los tres nodos Control:

  1. Dentro de «Custom Fonts», seleccionar «New DynamicFont»
../../_images/custom_font1.png
  1. Haz clic en la «DynamicFont» agregada y, dentro de «Font Data», elige «Load» y selecciona el archivo «Xolonium-Regular.ttf». Deberás también cambiar el Size (tamaño) de la fuente. Un valor de 64 funcionará bien.
../../_images/custom_font2.png

Nota

Anclas y Márgenes: Nodos Control tienen posición y tamaño, pero también tienen anclas (anchor) y márgenes (margin). Anclas definen el origin - El punto de referencia para los bordes del nodo. Los márgenes se actualizan automaticamente cuando cambias el tamaño de un nodo Control. Representan la distancia del borde al ancla del nodo Control. Para más detalles, ver Diseño de interfaces con nodos Control.

Arrange the nodes as shown below. Click the «Layout» button to set a Control node’s layout:

../../_images/ui_anchor.png

Puedes arrastrar y ubicarlos manualmente o, para un modo más preciso, usa la siguiente configuración:

ScoreLabel

  • Text : 0
  • Layout: «Top Wide»
  • Align: «Center»

MessageLabel

  • Text : Dodge the Creeps!
  • Disposición : «HCenter Wide»
  • Align: «Center»
  • Autowrap : «On»

StartButton

  • Text : Start
  • Disposición : «Center Bottom»
  • Margin:
    • Top: -200
    • Bottom: -100

Ahora agrega este script a HUD:

extends CanvasLayer

signal start_game
public class HUD : CanvasLayer
{
    // Don't forget to rebuild the project so the editor knows about the new signal.

    [Signal]
    public delegate void StartGame();
}

La señal start_game dirá al nodo Main que se ha presionado un botón.

func show_message(text):
    $MessageLabel.text = text
    $MessageLabel.show()
    $MessageTimer.start()
public void ShowMessage(string text)
{
    var messageLabel = GetNode<Label>("MessageLabel");
    messageLabel.Text = text;
    messageLabel.Show();

    GetNode<Timer>("MessageTimer").Start();
}

Esta función se llamará cuando queramos mostrar un mensaje temporalmente, como «Get Ready». En el MessageTimer, ajustar el Wait Time en 2 y la propiedad One Shot en «Activado».

func show_game_over():
    show_message("Game Over")

    yield($MessageTimer, "timeout")

    $MessageLabel.text = "Dodge the\nCreeps!"
    $MessageLabel.show()

    yield(get_tree().create_timer(1), "timeout")

    $StartButton.show()
async public void ShowGameOver()
{
    ShowMessage("Game Over");

    var messageTimer = GetNode<Timer>("MessageTimer");
    await ToSignal(messageTimer, "timeout");

    var messageLabel = GetNode<Label>("MessageLabel");
    messageLabel.Text = "Dodge the\nCreeps!";
    messageLabel.Show();

    GetNode<Button>("StartButton").Show();
}

Esta función se llamará cuando el jugador pierde. Mostrará «Game Over» durante 2 segundos, luego volverá a la pantalla de título y revelará el botón «Start».

Nota

Cuando necesite hacer una pausa por un breve tiempo, una alternativa al uso de un nodo temporizador es usar la función create_timer() del arbol de escena. Esto puede ser muy útil para retrasar, como en el código anterior, donde queremos esperar un poco de tiempo antes de mostrar el botón «Start».

func update_score(score):
    $ScoreLabel.text = str(score)
public void UpdateScore(int score)
{
    GetNode<Label>("ScoreLabel").Text = score.ToString();
}

Esta función será llamada por Main cada vez que el puntaje cambie.

Conecta la señal timeout() del MessageTimer y la señal pressed() de StartButton.

func _on_StartButton_pressed():
    $StartButton.hide()
    emit_signal("start_game")

func _on_MessageTimer_timeout():
    $MessageLabel.hide()
public void OnStartButtonPressed()
{
    GetNode<Button>("StartButton").Hide();
    EmitSignal("StartGame");
}

public void OnMessageTimerTimeout()
{
    GetNode<Label>("MessageLabel").Hide();
}

Conectando HUD a Main

Ahora que terminamos la creación de la escena HUD, grábela y vuelva a Main. Instancia la escena HUD en Main como lo hizo con la escena Player, y colócala al final del árbol. El árbol completo debería verse así, así que asegúrate de que no falta nada:

../../_images/completed_main_scene.png

Ahora conectaremos la funcionalidad de HUD a nuestro script Main. Esto requiere unas pocas adiciones a la escena Main:

En el panel Node conecta la señal start_game de HUD a la función new_game().

En``new_game()``, actualiza la puntuación y muestra el mensaje «Get Ready»:

$HUD.update_score(score)
$HUD.show_message("Get Ready")
var hud = GetNode<HUD>("HUD");
hud.UpdateScore(_score);
hud.ShowMessage("Get Ready!");

En game_over() necesitaremos llamar la función correspondiente en HUD:

$HUD.show_game_over()
GetNode<HUD>("HUD").ShowGameOver();

Finalmente, agregua _on_ScoreTimer_timeout() para mantener la interfaz en sincronía al cambiar la puntuación:

$HUD.update_score(score)
GetNode<HUD>("HUD").UpdateScore(_score);

¡Ahora estás listo para jugar! Haz clic en el botón «Reproducir el proyecto». Se te pedirá que selecciones una escena principal, así que elige Main.tscn.

Removiendo los viejos «creeps»

Si juegas hasta el «Game Over» y comienzas un juego nuevo, los «creeps» del juego previo estarán todavía en pantalla. Sería mejor si todos desaparecen al comienzo del juego nuevo.

Usaremos la señal start_game que ya fue emitida por el nodo HUD para remover los creeps remanentes. No podemos usar el editor para conectar la señal a los enemigos en la forma que necesitamos porque no habrá nodos Mob en la escena Main hasta que hayamos iniciado el juego. Entonces usaremos código.

Comienza añadiendo una nueva función en el script Mob.gd. En esta función queue_free() removerá el nodo actual al final del frame.

func _on_start_game():
    queue_free()
public void OnStartGame()
{
    QueueFree();
}

Luego, en Main.gd agrega una nueva línea dentro de la función _on_MobTimer_timeout(), al final.

$HUD.connect("start_game", mob, "_on_start_game")
GetNode("HUD").Connect("StartGame", mobInstance, "OnStartGame");

Esta línea le dice al nuevo nodo Mob (referenciado por la variable mob) que responda a cualquier señal start_game emitida por el nodo HUD, ejecutando su función _on_start_game().

Finalizando

Ahora que hemos completado toda la funcionalidad de nuestro juego. Debajo están los pasos restantes para agregar más «jugo» y mejorar la experiencia de juego. Eres libre de expandir el juego con sus propias ideas.

Imagen de fondo

El gris por defecto no es muy atractivo, así que cambiemos el color. Un modo de hacer esto es usar un nodo ColorRect. Lo haremos el primer nodo dentro de Main así aparecerá detrás de los otros nodos. ColorRect tiene una sola propiedad: Color. Escoge el color que quieras y arrastra el borde del ColorRect para que cubra la pantalla.

También puedes agregar una imagen de fondo, si tienes una, utilizando un nodo Sprite.

Efectos de sonido

La música y el sonido pueden ser el modo más efectivo de agregar atractivo a la experiencia de juego. En la carpeta de recursos hay dos archivos de sonido: «House In a Forest Loop.ogg» para música de fondo y «gameover.wav» para cuando el jugador pierde.

Agrega dos nodos AudioStreamPlayer hijos de Main. Nombre uno de ellos Music y el otro DeathSound. En cada uno, haz clic en la propiedad Stream, selecciona «Load» y escoge el archivo de audio correspondiente.

Para reproducir la música, agregua $Music.play() en la función new_game() y $Music.stop() en game_over().

Finalmente, agregua $DeathSound.play() en la función game_over().

Atajo del teclado

Dado que el juego se juega con los controles del teclado, sería conveniente si también pudiéramos iniciar el juego presionando una tecla en el teclado. Una forma de hacerlo es usando la propiedad «Shortcut» del nodo Button.

En la escena HUD, selecciona el StartButton y encuentra su propiedad Shortcut* en el Inspector. Selecciona «New Shortcut» y haz clic en la opción «Shortcut». Aparecerá una segunda propiedad Shortcut. Selecciona «New InputEventAction» y haz clic en el nuevo «InputEvent». Finalmente, en la propiedad Action, escribe el nombre ui_select. Este es el evento de entrada predeterminado asociado a la barra espaciadora.

../../_images/start_button_shortcut.png

Ahora, cuando aparezca el botón de inicio, puedes hacer clic en él o presionar la barra espaciadora para iniciar el juego.

Archivos del Proyecto

Encontrarás una versión completada de este proyecto en las siguientes direcciones: