Tutorial de introducción a Godot 3.0. Juego de Snake en C#
30/01/2018
Llegó el día. Godot 3.0 salió a la luz. Se trata de una versión con muchas mejoras respecto a Godot 2.x. Se trata de un motor de videojuegos software libre, compatible con la mayoría de sistemas operativos (y consolas a través de una compañía privada). Aprovechando la ocasión voy a explicar como hacer un juego simple, usando C# y el motor 2D de Godot. Este tutorial sirve para familiarizarse con el motor.
Lo primero que tenemos que hacer es instalar Godot. Para ello vamos a la página de descarga y descargamos la versión que corresponda a nuestro sistema con Mono.
Es requisito indispensable tener instalado Mono SDK, tanto en Linux como en Windows. El Mono SDK se descarga desde http://www.mono-project.com/download/.
Una vez descargado tendremos un fichero con tres archivos. Descomprímelos en una carpeta. Ahora simplemente puedes ejecutar el fichero ejecutable.
Nada más arrancar tendremos una ventana de proyectos. Desde ahí podemos abrir proyectos que hayamos creado, descargar demos y plantillas o simplemente crear un proyecto nuevo.
Si le damos a Proyecto nuevo procederemos a la creación de un nuevo proyecto (¡¿qué complicado, verdad?!).
Una vez hecho esto ya estaríamos en el editor propiamente dicho.
Rápidamente cambiamos al modo 2D y hacemos zoom hacia atrás. Vamos a ajustar la pantalla. Para ello vamos a Proyecto->Ajustes de proyecto. Buscamos el apartado Display->Window y ajustamos la resolución base. Esta será la que usaremos para poner los elementos en pantalla. Después vamos a Stretch y configuramos el modo 2D y mantener el aspect ratio.
Esta configuración sirve para empezar con juegos simples en 2D puedan escalar fácilmente a distintas pantallas sin tener problemas de descuadres.
Ahora hacemos click en el más (+) para añadir un nodo a la escena.
Para nuestro juego, voy a añadir un Node2D, que hereda de CanvasItem y tiene APIs para dibujar rectángulos y círculos..
No lo tocamos y hacemos click derecho en el Nodo. Damos click a Attach Script:
Es importante tener un nombre de script distinto a Node2D. Esto puede dar problemas. Una vez hecho esto, se nos mostrará esta pantalla, que nos permite escribir el código en C#. A partir de ahora voy a usar Visual Studio Code, porque lo considero mejor para escribir código en C# que el editor de Godot. Creamos también dos Sprite hijos del Node2D. Apple y SnakeBody van a llamarse y les creamos scripts de C# también.
Abrimos con el VS Code el fichero .cs que hemos creado, en mi caso, Snake.cs.
Dentro veremos una clase con dos funciones, _Ready y _Process. Estas son solo un ejemplo de las funciones que podemos sobreescribir en las clases asociadas a nodos. Otras funciones serían: _Draw, _Input. ¿Cuándo tenemos que usar cada una? Ready es una especie de constructor, ahí inicializamos todo lo que haga falta. Process es llamada constantemente, ahí hacemos las actualizaciones y comprobaciones pertinentes. Draw permite dibujar (si no trabajamos con imágenes es muy útil). Draw no se llama útil. ¿Qué tenemos que hacer en Snake.cs? Bueno, hay que tener un timer para ir generando manzanas, así como comprobar que la serpiente se ha comido la manzana.
En este código ya vemos cosas interesantes. La primera es que podemos usar cualquier librería de .NET, en este caso he usado clases que forman parte de .NET Standard pero cualquier librería que funcione con Mono debería funcionar. También se puede usar NuGet, al final, Godot tiene un fichero SLN y un CSPROJ, por lo que no es más que un proyecto normal de C#.
La segunda cosa es que hay varias funciones para interactuar con Godot en sí: GetNode (para obtener un nodo, hay que hacer casting), AddChild (para añadir un nodo a una escena) y RemoveChild (para quitar un nodo a una escena).
El código de SnakeBody.cs es más largo, pero no más complejo. Como vemos la serpiente la represento como List<Rect2>. Además hay una variable time puesta para que la serpiente vaya a trompicones (como el snake auténtico).
Se puede ver como la función DrawRect nos sirve para dibujar un rectángulo. La API no tiene pérdida y es parecida a la API de Cairo o la Canvas de HTML5.
También se puede ver como se puede usar LINQ para comprobar las intersecciones de la serpiente consigo misma (en realidad se comprueba la cabeza con el resto de trozos, ya que la cabeza es la parte que va a estar presente en todos los golpes).
Con Update se fuerza una nueva llamada a _Draw.
Por último tenemos _Input. En Godot, la entrada se maneja por acciones, una capa de abstracción. Esto quiere decir que no es recomendable comprobar si una tecla ha sido pulsada, simplemente se asignan teclas a acciones desde Godot (o desde el juego en un panel de configuración) y en nuestro código comprobar las acciones.
Para crear acciones vamos a Proyecto->Ajustes de Proyecto->Mapa de entrada y creamos las acciones que creamos convenientes. Yo las he llamado move_left, move_right, move_down y move_up. Luego las asignamos teclas de forma muy intuitiva.
Con esto ya tendríamos todo para un snake completito. Si le damos a ejecutar, podemos ver el juego en acción.
Todo el código del juego lo tenéis en el GitHub de ejemplos del blog y se puede importar a vuestro Godot.
Al juego le faltan muchas cosas, como una pantalla de game over cuando se produce un choque. Y puntuación. Y más detalles. Pero eso ya os lo dejo a vosotros.
Instalando Godot
Lo primero que tenemos que hacer es instalar Godot. Para ello vamos a la página de descarga y descargamos la versión que corresponda a nuestro sistema con Mono.
Es requisito indispensable tener instalado Mono SDK, tanto en Linux como en Windows. El Mono SDK se descarga desde http://www.mono-project.com/download/.
Una vez descargado tendremos un fichero con tres archivos. Descomprímelos en una carpeta. Ahora simplemente puedes ejecutar el fichero ejecutable.
Ventana de proyectos
Nada más arrancar tendremos una ventana de proyectos. Desde ahí podemos abrir proyectos que hayamos creado, descargar demos y plantillas o simplemente crear un proyecto nuevo.
Si le damos a Proyecto nuevo procederemos a la creación de un nuevo proyecto (¡¿qué complicado, verdad?!).
Una vez hecho esto ya estaríamos en el editor propiamente dicho.
Editor
Rápidamente cambiamos al modo 2D y hacemos zoom hacia atrás. Vamos a ajustar la pantalla. Para ello vamos a Proyecto->Ajustes de proyecto. Buscamos el apartado Display->Window y ajustamos la resolución base. Esta será la que usaremos para poner los elementos en pantalla. Después vamos a Stretch y configuramos el modo 2D y mantener el aspect ratio.
Esta configuración sirve para empezar con juegos simples en 2D puedan escalar fácilmente a distintas pantallas sin tener problemas de descuadres.
Ahora hacemos click en el más (+) para añadir un nodo a la escena.
Para nuestro juego, voy a añadir un Node2D, que hereda de CanvasItem y tiene APIs para dibujar rectángulos y círculos..
No lo tocamos y hacemos click derecho en el Nodo. Damos click a Attach Script:
Es importante tener un nombre de script distinto a Node2D. Esto puede dar problemas. Una vez hecho esto, se nos mostrará esta pantalla, que nos permite escribir el código en C#. A partir de ahora voy a usar Visual Studio Code, porque lo considero mejor para escribir código en C# que el editor de Godot. Creamos también dos Sprite hijos del Node2D. Apple y SnakeBody van a llamarse y les creamos scripts de C# también.
Dibujar en pantalla
Abrimos con el VS Code el fichero .cs que hemos creado, en mi caso, Snake.cs.
Dentro veremos una clase con dos funciones, _Ready y _Process. Estas son solo un ejemplo de las funciones que podemos sobreescribir en las clases asociadas a nodos. Otras funciones serían: _Draw, _Input. ¿Cuándo tenemos que usar cada una? Ready es una especie de constructor, ahí inicializamos todo lo que haga falta. Process es llamada constantemente, ahí hacemos las actualizaciones y comprobaciones pertinentes. Draw permite dibujar (si no trabajamos con imágenes es muy útil). Draw no se llama útil. ¿Qué tenemos que hacer en Snake.cs? Bueno, hay que tener un timer para ir generando manzanas, así como comprobar que la serpiente se ha comido la manzana.
using Godot;
using System;
using System.Timers;
public class Snake : Node2D
{
// 640x360
// casillas de 40x40 (mcd 640,360)
private System.Timers.Timer timer; // usamos los timers de .NET
private static readonly Random rnd = new Random(); // random de .NET
private Apple apple; // manzana activa
private SnakeBody body; // serpiente
public override void _Ready()
{
body = GetNode("SnakeBody") as SnakeBody; // obtenemos el nodo SnakeBody
body.Position = new Vector2(0,0); // arriba a la izquierda
timer = new System.Timers.Timer(10000); // cada 10 s
timer.Elapsed += NewApple; // se llama a NewApple
timer.AutoReset = true; // de forma infinita
timer.Start(); // y empezamos a contar ya!
apple = GetNode("Apple") as Apple; // obtenemos el nodo Apple
apple.Position = new Vector2(rnd.Next(15)*40,rnd.Next(8)*40); // posicion aleatoria
}
public void NewApple(object src ,ElapsedEventArgs e)
{
if(apple != null){
RemoveChild(apple); // quita el nodo de la escena si estaba
}
apple = new Apple();
apple.Position = new Vector2(rnd.Next(0,15)*40,rnd.Next(0,8)*40);
AddChild(apple); // anade motor a la escena
}
public override void _Process(float delta)
{
// siempre se comprueba si la serpiente se come la manzana
if(apple != null){
if(body.TryEat(apple)){
RemoveChild(apple);
apple = null;
}
}
}
}
En este código ya vemos cosas interesantes. La primera es que podemos usar cualquier librería de .NET, en este caso he usado clases que forman parte de .NET Standard pero cualquier librería que funcione con Mono debería funcionar. También se puede usar NuGet, al final, Godot tiene un fichero SLN y un CSPROJ, por lo que no es más que un proyecto normal de C#.
La segunda cosa es que hay varias funciones para interactuar con Godot en sí: GetNode (para obtener un nodo, hay que hacer casting), AddChild (para añadir un nodo a una escena) y RemoveChild (para quitar un nodo a una escena).
using Godot;
using System;
using System.Collections.Generic;
using System.Linq;
public class SnakeBody : Sprite
{
private float time = 0;
private enum Direction{
LEFT,
RIGHT,
UP,
DOWN
};
private Direction direction;
private List<Rect2> body;
private bool eat = false;
public override void _Ready()
{
// Called every time the node is added to the scene.
// Initialization here
direction = Direction.RIGHT;
body = new List<Rect2>();
body.Add(new Rect2(0,0,40,40));
body.Add(new Rect2(40,0,40,40));
SetZIndex(1);
}
public override void _Draw()
{
var color = new Color(1,0,0);
foreach(var rect in body){
this.DrawRect(new Rect2(rect.Position.x+2,rect.Position.y+2,36,36),color);
}
}
public bool TryEat(Apple apple)
{
if(body[0].Position.x == apple.Position.x && body[0].Position.y == apple.Position.y){
Console.WriteLine("EAT!");
eat = true;
}
return eat;
}
public bool Crash()
{
return body.Skip(1).Any(t=>{
return t.Position.x == body[0].Position.x && t.Position.y == body[0].Position.y;
});
}
public override void _Process(float delta)
{
// Called every frame. Delta is time since last frame.
// Update game logic here.
time += delta;
if(time > 0.5){
Vector2 translation;
switch(direction){
case Direction.RIGHT: translation=new Vector2(40,0);break;
case Direction.LEFT: translation=new Vector2(-40,0);break;
case Direction.UP: translation = new Vector2(0,-40);break;
default: translation = new Vector2(0,40);break;
}
if(body.Count > 0){
var newRect = new Rect2(body[0].Position,body[0].Size);
newRect.Position += translation;
if(newRect.Position.x < 0){
newRect.Position = new Vector2(600,newRect.Position.y);
}
if(newRect.Position.x > 600){
newRect.Position = new Vector2(0,newRect.Position.y);
}
if(newRect.Position.y < 0){
newRect.Position = new Vector2(newRect.Position.x,320);
}
if(newRect.Position.y > 320){
newRect.Position = new Vector2(newRect.Position.x,0);
}
body.Insert(0,newRect);
if(!eat){
body.RemoveAt(body.Count-1);
}
if(Crash()){
Console.WriteLine("CRASH! Game Over");
}
}
this.Update();
time = 0;
eat = false;
}
}
public override void _Input(InputEvent @event)
{
if(@event.IsAction("move_left") && direction != Direction.RIGHT)
{
direction = Direction.LEFT;
return;
}
if(@event.IsAction("move_right") && direction != Direction.LEFT)
{
direction = Direction.RIGHT;
return;
}
if(@event.IsAction("move_up") && direction != Direction.DOWN)
{
direction = Direction.UP;
return;
}
if(@event.IsAction("move_down") && direction != Direction.UP)
{
direction = Direction.DOWN;
return;
}
}
}
El código de SnakeBody.cs es más largo, pero no más complejo. Como vemos la serpiente la represento como List<Rect2>. Además hay una variable time puesta para que la serpiente vaya a trompicones (como el snake auténtico).
Se puede ver como la función DrawRect nos sirve para dibujar un rectángulo. La API no tiene pérdida y es parecida a la API de Cairo o la Canvas de HTML5.
También se puede ver como se puede usar LINQ para comprobar las intersecciones de la serpiente consigo misma (en realidad se comprueba la cabeza con el resto de trozos, ya que la cabeza es la parte que va a estar presente en todos los golpes).
Con Update se fuerza una nueva llamada a _Draw.
Por último tenemos _Input. En Godot, la entrada se maneja por acciones, una capa de abstracción. Esto quiere decir que no es recomendable comprobar si una tecla ha sido pulsada, simplemente se asignan teclas a acciones desde Godot (o desde el juego en un panel de configuración) y en nuestro código comprobar las acciones.
Crear acciones
Para crear acciones vamos a Proyecto->Ajustes de Proyecto->Mapa de entrada y creamos las acciones que creamos convenientes. Yo las he llamado move_left, move_right, move_down y move_up. Luego las asignamos teclas de forma muy intuitiva.
Con esto ya tendríamos todo para un snake completito. Si le damos a ejecutar, podemos ver el juego en acción.
Todo el código del juego lo tenéis en el GitHub de ejemplos del blog y se puede importar a vuestro Godot.
Al juego le faltan muchas cosas, como una pantalla de game over cuando se produce un choque. Y puntuación. Y más detalles. Pero eso ya os lo dejo a vosotros.