Programando el jugador¶
En esta lección, agregaremos el movimiento del jugador, la animación y lo configuraremos para detectar colisiones.
Para hacerlo, debemos agregar alguna funcionalidad que no podemos obtener de un nodo integrado, por lo que agregaremos un script. Haga clic en el nodo Player
y haga clic en el botón "Añadir script":
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.
Nota
Si es la primera vez que se encuentra con GDScript, lea Lenguaje de 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.
using Godot;
using System;
public class Player : Area2D
{
[Export]
public int Speed = 400; // How fast the player will move (pixels/sec).
public Vector2 ScreenSize; // Size of the game window.
}
// A `player.gdns` file has already been created for you. Attach it to the Player node.
// Create two files `player.cpp` and `player.hpp` next to `entry.cpp` in `src`.
// This code goes in `player.hpp`. We also define the methods we'll be using here.
#ifndef PLAYER_H
#define PLAYER_H
#include <AnimatedSprite.hpp>
#include <Area2D.hpp>
#include <CollisionShape2D.hpp>
#include <Godot.hpp>
#include <Input.hpp>
class Player : public godot::Area2D {
GODOT_CLASS(Player, godot::Area2D)
godot::AnimatedSprite *_animated_sprite;
godot::CollisionShape2D *_collision_shape;
godot::Input *_input;
godot::Vector2 _screen_size; // Size of the game window.
public:
real_t speed = 400; // How fast the player will move (pixels/sec).
void _init() {}
void _ready();
void _process(const double p_delta);
void start(const godot::Vector2 p_position);
void _on_Player_body_entered(godot::Node2D *_body);
static void _register_methods();
};
#endif // PLAYER_H
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".
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 = GetViewportRect().Size;
}
// This code goes in `player.cpp`.
#include "player.hpp"
void Player::_ready() {
_animated_sprite = get_node<godot::AnimatedSprite>("AnimatedSprite");
_collision_shape = get_node<godot::CollisionShape2D>("CollisionShape2D");
_input = godot::Input::get_singleton();
_screen_size = get_viewport_rect().size;
}
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 las entradas - ¿está el jugador presionando una tecla? Para este juego, tenemos 4 entradas direccionales para comprobar. Las acciones de entrada (Input actions) se definen en Configuración del Proyecto, dentro de la pestaña Mapa de Entrada. Puedes definir eventos personalizados y asignarlos a diferentes claves, eventos del ratón u otras entradas. Para este juego, mapearemos las teclas direccionales con las cuatro direcciones.
Haga clic en Proyecto -> Configuración del proyecto para abrir la ventana de configuración del proyecto y haga clic en la pestaña Mapa de Entrada en la parte superior. Escriba "move_right" en la barra superior y haga clic en el botón "Añadir" para agregar la acción move_right
.
Necesitamos asignar una clave a esta acción. Haga clic en el ícono "+" a la derecha, luego haga clic en la opción "Tecla" en el menú desplegable. Un cuadro de diálogo le pide que escriba la tecla deseada. Presione la flecha derecha en su teclado y haga clic en "Aceptar".
Repita estos pasos para agregar tres mapeos más:
move_left
mapeado con la tecla direccional hacia la izquierda.move_up
mapeado con la tecla direccional hacia arriba.Y
move_down
mapeado con la tecla direccional hacia abajo.
Tu mapa de entradas debería verse así:
Haga clic en el botón "Cerrar" para cerrar la configuración del proyecto.
Nota
Solo asignamos una tecla a cada acción de entrada, pero puede asignar varias teclas, botones del mando o botones del ratón a la misma acción de entrada.
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.ZERO # The player's movement vector.
if Input.is_action_pressed("move_right"):
velocity.x += 1
if Input.is_action_pressed("move_left"):
velocity.x -= 1
if Input.is_action_pressed("move_down"):
velocity.y += 1
if Input.is_action_pressed("move_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 = Vector2.Zero; // The player's movement vector.
if (Input.IsActionPressed("move_right"))
{
velocity.x += 1;
}
if (Input.IsActionPressed("move_left"))
{
velocity.x -= 1;
}
if (Input.IsActionPressed("move_down"))
{
velocity.y += 1;
}
if (Input.IsActionPressed("move_up"))
{
velocity.y -= 1;
}
var animatedSprite = GetNode<AnimatedSprite>("AnimatedSprite");
if (velocity.Length() > 0)
{
velocity = velocity.Normalized() * Speed;
animatedSprite.Play();
}
else
{
animatedSprite.Stop();
}
}
// This code goes in `player.cpp`.
void Player::_process(const double p_delta) {
godot::Vector2 velocity(0, 0);
velocity.x = _input->get_action_strength("move_right") - _input->get_action_strength("move_left");
velocity.y = _input->get_action_strength("move_down") - _input->get_action_strength("move_up");
if (velocity.length() > 0) {
velocity = velocity.normalized() * speed;
_animated_sprite->play();
} else {
_animated_sprite->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 verificamos si el jugador se está moviendo para poder llamar a play()
o stop()
en el AnimatedSprite.
Truco
$
es la abreviatura de get_node()
. Así, en el código anterior, $AnimatedSprite.play()
es lo mismo que get_node("AnimatedSprite").play()
.
En GDScript, $
retorna el nodo a la ruta relativa del nodo actual, o retorna null
si el nodo no se encuentra. Como AnimatedSprite es hijo del nodo actual, podemos utilizar $AnimatedSprite
.
Ahora que tenemos la dirección del movimiento, podemos actualizar la posición de Player
. También podemos usar clamp()
para prevenir que abandone la pantalla. Aplicar clamp quiere decir que vamos a restringir un valor a un determinado rango. Agrega lo siguiente al final de la función _process
(asegúrate de que no esté indentada dentro del else):
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)
);
godot::Vector2 position = get_position();
position += velocity * (real_t)p_delta;
position.x = godot::Math::clamp(position.x, (real_t)0.0, _screen_size.x);
position.y = godot::Math::clamp(position.y, (real_t)0.0, _screen_size.y);
set_position(position);
Truco
El parámetro delta en la función _process() se refiere al frame length -la cantidad de tiempo que le tomo al frame anterior para completarse. Usando este valor aseguras que tu movimiento sera consistente incluso si el frame rate cambia .
Haz clic en "Reproducir Escena" (F6, Cmd + R on macOS) y confirme que puede mover el player por la pantalla en todas las direcciones.
Advertencia
Si tu obtienes un error en el panel "Debugger" que dice
`` Intento de llamar a la función 'play' en la base 'null instance' en una instancia nula ''
esto puede significar que escribiste mal el nombre del nodo AnimatedSprite. Los nombres de nodo son sensible a mayúsculas y $NodeName
debe coincidir con el nombre en el árbol de escenas.
Seleccionar animaciones¶
Ahora que el jugador puede moverse, necesitamos cambiar qué animación está reproduciendo el AnimatedSprite en función de su dirección. Tenemos la animación "walk", que muestra al jugador caminando hacia la derecha. Esta animación debe ser volteada horizontalmente usando la propiedad flip_h
para el movimiento hacia la izquierda. También tenemos la animación "arriba", que debe ser volteada verticalmente con flip_v
para el movimiento hacia abajo. Coloquemos este código al final de la función _process()
:
if velocity.x != 0:
$AnimatedSprite.animation = "walk"
$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 = "walk";
animatedSprite.FlipV = false;
// See the note below about boolean assignment.
animatedSprite.FlipH = velocity.x < 0;
}
else if (velocity.y != 0)
{
animatedSprite.Animation = "up";
animatedSprite.FlipV = velocity.y > 0;
}
if (velocity.x != 0) {
_animated_sprite->set_animation("walk");
_animated_sprite->set_flip_v(false);
// See the note below about boolean assignment.
_animated_sprite->set_flip_h(velocity.x < 0);
} else if (velocity.y != 0) {
_animated_sprite->set_animation("up");
_animated_sprite->set_flip_v(velocity.y > 0);
}
Nota
Las asignaciones booleanas en el código de arriba son una abreviatura para los programadores. Como estamos haciendo una comparación (booleana) y al mismo tiempo asignando un valor booleano, podemos hacer las dos cosas a la vez. 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 si las animaciones son correctas en cada una de las direcciones.
Truco
Un error común aquí es escribir los nombres de las animaciones mal. Los nombres de las animaciones en el SpriteFrames panel deberán coincidir con lo que tu escribes en tu código. si tu llamas la animación "Caminar"
, tu también deberás usar la mayúscula "C" en el código.
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();
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();
// This code goes in `player.cpp`.
// We need to register the signal here, and while we're here, we can also
// register the other methods and register the speed property.
void Player::_register_methods() {
godot::register_method("_ready", &Player::_ready);
godot::register_method("_process", &Player::_process);
godot::register_method("start", &Player::start);
godot::register_method("_on_Player_body_entered", &Player::_on_Player_body_entered);
godot::register_property("speed", &Player::speed, (real_t)400.0);
// This below line is the signal.
godot::register_signal<Player>("hit", godot::Dictionary());
}
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:
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ñal 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 entonces creará automáticamente una función en el script de Player.
Nota el icono verde indicando que una señal es conectada a esta función. Agrega este código a la función:
func _on_Player_body_entered(body):
hide() # Player disappears after being hit.
emit_signal("hit")
# Must be deferred as we can't change physics properties on a physics callback.
$CollisionShape2D.set_deferred("disabled", true)
public void OnPlayerBodyEntered(PhysicsBody2D body)
{
Hide(); // Player disappears after being hit.
EmitSignal(nameof(Hit));
// Must be deferred as we can't change physics properties on a physics callback.
GetNode<CollisionShape2D>("CollisionShape2D").SetDeferred("disabled", true);
}
// This code goes in `player.cpp`.
void Player::_on_Player_body_entered(godot::Node2D *_body) {
hide(); // Player disappears after being hit.
emit_signal("hit");
// Must be deferred as we can't change physics properties on a physics callback.
_collision_shape->set_deferred("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.
Lo último será 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;
}
// This code goes in `player.cpp`.
void Player::start(const godot::Vector2 p_position) {
set_position(p_position);
show();
_collision_shape->set_disabled(false);
}
Con el jugador funcionando, trabajaremos con el enemigo en la próxima lección.