Home »

Tutorial de Patapang VII: Rompiendo burbujas

Ahora mismo nuestro protagonista solo es capaz de sobrevivir esquivando burbujas. Ha llegado el momento de que pueda defenderse. Hoy veremos cómo hacer que el protagonista dispare flechas con las que romper las burbujas ¡hasta que no quede ninguna!.

Para esta parte del tutorial necesitaremos un nuevo recurso gráfico:

resources/images/arrow.png

Puedes poner directamente el proyecto en este punto desde esta rama de git:

https://gitlab.com/pablo_alba/patapangtutorial/-/tree/step24

Creando una nueva escena para la flecha

Después de todo lo que llevamos esta tarea será muy sencilla. Vamos a hacer una escena para la flecha, con la imagen de la flecha y capaz de detectar cuando una burbuja entra en su espacio.

Crea una nueva escena “Arrow.tscn”, y ponle como nodo raíz un Area2D, con el nombre “Arrow”. Añadele un primer hijo de tipo “TextureRect”. Como ya habrás adivinado, vamos a poner en su propiedad “Texture” la imagen “resources/images/arrow.png”.
Ahora añade un segundo hijo de tipo “CollisionShape2D” y en su propiedad “Shape” elige “RectangleShape2D”. Para que coincida con la imagen pon en su propiedad “Extents” una “x” de 17 y una “y” de 540. Y en “Position” idénticos valores: “x” de 17 e “y” de 540.

Ahora crea un fichero de código asociado a la escena llamado “Arrow.gd”. La flecha tiene un comportamiento muy simple: va a moverse hacia arriba, hasta que llegue al límite de la pantalla, y entonces desaparecerá.

Moverla es muy sencillo. Vamos a llamar a la función “translate”, y pasarle el vector de movimiento multiplicado por el delta.

1
2
var motion = Vector2(0, -1000)
translate(motion * delta)

Para comprobar cuando ha llegado a la parte superior de la pantalla vamos a hacerlo igual que con las burbujas, mirando su “position.y”.

Cuando la flecha llegue arriba, vamos a emitir una señal (esa señal provocará que la flecha desaparezca, pero eso lo veremos luego).

Con todo esto, el código queda así:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
extends Area2D

signal arrow_touch_ceil_signal

var motion = Vector2(0, -1000)

func _ready():
pass # Replace with function body.


func _physics_process(delta):
translate(motion * delta)
if position.y < 0:
emit_signal("arrow_touch_ceil_signal")

Puedes poner directamente el proyecto en este punto desde esta rama de git:

https://gitlab.com/pablo_alba/patapangtutorial/-/tree/step25

Disparando flechas

Para poder disparar las flechas tenemos que coordinar diferentes escenas entre sí, con la escena “Play” como punto central de coordinación. Para empezar, “Player” debe detectar que el usuario quiere disparar e informar de ello a “Play”. Esto lo haremos con una señal. Cuando “Play” detecte esa señal, debe crear una nueva flecha. Y cuando esa flecha llegue a la parte superior, emitirá otra señal, “Play” la detectará y hará que la flecha desaparezca. Por si esto fuera poco, para ponerselo dificil al jugador queremos limitar a que haya solo una flecha en pantalla en cada momento.

Emitiendo la señal de disparo

Vamos a modificar el fichero “Player.gd” para emitir una señal cuando el jugador dispare. Como ya sabes, lo primero que hace falta es definir esa señal:

1
signal player_fire_signal

¿Y cuando queremos emitirla? Cuando el jugador presione la tecla espacio (“ui_accept”). Para esto basta con añadir otra condición en “_physics_process”, donde ya miramos el resto de teclas:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func _physics_process(delta):
if ! paused:
if Input.is_action_pressed("ui_accept"):
emit_signal("player_fire_signal")
elif 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)

Recibiendo la señal de disparo

Como hemos dicho, es en la escena “Play” donde queremos recibir la señal. Así que vamos a hacer varias cosas en el fichero “Play.gd”.

Primero, vamos a crear una nueva variable “arrow” donde almacenaremos nuestro nodo de disparo, cuando exista.

1
var arrow

Luego, vamos a conectar la señal “player_fire_signal” con una nueva función.

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

La función “player_fire” debe hacer varias cosas. Primero, debe crear una nueva instancia de la escena “Arrow”. Godot permite instanciar escenas desde código usando load("res://Arrow.tscn").instance(). A continuación debe colocar la posición “x” de la flecha centrada en el protagonista, y en la misma posición “y” que el suelo. Luego, debe conectar la señal “arrow_touch_ceil_signal” de esta flecha recién creada. Y por último, debe añadir “arrow” a la escena, para que se vea.

