Tutorial de Godot – Parte 11: Ataques, Daño y Muerte

En el tutorial anterior , agregamos enemigos al juego, pero por ahora solo se mueven por el mapa del juego siguiendo al jugador. En este añadiremos la posibilidad de atacarlos, tanto usando la espada como lanzando bolas de fuego. A su vez, los enemigos atacarán al jugador con su arma.

Agregar tiempo de enfriamiento para atacar

En el tutorial 2D Sprite Animation ya hemos escrito el código que maneja la entrada del ataque. Por el momento, cada vez que el jugador presiona la barra espaciadora o el botón de ataque en el joypad, se reproduce la animación de ataque con espada. Como resultado, teóricamente es posible atacar un número indefinido de veces por segundo , dependiendo de qué tan rápido presione el botón el jugador (pero la animación no puede seguir el ritmo de las pulsaciones de teclas, por lo que puede no parecerlo).

Entonces, lo primero que debemos hacer es introducir el llamado Attack Cooldown , que es la cantidad de retraso entre dos ataques.

Agregue estas variables al script Player.gd :

# Attack variables
var attack_cooldown_time = 1000
var next_attack_time = 0
var attack_damage = 30
  • attack_cooldown_time : esta variable representa el tiempo mínimo (en milisegundos) que debe pasar entre un ataque y otro.
  • next_attack_time : Godot realiza un seguimiento de la cantidad de tiempo transcurrido desde que se inició el motor; este tiempo se puede utilizar como referencia para planificar acciones futuras. Esta variable almacenará el tiempo en el que será posible atacar de nuevo.
  • attack_damage : la cantidad de puntos que se restarán de la salud del enemigo cuando sea golpeado.

Para agregar el enfriamiento del ataque, edite el código que maneja el ataque en la función _input() de esta manera:

if event.is_action_pressed("attack"):
	# Check if player can attack
	var now = OS.get_ticks_msec()
	if now >= next_attack_time:
		# Play attack animation
		attack_playing = true
		var animation = get_animation_direction(last_direction) + "_attack"
		$Sprite.play(animation)
		# Add cooldown time to current time
		next_attack_time = now + attack_cooldown_time

El primer cambio es la llamada al método get_ticks_msec() del objeto OS . El sistema operativo envuelve la funcionalidad más común para comunicarse con el sistema operativo host, como: portapapeles, modo de video, fecha y hora, temporizadores, variables de entorno, ejecución de binarios, línea de comando, etc. La función get_ticks_msec() devuelve la hora actual como la cantidad de tiempo transcurrido en milisegundos desde que se puso en marcha el motor. Si el tiempo actual es igual o mayor que next_attack_time , la función reproduce la animación de ataque.

Al final de la función, hay una nueva línea que actualiza el tiempo en que el jugador podrá realizar el próximo ataque, calculado como la suma del tiempo actual y el tiempo de recuperación.

Si pruebas el juego ahora, verás que solo puedes hacer un ataque por segundo.

Comprobando si el enemigo está siendo golpeado

Cuando el jugador realiza un ataque, debemos comprobar si hay un monstruo frente a él y dentro del alcance de la espada.

Godot tiene un nodo que fue diseñado para hacer cosas como esta: RayCast2D . Un nodo RayCast2D representa una línea desde su origen hasta su posición de destino, que se puede utilizar para consultar el espacio 2D con el fin de encontrar el objeto más cercano a lo largo de la ruta del rayo.

Entonces, agregue un nodo RayCast2D a Player :

En el Inspector , active la propiedad Habilitado y establezca Cast To en (0,8) para establecer el rayo en su dirección y longitud iniciales (hacia abajo, 8 píxeles).

Cuando el jugador se mueve, debemos rotar el rayo para apuntarlo en la dirección en la que mira el jugador. Para hacerlo, agregue este código al final de la función _physics_process() :

# Turn RayCast2D toward movement direction
if direction != Vector2.ZERO:
	$RayCast2D.cast_to = direction.normalized() * 8

Si prueba el juego ahora, habilitando el menú Depurar → Formas de colisión visibles , verá una flecha frente al jugador (que representa el nodo RayCast2D ) que gira en la dirección del movimiento del jugador.

En este punto, solo tenemos que verificar si el rayo golpea un esqueleto cuando el jugador ataca. Edite el código que maneja el ataque en la función _input() de esta manera:

if event.is_action_pressed("attack"):
	# Check if player can attack
	var now = OS.get_ticks_msec()
	if now >= next_attack_time:
		# What's the target?
		var target = $RayCast2D.get_collider()
		if target != null:
			if target.name.find("Skeleton") >= 0:
				# Skeleton hit!
				target.hit(attack_damage)
		# Play attack animation
		attack_playing = true
		var animation = get_animation_direction(last_direction) + "_attack"
		$Sprite.play(animation)
		# Add cooldown time to current time
		next_attack_time = now + attack_cooldown_time

Para obtener el primer objeto que choca con el rayo, usamos la función get_collider() del nodo RayCast2D .

Si el objeto devuelto no es nulo, verificamos que su nombre contenga la cadena «Esqueleto» usando la función find() de String , que devuelve el índice de la primera aparición de la cadena o -1 si no se encuentra.

Si el objeto es un esqueleto , llamamos a su método hit() , al que le pasamos attack_damage como argumento. Este método, que escribiremos en breve, aplicará el daño al esqueleto y manejará todas las consecuencias (como la muerte del esqueleto).

Estadísticas del esqueleto

El esqueleto, como el jugador, necesita algunas variables para almacenar sus características y su estado actual. Entonces, agregue estas variables a Skeleton.gd :

# Skeleton stats
var health = 100
var health_max = 100
var health_regeneration = 1

Luego, agregue la función _process() para manejar la regeneración de salud:

func _process(delta):
	# Regenerates health
	health = min(health + health_regeneration * delta, health_max)

Infligir daño a los enemigos

Cuando el esqueleto es golpeado durante un ataque, llamamos a la función hit() . Agregue esta función al script Skeleton.gd :

func hit(damage):
	health -= damage
	if health > 0:
		pass #Replace with damage code
	else:
		pass #Replace with death code

La función hit () simplemente resta el daño que se le pasa como argumento del valor de salud actual. En este punto pueden pasar dos cosas:

  • la salud se mantiene por encima de cero
  • la salud cae a cero o menos y el esqueleto muere.

Comencemos analizando el caso en que el esqueleto está herido pero no muere.

En este caso, queremos que el sprite de skelton se vuelva rojo por un momento, para darle al jugador una respuesta visual de que el esqueleto fue golpeado.

Para darle al sprite un tinte rojo, usaremos su propiedad de modulación , heredada de CanvasItem . Esta propiedad es un color, que se utiliza como multiplicador al dibujar en la pantalla. Por defecto el valor de esta propiedad es (1, 1, 1, 1). Los componentes rojo, verde, azul y alfa son todos 1, por lo que los colores del sprite permanecen sin cambios (porque todos los componentes se multiplican por 1).

Si ponemos modulate a (1, 0, 0, 1), los componentes verde y azul del sprite se cancelarían (ya que se multiplican por cero) y por lo tanto solo quedaría visible el componente rojo, obteniendo el resultado que queremos.

Para animar esta propiedad de un nodo, debemos usar el nodo AnimationPlayer . Abra la escena Skelton.tscn y agréguele un nodo AnimationPlayer :

Una vez que se agrega el nodo AnimationPlayer , el panel Animación se abrirá automáticamente en la parte inferior del editor.

Para crear una nueva animación, haga clic en Animación y elija Nuevo .

Nombra la animación Hit y presiona Ok .

Una animación está compuesta por una serie de pistas, cada una de las cuales anima una propiedad de un nodo. Para animar la propiedad modular de Skeleton , haga clic en Add Track y seleccione Property Track .

Ahora tienes que elegir el nodo para animar. Selecciona AnimatedSprite y presiona Ok .

Finalmente, seleccione la propiedad modular y haga clic en Abrir . Si lo desea, puede utilizar el campo de búsqueda para encontrar rápidamente la propiedad que desea animar.

