Controla la UI del juego con código

Introducción

En este tutorial conectarás un personaje a una barra de vida y animarás la pérdida de salud.

../../_images/lifebar_tutorial_final_result.gif

Esto es lo que vas a crear: la barra y el contador se animan cuando el personaje recibe un golpe. Se desvanecen cuando muere.

Aprenderás:

  • Cómo conectar un personaje a una GUI mediante señales
  • Cómo controlar una GUI mediante GDscript
  • Cómo animar una barra de vida con el nodo Tween

Si deseas aprender a configurar la interfaz en su lugar, echa un vistazo a los tutoriales de UI paso a paso:

  • Crear una pantalla de menú principal
  • Crear una interfaz de usuario para el juego

Cuando codificas un juego, primero quieres construir el núcleo del juego: la mecánica principal, el comportamiento del jugador, las condiciones de ganancia y pérdida. La UI viene un poco más tarde. Si es posible, debes mantener separados todos los elementos que componen tu proyecto. Cada personaje debe estar en su propia escena, con sus propios scripts, y también los elementos de la interfaz de usuario. Esto previene errores, mantiene su proyecto manejable y permite que diferentes miembros del equipo trabajen en diferentes partes del juego.

Una vez que el núcleo del juego y la UI estén listos, tendrás que conectarlos de alguna manera. En nuestro ejemplo, tenemos al Enemy (Enemigo) que ataca al Player (Jugador) a intervalos de tiempo constantes. Queremos que la barra de vida se actualice cuando el Player sufra daños.

Para hacer esto, usaremos señales.

Nota

Las señales son la versión de Godot del patrón Observador. Estas nos permiten enviar algún mensaje. Otros nodos pueden conectarse al objeto que emite la señal y recibir la información. Es una herramienta poderosa que usamos mucho para la interfaz de usuario y los sistemas de logros. Sin embargo, no es conveniente usarlos en todas partes. La conexión de dos nodos añade algo de acoplamiento entre ellos. Cuando hay muchas conexiones, se vuelven difíciles de manejar. Para más información revisa el tutorial en vídeo Signals de GDquest (en inglés).

Descarga y explora el proyecto de inicio

Descarga el proyecto Godot: ui_code_life_bar.zip. Contiene todos los recursos y scripts que necesitas para empezar. Extrae el archivo .zip para obtener las dos carpetas:start and end.

Carga el proyecto start en Godot. En el panel Sistema de Archivos haz doble clic en LevelMockup.tscn para abrirlo. Es una maqueta de un juego de RPG donde dos personajes se enfrentan. El enemigo rosa ataca y daña al cuadrado verde a intervalos regulares, hasta su muerte. Tómate la libertad de probar el juego: la mecánica básica de combate ya funciona. Pero como el personaje no está conectado a la barra de vida, el GUI no hace nada.

Nota

Esto es típico de como codificarías un juego: primero implementas el núcleo de la mecánica de juego, te encargas de la muerte del personaje, y sólo entonces añades la interfaz. Eso es porque la interfaz de usuario escucha lo que pasa en el juego, así que no puede funcionar si los demás sistemas todavía no están implementados. Si diseñas la interfaz de usuario antes de construir y probar la mecánica de juego, es probable que no funcionará bien y tendrás que reconstruirlo desde cero.

La escena contiene un sprite del fondo, una interfaz gráfica de usuario, y dos personajes.

../../_images/lifebar_tutorial_life_bar_step_tut_LevelMockup_scene_tree.png

El árbol de escena, con la escena GUI puesto para mostrar sus hijos

La escena GUI encapsula toda la Interfaz de Usuario del Juego. Viene con un script básico donde obtenemos la ruta a los nodos que existen dentro de la escena:

onready var number_label = $Bars/LifeBar/Count/Background/Number
onready var bar = $Bars/LifeBar/TextureProgress
onready var tween = $Tween
public class Gui : MarginContainer
{
    private Tween _tween;
    private Label _numberLabel;
    private TextureProgress _bar;

    public override void _Ready()
    {
        // C# doesn't have an onready feature, this works just the same.
        _bar = (TextureProgress) GetNode("Bars/LifeBar/TextureProgress");
        _tween = (Tween) GetNode("Tween");
        _numberLabel = (Label) GetNode("Bars/LifeBar/Count/Background/Number");
    }
}
  • number_label muestra un contador de las vidas como número. Es un nodo Label
  • bar es la barra de vida misma. Es un nodo TextureProgress
  • tween es un nodo estilo-componente que puede controlar y hacer una animación de cualquier valor o método de cualquier otro nodo

Nota

El proyecto utiliza una organización simple que funciona para los game jams y juegos pequeños.

En la raíz del proyecto, en la carpeta res://, encontrará el LevelMockup. Esa es la escena principal del juego y con la que trabajaremos. Todos los componentes que componen el juego están en la carpeta scenes/. La carpeta assets/ contiene los sprites del juego y la fuente para el contador de HP. En la carpeta scripts/ encontrará el enemigo, el jugador y los scripts del controlador GUI.

Haz clic en el icono de editar escena a la derecha del nodo en el árbol de escenas para abrir la escena en el editor. Verás que LifeBar y EnergyBar son subescenas en sí mismas.

../../_images/lifebar_tutorial_Player_with_editable_children_on.png

El árbol de escenas, con la escena Player lista para mostrar a sus hijos

Configura el Lifebar del Player con max_health

Tenemos que decirle al GUI de alguna manera cuál es la salud actual del jugador, para actualizar la textura de la barra de vida, y para mostrar la salud restante en el contador de HP en la esquina superior izquierda de la pantalla. Para ello enviamos la salud del jugador a la GUI cada vez que sufre un daño. La GUI actualizará los nodos Lifebar y Number con este valor.

Podríamos parar aquí para mostrar el número, pero necesitamos inicializar el max_value de la barra para que se actualice en las proporciones correctas. El primer paso es, por lo tanto, decirle al GUI cuál es el max_health del personaje verde.

Truco

La barra, un TextureProgress, tiene un max_value de 100 por defecto. Si no necesitas mostrar la salud del personaje con un número, no necesitas cambiar su propiedad max_value. En su lugar, envía un porcentaje del Player al GUI: health / max_health * 100.

../../_images/lifebar_tutorial_TextureProgress_default_max_value.png

Haz clic en el icono de script a la derecha de la GUI en el panel de Escenas para abrir su script. En la función _ready, vamos a guardar el Player’s max_health en una nueva variable y lo usaremos para establecer el bar’s max_value:

func _ready():
    var player_max_health = $"../Characters/Player".max_health
    bar.max_value = player_max_health
public override void _Ready()
{
    // Add this below _bar, _tween, and _numberLabel.
    var player = (Player) GetNode("../Characters/Player");
    _bar.MaxValue = player.MaxHealth;
}

Vamos a desglosarlo. $"../Characters/Player" es una abreviatura que sube un nodo en el árbol de escenas, y recupera el nodo Characters/Player desde allí, así obtendremos el acceso al nodo. La segunda parte de la declaración, .max_health, accede a max_health en el nodo Player.

La segunda línea asigna este valor a bar.max_value. Se podrían combinar las dos líneas en una sola, pero necesitaremos usar player_max_health más adelante en el tutorial.

Player.gd establece health en max_health al principio del juego, para que podamos trabajar con esto. ¿Por qué seguimos usando max_health? Hay dos razones:

No tenemos la garantía de que health sea siempre igual a la max_salud: una futura versión del juego puede cargar un nivel en el que el jugador ya haya perdido algo de salud.

Nota

Cuando abres una escena en el juego, Godot crea nodos uno a uno, siguiendo el orden de tu panel de Escenas, de arriba a abajo. GUI` y Player no son parte de la misma rama del nodo. Para asegurarnos de que ambos existen cuando accedemos el uno al otro, tenemos que usar la función _ready. Godot llama _ready justo después de haber cargado todos los nodos, antes de que el juego comience. Es la función perfecta para configurar todo y preparar la sesión de juego. Más información about _ready: Scripting (continuación)

Actualizar la salud con una señal cuando el jugador recibe un golpe

Nuestra GUI está lista para recibir las actualizaciones de valor de health desde Player. Para lograr esto vamos a usar señales.

Nota

Hay muchas señales útiles incorporadas como enter_tree y exit_tree, que todos los nodos emiten cuando son creados y destruidos respectivamente. También puedes crear tu propia señal usando la palabra clave signal. En el nodo Player, encontrarás dos señales que hemos creado para ti: died y health_changed.

¿Por qué no obtenemos directamente el nodo Player en la función _process y miramos el valor de su salud? Accediendo a los nodos de esta manera se crea un estrecho acoplamiento entre ellos. Si lo hicieras con moderación, podría funcionar. A medida que el juego crece, es posible que tengas muchas más conexiones. Si obtienes nodos de esta manera, se complica rápidamente. No sólo eso: necesitas escuchar el cambio de estado constantemente en la función _process. Esta comprobación se realiza 60 veces por segundo y es probable que rompas el juego debido al orden en el que se ejecuta el código.

En un determinado frame se puede ver la propiedad de otro nodo antes de que se actualizara: se obtiene un valor del último frame. Esto conduce a errores que son difíciles de arreglar. Por otro lado, se emite una señal justo después de que ha ocurrido un cambio. Esto garantiza que estás recibiendo un nuevo fragmento de información. Se actualizará el estado del nodo conectado justo después de que se haya producido el cambio.

Nota

El patrón Observador, del que derivan las señales, todavía añade un poco de acoplamiento entre las ramas de los nodos. Pero generalmente es más ligero y seguro que acceder a los nodos directamente para comunicarse entre dos clases separadas. Puede estar bien que un nodo padre obtenga valores de sus hijos. Pero querrás favorecer las señales si estás trabajando con dos ramas separadas. Lee Patrones de Programación de Juegos para obtener más información sobre el patrón Observador. El libro completo está disponible en línea de forma gratuita.

Con esto en mente, vamos a conectar el GUI con el Player. Haz clic en el nodo Player del panel de escenas para seleccionarlo. Dirígete al Inspector y haz clic en la pestaña Nodos. Este es el lugar donde se conectan los nodos para que «escuche» el que seleccionaste.

La primera sección enlista las señales personalizadas definidas en Player.gd:

  • died se emite cuando el personaje muere. Lo usaremos en un momento para ocultar la UI.
  • health_changed se emite cuando el personaje es golpeado.
../../_images/lifebar_tutorial_health_changed_signal.png

Nos estamos conectando a la señal health_changed

Selecciona health_changed y haz clic en el botón Conectar situado en la esquina inferior derecha para abrir la ventana Contectando señal. En el lado izquierdo puedes elegir el nodo que escuchará esta señal. Selecciona el nodo GUI. El lado derecho de la pantalla permite agrupar valores opcionales con la señal. Ya nos encargamos de ello en ``Player.gd`. En general se recomienda no añadir demasiados argumentos usando esta ventana ya que son menos convenientes que hacerlo desde el código.

../../_images/lifebar_tutorial_connect_signal_window_health_changed.png

La ventana Conectando Señal con el nodo GUI seleccionado

Truco

Opcionalmente se pueden conectar nodos desde el código. Pero hacerlo desde el editor tiene dos ventajas:

  1. Godot puede escribir nuevas funciones de devolución de llamada para ti en el script conectado
  2. Aparece un icono en forma de emisor junto al nodo que emite la señal en el panel de Escenas

En la parte inferior de la ventana se encuentra la ruta al nodo seleccionado. Nos interesa la segunda fila llamada «Method in Node». Este es el método en el nodo GUI que se llama cuando se emite la señal. Este método recibe los valores enviados con la señal y permite procesarlos. Si observas a la derecha, hay un botón de radio que dice «Make Function» y está activado por defecto. Haz clic en el botón de conexión en la parte inferior de la ventana. Godot crea el método dentro del nodo GUI. El editor de scripts se abre con el cursor dentro de una nueva función _on_Player_health_changed.

Nota

Cuando conectas nodos desde el editor, Godot genera un nombre de método con el siguiente patrón: _on_EmitterName_signal_name. Si ya se ha escrito el método, la opción «Make Function» lo mantendrá. Puedes reemplazar el nombre con lo que quieras.

../../_images/lifebar_tutorial_godot_generates_signal_callback.png

Godot escribe el método de devolución de llamada para ti y te lleva a él

Dentro de los paréntesis después del nombre de la función, agrega un argumento player_health. Cuando el jugador emite la señal health_changed enviará su health actual junto a él. Tu código debería verse así:

func _on_Player_health_changed(player_health):
    pass
public void OnPlayerHealthChanged(int playerHealth)
{
}

Nota

El motor no convierte PascalCase a snake_case, para ejemplos de C# usaremos PascalCase para nombres de métodos y camelCase para parámetros siguiendo las convenciones oficiales de nomenclatura de C#.

../../_images/lifebar_tutorial_player_gd_emits_health_changed_code.png

En Player.gd, cuando el Player emite la señal health_changed, también envía su valor de salud

Dentro de _on_Player_health_changed llamaremos a una segunda función llamada update_health y le pasaremos la variable player_health.

Nota

Podríamos actualizar directamente el valor de salud en LifeBar y Number. Hay dos razones para usar este método en su lugar:

  1. El nombre deja claro para nuestro futuro yo y compañeros de equipo que cuando el jugador sufrió un daño, actualizamos el conteo de salud con la GUI
  2. Reutilizaremos este método un poco más tarde

Crea un nuevo método update_health debajo de _on_Player_health_changed. Solo necesita new_value como único argumento:

func update_health(new_value):
    pass
public void UpdateHealth(int health)
{
}

Este método necesita hacer:

  • establece text del nodo Number en new_value convertido en un string
  • establece el value de TextureProgress en new_value
func update_health(new_value):
    number_label.text = str(new_value)
    bar.value = new_value
public void UpdateHealth(int health)
{
    _numberLabel.Text = health.ToString();
    _bar.Value = health;
}

Truco

str es una función incorporada que convierte cualquier valor en texto (string). La propiedad text de Number requiere un string por lo que no podemos asignarlo a new_value directamente

Llama también a update_health al final de la función _ready para inicializar el texto del nodo Number con el valor correcto al principio del juego. Pulsa F5 para probar el juego: ¡la barra de vida se actualiza con cada ataque!

../../_images/lifebar_tutorial_LifeBar_health_update_no_anim.gif

Tanto el nodo Number como el TextureProgress se actualizan cuando el jugador recibe un golpe

Animar la pérdida de vida con el nodo Tween

Nuestra interfaz es funcional, pero le vendría bien un poco de animación. Esta es una buena oportunidad para presentar el nodo Tween, una herramienta esencial para animar propiedades. Tween anima cualquier cosa que quieras desde un estado inicial hasta un estado final durante un periodo de tiempo dado. Por ejemplo, puede animar la salud del TextureProgress desde su nivel actual hasta el nuevo valor de health del Player cuando el personaje sufre daños.

La escena GUI ya contiene un nodo hijo Tween almacenado en la variable tween. Ahora vamos a usarlo. Tenemos que hacer algunos cambios en update_health.

Usaremos el método Tween del nodo interpolate_property. Se necesitan siete argumentos:

  1. Una referencia al nodo que posee la propiedad para animar
  2. El identificador de la propiedad es un string
  3. El valor inicial
  4. El valor final
  5. Duración de la animación en segundos
  6. El tipo de transición
  7. La facilidad de uso en combinación con la ecuación.

Los dos últimos argumentos combinados corresponden a una ecuación de «easing» (curva de aceleración). Esto controla cómo evoluciona el valor desde el punto de inicio hasta el punto final.

Haz clic en el icono del script junto al nodo GUI para abrirlo de nuevo. El nodo Number necesita texto para actualizarse, y Bar necesita un float o un integer. Podemos usar interpolate_property para animar un número, pero no para animar texto directamente. Vamos a usarlo para animar una nueva variable GUI llamada animated_health.

En la parte superior del script, define una nueva variable, llámala animated_health, y establece su valor en 0. Regresa al método update_health y borra su contenido. Vamos a animar el valor de animated_health. Llama al método Tween del nodo interpolate_property:

func update_health(new_value):
    tween.interpolate_property(self, "animated_health", animated_health, new_value, 0.6, Tween.TRANS_LINEAR, Tween.EASE_IN)
// Add this to the top of your class.
private float _animatedHealth = 0;

public void UpdateHealth(int health)
{
    _tween.InterpolateProperty(this, "_animatedHealth", _animatedHealth, health, 0.6f, Tween.TransitionType.Linear,
        Tween.EaseType.In);
}

Vamos a interrumpir la llamada:

tween.interpolate_property(self, "animated_health", ...

Apuntamos a animated_health en self, es decir, el nodo GUI. Interpolate_property toma el nombre de la propiedad como un string. Por eso lo escribimos como "animated_health".

... _health", animated_health, new_value, 0.6 ...

El punto de partida es el valor actual de la barra. Todavía tenemos que programar esta parte, pero va a ser animated_health. El punto final de la animación es el valor de health del Player después de health_changed: ese es el new_value. Y 0.6 es la duración de la animación en segundos.

...  0.6, tween.TRANS_LINEAR, Tween.EASE_IN)

Los dos últimos argumentos son constantes de la clase Tween. TRANS_LINEAR significa que la animación debe ser lineal. EASE_IN no hace nada con una transición lineal, pero debemos proporcionar este último argumento si no queremos recibir un error.

La animación no se reproducirá hasta que activemos el nodo Tween con tween.start(). Sólo tenemos que hacerlo una vez si el nodo no está activo. Añade este código después de la última línea:

if not tween.is_active():
    tween.start()
if (!_tween.IsActive())
{
    _tween.Start();
}

Nota

Aunque podríamos animar la propiedad health en el Player, no deberíamos. Los personajes deben perder la vida instantáneamente cuando son golpeados. Hace que sea mucho más fácil manejar su estado, como para saber cuándo ha muerto. Siempre es conveniente almacenar las animaciones en un container data o nodo separado. El nodo tween es perfecto para animaciones controladas por código. Para animaciones hechas a mano, echa un vistazo a AnimationPlayer.

Asignar animated_health a LifeBar

Ahora la variable animated_health anima pero ya no se actualizan los nodos Bar y Number existentes. Solucionemos esto.

Hasta ahora, el método update_health se ve así:

func update_health(new_value):
    tween.interpolate_property(self, "animated_health", animated_health, new_value, 0.6, Tween.TRANS_LINEAR, Tween.EASE_IN)
    if not tween.is_active():
        tween.start()
public void UpdateHealth(int health)
{
    _tween.InterpolateProperty(this, "_animatedHealth", _animatedHealth, health, 0.6f, Tween.TransitionType.Linear,
        Tween.EaseType.In);

    if(!_tween.IsActive())
    {
        _tween.Start();
    }
}

En este caso en particular, debido a que number_label recibe texto, necesitamos usar el método _process para animarlo. Vamos a actualizar los nodos Number y TextureProgress como antes, dentro de _process:

func _process(delta):
    number_label.text = str(animated_health)
    bar.value = animated_health
public override void _Process(float delta)
{
    _numberLabel.Text = _animatedHealth.ToString();
    _bar.Value = _animatedHealth;
}

Nota

number_label y bar son variables que guardan referencias a los nodos Number y TextureProgress.

Reproduce el juego para ver la barra animada sin problemas. Pero el texto muestra un número decimal y parece un desastre. Tomando en cuenta el estilo del juego, sería bueno que la barra de vida se animara de una manera más atractiva.

../../_images/lifebar_tutorial_number_animation_messed_up.gif

La animación es suave pero el número está roto

Podemos solucionar ambos problemas redondeando animated_health. Usa la variable local llamada round_value para guardar el valor redondeado animated_health. Luego asígnalo a number_label.text y bar.value:

func _process(delta):
    var round_value = round(animated_health)
    number_label.text = str(round_value)
    bar.value = round_value
public override void _Process(float delta)
{
    var roundValue = Mathf.Round(_animatedHealth);
    _numberLabel.Text = roundValue.ToString();
    _bar.Value = roundValue;
}

Intenta jugar de nuevo para ver una bonita animación de bloques.

../../_images/lifebar_tutorial_number_animation_working.gif

Redondeando animated_health matamos dos pájaros de un tiro

Truco

Cada vez que el jugador recibe un golpe, la GUI llama on_Player_health_changed, que a su vez llama update_health. Esto actualiza la animación y el number_label y bar siguen en _process. La barra de vida animada que muestra la salud bajando gradualmente es un truco. Hace que el GUI se sienta vivo. Si el Player recibe 3 de daño, ocurre en un instante.

Desvanecer la barra cuando el jugador muere

Cuando el personaje verde muere, reproduce una animación de muerte y se desvanece. En este punto, no deberíamos mostrar más la interfaz. Desvanezcamos la barra también cuando el personaje muera. Reutilizaremos el mismo nodo Tween ya que gestiona múltiples animaciones en paralelo por nosotros.

Primero, GUI necesita conectarse a la señal de died del Player’s para saber cuándo murió. Pulsa F1 para volver al espacio de trabajo 2D. Selecciona el nodo Player en el panel de Escenas y haz clic en la pestaña Nodos junto al Inspector.

Busca la señal died, selecciónala y haz clic en el botón Conectar.

../../_images/lifebar_tutorial_player_died_signal_enemy_connected.png

La señal ya debería tener al enemigo conectado

En la ventana Contectando Señal, conecta de nuevo con el nodo GUI. La ruta al nodo debe ser ../../GUI y el método en el nodo debe mostrar _on_Player_died. Deja la opción Make Function activada y haz clic en Conectar en la parte inferior de la ventana. Esto te llevará al archivo GUI.gd en el Área de Trabajo de Scripts.

../../_images/lifebar_tutorial_player_died_connecting_signal_window.png

Se debe obtener estos valores en la ventana Conectando Señal

Nota

Ya deberías notar un patrón: cada vez que la GUI necesita una nueva información, emitimos una nueva señal. Úsalas sabiamente: mientras más conexiones añadas, serán más difíciles de seguir.

Para animar el desvanecimiento de un elemento de UI, tenemos que usar su propiedad modulate. modulate es un ``Color``que multiplica los colores de nuestras texturas.

Nota

modulate proviene de nuestra clase CanvasItem, Todos los nodos 2D y de UI heredan de él. Este permite alternar la visiblidad del nodo, asignarle un shader y modificarlo usando un color mediante modulate.

modulate recibe un valor de Color con 4 canales: rojo, verde, azul y alfa (rgba). Si oscurecemos cualquiera de los tres primeros canales, la interfaz se oscurece. Si reducimos el valor del canal alfa la interfaz desvanece.

Vamos a elegir entre dos valores de color: desde un blanco con un alfa de 1, es decir, con opacidad total, hasta un blanco puro con un valor alfa de 0, completamente transparente. Añadamos dos variables en la parte superior del método _on_Player_died y las nombraremos start_color y end_color. Usa el constructor Color() para crear dos valores Color.

func _on_Player_died():
    var start_color = Color(1.0, 1.0, 1.0, 1.0)
    var end_color = Color(1.0, 1.0, 1.0, 0.0)
public void OnPlayerDied()
{
    var startColor = new Color(1.0f, 1.0f, 1.0f);
    var endColor = new Color(1.0f, 1.0f, 1.0f, 0.0f);
}

Color(1.0, 1.0, 1.0) corresponde a blanco. El cuarto argumento, respectivamente 1.0 y 0.0``en ``start_color y end_color, es el canal alfa.

Luego debemos llamar nuevamente al método interpolate_property del nodo Tween:

tween.interpolate_property(self, "modulate", start_color, end_color, 1.0, Tween.TRANS_LINEAR, Tween.EASE_IN)
_tween.InterpolateProperty(this, "modulate", startColor, endColor, 1.0f, Tween.TransitionType.Linear,
  Tween.EaseType.In);

Esta vez cambiaremos la propiedad modulate y la tendremos animada de start_color a end_color. La duración es de un segundo, con una transición lineal. Aquí también, debido a que la transición es lineal, el easing no importa. Aquí está el método completo _on_Player_died:

func _on_Player_died():
    var start_color = Color(1.0, 1.0, 1.0, 1.0)
    var end_color = Color(1.0, 1.0, 1.0, 0.0)
    tween.interpolate_property(self, "modulate", start_color, end_color, 1.0, Tween.TRANS_LINEAR, Tween.EASE_IN)
public void OnPlayerDied()
{
    var startColor = new Color(1.0f, 1.0f, 1.0f);
    var endColor = new Color(1.0f, 1.0f, 1.0f, 0.0f);

    _tween.InterpolateProperty(this, "modulate", startColor, endColor, 1.0f, Tween.TransitionType.Linear,
        Tween.EaseType.In);
}

Y eso es todo ¡Ahora puedes jugar el juego para ver el resultado final!

../../_images/lifebar_tutorial_final_result.gif

El resultado final ¡Felicidades por llegar hasta aquí!

Nota

Usando exactamente las mismas técnicas, puedes cambiar el color de la barra cuando el jugador se envenena, poner la barra roja cuando su salud baja, sacudir la UI cuando reciben un golpe crítico… el principio es el mismo: emitir una señal para enviar la información del Player al GUI y dejar que el GUI la procese.