Parte 6

Resumen

En esta parte, vamos a añadir un menú principal y un menú de pausa, añadir un sistema de regeneración para el jugador, y cambiar/mover el sistema de sonido para que podamos usarlo desde cualquier script.

¡Esta es la última parte del tutorial de FPS; al final de esto, tendrás una base sólida para construir increíbles juegos FPS con Godot!

../../../_images/FinishedTutorialPicture.png

Nota

Se supone que has terminado Parte 5 antes de pasar a esta parte del tutorial. El proyecto terminado de Parte 5 será el proyecto inicial de la parte 6

¡Vamos a empezar!

Añadiendo el menú principal

En primer lugar, abre el Main_Menu.tscn y echa un vistazo a cómo está montada la escena.

El menú principal está dividido en tres paneles diferentes, cada uno de los cuales representa una 'pantalla' diferente de nuestro menú principal.

Nota

El nodo Background_Animation es sólo para que el fondo del menú sea un poco más interesante que un color sólido. Es una cámara mirando alrededor del skybox, nada elegante.

Siéntete libre de expandir todos los nodos y ver cómo están configurados. Recuerda mantener sólo Start_Menu visible cuando termines, ya que es la pantalla que queremos mostrar primero cuando entremos al menú principal.

Selecciona Main_Menu (el nodo raíz) y crea un nuevo script llamado Main_Menu.gd. Añade lo siguiente:

extends Control

var start_menu
var level_select_menu
var options_menu

export (String, FILE) var testing_area_scene
export (String, FILE) var space_level_scene
export (String, FILE) var ruins_level_scene

func _ready():
    start_menu = $Start_Menu
    level_select_menu = $Level_Select_Menu
    options_menu = $Options_Menu

    $Start_Menu/Button_Start.connect("pressed", self, "start_menu_button_pressed", ["start"])
    $Start_Menu/Button_Open_Godot.connect("pressed", self, "start_menu_button_pressed", ["open_godot"])
    $Start_Menu/Button_Options.connect("pressed", self, "start_menu_button_pressed", ["options"])
    $Start_Menu/Button_Quit.connect("pressed", self, "start_menu_button_pressed", ["quit"])

    $Level_Select_Menu/Button_Back.connect("pressed", self, "level_select_menu_button_pressed", ["back"])
    $Level_Select_Menu/Button_Level_Testing_Area.connect("pressed", self, "level_select_menu_button_pressed", ["testing_scene"])
    $Level_Select_Menu/Button_Level_Space.connect("pressed", self, "level_select_menu_button_pressed", ["space_level"])
    $Level_Select_Menu/Button_Level_Ruins.connect("pressed", self, "level_select_menu_button_pressed", ["ruins_level"])

    $Options_Menu/Button_Back.connect("pressed", self, "options_menu_button_pressed", ["back"])
    $Options_Menu/Button_Fullscreen.connect("pressed", self, "options_menu_button_pressed", ["fullscreen"])
    $Options_Menu/Check_Button_VSync.connect("pressed", self, "options_menu_button_pressed", ["vsync"])
    $Options_Menu/Check_Button_Debug.connect("pressed", self, "options_menu_button_pressed", ["debug"])

    Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)

    var globals = get_node("/root/Globals")
    $Options_Menu/HSlider_Mouse_Sensitivity.value = globals.mouse_sensitivity
    $Options_Menu/HSlider_Joypad_Sensitivity.value = globals.joypad_sensitivity


func start_menu_button_pressed(button_name):
    if button_name == "start":
        level_select_menu.visible = true
        start_menu.visible = false
    elif button_name == "open_godot":
        OS.shell_open("https://godotengine.org/")
    elif button_name == "options":
        options_menu.visible = true
        start_menu.visible = false
    elif button_name == "quit":
        get_tree().quit()


func level_select_menu_button_pressed(button_name):
    if button_name == "back":
        start_menu.visible = true
        level_select_menu.visible = false
    elif button_name == "testing_scene":
        set_mouse_and_joypad_sensitivity()
        get_node("/root/Globals").load_new_scene(testing_area_scene)
    elif button_name == "space_level":
        set_mouse_and_joypad_sensitivity()
        get_node("/root/Globals").load_new_scene(space_level_scene)
    elif button_name == "ruins_level":
        set_mouse_and_joypad_sensitivity()
        get_node("/root/Globals").load_new_scene(ruins_level_scene)


func options_menu_button_pressed(button_name):
    if button_name == "back":
        start_menu.visible = true
        options_menu.visible = false
    elif button_name == "fullscreen":
        OS.window_fullscreen = !OS.window_fullscreen
    elif button_name == "vsync":
        OS.vsync_enabled = $Options_Menu/Check_Button_VSync.pressed
    elif button_name == "debug":
        pass


func set_mouse_and_joypad_sensitivity():
    var globals = get_node("/root/Globals")
    globals.mouse_sensitivity = $Options_Menu/HSlider_Mouse_Sensitivity.value
    globals.joypad_sensitivity = $Options_Menu/HSlider_Joypad_Sensitivity.value

La mayor parte del código aquí se relaciona con hacer UIs, lo cual está fuera del propósito de esta serie de tutoriales. Sólo vamos a mirar el código relacionado con la UI brevemente.

Truco

¡Ver Diseñar una pantalla de título y los siguientes tutoriales para mejores formas de hacer GUIs y UIs!

Veamos primero las variables de clase.

  • start_menu: Una variable para mantener el Start_Menu Panel.

  • level_select_menu: Una variable para mantener el Level_Select_Menu Panel.

  • options_menu: Una variable para mantener el Options_Menu Panel.

  • testing_area_scene: La ruta del archivo Testing_Area.tscn, así que podemos cambiarla desde esta escena.

  • space_level_scene: La ruta del archivo Space_Level.tscn, así que podemos cambiarla desde esta escena.

  • ruins_level_scene: La ruta del archivo Ruins_Level.tscn, así que podemos cambiarla desde esta escena.

Advertencia

¡Tendrás que establecer las rutas de los archivos correctos en el editor antes de probar este script! ¡De lo contrario no funcionará!


Ahora vamos a repasar el _ready

Primero, obtenemos todos los nodos Panel y los asignamos a las variables correspondientes.

A continuación, conectamos todas las señales de los botones pressed a sus respectivas funciones [panel_name_here]_button_pressed.

Luego ponemos el modo del ratón a MOUSE_MODE_VISIBLE para asegurarnos de que siempre que el jugador regrese a esta escena, el ratón estará visible.

Luego tenemos un singleton, llamado Globals. A continuación establecemos los valores para los nodos HSlider de forma que sus valores se alineen con la sensibilidad del ratón y el joypad en el singleton.

Nota

Aún no hemos hecho el singleton Globals, así que no te preocupes! ¡Vamos a hacerlo pronto!


En start_menu_button_pressed, comprobamos qué botón está pulsado.

Basándonos en el botón pulsado, cambiamos el panel actualmente visible, salimos de la aplicación, o abrimos la página web de Godot.


En level_select_menu_button_pressed, comprobamos qué botón está pulsado.

Si se ha pulsado el botón back, cambiamos los paneles actualmente visibles para volver al menú principal.

Si se pulsa uno de los botones de cambio de escena, llamaremos primero a set_mouse_and_joypad_sensitivity para que el singleton (Globals.gd) tenga los valores de los nodos HSlider. Luego le decimos al singleton que cambie los nodos usando su función load_new_scene, pasando la ruta del archivo de la escena que el jugador ha seleccionado.

Nota

No te preocupes por el singleton ¡Llegaremos pronto!


En options_menu_button_pressed, comprobamos qué botón está pulsado.

Si se ha pulsado el botón back, cambiamos los paneles actualmente visibles para volver al menú principal.

Si se presiona el botón de fullscreen, cambiamos el modo de pantalla completa de OS ajustándolo a la versión volteada de su valor actual.

Si se pulsa el botón vsync, establecemos el OS's Vsync basado en el estado del botón de comprobación de Vsync.


Por último, echemos un vistazo a set_mouse_and_joypad_sensitivity.

Primero obtenemos el singleton Globals y lo asignamos a una variable local.

Luego ponemos las variables mouse_sensitivity y joypad_sensitivity a los valores de sus respectivos nodos HSlider.

Haciendo el singleton Globals

Ahora, para que todo esto funcione necesitamos crear el singleton Globals. Crea un nuevo script en la pestaña Script y llámalo Globals.gd.

Nota

Para hacer el singleton de Globals, ve a la pestaña Script en el editor, luego haz clic en New y aparecerá una caja de Create Script, deja todo sin cambios excepto la Path donde necesitas insertar el nombre del script Globals.gd.

Añade lo siguiente a Globals.gd.

extends Node

var mouse_sensitivity = 0.08
var joypad_sensitivity = 2

func _ready():
    pass

func load_new_scene(new_scene_path):
    get_tree().change_scene(new_scene_path)

Como puedes ver, es bastante pequeño y simple. A medida que esta parte avance, seguiremos añadiendo una lógica más compleja a Globals.gd, pero por ahora, todo lo que está haciendo es mantener dos variables de clase, y definir de forma abstracta cómo cambiamos de escena.

  • mouse_sensitivity: La sensibilidad actual de nuestro ratón, para que podamos cargarla en Player.gd.

  • joypad_sensitivity: La sensibilidad actual de nuestro joypad, para que podamos cargarlo en``Player.gd``.

Ahora mismo, todo lo que usaremos Globals.gd es una forma de llevar las variables a través de las escenas. Debido a que las sensibilidades de nuestro ratón y el joypad se almacenan en Globals.gd, cualquier cambio que hagamos en una escena (como en Options_Menu) afectará a la sensibilidad del jugador.

Todo lo que hacemos en load_new_scene es llamar SceneTree a la función change_scene, pasando por la ruta de la escena dada en load_new_scene.

¡Ese es todo el código necesario para Globals.gd ahora mismo! Antes de que podamos probar el menú principal, primero tenemos que configurar Globals.gd como un script de carga automática.

Abre la pestaña Project Settings y haz clic en la pestaña AutoLoad .

../../../_images/AutoloadAddSingleton.png

Luego selecciona el camino a Globals.gd en el campo Path haciendo clic en el botón (..) al lado. ¡Asegúrate de que el nombre en el campo Node Name es Globals. Si tienes todo como en la imagen de arriba, entonces presiona Add!

Esto hará de Globals.gd un script singleton/autoload, que nos permitirá acceder a él desde cualquier script, en cualquier escena.

Truco

Para más información sobre scripts singleton/autoload, ve Singletons (AutoLoad).

Ahora que Globals.gd es un script singleton/autoload ¡puedes probar el menú principal!

Puede que quieras cambiar la escena principal de Testing_Area.tscn a Menú_Mayor.tscn para que cuando exportemos el juego el jugador comience en el menú principal. Puedes hacerlo a través de la pestaña Project Settings, bajo la pestaña General. Luego, en la categoría Aplication, haz clic en la subcategoría Run y puedes cambiar la escena principal cambiando el valor en Main Scene.

Advertencia

¡Tendrás que establecer las rutas de los archivos correctos en Main_Menu en el editor antes de probar el menú principal! De lo contrario, no podrás cambiar de escena desde el menú/pantalla de selección de nivel.

Añadiendo el menú de depuración

Ahora, vamos a añadir una simple escena de depuración para que podamos rastrear cosas como FPS (Fotogramas Por Segundo) en el juego. Abre Debug_Display.tscn.

Puedes ver que es un Panel posicionado en la esquina superior derecha de la pantalla. Tiene tres Labels, una para mostrar el FPS en el que se está ejecutando el juego, otra para mostrar en qué sistema operativo se está ejecutando el juego, y una etiqueta para mostrar con qué versión de Godot se está ejecutando el juego.

Añadamos el código necesario para llenar estos Labels. Selecciona Debug_Display y crea un nuevo script llamado Debug_Display.gd. Añade lo siguiente:

extends Control

func _ready():
    $OS_Label.text = "OS: " + OS.get_name()
    $Engine_Label.text = "Godot version: " + Engine.get_version_info()["string"]

func _process(delta):
    $FPS_Label.text = "FPS: " + str(Engine.get_frames_per_second())

Repasemos lo que hace este script.


En _ready, ponemos el texto de la OS_Label al nombre proporcionado por OS usando la función get_name. Esto devolverá el nombre del SO (o Sistema Operativo) para el cual Godot fue compilado. Por ejemplo, cuando se ejecuta Windows, devolverá Windows, mientras que cuando se ejecuta Linux, devolverá X11.

Entonces, ponemos el texto de la Engine_Label en la información de la versión proporcionada por Engine.get_version_info. Engine.get_version_info devuelve un diccionario lleno de información útil sobre la versión de Godot que se está ejecutando actualmente. Sólo nos importa la versión de la string, al menos para esta etiqueta, por lo que obtenemos la string y la asignamos como el text en Engine_Label. Ver Engine para más información sobre los valores que devuelve get_version_info.

En _process, ponemos el texto de la FPS_Label a Engine.get_frames_per_second, pero como get_frames_per_second devuelve un entero, tenemos que convertirlo a una string usando str antes de que podamos añadirlo al Label.


Ahora volvamos al Main_Menu.gd y cambiemos lo siguiente en options_menu_button_pressed:

elif button_name == "debug":
    pass

a esto en cambio:

elif button_name == "debug":
    get_node("/root/Globals").set_debug_display($Options_Menu/Check_Button_Debug.pressed)

Esto llamará a una nueva función en nuestra llamada singleton set_debug_display ¡así que agreguemos eso a continuación!


Abre Globals.gd y añade las siguientes variables de clase:

# ------------------------------------
# All the GUI/UI-related variables

var canvas_layer = null

const DEBUG_DISPLAY_SCENE = preload("res://Debug_Display.tscn")
var debug_display = null

# ------------------------------------
  • canvas_layer: Una capa de lienzo para que el GUI/UI creado en Globals.gd esté siempre dibujado encima.

  • DEBUG_DISPLAY: La escena de visualización de depuración en la que trabajamos antes.

  • debug_display: Una variable para mantener la pantalla de depuración cuando/si la hay.

Ahora que tenemos las variables de clase definidas, necesitamos añadir unas pocas líneas a _ready para que Globals.gd tenga una capa de lienzo para usar (que guardaremos en canvas_layer). Cambia _ready por lo siguiente:

func _ready():
    canvas_layer = CanvasLayer.new()
    add_child(canvas_layer)

Ahora en _ready, creamos una nueva capa de lienzo, la asignamos a canvas_layer y la agregamos como un niño. Debido a que Globals.gd es un autoload/singleton, Godot hará un Node cuando el juego sea lanzado, y tendrá Globals.gd adjunto a él. Ya que Godot hace un Node, podemos tratar Globals.gd como cualquier otro nodo con respecto a agregar/quitar nodos hijos.

La razón por la que añadimos un CanvasLayer es para que todos nuestros nodos GUI y UI que instancia/aparecen en Globals.gd estén siempre dibujados encima de todo lo demás.

Al añadir nodos a un singleton/autoload, hay que tener cuidado de no perder la referencia a ninguno de los nodos hijos. Esto se debe a que los nodos no serán liberados/destruidos cuando se cambia de escena, lo que significa que se pueden producir problemas de memoria si se están instanciando/generando muchos nodos y no se están liberando.


Ahora necesitamos añadir set_debug_display a Globals.gd:

func set_debug_display(display_on):
    if display_on == false:
        if debug_display != null:
            debug_display.queue_free()
            debug_display = null
    else:
        if debug_display == null:
            debug_display = DEBUG_DISPLAY_SCENE.instance()
            canvas_layer.add_child(debug_display)

Repasemos lo que está pasando.

Primero comprobamos si Globals.gd está intentando encender la pantalla de depuración, o apagarla.

Si Globals.gd está apagando la pantalla, entonces comprobamos si debug_display no es igual a null. Si debug_display no es igual a null, entonces Globals.gd debe tener una pantalla de depuración actualmente activa. Si Globals.gd tiene una pantalla de depuración activa, la liberamos usando queue_free y luego asignamos debug_display a null.

Si Globals.gd está encendiendo la pantalla, entonces comprobamos que Globals.gd no tenga ya una pantalla de depuración activa. Lo hacemos asegurándonos de que debug_display es igual a null. Si debug_display es null, ponemos una nueva DEBUG_DISPLAY_SCENE, y la añadimos como un hijo de canvas_layer.


Una vez hecho esto, ahora podemos activar y desactivar la pantalla de depuración cambiando el CheckButton en el panel Options_Menu. ¡Inténtalo!

Observa cómo la pantalla de depuración permanece incluso cuando cambias escenas del Menu_Main.tscn a otra escena (como Testing_Area.tscn). Esta es la belleza de instanciar/generar nodos en un singleton/autoload y añadirlos como hijos al singleton/autoload. Cualquiera de los nodos añadidos como hijos del singleton/autoload permanecerán mientras el juego esté en marcha ¡sin ningún trabajo adicional por nuestra parte!

Añadir un menú de pausa

Añadamos un menú de pausa para que podamos volver al menú principal cuando presionemos la acción ui_cancel.

Abre Pause_Popup.tscn.

Fíjate en cómo el nodo raíz en Pause_Popup es un WindowDialog; WindowDialog hereda de Popup, lo que significa que WindowDialog puede actuar como un popup.

Selecciona Pause_Popup y desplázate hacia abajo hasta llegar al menú Pause del inspector. Observe cómo el modo de pausa está configurado como process en lugar de inherit como se establece normalmente por defecto. Esto hace que continúe procesándose incluso cuando el juego está en pausa, lo cual necesitamos para poder interactuar con los elementos de la UI.

Ahora que hemos visto como Pause_Popup.tscn está configurado, escribamos el código para que funcione. Normalmente, adjuntaríamos un script al nodo raíz de la escena, Pause_Popup en este caso, pero como necesitaremos recibir un par de señales en Globals.gd, escribiremos todo el código para el popup allí.

Abre Globals.gd y añade las siguientes variables de clase:

const MAIN_MENU_PATH = "res://Main_Menu.tscn"
const POPUP_SCENE = preload("res://Pause_Popup.tscn")
var popup = null
  • MAIN_MENU_PATH: La ruta a la escena del menú principal.

  • POPUP_SCENE: La escena pop up que vimos antes.

  • popup: Una variable para mantener la escena pop up.

Ahora tenemos que añadir _process a Globals.gd para que pueda responder cuando la acción ui_cancel sea presionada. Añade lo siguiente a _process:

func _process(delta):
    if Input.is_action_just_pressed("ui_cancel"):
        if popup == null:
            popup = POPUP_SCENE.instance()

            popup.get_node("Button_quit").connect("pressed", self, "popup_quit")
            popup.connect("popup_hide", self, "popup_closed")
            popup.get_node("Button_resume").connect("pressed", self, "popup_closed")

            canvas_layer.add_child(popup)
            popup.popup_centered()

            Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)

            get_tree().paused = true

Repasemos lo que está pasando aquí.


En primer lugar, comprobamos si la acción ui_cancel está presionada. Luego, comprobamos que Globals.gd no tenga ya un popup abierto comprobando si el popup es igual a null.

Si Globals.gd no tiene una ventana emergente abierta, escribimos POPUP_SCENE y la asignamos a popup.

Entonces obtenemos el botón de salir y asignamos su señal de pressed a popup_quit, que añadiremos en breve.

A continuación, asignamos tanto la señal popup_hide de la WindowDialog como la señal pressed del botón de reanudación a popup_closed, que añadiremos en breve.

Luego, agregamos popup como un hijo de canvas_layer para que se dibuje en la parte superior. Luego le decimos al popup que aparezca en el centro de la pantalla usando popup_centered.

A continuación, nos aseguramos de que el modo del ratón es MOUSE_MODE_VISIBLE para que el jugador pueda interactuar con el pop-up. Si no lo hiciéramos, el jugador no podría interactuar con el pop-up en ninguna escena en la que el modo del ratón sea MOUSE_MODE_CAPTURED.

Finalmente, hacemos una pausa en todo el SceneTree.

Nota

Para más información sobre la pausa en Godot, ver Pausing games


Ahora, necesitamos añadir las funciones a las que hemos conectado las señales. Primero agreguemos popup_closed.

Añade lo siguiente a Globals.gd:

func popup_closed():
    get_tree().paused = false

    if popup != null:
        popup.queue_free()
        popup = null

popup_cerrado reanudará el juego y destruirá el popup si lo hay.

popup_quit es similar, pero también nos aseguramos de que el ratón esté visible y cambiamos las escenas a la pantalla de título.

Añade lo siguiente a Globals.gd:

func popup_quit():
    get_tree().paused = false

    Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)

    if popup != null:
        popup.queue_free()
        popup = null

    load_new_scene(MAIN_MENU_PATH)

popup_quit reanudará el juego, establece el modo del ratón en MOUSE_MODE_VISIBLE para asegurar que el ratón esté visible en el menú principal, destruye el pop-up si lo hay, y cambia las escenas al menú principal.


Antes de que estemos listos para probar el pop-up, deberíamos cambiar una cosa en Player.gd.

Abre Player.gd y en process_input, cambia el código para capturar/liberar el cursor a lo siguiente:

En lugar de:

# Capturing/Freeing cursor
if Input.is_action_just_pressed("ui_cancel"):
    if Input.get_mouse_mode() == Input.MOUSE_MODE_VISIBLE:
        Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
    else:
        Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)

Sólo te vas a ir:

# Capturing/Freeing cursor
if Input.get_mouse_mode() == Input.MOUSE_MODE_VISIBLE:
    Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

Ahora, en lugar de capturar/liberar el ratón, comprobamos si el modo actual del ratón es MOUSE_MODE_VISIBLE. Si lo es, lo ponemos de nuevo en MOUSE_MODE_CAPTURED.

Debido a que el pop-up hace que el modo del ratón MOUSE_MODE_VISIBLE cada vez que se hace una pausa, ya no tenemos que preocuparnos de liberar y capturar el cursor en Player.gd.


Ahora que el pop-up del menú de la pausa ha terminado. ¡Ahora puedes hacer una pausa en cualquier punto del juego y volver al menú principal!

Iniciando el sistema de regeneración

Ya que el jugador puede perder toda su salud, sería ideal que el jugador muriera y volviera a nacer también, ¡así que añadamos eso a continuación!

En primer lugar, abre Player.tscn y expande HUD. Fíjate en cómo hay una ColorRect llamada Death_Screen. Cuando el jugador muera, vamos a hacer visible la Death_Screen, y mostrarles cuánto tiempo tienen que esperar antes de que el jugador sea capaz de volver a aparecer.

Abre Player.gd y añade las siguientes variables de clase:

const RESPAWN_TIME = 4
var dead_time = 0
var is_dead = false

var globals
  • RESPAWN_TIME: La cantidad de tiempo (en segundos) que se tarda en reaparecer.

  • dead_time: Una variable para saber cuánto tiempo lleva muerto el jugador.

  • is_dead: Una variable para saber si el jugador está muerto o no.

  • globals: Una variable para contener el singleton Globals.gd.


Ahora necesitamos añadir un par de líneas a _ready, para poder usar Globals.gd en Player.gd. Añade lo siguiente a _ready:

globals = get_node("/root/Globals")
global_transform.origin = globals.get_respawn_position()

Ahora obtenemos el singleton Globals.gd y lo asignamos a globals. También establecemos nuestra posición global usando el origen de nuestro Transform a la posición devuelta por globals.get_respawn_position.

Nota

No te preocupes, añadiremos get_respawn_position más abajo!


A continuación, tenemos que hacer algunos cambios en el _physics_process. Cambiar _physics_process a lo siguiente:

func _physics_process(delta):

    if !is_dead:
        process_input(delta)
        process_view_input(delta)
        process_movement(delta)

    if (grabbed_object == null):
        process_changing_weapons(delta)
        process_reloading(delta)

    process_UI(delta)
    process_respawn(delta)

Ahora el jugador no procesará la entrada o el movimiento de entrada cuando el jugador esté muerto. También estamos llamando ahora a process_respawn.

Nota

La expresión if !is_dead: es equivalente y funciona de la misma manera que la expresión if is_dead == false:. Y eliminando el signo ! de la expresión obtenemos la expresión opuesta if is_dead == true:. Es una forma más corta de escribir la misma funcionalidad del código.

No hemos hecho process_respawn todavía, así que cambiemos eso.


Añadamos process_respawn. Añade lo siguiente a Player.gd:

func process_respawn(delta):

    # If we've just died
    if health <= 0 and !is_dead:
        $Body_CollisionShape.disabled = true
        $Feet_CollisionShape.disabled = true

        changing_weapon = true
        changing_weapon_name = "UNARMED"

        $HUD/Death_Screen.visible = true

        $HUD/Panel.visible = false
        $HUD/Crosshair.visible = false

        dead_time = RESPAWN_TIME
        is_dead = true

        if grabbed_object != null:
            grabbed_object.mode = RigidBody.MODE_RIGID
            grabbed_object.apply_impulse(Vector3(0, 0, 0), -camera.global_transform.basis.z.normalized() * OBJECT_THROW_FORCE / 2)

            grabbed_object.collision_layer = 1
            grabbed_object.collision_mask = 1

            grabbed_object = null

    if is_dead:
        dead_time -= delta

        var dead_time_pretty = str(dead_time).left(3)
        $HUD/Death_Screen/Label.text = "You died\n" + dead_time_pretty + " seconds till respawn"

        if dead_time <= 0:
            global_transform.origin = globals.get_respawn_position()

            $Body_CollisionShape.disabled = false
            $Feet_CollisionShape.disabled = false

            $HUD/Death_Screen.visible = false

            $HUD/Panel.visible = true
            $HUD/Crosshair.visible = true

            for weapon in weapons:
                var weapon_node = weapons[weapon]
                if weapon_node != null:
                    weapon_node.reset_weapon()

            health = 100
            grenade_amounts = {"Grenade":2, "Sticky Grenade":2}
            current_grenade = "Grenade"

            is_dead = false

Repasemos lo que está haciendo esta función.


En primer lugar, comprobamos si el jugador acaba de morir comprobando si health es menor o igual a 0 y is_dead es falso.

Si el jugador acaba de morir, deshabilitamos las formas de colisión para el jugador. Hacemos esto para asegurarnos de que el jugador no bloquee nada con su cadáver.

Luego, ponemos changing_weapon a true y ponemos changing_weapon_name en UNARMED. Esto es así, si el jugador está usando un arma, se guarda cuando muere.

Luego hacemos la Death_Screen ColorRect visible para que el jugador obtenga una bonita capa gris sobre todo cuando haya muerto. Luego hacemos el resto de la interfaz de usuario, los nodos Panel y "Crosshair", invisibles.

A continuación, ponemos dead_time en RESPAWN_TIME para que podamos empezar la cuenta atrás del tiempo que el jugador ha estado muerto. También ponemos is_dead a true para saber que el jugador ha muerto.

Si el jugador tiene un objeto en la mano cuando murió, tenemos que tirarlo. Primero comprobamos si el jugador está sosteniendo un objeto o no. Si el jugador está sosteniendo un objeto, lo lanzamos usando el mismo código de lanzamiento que añadimos en Parte 5.

Nota

La combinación \n de la expresión You have died\n es un comando usado para mostrar el texto que sigue a continuación en una nueva línea. Esto es siempre útil cuando quieres agrupar el texto mostrado en varias líneas para que se vea mejor y sea más legible para los jugadores de tus juegos.


Entonces comprobamos si el jugador está muerto. Si es así, entonces quitamos delta del dead_time.

Luego hacemos una nueva variable llamada dead_time_pretty, donde convertimos dead_time en una string, usando sólo los tres primeros caracteres que empiezan por la izquierda. Esto le da al jugador una string de aspecto agradable que muestra cuánto tiempo le queda de espera antes de que el jugador pueda volver a aparecer.

Luego cambiamos el Label en Death_Screen para mostrar cuánto tiempo le queda al jugador.

A continuación comprobamos si el jugador ha esperado lo suficiente y puede volver a aparecer. Lo hacemos comprobando si el dead_time es 0 o menos.

Si el jugador ha esperado lo suficiente para reaparecer, ponemos la posición del jugador en una nueva posición de reaparición proporcionada por get_respawn_position.

Luego habilitamos las dos formas de colisión del jugador para que pueda colisionar de nuevo con el entorno.

Luego, hacemos invisible la Death_Screen y hacemos el resto de la interfaz de usuario, el Panel y los nodos de Crosshair, visibles de nuevo.

Luego revisamos cada arma y llamamos a su función de reset_weapon, que añadiremos pronto.

Luego, reajustamos health a 100, grenade_amounts a sus valores por defecto, y cambiamos current_grenade a Grenade. Esto efectivamente restablece estas variables a sus valores por defecto.

Por último, ponemos is_dead a false.


Antes de dejar Player.gd, necesitamos añadir una cosa rápida a _input. Añade lo siguiente al principio de _input:

if is_dead:
    return

Ahora, cuando el jugador está muerto, no pueden mirar a su alrededor con el ratón.

Terminando el sistema de resucitación

Primero, abramos Weapon_Pistol.gd y agreguemos la función reset_weapon. Añade lo siguiente:

func reset_weapon():
    ammo_in_weapon = 10
    spare_ammo = 20

Ahora, cuando llamemos a reset_weapon, la munición de la pistola y la munición sobrante se restablecerán a sus valores por defecto.

Ahora añadamos reset_weapon en Weapon_Rifle.gd:

func reset_weapon():
    ammo_in_weapon = 50
    spare_ammo = 100

Y añadimos lo siguiente a Weapon_Knife.gd:

func reset_weapon():
    ammo_in_weapon = 1
    spare_ammo = 1

Ahora todas las armas se resetearán cuando el jugador muera.


Ahora tenemos que agregar unas cosas más a Globals.gd. Primero agrega la siguiente variable de clase:

var respawn_points = null
  • respawn_points: Una variable para mantener todos los puntos de respawn en el nivel

Como estamos obteniendo un punto de regeneración al azar cada vez, necesitamos aleatorizar el generador de números. Añade lo siguiente a _ready:

randomize()

randomize nos dará una nueva semilla aleatoria para que obtengamos una string de números (relativamente) aleatoria cuando usemos cualquiera de las funciones aleatorias.

Ahora agreguemos get_respawn_position a Globals.gd:

func get_respawn_position():
    if respawn_points == null:
        return Vector3(0, 0, 0)
    else:
        var respawn_point = rand_range(0, respawn_points.size() - 1)
        return respawn_points[respawn_point].global_transform.origin

Repasemos lo que hace esta función.


Primero, comprobamos si Globals.gd tiene algún respawn_points comprobando si respawn_points es "null" o no.

Si respawn_points es null, devolvemos una posición vacía, Vector 3 con la posición (0, 0, 0).

Si respawn_points no es null, entonces obtenemos un número aleatorio entre 0 y el número de elementos que tenemos en respawn_points, menos 1 ya que la mayoría de los lenguajes de programación, incluyendo GDScript, comienzan a contar desde 0 cuando accedes a los elementos de una lista.

Entonces devolvemos la posición del nodo Spatial a la posición de respawn_point en respawn_points.


Antes de que terminemos con Globals.gd, necesitamos añadir lo siguiente a load_new_scene:

respawn_points = null

Ponemos respawn_points en null para que cuando si el jugador llegue a un nivel sin puntos de regeneración, no reaparezca en los puntos de regeneración que estaban en el nivel anterior.


Ahora todo lo que necesitamos es una forma de establecer los puntos de regeneración. Abre Ruins_Level.tscn y selecciona Spawn_Points. Añade un nuevo script llamado Respawn_Point_Setter.gd y adjúntalo a Spawn_Points. Añade lo siguiente a Respawn_Point_Setter.gd:

extends Spatial

func _ready():
    var globals = get_node("/root/Globals")
    globals.respawn_points = get_children()

Ahora, cuando un nodo con Respawn_Point_Setter.gd tiene su función _ready llamada, todos los nodos hijos del nodo con Respawn_Point_Setter.gd, Spawn_Points en el caso de Ruins_Level.tscn, serán añadidos a respawn_points en Globals.gd.

Advertencia

Cualquier nodo con Respawn_Point_Setter.gd tiene que estar por encima del jugador en el SceneTree así que los puntos de regeneración se establecen antes de que el jugador los necesite en la función _ready del jugador.


¡Ahora, cuando el jugador muera, se resucitará después de esperar 4 segundos!

Nota

¡No hay puntos de generación en ninguno de los niveles, aparte de "Ruins_Level.tscn"! Agregar puntos de generación a "Space_Level.tscn" es un ejercicio para el lector.

Escribir un sistema de sonido que podamos usar en cualquier lugar

Por último, hagamos un sistema de sonido para que podamos reproducir sonidos de cualquier lugar, sin tener que usar el reproductor.

Primero, abre SimpleAudioPlayer.gd y cambia lo siguiente:

extends Spatial

var audio_node = null
var should_loop = false
var globals = null

func _ready():
    audio_node = $Audio_Stream_Player
    audio_node.connect("finished", self, "sound_finished")
    audio_node.stop()

    globals = get_node("/root/Globals")


func play_sound(audio_stream, position=null):
    if audio_stream == null:
        print ("No audio stream passed; cannot play sound")
        globals.created_audio.remove(globals.created_audio.find(self))
        queue_free()
        return

    audio_node.stream = audio_stream

    # If you are using an AudioStreamPlayer3D, then uncomment these lines to set the position.
    #if audio_node is AudioStreamPlayer3D:
    #    if position != null:
    #        audio_node.global_transform.origin = position

    audio_node.play(0.0)


func sound_finished():
    if should_loop:
        audio_node.play(0.0)
    else:
        globals.created_audio.remove(globals.created_audio.find(self))
        audio_node.stop()
        queue_free()

Hay varios cambios con respecto a la versión antigua, en primer lugar ya no estamos almacenando los archivos de sonido en SimpleAudioPlayer.gd. Esto es mucho mejor para el rendimiento ya que ya no estamos cargando cada clip de audio cuando creamos un sonido, sino que estamos forzando a que se pase un flujo de audio a play_sound.

Otro cambio es que tenemos una nueva variable de clase llamada should_loop. En lugar de destruir el reproductor de audio cada vez que se termina, queremos comprobar y ver si el reproductor de audio está configurado para hacer un bucle o no. Esto nos permite tener audio como música de fondo en bucle sin tener que generar un nuevo reproductor de audio con la música cuando el antiguo esté terminado.

Finalmente, en lugar de ser instanciado/generado en Player.gd, se va a generar en Globals.gd para que podamos crear sonidos desde cualquier escena. Ahora necesitamos almacenar el singleton Globals.gd para que cuando destruyamos el reproductor de audio, también lo eliminemos de una lista en Globals.gd.

Repasemos los cambios.


Para las variables de clase, hemos eliminado todas las variables audio_[insertar nombre aqui] ya que en su lugar las tendremos pasadas de Globals.gd.

También agregamos dos nuevas variables de clase, should_loop y globals. Usaremos should_loop para decir si el reproductor de audio debe hacer un bucle cuando el sonido haya terminado, y globals contendrá el singleton Globals.gd.

El único cambio en _ready es que ahora el reproductor de audio obtiene el singleton Globals.gd y lo asigna a globals.

play_sound espera ahora que se pase un stream de audio, llamado audio_stream, en lugar de sound_name. En lugar de comprobar el nombre del sonido y configurar el stream para el reproductor de audio, comprobamos que se ha introducido un stream de audio. Si no se ha pasado un stream de audio, se imprime un mensaje de error, se elimina el reproductor de audio de la lista en el singleton Globals.gd llamado created_audio, y luego se libera el reproductor de audio.

Finalmente, en sound_finished primero comprobamos si se debe usar should_loop o no. Si se supone que debemos hacer un bucle, reproducimos el sonido de nuevo desde el principio del audio, en la posición 0.0. Si no se supone que debemos hacer un bucle, quitamos el reproductor de audio de la lista en el singleton Globals.gd llamado created_audio, y luego liberamos el reproductor de audio.


Ahora que hemos terminado los cambios en SimpleAudioPlayer.gd, necesitamos prestar atención a Globals.gd. Primero, agrega las siguientes variables de clase:

# All the audio files.

# You will need to provide your own sound files.
var audio_clips = {
    "Pistol_shot": null, #preload("res://path_to_your_audio_here!")
    "Rifle_shot": null, #preload("res://path_to_your_audio_here!")
    "Gun_cock": null, #preload("res://path_to_your_audio_here!")
}

const SIMPLE_AUDIO_PLAYER_SCENE = preload("res://Simple_Audio_Player.tscn")
var created_audio = []

Veamos esas variables globales.

  • audio_clips: Un diccionario que contiene todos los clips de audio que Globals.gd puede reproducir.

  • SIMPLE_AUDIO_PLAYER_SCENE: La simple escena del reproductor de audio.

  • created_audio: Una lista que tiene referencias a todos los audio player que Global.gd ha creado.

Nota

Si quieres añadir audio adicional, necesitas añadirlo a audio_clips. No se proporcionan archivos de audio en este tutorial, así que tendrás que proporcionar los tuyos propios.

Un sitio que recomendaría es GameSounds.xyz. Estoy usando el paquete de sonido de la pistola de audio Gamemaster incluido Sonniss' GDC Game Audio bundle for 2017. Las pistas que he usado (con algo de edición menor) son las siguientes:

  • gun_revolver_pistol_shot_04,

  • gun_semi_auto_rifle_cock_02,

  • gun_submachine_auto_shot_00_automatic_preview_01


Ahora necesitamos añadir una nueva función llamada play_sound a Globals.gd:

func play_sound(sound_name, loop_sound=false, sound_position=null):
    if audio_clips.has(sound_name):
        var new_audio = SIMPLE_AUDIO_PLAYER_SCENE.instance()
        new_audio.should_loop = loop_sound

        add_child(new_audio)
        created_audio.append(new_audio)

        new_audio.play_sound(audio_clips[sound_name], sound_position)

    else:
        print ("ERROR: cannot play sound that does not exist in audio_clips!")

Repasemos lo que hace esta función.

Primero, comprobamos si Globals.gd tiene un clip de audio con el nombre sound_name en audio_clips. Si no lo tiene, imprimimos un mensaje de error.

Si Globals.gd tiene un clip de audio con el nombre de sound_name, entonces creamos una nueva SIMPLE_AUDIO_PLAYER_SCENE y la asignamos a new_audio.

Luego ponemos should_loop, y agregamos new_audio como un hijo de Globals.gd.

Nota

Recuerda, debemos tener cuidado al añadir nodos a un singleton, ya que estos nodos no se destruirán al cambiar de escena.

Añadimos el new_audio a la lista de created_audio para mantener todos los audios creados.

Luego llamamos a play_sound, pasando el clip de audio asociado a sound_name y la posición del sonido.


Antes de salir de Globals.gd, necesitamos agregar algunas líneas de código a load_new_scene para que cuando el jugador cambie de escena, todo el audio sea destruido.

Añade lo siguiente a load_new_scene:

for sound in created_audio:
    if (sound != null):
        sound.queue_free()
created_audio.clear()

Ahora, antes de que Globals.gd cambie de escena, pasa por cada simple reproductor de audio en created_sounds y los libera/destruye. Una vez que Globals.gd ha pasado por todos los sonidos en created_audio, limpiamos created_audio para que ya no contenga ninguna referencia a ningún (ahora liberado/destruido) reproductor de audio simple.


Cambiemos create_sound en Player.gd para usar este nuevo sistema. Primero, elimina simple_audio_player de las variables de clase de Player.gd, ya que no estaremos directamente instanciando/solicitando sonidos en Player.gd.

Ahora, cambia create_sound por lo siguiente:

func create_sound(sound_name, position=null):
    globals.play_sound(sound_name, false, position)

Ahora, cada vez que llamamos a create_sound, simplemente llamamos a play_sound en Globals.gd, pasando todos los argumentos recibidos.


Ahora todos los sonidos de nuestro FPS se pueden reproducir desde cualquier lugar. Todo lo que tenemos que hacer es tomar el singleton Globals.gd, y llamar play_sound, pasando el nombre del sonido que queremos reproducir, si queremos que se reproduzca en bucle o no, y la posición para reproducir el sonido.

Por ejemplo, si quieres reproducir un sonido de explosión cuando las granadas explotan necesitarás añadir un nuevo sonido a audio_clips en Globals.gd, ve al singleton Globals.gd, y luego sólo necesitas añadir algo como globals.play_sound("explosion", false, global_transform.origin) en la función _process de la granada, justo después de que la granada dañe todos los cuerpos dentro de su radio de explosión.

Notas finales

../../../_images/FinishedTutorialPicture.png

¡Ahora tienes un FPS de un solo jugador que funciona completamente!

En este punto, tienes una buena base para construir juegos FPS más complicados.

Advertencia

¡Si alguna vez te pierdes, asegúrate de leer el código de nuevo!

Puedes descargar el proyecto terminado para el tutorial completo aquí: Godot_FPS_Part_6.zip

Nota

Los archivos fuente del proyecto terminado contienen el mismo código, sólo que escrito en un orden diferente. Esto se debe a que los archivos fuente del proyecto terminado son en loÑ que se basa el tutorial.

El código del proyecto terminado se escribió en el orden en que se crearon las características, no necesariamente en un orden ideal para el aprendizaje.

Aparte de eso, la fuente es exactamente la misma, sólo que con comentarios útiles que explican lo que hace cada parte.

Truco

La fuente del proyecto terminado está alojada en GitHub también: https://github.com/TwistedTwigleg/Godot_FPS_Tutorial

Por favor, tenga en cuenta que el código de GitHub puede o no estar sincronizado con el tutorial de la documentación.

Es probable que el código de la documentación esté mejor gestionado y/o más actualizado. Si no está seguro de cuál utilizar, utilice el proyecto o proyectos proporcionados en la documentación, ya que son mantenidos por la comunidad Godot.

Puedes encontrar los archivos .blend usados en este tutorial aquí: Godot_FPS_Starter.zip

Todos los activos proporcionados en los activos iniciados (a menos que se indique lo contrario) fueron originalmente creados por TwistedTwigleg, con cambios/adiciones por la comunidad Godot. Todos los activos originales proporcionados para este tutorial son liberados bajo la licencia MIT.

¡Siéntete libre de usar estos recursos como quieras! Todos los recursos originales pertenecen a la comunidad Godot, con los otros recursos pertenecientes a los enumerados a continuación:

El skybox es creado por StumpyStrust y se puede encontrar en OpenGameArt.org. https://opengameart.org/content/space-skyboxes-0 . El skybox está licenciado bajo la licencia CC0.

La fuente utilizada es Titillium-Regular, y está licenciada bajo la SIL Open Font License, Version 1.1.

La caja celeste fue convertida en una imagen equirectangular de 360º usando esta herramienta: https://www.360toolkit.co/convert-cubemap-to-spherical-equirectangular.html

Aunque no se proporcionan sonidos, puede encontrar muchos sonidos listos para el juego en https://gamesounds.xyz/

Advertencia

OpenGameArt.org, 360toolkit.co, los creadores de Titillium-Regular, StumpyStrust, y GameSounds.xyz no están de ninguna manera involucrados en este tutorial.