Queremos crear una animación muy simple: el esqueleto debe ponerse rojo durante 2 décimas de segundo y luego volver a la normalidad. Primero, establezcamos la duración de la animación en 0,2 segundos.

A continuación, necesitamos crear los fotogramas clave de la animación. Haga clic con el botón derecho en la pista de modulación en la posición de 0 segundos y seleccione Insertar clave .

Se agregará un nuevo fotograma clave. Repita la operación en la posición de 0,2 segundos para obtener este resultado:

Puede usar el control deslizante de zoom en la parte inferior derecha del panel para cambiar la escala de tiempo y facilitarle el trabajo. Si coloca un fotograma clave en una posición de tiempo incorrecta, puede moverlo arrastrándolo a lo largo de la pista.

Ahora, haga clic en el primer fotograma clave que creó. En el Inspector , puede establecer el valor de la propiedad modular en ese punto de la animación.

Haga clic en el rectángulo blanco junto a Valor y establezca el color en (255, 0, 0, 255). En el panel Animación , verás que la línea que une los dos fotogramas clave cambiará gradualmente de rojo a blanco, lo que indica que hay una transición en esa propiedad.

Tenga en cuenta : en el editor, los componentes de color tienen valores que van de 0 a 255 (componentes de 8 bits), porque representan colores RGBA de 32 bits. En cambio, los objetos Color (como la propiedad de modulación ) almacenan componentes como flotantes, por lo que tienen valores que van desde 0,0 a 1,0. Si desea utilizar valores flotantes en el editor, puede habilitar la opción Modo sin formato en el panel de selección de color.

Lo último que debe hacer para completar la animación Hit es cambiar el tipo de transición. Si ejecuta la animación ahora, el color modulado cambiará gradualmente de rojo a blanco. En cambio, queremos que cambie instantáneamente de rojo a blanco cuando se alcance el segundo fotograma clave. Para hacer esto, cambiamos el tipo de transición a Discreto .

Para reproducir esta animación cuando se golpea el esqueleto, abra el script Skeleton.gd y edite la función hit() de esta manera:

func hit(damage):
	health -= damage
	if health > 0:
		$AnimationPlayer.play("Hit")
	else:
		pass #Replace with death code

la muerte del esqueleto

Cuando la salud del esqueleto cae a cero o menos, el esqueleto «muere». En este caso, debemos detener todas sus actividades (inteligencia artificial y movimiento), reproducir la animación de muerte (que creamos en el tutorial anterior ) y emitir una señal que el generador de monstruos usará para actualizar el conteo de esqueletos.

Comencemos declarando esta nueva señal:

signal death

Luego agregue el código para manejar la muerte en la función hit() :

func hit(damage):
	health -= damage
	if health > 0:
		$AnimationPlayer.play("Hit")
	else:
		$Timer.stop()
		direction = Vector2.ZERO
		set_process(false)
		other_animation_playing = true
		$AnimatedSprite.play("death")
		emit_signal("death")

Cuando el esqueleto muere, lo primero que hacemos es detener el temporizador que maneja la inteligencia artificial. Luego, establecemos la dirección en Vector2.ZERO para detener el movimiento.

La función set_process() se usa para habilitar/deshabilitar la llamada a la función _process() en cada cuadro. Lo deshabilitamos, para evitar la regeneración de la salud del esqueleto.

Finalmente, reproducimos la animación de muerte y emitimos la señal para el generador.

Una vez completada la animación de la muerte, podemos eliminar el esqueleto del árbol de escenas. Para hacer esto, edite _on_AnimatedSprite_animation_finished() para manejar el final de la animación de muerte:

func _on_AnimatedSprite_animation_finished():
	if $AnimatedSprite.animation == "birth":
		$AnimatedSprite.animation = "down_idle"
		$Timer.start()
	elif $AnimatedSprite.animation == "death":
		get_tree().queue_delete(self)
	other_animation_playing = false

La función queue_delete() de SceneTree pone en cola el objeto dado para su eliminación, retrasando la llamada a Object.free() después del cuadro actual.

Ahora, necesitamos conectar la señal death() al generador. Si usa el método habitual para conectar señales, verá que no puede seleccionar nodos fuera de la escena Skeleton . Tenemos que conectar la señal del script generador.

Abra el script SkeletonSpawner.gd y agregue este código a la función instance_skeleton() , después de la llamada add_child() :

# Connect Skeleton's death signal to the spawner
skeleton.connect("death", self, "_on_Skeleton_death")

El método connect() , heredado de Object , se utiliza para conectar una señal (cuyo nombre se pasa como primer argumento) a un método de un objeto (pasado como tercer y segundo parámetro respectivamente). Usamos esta función para conectar la señal death() a la función _on_Skeleton_death() de este objeto ( self ).

La función que maneja la señal es muy simple, simplemente disminuye la cantidad de esqueletos en uno:

func _on_Skeleton_death():
	skeleton_count = skeleton_count - 1

Ejecuta el juego e intenta matar algunos esqueletos.

ataque de esqueleto

El ataque del esqueleto funciona de manera muy similar al del jugador. La única gran diferencia es que no depende de la entrada del jugador, sino que debe ser automático.

Para comenzar, agreguemos a Skeleton.gd las mismas variables de ataque que agregamos a Player (solo cambian los valores):

# Attack variables
var attack_damage = 10
var attack_cooldown_time = 1500
var next_attack_time = 0

Vaya a la escena Skeleton.tscn y agregue un nodo RayCast2D a Skeleton . En el Inspector , active la propiedad Habilitado y establezca Cast To en (0,16).

Luego, en la parte inferior de la función _physics_process() del script Skeleton.gd , agregue el código para rotar el RayCast2D en la dirección del movimiento:

# Turn RayCast2D toward movement direction
if direction != Vector2.ZERO:
	$RayCast2D.cast_to = direction.normalized() * 16

Como dijimos anteriormente, el ataque no se activa presionando un botón como para el jugador, sino que debe ser automático. Entonces, en la función _process() , debemos verificar si RayCast2D golpea al reproductor. Si es así, lo atacamos.

Como es posible que el jugador se haya movido mientras tanto, no lo dañamos de inmediato, pero esperamos el cuadro 1 de la animación de ataque, cuando la guadaña del esqueleto está completamente extendida, para verificar si el jugador todavía está frente al esqueleto y dentro. rango.

Agrega este código a _process() :

# Check if Skeleton can attack
var now = OS.get_ticks_msec()
if now >= next_attack_time:
	# What's the target?
	var target = $RayCast2D.get_collider()
	if target != null and target.name == "Player" and player.health > 0:
		# Play attack animation
		other_animation_playing = true
		var animation = get_animation_direction(last_direction) + "_attack"
		$AnimatedSprite.play(animation)
		# Add cooldown time to current time
		next_attack_time = now + attack_cooldown_time

El código es casi idéntico al del ataque del jugador , aparte del hecho de que el jugador se daña más tarde.

Dado que necesitamos verificar si el jugador todavía está dentro del rango en el cuadro 1 de la animación de ataque, conectaremos la señal frame_changed() de AnimatedSprite a Skeleton. Conecta la señal y edita el código de la función que la maneja:

func _on_AnimatedSprite_frame_changed():
	if $AnimatedSprite.animation.ends_with("_attack") and $AnimatedSprite.frame == 1:
		var target = $RayCast2D.get_collider()
		if target != null and target.name == "Player" and player.health > 0:
			player.hit(attack_damage)

Al principio, la función verifica si la animación actual es una animación de ataque y si el cuadro actual es 1. Si estas condiciones son ciertas, verifica si el jugador todavía está frente al esqueleto y dentro del alcance y, en ese caso, llama a la función hit() de Player .

Tomando daño

Cuando el jugador es golpeado pero no muere, queremos que su nodo Sprite parpadee en color rojo, como hicimos con los esqueletos. Entonces, regrese a la escena principal, agregue un nodo AnimationPlayer a Player y cree la animación Hit , siguiendo exactamente el mismo procedimiento que usamos anteriormente para el nodo Skeleton .

Una vez hecho esto, agregue la función hit() al script del reproductor :

func hit(damage):
	health -= damage
	emit_signal("player_stats_changed", self)
	if health <= 0:
		pass
	else:
		$AnimationPlayer.play("Hit")

La única diferencia con la función hit() de Skeleton es que, después de que se reduce la salud, esta función emite la señal que actualiza la GUI.

muerte del jugador

Cuando el jugador muere, mostraremos una pantalla Game Over muy simple.

En la escena principal, agregue un nodo ColorRect como elemento secundario de CanvasLayer y asígnele el nombre GameOver . Con GameOver seleccionado, en el Inspector establezca la propiedad Rect → Size en (320, 180) y establezca su Color en rojo oscuro, por ejemplo (140, 0, 0, 255). Luego configure la propiedad Ratón → Filtro en Ignorar para evitar que ColorRect intercepte los eventos del mouse (esto es necesario si usa el código para arrastrar el reproductor con el mouse ).

Agregue un nodo Etiqueta a GameOver y vaya al Inspector . Establezca la propiedad Text en GAME OVER y Align to Center . Luego, en la sección Fuentes personalizadas , establezca Fuente en el recurso Font.tres que creamos en el tutorial de GUI . Finalmente, establezca Rect → Position en (125, 85) y Rect → Size en (70, 10).

Queremos que la pantalla de finalización del juego se desvanezca cuando el jugador muera, por lo que debemos animar su propiedad de modulación de (255, 255, 255, 0) a (255, 255, 255, 255). Para animarlo, podemos usar el mismo nodo AnimationPlayer que creamos antes, incluso si es un elemento secundario de Player , ya que puede acceder a todos los nodos de la escena independientemente de su posición en el árbol de la escena.

Entonces, seleccione AnimationPlayer y cree una nueva animación. llamado Game Over , para animar la propiedad modular del nodo GameOver . Siga el procedimiento visto anteriormente, pero esta vez, la animación tendrá que durar 1 segundo, así que coloque el segundo fotograma clave en ese momento. Para el primer fotograma clave, establezca el Valor de modular en (255, 255, 255, 0). Además, no cambie el tipo de transición a Discreto : esta vez queremos que sea Continuo . Obtendrás algo como esto:

Como último paso, en el Inspector establece la propiedad modular de G ameOver en (255, 255, 255, 0) para ocultarlo (lo encontrarás en la sección CanvasItem → Visibilidad ).

Para reproducir la animación cuando el jugador muere, edite la función hit() del jugador :

func hit(damage):
	health -= damage
	emit_signal("player_stats_changed", self)
	if health <= 0:
		set_process(false)
		$AnimationPlayer.play("Game Over")
	else:
		$AnimationPlayer.play("Hit")

Antes de reproducir la animación, la función llama a set_process() para deshabilitar la función _process() , para evitar que la salud del jugador se regenere.

Ejecuta el juego y deja que los esqueletos te maten para probar el juego en pantalla.

lanzando bolas de fuego

Lo último que queda por hacer para completar el sistema de combate del juego es lanzar bolas de fuego.

Primero, creemos una nueva carpeta dentro de las Entidades llamada Fireball . Luego, descargue las imágenes de la bola de fuego haciendo clic en el botón a continuación. Una vez descargadas, impórtalas a la carpeta Fireball (recuerda volver a importar las imágenes deshabilitando Filter ).

Descargar «Marcos de bola de fuego SimpleRPG»fireball_frames.zip – 2 KB

Cree una nueva escena para la bola de fuego y utilice un nodo Area2D como nodo raíz haciendo clic en el botón Nodo personalizado . Usaremos Area2D para detectar cuándo la bola de fuego choca con un esqueleto u otros obstáculos en la escena.

Alguien podría preguntar: ¿por qué no usar un KinematicBody2D para la bola de fuego? Sería la mejor solución pero, desafortunadamente, en Godot los mapas de mosaicos no admiten capas y máscaras de colisión a nivel de una sola celda, por lo que la bola de fuego también sería detenida por mosaicos de agua. Tenemos que usar un sistema personalizado para permitir que las bolas de fuego crucen el agua pero sean detenidas por otros obstáculos.

Cambie el nombre de Area2D a Fireball y agréguele un AnimatedSprite . En el Inspector , crea un nuevo recurso SpriteFrames y crea las siguientes animaciones (si no recuerdas cómo hacerlo, mira el tutorial de Animación de Sprite 2D ):

NombreMarcosVelocidad (FPS)Círculo
volarbola_de_fuego_vuela_1.png
bola_de_fuego_vuela_2.png
bola_de_fuego_vuela_3.png
bola_de_fuego_vuela_4.png
5
explotarbola_fuego_explota_1.png
bola_fuego_explota_2.png
bola_fuego_explota_3.png
bola_fuego_explota_4.png
5No

Configure la propiedad Animation de AnimatedSprite para que vuele y habilite la reproducción .

Agregue un nodo CollisionShape2D a Fireball y establezca su propiedad Shape en un nuevo CircleShape2D nuevo . Haga clic en CircleShape2D y establezca su propiedad Radius en 3.

Ahora, agregue un nodo de temporizador a Fireball , establezca su tiempo de espera en 2 segundos y habilite el inicio automático . Usaremos este temporizador para autodestruir la bola de fuego si no encuentra un obstáculo en 2 segundos.

Adjunte un nuevo script a Fireball , llámelo Fireball.gd y guárdelo en Entities/Fireball .

Comience a agregar estas variables al script:

var tilemap
var speed = 80
var direction : Vector2
var attack_damage

La variable de mapa de mosaicos contendrá un mapa de mosaicos de referencia, que nos permitirá saber si la bola de fuego está por encima del agua. Usaremos variables de velocidad y dirección para el movimiento de la bola de fuego, mientras que attack_damage almacenará los puntos de daño que se restarán de un esqueleto cuando sea golpeado.

Al instanciar la bola de fuego, lo primero que debe hacer es obtener la referencia a Tilemap en la función _ready() :

func _ready():
	tilemap = get_tree().root.get_node("Root/TileMap")

La posición de la bola de fuego debe recalcularse en cada cuadro. Lo haremos dentro de la función _process() :

func _process(delta):
	position = position + speed * delta * direction

La posición se actualiza sumando a la posición actual el movimiento en este marco, calculado como velocidad * delta * dirección .

Pasemos a la detección de colisiones. Los nodos Area2D emiten la señal _body_entered() cada vez que un cuerpo entra en su forma de colisión. Conecte la señal _body_entered() de Fireball al nodo mismo. La función _on_Fireball_body_entered() se creará automáticamente.

En esta función comprobaremos con qué tipo de objeto colisionó la bola de fuego. Si ha chocado con una loseta de agua, se ignorará la colisión. De lo contrario, la bola de fuego explotará y, si el objeto es un esqueleto, lo dañará llamando a la función hit() .

Ingrese este código para la función _on_Fireball_body_entered() :

func _on_Fireball_body_entered(body):
	# Ignore collision with Player and Water
	if body.name == "Player":
		return
	
	if body.name == "TileMap":
		var cell_coord = tilemap.world_to_map(position)
		var cell_type_id = tilemap.get_cellv(cell_coord)
		if cell_type_id == tilemap.tile_set.find_tile_by_name("Water"):
			return
	
	# If the fireball hit a Skeleton, call the hit() function
	if body.name.find("Skeleton") >= 0:
		body.hit(attack_damage)
	
	# Stop the movement and explode
	direction = Vector2.ZERO
	$AnimatedSprite.play("explode")

El cuerpo del argumento de la función es el objeto que ha entrado en la forma de colisión de la bola de fuego. Lo primero que hace la función es verificar si el cuerpo es el jugador, y si es así, la función sale inmediatamente. Cuando se lanza la bola de fuego, el jugador está dentro de la forma de Area2D y obviamente queremos evitar que la bola de fuego explote inmediatamente.

Cuando Fireball choca con un mosaico de TileMap , el objeto pasado a la función _on_Fireball_body_entered() es el TileMap completo . Por lo tanto, debemos entender con qué tipo de mosaico hemos chocado. Usando los mismos métodos TileMap y TileSet que usamos en el tutorial de monstruos , la función verifica si la bola de fuego está sobre un mosaico de agua. Si es así, sale inmediatamente.

La siguiente sección de código verifica si el objeto es un esqueleto y llama a la función hit() para dañarlo.

El final de la función se alcanza solo si la bola de fuego golpea algo. La bola de fuego deja de moverse (estableciendo la dirección en Vector2.ZERO ) y se reproduce la animación de explosión .

Cuando finaliza la animación de la explosión , podemos eliminar la bola de fuego del árbol de escenas. Conecte la señal animation_finished() de AnimatedSprite a Fireball y, en el script, edite la función que se creó automáticamente:

func _on_AnimatedSprite_animation_finished():
	if $AnimatedSprite.animation == "explode":
		get_tree().queue_delete(self)

Si la bola de fuego no golpea nada, se autodestruirá en 2 segundos. Para hacer esto, vincule la señal timeout() de Timer a Fireball para reproducir la animación de explosión:

func _on_Timer_timeout():
	$AnimatedSprite.play("explode")

Finalmente, guarde la escena como Fireball.tscn dentro de la carpeta Entities/Fireball .

Instanciando una bola de fuego

Al igual que para los ataques normales, incluso para las bolas de fuego necesitaremos algunas variables para el tiempo de reutilización y para almacenar los puntos de daño. En Player.gd , agregue este código:

# Fireball variables
var fireball_damage = 50
var fireball_cooldown_time = 1000
var next_fireball_time = 0

Dado que tendremos que instanciar la escena de la bola de fuego cada vez que lancemos una, necesitamos una referencia a ella:

var fireball_scene = preload("res://Entities/Fireball/Fireball.tscn")

Luego, en la función _input() , edite el código que maneja la entrada de la bola de fuego para incluir el enfriamiento (es similar a lo que hicimos para el ataque con espada):

elif event.is_action_pressed("fireball"):
	var now = OS.get_ticks_msec()
	if mana >= 25 and now >= next_fireball_time:
		# Update mana
		mana = mana - 25
		emit_signal("player_stats_changed", self)
		# Play fireball animation
		attack_playing = true
		var animation = get_animation_direction(last_direction) + "_fireball"
		$Sprite.play(animation)
		# Add cooldown time to current time
		next_fireball_time = now + fireball_cooldown_time

Cuando finaliza la animación de la bola de fuego, la función crea una nueva escena de bola de fuego, colocándola a 4 píxeles de distancia frente al jugador. Agrega este código al final de la función _on_Sprite_animation_finished() :

if $Sprite.animation.ends_with("_fireball"):
	# Instantiate Fireball
	var fireball = fireball_scene.instance()
	fireball.attack_damage = fireball_damage
	fireball.direction = last_direction.normalized()
	fireball.position = position + last_direction.normalized() * 4
	get_tree().root.get_node("Root").add_child(fireball)

Ahora ejecuta el juego e intenta lanzar bolas de fuego a los esqueletos.

Conclusiones

En este tutorial aprendimos:

  • cómo implementar el tiempo de reutilización del ataque
  • cómo usar el nodo RayCast2D para detectar otros objetos
  • cómo usar AnimationPlayer para animar las propiedades del nodo
  • cómo conectar señales y nodos que están en diferentes escenas
  • cómo superar algunas limitaciones en las colisiones de TileMap

Algunas cosas que hemos hecho en este tutorial están simplificadas en comparación con lo que harías normalmente. Por ejemplo, la pantalla Game Over generalmente se crea como una escena separada y le permite al jugador comenzar un nuevo juego o salir del juego. Creo que si siguió todos los tutoriales de la serie, debería poder crear una pantalla Game Over más completa por su cuenta. Así que lo dejaré como ejercicio.