1
2
3
4
5
6
7
func player_fire():
if arrow == null:
arrow = load("res://Arrow.tscn").instance()
arrow.position.x = $Player.position.x + 150
arrow.position.y = $Floor.position.y
arrow.connect("arrow_touch_ceil_signal", self, "arrow_touch_ceil")
add_child(arrow)

Recibiendo la señal de que la flecha ha llegado arriba

Cuando recibimos la señal de que la flecha ha llegado arriba, debemos eliminar la flecha de la escena con la función remove_child, y asignar “null” a la variable “arrow” para indicar que ya no hay ninguna flecha en pantalla.

1
2
3
func arrow_touch_ceil():
remove_child(arrow)
arrow = null

Puedes poner directamente el proyecto en este punto desde esta rama de git:

https://gitlab.com/pablo_alba/patapangtutorial/-/tree/step26

Rompiendo la burbuja

Si ejecutas ahora el juego, verás que las flechas chocan contra las burbujas, pero no las rompen. Como primera aproximación, vamos a hacer que cuando una flecha alcance a una burbuja, la burbuja desaparezca.

Las flechas son un nodo de tipo área. Y estos nodos son capaces de decirnos qué objetos están dentro de ese área. Así que lo que vamos a hacer es comprobar si la burbuja ha entrado en el área de la flecha. Cuando suceda, lanzaremos una nueva señal “arrow_hit_signal”.

Vamos a definir por lo tanto en “Arrow.gd” una nueva señal

1
signal arrow_hit_signal

Y vamos a detectar si la burbuja ha entrado en el área con la función get_overlapping_bodies

1
2
3
4
5
6
7
8
9
func _physics_process(delta):
translate(motion * delta)
if position.y < 0:
emit_signal("arrow_touch_ceil_signal")
else:
var bodies = get_overlapping_bodies()
for body in bodies:
if "Ball" in body.name:
emit_signal("player_hit_signal")

Ahora vamos a ir a “Play.gd” para, primero, asociar esa señal con la flecha dentro de “player_fire”:

1
arrow.connect("arrow_hit_signal", self, "arrow_hit")

Y luego, hacer una función que haga desaparecer tanto la burbuja como la flecha cuando se detecte la señal:

1
2
3
4
5
func arrow_hit():
if arrow != null:
remove_child($Ball)
remove_child(arrow)
arrow = null

Un pequeño retoque

Antes de seguir hay un retoque que tenemos que hacer urgentemente. Queda muy mal que la flecha se vea por delante del suelo. Ve a la escena “Play”, elige el nodo “Floor” y en el inspector cambia su propiedad “Z Index” a 100, de forma que siempre esté por delante del reto de elementos.

TIP: Aunque en un juego 2D solo trabajamos con las coordenada X e Y, la propiedad “Z Index” permite definir que elementos se ven por delante de otros (a más alto el valor, más “por delante” se ve)

Ahora si, ¡el juego ha mejorado mucho! Ya puedes esquivar y romper la burbuja. ¡A jugar!

Puedes poner directamente el proyecto en este punto desde esta rama de git:

https://gitlab.com/pablo_alba/patapangtutorial/-/tree/step28

¡Muchas burbujas!

Pero en realidad el juego no es así. Cuando el jugador rompe una burbuja grande, queremos que se generen dos medianas. Cuando rompe una mediana, que se generen dos pequeñas. Y son solo estas burbujas pequeñas las que finalmente desaparecen al romperlas.

Instanciando una burbuja

Para conseguir esto ya no podemos tener solo una burbuja en “Play”. Debemos instanciarlas cuando queramos, al igual que hacemos con las flechas. Así que en el árbol de nodos de “Play” elige el nodo “Ball” y borralo.

Ahora en el código de “Play.gd” vamos a hacer algunos cambios. Vamos a ir poco a poco. Lo primero, conseguir que funcione como hasta ahora, pero instanciando las burbujas.

Para ello, tenemos que definir una nueva variable llamada “ball” e inicializarla en la función “_ready”. Luego, hay que cambiar todas las veces que en el código aparece “$Ball” por “ball”.

1
2
3
4
5
6
7
8
var ball

func _ready():
ball = load("res://Ball.tscn").instance()
add_child(ball)
ball.connect("player_hit_signal", self, "player_hit")
ball.connect("arrow_hit_signal", self, "arrow_hit")
$Player.connect("player_fire_signal", self, "player_fire")

Si ejecutas ahora, todo debería funcionar como antes.

Instanciando muchas burbujas

Vamos a crear en “Play” una función que nos permita instanciar burbujas facilmente. A esta función vamos a pasarle tres parámetros: la posición donde queremos añadir la burbuja, el vector de movimiento para ella, y la escala (el tamaño) de la burbuja. Así podemos crear burbujas de cualquier tamaño, en cualquier punto, que estén subiendo o bajando, y que se muevan a derecha o izquierda.

Además, si vamos a tener muchas burbujas a la vez, tenemos que cambiar nuestra variable “ball” para que sea un array “balls”, y añadir a ese array las burbujas que creemos.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var balls

func _ready():
balls = []
create_ball(Vector2(0,0), Vector2(5, 0), 1)
$Player.connect("player_fire_signal", self, "player_fire")

func create_ball(position, motion, scale):
var ball = load("res://Ball.tscn").instance()
ball.position.x = position.x
ball.position.y = position.y
ball.motion.x = motion.x
ball.motion.y = motion.y
ball.scale.x = scale
ball.scale.y = scale
add_child(ball)
ball.connect("player_hit_signal", self, "player_hit")
balls.append(ball)

Como ahora podemos tener no una, sino muchas burbujas, en la función “player_hit” tenemos que pausar todas ellas:

1
2
3
4
func player_hit():
for ball in balls:
ball.pause()
$Player.die()

Y además, cuando recibamos la señal arrow_hit_signal, tenemos que saber cual es la burbuja que tenemos que romper (quitarla del juego, y eliminarla del array de burbujas). Para ello, vamos a añadirlo como parámetro a la función “arrow_hit:

1
2
3
4
5
6
func arrow_hit(ball):
if arrow != null:
balls.erase(ball)
remove_child(ball)
remove_child(arrow)
arrow = null

Y vamos a pasar esa burbuja como un parámetro al enviar la señal desde “Arrow.gd”:

1
2
3
4
5
6
7
8
9
func _physics_process(delta):
translate(motion * delta)
if position.y < 0:
emit_signal("arrow_touch_ceil_signal")
else:
var bodies = get_overlapping_bodies()
for body in bodies:
if "Ball" in body.name:
emit_signal("arrow_hit_signal", body)

Después de tanto trabajo, si ejecutas ahora el juego… Se verá exactamente como antes. Pero sin embargo, ¡ya hemos abierto un montón de posibilidades!

Podemos meter fácilmente varias burbujas, de varios tamaños. Si en “Play.gd” cambias el “_ready” por:

1
2
3
4
5
func _ready():
balls = []
create_ball(Vector2(0,0), Vector2(5, 0), 1)
create_ball(Vector2(1500,0), Vector2(-5, 0), 0.5)
$Player.connect("player_fire_signal", self, "player_fire")

¡Ahora tenemos dos burbujas!

Puedes poner directamente el proyecto en este punto desde esta rama de git:

https://gitlab.com/pablo_alba/patapangtutorial/-/tree/step29

Las burbujas no deben chocar entre sí. Collision Layer y Collision Mask

Cuando hacemos un juego es frecuente que no queramos que todos los elementos puedan colisionar con todos los elementos. Y ese justo es nuestro caso. No queremos que las burbujas choquen entre sí, sino que se muevan libremente, sin interactuar entre ellas. Godot tiene una forma muy elegante de solucionar esto: las capas de colisiones (“Collision Layers”).

Godot tiene 20 capas donde puede “estar” un elemento. Y además permite definir para cada elemento con qué capas quiere interactuar. ¡Incluso puedes definir que un elemento no interactúe con elementos de su propia capa!

Cada una de esas capas se llaman Collision Layer. Y la definición de “Con qué capas pueden actuar” se llama Collision Mask

Esto es más fácil de ver con el caso real:

  • Nuestro suelo va a estar en la capa 1
  • Nuestro protagonista va a estar en la capa 2
  • Las burbujas van a estar en la capa 3
  • Las flechas van a estar en la capa 4

Y las interacciones que queremos son:

  • El suelo va a interactuar solo con el protagonista (capa2) y las burbujas (capa 3)
  • El protagonista va a interactuar solo el suelo (capa1), y con las burbujas (capa 3)
  • Las burbujas van a interactuar con el suelo (capa 1), el protagonista (capa 2), y las flechas (capa 4)
  • Las flechas van a interactuar solo con las burbujas (capa 3)

¿Y cómo definimos estos valores? Pues en el inspector de cada uno de estos elementos, en una sección llamada “PhysicsBody2D”. En la propiedad “Layer” elegimos en qué capa queremos que esté el elemento, y la propiedad “Mask” elegimos con qué capas queremos que interactúe. Por ejemplo, estos son los valores para “Player”:

TIP: Debes definir los valores de las propiedades de “PhysicsBody2D” en cada una de las escenas: “Player”, “Ball” y “Arrow”. Como el suelo no tiene una escena propia, simplemente seleccionalo en el árbol de nodos de la escena “Play” y podrás ponerle los valores adecuados.

Arreglando algunos bugs

Quick and dirty fix

En este punto las burbujas ya han dejado de chocar, y podemos romperlas. Todo debería funcionar como queremos. Pero se está dando un comportamiento extraño, un bug. Y, lo reconozco, es uno para el que no he conseguido saber la causa pero sí sé cuándo sucede, cada vez que empiezo una partida moviendo el personaje hacia la izquierda. El protagonista empieza a “subir”, como si estuviese chocando con algún elemento o elevándose como un globo. Por fortuna, aunque desconozco el problema que lo origina, la solución es sencilla. En “Player.gd” vamos a asegurarnos de que su vector de movimiento siempre es 0 para la coordenada y cuando comprobamos el movimiento a la izquierda.

1
2
3
4
5
6
7
8
9
func _physics_process(delta):
.
.
.
elif Input.is_action_pressed("ui_left"):
motion.x = -SPEED
motion.y = 0
$AnimatedSprite.flip_h = true
$AnimatedSprite.animation = "adventure_guy_running"

Otra forma de vitarlo en ir a la Scene de Player y ajustar el valor de la propiedad de Transform > Position > y a 603 y mágicamente desaparece esa aparente colisión que impulsa al personaje hacia arriba. En ese caso no es necesario introducir el cambio de motion.y = 0 en “Player.gd”.

Rebote de las burbujas a la derecha

¿Recuerdas que quisimos hacer que el rebote de las burbujas a derecha e izquierda fuera simple? Pues muchas veces pasa que al hacer una cosa de forma simple, en lugar de hacerla bien, acaba generando bugs en el futuro. Y eso nos ha pasado. Teníamos fijado que la burbuja rebotase a la derecha cuando position.x > 1664. Pero ese valor solo funciona para burbujas a escala 1. Tenemos que reajustarlo para las burbujas más pequeñas:

1
2
3
4
5
6
7
func _physics_process(delta):
if ! paused:
if position.x < 0 or position.x + (256 * scale.x) > 1920:
motion.x = - motion.x
.
.
.

Puedes poner directamente el proyecto en este punto desde esta rama de git:

https://gitlab.com/pablo_alba/patapangtutorial/-/tree/step30

Rompiendo las burbujas

Ya casi lo tenemos. Solo nos falta hacer que cuando una flecha golpee a una burbuja grande, en lugar de simplemente hacerla desaparecer, lo que haga sea crear otras dos burbujas más pequeñas.

Vamos a tener 3 tamaños de burbujas. Las grandes, con una “scale” de 1. Las medianas, con una “scale” de 0.5. Y las pequeñas, con una “scale” de 0.25. Así que si la burbuja que hemos roto tiene una “scale” mayor de 0.25, crearemos dos burbujas con la mitad de su tamaño.

Cuando creemos dos burbujas más pequeñas vamos a querer que mantengan la velocidad de movimiento de la burbuja original, pero que una de ellas vaya a la derecha, y otra a la izquierda. Y además, como si la flecha las hubiese golpeado, que comiencen moviéndose hacia arriba.

Con todo esto, el código de “arrow_hit” queda así:

1
2
3
4
5
6
7
8
9
10
11
12
13
func arrow_hit(ball):
if arrow != null:
balls.erase(ball)
remove_child(ball)
remove_child(arrow)
arrow = null
if ball.scale.x > 0.25:
var scale = ball.scale.x / 2
var motion1 = Vector2(ball.motion.x, -abs(ball.motion.y))
var motion2 = Vector2(-ball.motion.x, -abs(ball.motion.y))
create_ball(ball.position, motion1, scale)
create_ball(ball.position, motion2, scale)

Puedes poner directamente el proyecto en este punto desde esta rama de git:

https://gitlab.com/pablo_alba/patapangtutorial/-/tree/step31

¡Por fin lo tenemos! ¡La base del juego está funcionando! Disfruta rompiendo estas burbujas hasta la próxima semana.

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


Tutorial: