Home »

Tutorial de Patapang VIII: Retoques finales

Pues hemos llegado a un punto en que la jugabilidad está lista. Nos quedan unas pocas cosas para acabar: un diálogo de final de juego, añadir música y sonidos, y meter controles para móvil. Con esto tendremos nuestra primera versión del juego.

Para esta parte del tutorial necesitaremos algunos nuevos recursos gráficos y de sonido:

resources/images/gui/gameover.png
resources/images/gui/home.png
resources/images/gui/reload.png
resources/images/gui/left.png
resources/images/gui/right.png
resources/images/gui/fire.png
resources/sound/b-roll.ogg

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

Diálogo de fin de juego

La partida puede acabar de dos formas. O bien el protagonista muere, o bien destruye todas las burbujas. En ambos casos vamos a presentarle un diálogo donde pueda elegir entre jugar de nuevo, o volver al menú principal.

En la escena Play añade un nuevo nodo de tipo Node2D, y ponle como nombre GameOver. Para que siempre aparezca delante de todos los elementos, ponle la propiedad “Z Index” a 2000.

A continuación añade a este nodo un hijo de tipo TextureRect, llámalo “Background”, y elige como su textura la imagen “gameover.png”. Para centrarlo, pon en su propiedad “Rect/Position” x=590 e y=238.

Ahora, añade a este nodo “Background” dos hijos de tipo TextureRect. Al primero llámalo “Home”, ponle como textura la imagen “home.png”, y en su propiedad “Rect/Position” x=170 e y=350. Al segundo llámalo “Reload”, ponle como textura “reload.png”, y en su propiedad “Rect/Position” x=390 e y=350.

Vamos a hacer una función que haga un poco de limpieza y muestre este diálogo. La llamaremos “show_gameover” y va a hacer lo siguiente:

  • Eliminar la flecha que pueda haber en pantalla
  • Eliminar todas las burbujas que pueda haber en pantalla
  • Mostrar la pantalla de “GameOver”
1
2
3
4
5
6
7
8
9
func show_gameover():
if (arrow != null):
remove_child(arrow)
arrow = null

for ball in balls:
remove_child(ball)
balls.clear()
$Gameover.show()

Hay dos momentos en los que queremos llamar a show_gameover. Primero, cuando el personaje muere, tras un par de segundos para dar tiempo a ver la animación:

1
2
3
4
5
6
func player_hit():
for ball in balls:
ball.pause()
$Player.die()
yield(get_tree().create_timer(2.0), "timeout")
show_gameover()

Y segundo, cuando no queden más burbujas, con medio segundo de retardo. Para esto hay que añadir a la función “arrow_hit” lo siguiente:

1
2
3
if balls.size() == 0:
yield(get_tree().create_timer(0.5), "timeout")
show_gameover()
TIP: Hay diferentes formas en Godot de ejecutar un código tras un tiempo. La combinación de yield y get_tree es de las más elegantes, pero puedes ver otras opciones en https://gdscript.com/godot-timing-tutorial

Ahora nos toca darle funcionalidad a los botones. Vamos con el más fácil. Elige “Home”, y en la pestaña “Node” (al lado del inspector, por si no lo recuerdas) haz doble click en “gui_input” y acepta los valores por defecto. Esto te creará una nueva función llamada “_on_Home_gui_input”. Lo que queremos hacer aquí es que cuando el usuario haga click, volvamos al menú principal. Así que el código será:

1
2
3
func _on_Home_gui_input(event):
if event is InputEventMouseButton and event.pressed and event.button_index == 1:
get_tree().change_scene("res://MainMenu.tscn")

El otro botón tiene que hacer recomenzar la partida. Elige “Reset”, y en la pestaña “Node” haz doble click en “gui_input” y acepta los valores por defecto. El código del botón será muy simple:

1
2
3
func _on_Reload_gui_input(event):
if event is InputEventMouseButton and event.pressed and event.button_index == 1:
reset()

El método reset aún no existe, pero debería hacer varias cosas:

  • Colocar al protagonista en su posición inicial. con la animación “idle”.

    1
    2
       $Player.position.x = 845
    $Player.idle()
    • Esto además requiere que creemos en “Player.gd” la función “idle”:
      1
      2
      3
      func idle():
      paused = false
      $AnimatedSprite.animation = "adventure_guy_idle"
  • Crear y colocar las burbujas en su posición inicial. Mejor aún, para que el juego sea más variado, crear de forma aleatoria de 1 a 3 burbujas (si son 3, de tamaño mediano), en una posición x entre 500 y 1700, y yendo hacia la derecha o hacia la izquierda.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var num_balls = randi()%3 + 1
    var size = 1
    if num_balls == 3:
    size = 0.5
    for ball in range(num_balls):
    var x = randi()%1500 + 200
    var dir = 1
    if randi() % 2:
    dir = -1
    create_ball(Vector2(x,0), Vector2(5 * dir, 0), size)
  • Ocultar la pantalla de “GameOver”

    1
    $Gameover.hide()
  • Actualizar la función “_ready” para usar “reset”

    1
    2
    3
    4
    5
    func _ready():
    randomize()
    balls = []
    $Player.connect("player_fire_signal", self, "player_fire")
    reset()
TIP: Si vas a usar números aleatorios en tu juego, es importante llamar a "randomize()" al arrancar, para asegurarte de que los números son diferentes en cada ejecución. "randi" devuelve un número entero entre 0 y 4294967295 (inclusive), de forma que si queremos un número del 1 al 3 debemos hacer un módulo 3 al resultado, que nos dará un número entre 0 y 2, y luego sumarle 1: randi()%3 + 1.

Y con esto ya tendríamos una pantalla de juego conectada con la pantalla principal, en la que cada partida es diferente. Como pequeña mejora estética, si al nodo “Player” dentro de la escena “Play” le pones un “Z Index” 101, la animación cuando muere queda mejor, ya que se ve por delante del suelo.

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

Compartiendo información entre escenas: Música y sonido

Para que nuestro juego termine de molar de verdad nos falta añadirle música y sonido. Pero, si recuerdas, en el menú principal el usuario elige si va a querer tenerlos activados o no, y debemos mantener esa configuración.

Un script que se carga en todas las escenas

Vamos a hacer eso con un script de código, “Globals.gd” que se va a compartir entre todas las escenas. Para esto ve a la sección “Script”, y elige “File / New Script…”. En el diálogo que aparece elige como nombre “Globals.gd” y pulsa Create.

En este fichero vamos a guardar variables (y si queremos funciones) que queremos que estén accesibles en todas las escenas. En nuestro caso, crea variables que indiquen si la música y los sonidos están activados:

1
2
3
4
extends Node

var music_enabled = true
var fx_enabled = true

Ahora vamos a hacer que “Globals.gd” sea accesible desde todas las escenas. Elige “Project / Project Settings…”, ve a la pestaña “Autoload”, y en el campo “Path” elige el fichero “Globals.gd”. Como “Node Name” deja Globals, y pulsa el botón Add.

Usando Globals desde MainMenu

Lo siguiente es configurar “MainMenu” para activar o desactivar la música según el valor de esas variables:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func update_music():
if Globals.music_enabled:
$AudioStreamPlayer.play()
$MusicButtonOff.hide()
$MusicButton.show()
else:
$AudioStreamPlayer.stop()
$MusicButtonOff.show()
$MusicButton.hide()

func update_fx():
if Globals.fx_enabled:
$AudioStreamPop.play()
$FxButtonOff.hide()
$FxButton.show()
else:
$FxButtonOff.show()
$FxButton.hide()

Y vamos a modificar las funciones que se llaman al pulsar los botones para que cambien las variables de Globals, y llamen a nuestras nuevas funciones:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func _on_MusicButton_gui_input(event):
if event is InputEventMouseButton and event.pressed and event.button_index == 1:
Globals.music_enabled = false
update_music()

func _on_MusicButtonOff_gui_input(event):
if event is InputEventMouseButton and event.pressed and event.button_index == 1:
Globals.music_enabled = true
update_music()

func _on_FxButton_gui_input(event):
if event is InputEventMouseButton and event.pressed and event.button_index == 1:
Globals.fx_enabled = false
update_fx()

func _on_FxButtonOff_gui_input(event):
if event is InputEventMouseButton and event.pressed and event.button_index == 1:
Globals.fx_enabled = true
update_fx()

Además, queremos que se ejecuten estas funciones al cargar la escena, para que la configuración del usuario se mantenga cuando volvamos al menú principal desde la pantalla de juego:

1
2
3
func _ready():
update_music()
update_fx()

Como ahora el arranque de la música se va a hacer desde código, nos queda ir al árbol de nodos, elegir “AudioStreamPlayer” y en sus propiedades quitarle el Autoplay. Así la música no comenzará al cargar la escena a menos que “music_enabled” esté a true.

Usando Globals desde Play

Ahora que ya tenemos una configuración de sonido compartida entre todas las escenas, vamos a meter sonidos a la escena “Play”. Añade dos nuevos nodos de tipo “AudioStreamPlayer”, y llámalos “AudioStreamMusic” y “AutoStreamPop”. Al primero ponle como Stream el recurso “b-roll.ogg”, y al segundo “pop.wav”.

Ahora vamos a hacer que comience la música en la pantalla de juego, pero solo si está activada. Añade estas líneas a la función “_ready”:

1
2
if Globals.music_enabled:
$AudioStreamMusic.play()

De forma similar, cuando reventemos una burbuja haremos que suene “pop”, solo si los efectos de sonido están activados. Añade estas líneas a “arrow_hit”:

1
2
3
4
5
6
7
func arrow_hit(ball):
if arrow != null:
if Globals.fx_enabled:
$AudioStreamPop.play()
.
.
.

Y con esto el juego tiene música y efectos. ¿A que es impresionante la diferencia que supone?

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

Controles para movil

Nuestro juego ahora mismo solo es jugable desde un teclado. Ahora nos toca añadir botones en la pantalla para poder jugar desde dispositivos móviles.

Antes de nada, para poder probar el comportamiento, tenemos que configurar Godot para que trate el ratón como si fuese un dedo tocando una pantalla táctil. Abre “Project / Project Settings…”, ve a la sección “Input Devices / Pointing” y marca la casilla “Emulate touch from mouse”.

Ahora ya solo queda añadir los controles. Vamos a utilizar nodos de tipo “TouchScreenButton”, que son específicos para pantallas táctiles, y además tienen un atributo utilísimo: “Action”. Este atributo indica qué evento queremos que esté activo mientras el jugador esté tocando el botón. Así podemos lanzar los mismos eventos que ya lanzamos con el teclado, con lo que el manejo será inmediato.

En la escena “Play” añade tres nuevos nodos de tipo “TouchScreenButton”, y configuralos así:

  • Nombre: “LeftButton”, “Action”: “ui_left”, “Transform / Position”: x=25, y=900, “Z Index”: 1001
  • Nombre: “RightButton”, “Action”: “ui_right”, “Transform / Position”: x=250, y=900, “Z Index”: 1001
  • Nombre: “FireButton”, “Action”: “ui_accept”, “Transform / Position”: x=1700, y=900, “Z Index”: 1001
Puedes poner directamente el proyecto en este punto desde esta rama de git: https://gitlab.com/pablo_alba/patapangtutorial/-/tree/step35

El último fix

Antes de terminar, hay una última cosa que nos hemos dejado por hacer. El protagonista es capaz de moverse fuera de los límites de la pantalla. Para solucionarlo, vamos a añadir estas líneas al final del método “_physics_process” de “Player.gd”:

1
2
3
4
if position.x < 0:
position.x = 0
elif position.x > 1750:
position.x = 1750
Puedes poner directamente el proyecto en este punto desde esta rama de git: https://gitlab.com/pablo_alba/patapangtutorial/-/tree/step36

Versión 1.0

Y con esto, damos por terminada la versión 1.0 de Patapang. Nos ha quedado bastante bien, ¿no crees?. En este punto ya tienes conocimientos suficientes de Godot para seguir por tu cuenta, así que no veremos ninguna funcionalidad más. Por supuesto, hay un montón de ideas que puedes implementar para seguir practicando:

  • Meter niveles prediseñados, y que el jugador vaya avanzando
  • Meter diferentes power-ups como disparos dobles, escudos…
  • Añadir dificultades adicionales como pinchos o cajas que no dejen que el protagonista se mueva por todo el escenario
  • Diferentes personajes, que se puedan “comprar” con monedas que ganas en los niveles
  • ¿Se te ocurren algunas más?

Como inspiración, puedes ver el código de mi primera versión de Patapang (Y la tienes publicada para Android). Esa versión implementa muchas más funcionalidades, pero es mucho menos didáctica, con un código menos limpio, y con diferentes experimentos y aproximaciones a los problemas de los que hemos visto aquí. Puede ser interesante para que veas otras formas de abordar los problemas.

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


Tutorial: