Home »

Tutorial de Patapang VI: Esquivando burbujas

Una vez tenemos un protagonista capaz de moverse, el siguiente paso es crear unos enemigos dignos de nosotros: las burbujas. Empezaremos añadiendo una burbuja a nuestro juego, luego le daremos movimiento, y por fin interactividad: será capaz de acabar con nuestro protagonista. ¡Vamos a por ello!

Creando una nueva escena para las burbujas

Lo primero que vamos a hacer es crear una nueva escena llamada “Ball.tscn”. Esta escena va a ser muy similar a la de “Player”, vamos a hacer que el nodo raíz sea un KinematicBody2D, y vamos a crearle dos nodos hijos, un CollisionShape2D y un TextureRect.

En el TextureRect tenemos poco que hacer. Simplemente pon la imagen “resources/images/bubble_red.png” como Texture.

El CollisionShape2D tampoco nos va a dar muchos problemas. Elige en su propiedad Shape un nuevo CircleShape2D. Pincha sobre ese CircleShape2D para expandir sus propiedades, y ponle como Radius 115, y en la propiedad Transform pon position.x y position.y a 128, de forma que tanto la posición como el tamaño encajen con la imagen.

TIP: En realidad el tamaño es un poco más pequeño que la imagen. Esto es así para asegurar que cuando hay una colisión, visualmente queda bien. Cuando tengamos la jugabilidad implementada, puedes experimentar con el valor de "Radius" y ver los resultados de ponerlo demasiado grande o demasiado pequeño.

Ahora, para poder ver nuestra nueva burbuja, ve a la escena “Play”, dale al botón derecho sobre el nodo raíz, selecciona “Instance child scene”, y elige la escena “Ball.tscn”. Si ejecutas ahora el juego verás una burbuja flotando en el aire.

Haciendo que la burbuja se mueva

Ahora vamos a darle vida. Crea un script “Ball.gd” asociado a la escena “Ball.tscn”.

Nuestra primera idea para hacer que la burbuja se mueva sería darle una velocidad constante, igual que hicimos con “Player”. Sin embargo, lo que queremos es simular una especie de gravedad, y eso significa una aceleración. La burbuja empezará alto, debe caer cada vez más rápido, rebotar contra el suelo, empezar a subir, ir perdiendo velocidad, y en un momento dado empezar a caer de nuevo. ¿Complicado? ¡Que va!

Ya que vamos a usar una especie de gravedad, vamos a crear una constante llamada GRAVITY. Y al igual que con el protagonista, también vamos a crear un vector de movimiento.

1
2
const GRAVITY = 10
var motion = Vector2(0, 0)

Ahora vamos a hacer que cada vez que Godot procese las físicas del juego, se actualice la posición y la velocidad de la burbuja. La gravedad siempre hace un efecto hacia abajo, así que no nos importa si la burbuja está subiendo (motion.y negativa) o bajando (motion.y positiva). En cualquier caso vamos a sumarle el efecto de la gravedad en el delta de tiempo que ha pasado, y hacer que se mueva.

1
2
3
func _physics_process(delta):
motion.y += delta * GRAVITY
move_and_collide(motion)

Si recuerdas, en “Player” usabamos la función “move_and_slide” para hacer que se moviese. Ahora vamos a usar “move_and_collide” porque, además de moverse, esa función nos devuelve información sobre los objetos con los que la burbuja choca.

Si ejecutas ahora el código, verás que la burbuja cae al suelo y se queda parada. Necesitamos hacer que rebote.

TIP: La gestión de rebotes se podría hacer de muchas formas, especialmente haciendo uso del motor de física de Godot. Por ejemplo, Los objetos `RigidBody2D` tienen un montón de propiedades físicas interesantes, como "Mass", "Friction", o "Bounce". Pero vamos a mantener las cosas simples gestionando nosotros mismos los rebotes. Si quieres meterte en el apasionante mundo del motór de física de Godot, puedes empezar por https://docs.godotengine.org/en/3.0/tutorials/physics/physics_introduction.html

Para hacer un rebote simple, vamos a detectar cuando el objeto que toca es el suelo, y cambiar su “motion.y”. En nuestro caso, con el valor que hemos puesto a la gravedad y la altura a la que queremos que lleguen las burbujas, un poco de prueba y error nos ha dado el valor -14. Pero te animamos a jugar con los valores de GRAVITY y BOUNCE_VEL y ver sus efectos.

1
2
3
4
5
6
7
8
const BOUNCE_VEL = -14

func _physics_process(delta):
motion.y += delta * GRAVITY
var collision = move_and_collide(motion)
if (collision != null):
if ("Floor" == collision.collider.name):
motion.y = BOUNCE_VEL

Si ejecutas ahora, verás como la burbuja rebota sin parar.

Puedes poner directamente el proyecto en este punto desde esta rama de git: https://gitlab.com/pablo_alba/patapangtutorial/-/tree/step21

Integración entre escenas: Haciendo que la burbuja sea peligrosa

Aunque tengamos un protagonista que se puede mover, y una burbuja que rebota, si pruebas a meter al protagonista debajo de la burbuja… No pasará nada. Simplemente la burbuja se detiene, ya que no puede seguir avanzando.

Tenemos que empezar a implementar algunas reglas de juego.

Lo primero es definir qué queremos conseguir. Como primera interacción, lo que queremos es que si la burbuja toca al protagonista pasen dos cosas:

  1. La burbuja se quedará detenida en el aire
  2. El protagonista mostrará la animación de “muerto”, y ya no se podrá mover más.

¡Manos a la obra!

Detectando la colisión entre la burbuja y el protagonista

Esto lo tenemos prácticamente hecho. La burbuja ya es capaz de detectar colisiones, e incluso discriminar cuando ha chocado con el suelo. Para detectar cuando ha chocado con el protagonista simplemente debemos añadir una nueva comprobación:

1
2
3
4
5
6
7
8
func _physics_process(delta):
motion.y += delta * GRAVITY
var collision = move_and_collide(motion)
if (collision != null):
if ("Floor" == collision.collider.name):
motion.y = BOUNCE_VEL
if ("Player" == collision.collider.name):
print("Player colision")

Comunicación entre escenas: Señales

Aunque por supuesto existen muchas formas de organizar el código de un juego, a nosotros nos gusta tener un sitio central que controle las reglas comunes a diferentes escenas. Está muy bien que la burbuja sepa rebotar, y el protagonista sepa moverse, pero qué hacer cuando chocan vamos a gestionarlo en la escena “Play”.

Pero claro, para poder gestionarlo ahí, necesitamos que “Play” reciba información. ¿Y cómo se logra eso en Godot? Mediante señales.

Una señal es simplemente algo que un elemento emite, y otro elemento puede escuchar. Así que vamos a definir una señal, “Ball” va a emitirla cuando detecte una colisión con el protagonista, y “Play” la va a escuchar.

Emitiendo la señal

Para decirle a Godot que “Ball” va a emitir una señal, basta con definirla en su código con el nombre que queramos:

1
signal player_hit_signal

Y emitirla es muy sencillo. Se hace con la función emit_signal. Por lo tanto, para emitir nuestra señal en la colisión el código quedaría así:

1
2
3
4
5
6
7
8
func _physics_process(delta):
motion.y += delta * GRAVITY
var collision = move_and_collide(motion)
if (collision != null):
if ("Floor" == collision.collider.name):
motion.y = BOUNCE_VEL
if ("Player" == collision.collider.name):
emit_signal("player_hit_signal")

Recibiendo la señal

Recibir la señal también es sencillo hay que usar la función connect que tienen todos los nodos de Godot para conectar esa señal con una función, que se ejecutará al recibirla. Por lo tanto, crea un fichero de código “Play.gd” asociado a la escena “Play”, y añade este código a la función “_ready”:

1
2
func _ready():
$Ball.connect("player_hit_signal", self, "player_hit")

A continuación crea la función “player_hit” que se ejecutará al recibir la señal.

1
2
func player_hit():
print("Play: player hit")

Si lanzas el juego, verás que cuando la burbuja toca al protagonista, en el output aparece (un montón de veces) el texto “Play: player hit”.

Puedes poner directamente el proyecto en este punto desde esta rama de git: https://gitlab.com/pablo_alba/patapangtutorial/-/tree/step22

Crónica de una muerte anunciada

Una vez que recibimos la señal, ya podemos hacer algo con ella. Primero vamos a modificar un poco el código de “Ball” para poder hacer que se quede quieta. Simplemente vamos a añadirle una variable “paused”, y solo moverla mientras esa variable sea falsa.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var paused = false

func pause():
paused = true

func _physics_process(delta):
if ! paused:
motion.y += delta * GRAVITY
var collision = move_and_collide(motion)
if (collision != null):
if ("Floor" == collision.collider.name):
motion.y = BOUNCE_VEL
if ("Player" == collision.collider.name):
emit_signal("player_hit_signal")

De hecho, vamos a hacer exactamente lo mismo en “Player”. Ahí, además, vamos a comenzar la animación para la muerte del personaje que creamos el otro día (“adventure_guy_dead”).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var paused = false

func die():
paused = true
$AnimatedSprite.animation = "adventure_guy_dead"

func _physics_process(delta):
if ! paused:
if Input.is_action_pressed("ui_right"):
motion.x = SPEED
$AnimatedSprite.flip_h = false
$AnimatedSprite.animation = "adventure_guy_running"
elif Input.is_action_pressed("ui_left"):
motion.x = -SPEED
$AnimatedSprite.flip_h = true
$AnimatedSprite.animation = "adventure_guy_running"
else:
$AnimatedSprite.animation = "adventure_guy_idle"
motion.x = 0

motion = move_and_slide(motion)
TIP: BUG Warning! En algún momento hemos subido un bug al código. La animación "adventure_guy_dead" tenía activado el `Loop`. Si te ha pasado lo mismo, ve a "Player", y en la sección de "Animations" desactívalo para esa animación.

Pues ya solo nos falta modificar el código de “Play” para llamar a las dos funciones que acabamos de crear:

1
2
3
func player_hit():
$Ball.pause()
$Player.die()

Ejecuta el juego para probarlo, pero ¡con cuidado! Ahora si la burbuja toca al protagonista… ¡morirá!

Haciendo la burbuja aún más peligrosa

La verdad es que una burbuja botando en un sitio no da mucho miedo… Vamos a modificar un poco su comportamiento para que sea un poco más interesante. Vamos a hacer que se mueva también horizontalmente.

Que empiece a moverse de forma horizontal es muy sencillo. Simplemente, cambia la inicialización del vector de movimiento para que se mueva en el eje x.

1
var motion = Vector2(5, 0)

Lo que pasa es que así la burbuja se moverá siempre a la derecha, hasta salirse de la pantalla. Para no complicarnos la vida, vamos a hacer entonces una comprobación muy simple: si la posición de la bola hace que se vaya a salir de la pantalla por la izquierda o por la derecha, invertimos el valor de la coordenada x del vector de movimiento.

La función “_physics_process” de “Ball” quedará por tanto:

1
2
3
4
5
6
7
8
9
10
11
12
func _physics_process(delta):
if ! paused:
if position.x < 0 or position.x > 1664:
motion.x = - motion.x

motion.y += delta * GRAVITY
var collision = move_and_collide(motion)
if (collision != null):
if ("Floor" == collision.collider.name):
motion.y = BOUNCE_VEL
elif ("Player" == collision.collider.name):
emit_signal("player_hit_signal")
TIP: ¿Por qué ese número extraño, 1664? Porque queremos que la bola rebote cuando el extremo derecho de la imagen se salga por la pantalla. Como la propiedad "position" nos da el punto a la izquierda y arriba de la bola, tenemos que tener en cuenta que la imagen mide 256 pixels. Y como hemos definido que nuestro juego tenga un ancho de 1920 pixels, 1920 - 256 = 1664.

¡Ahora sí! ¡Ya tenemos un juego de “esquivar una bola gigante” funcionando!

Puedes poner directamente el proyecto en este punto desde esta rama de git: https://gitlab.com/pablo_alba/patapangtutorial/-/tree/step23

¡Continuará! Cada semana, un nuevo post tutorial.


Tutorial: