03 Experimentos con Nodos - hektorprofe/godot-descubre GitHub Wiki
Escena con nombre SandboxExperiments.tscn
y los scripts en un directorio scripts/SandboxExperiments
.
Vamos escondiendo los nodos que no usemos y yastá.
18. Ilusión de Movimiento Básico
Aplicar variables para crear movimiento básico de izquierda a derecha.
- Después de este inciso aprendiendo GDScript podemos continuar con nuestros experimentos interactivos.
- En esta experimento vamos a aprender lo más básico, la forma en que se genera la ilusión de movimiento de los videojuegos.
- Volviendo a nuestra escena
Main
vamos a desvincular el script actual pulsando el pergamino con laX
roja. - También vamos a reiniciar las propiedades de transformación del icono que dejamos modificadas.
- Dejaremos la posición del Nodo
Icono
, que recordemos es unSprite2D
, en la coordenada(200, 150)
. - Bien, ahora ¿recordáis la función llamada
_process
que hemos estado utilizando sin saber muy bien qué es? - Vamos a crear un nuevo script para el nodo
Icon
, podemos llamarlores://scripts/Desplazamiento.gd
. - Y vamos a fijarnos en la documentación de esa función
_process
:
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
pass
- Lo que nos dice es que esta función se llama en cada fotograma y
delta
es una variable que contiene el tiempo que ha pasada entre el fotograma actual y el anterior. Vamos a imprimir por pantalla el valordelta
en cada fotograma:
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
print(delta)
- Nos aparecerá una ristra interminable de números en la terminal, son números decimales muy pequeños:
0.00833333333333
0.00833333333333
0.00833333333333
0.00833333333333
- Esto estimados son los segundos que pasan entre cada fotograma. ¿Qué creéis que obtendremos si dividimos 1 segundo entre el tiempo que tarda en ejecutarse cada fotograma?
func _process(delta: float) -> void:
print(1/delta)
- Pues nada más ni nada menos que la cantidad media de fotogramas a los que se ejecuta el videojuego:
120
120
120
120
-
¡Como veis se ejecuta muchas veces! La cantidad depende de vuestro monitor y posiblemente de la sincronización vertical, por ejemplo en mi caso obtengo unos 120 FPS (
fotogramas por segundo
). -
Si supiéramos un poco más os diría de crear un texto para visualizar los fotogramas en todo momento, pero como aún no sabemos tanto hagamos algo más sencillo.
-
Vamos a utilizar un método de la librería
DisplayServer
de Godot que nos permite modificar el título de la ventana por los FPS:
func _process(delta: float) -> void:
var fps = 1/delta
DisplayServer.window_set_title(fps)
- Veréis que este código fallará, nos dice que no podemos establecer el título con un número. Pero sí que podemos transformar un número a texto, eso no lo he explicado antes porque es mejor verlo sobre la marcha.
- Mediante la función
str()
podemos transformar un dato numérico a cadena de texto muy fácilmente:
func _process(delta: float) -> void:
var fps = str(1/delta) # cast de float a String
DisplayServer.window_set_title(fps)
- Continuando con el experimento, aprovechando que la función
_process
se ejecuta constantemente, ¿Qué ocurriría si incrementamos en1
la posición horizontalx
del sprite en cada fotograma? Hagámoslo. - Como estamos programando un script enlazado a un
Node2D
de tipoSprite2D
, éste tiene el conjunto de propiedadesTransform
. Si pasamos el raton por encima de la propiedadPosition
nos da el nombreposition
, ese el código para manipuarla desde el script. Vamos a imprimir la propiedad pantalla a ver que muestra:
func _process(delta: float) -> void:
var fps = str(1/delta) # cast de float a String
DisplayServer.window_set_title(fps)
print(position)
- Como era de esperar se nos muestra la posición
x
ey
del sprite en la escena:
(200, 150)
(200, 150)
(200, 150)
- Sin embargo fijaros que nos aparece entre paréntesis y separada por comas. ¿Parece un arreglo pero lo será?
- Si presionamos la tecla
Control
y a la vez hacemos clic en la palabraposition
nos llevará a la documentación. - En la documentación podemos aprender que el tipo de dato no es un arreglo sino un
Vector2
. - Si presionamos en la documentación la definición de
Vector2
nos lleva a otra sección donde explica este tipo. - Dice textualmente
Estructura de 2 elementos que puede usarse para representas coordenadas 2D u otro par de valores numéricos.
y un poco más abajo tenemos el apartadoPropiedades
donde nos explica que unVector2
se compone de dos valores decimalesfloat x
yfloat y
. - ¿Esto cuadra con las dos propiedades que conforman la posición no? Son las que vemos en el inspector.
- Si hacemos clic en ellas en la propia documentación nos dice:
float x: El componente X del vector. También se puede acceder utilizando la posición del índice [0].
float y: El componente Y del vector. También se puede acceder utilizando la posición del índice [1].
- Lo que nos está diciendo es que podemos manipular los componentes
x
ey
de dos formas:
func _process(delta: float) -> void:
prints("Acceso con punto", position.x, position.y)
prints("Acceso con índice", position[0], position[1])
- Sabiendo esto vamos a intentar sumar 1 en cada fotograma a la posición horizontal
x
:
func _process(delta: float) -> void:
# position.x = position.x + 1
position.x += 1
- Ahí lo tenemos, un efecto de movimiento de izquierda a derecha.
- Si durante la ejecución cambiamos al modo
Remoto
delTree View
y seleccionamos el nodoIcon
podemos consultar como se modifica el valor de las propiedades en tiempo real. - Cada vez que sumamos
1
estamos trasladando el sprite 1 píxel a la derecha, en mi caso como se ejecuta 120 veces en un segundo significa que el sprite se mueve a 120 píxeles por segundo en el eje horizontal. - Mover un nodo de esta forma tiene un problema. ¿Qué creéis que ocurriría si en vuestra máquina se ejecuta a 120 FPS pero un dispositivo de bajos recursos se ejecutase a 30 FPS?
- Pues es óbvio, en vuestra máquina se moverá cuatro veces más rápido que en dispositivo de bajos recursos.
- La idea es que nuestros videojuegos se ejecuten a una velocidad constante independiente de los FPS del dispositivo.
- Para solucionarlo podemos ayudarnos de esa variable llamada
delta
. - En lugar de mover un sprite una cantidad fija de píxeles por fotograma, lo que podemos hacer es moverlo una cantidad determinada en función del tiempo. Eso lo conseguiremos multiplicando la cantidad que queremos moverlo en un segundo por la variable
delta
:
@export var velocidad : int = 100 # en pixeles por segundo
func _process(delta: float) -> void:
position.x += velocidad * delta
- Trabajar en función del tiempo y no de los fotogramas es básico al crear videojuegos y esa es la razón por la que Godot nos facilita la variable
delta
siempre que sea necesaria.
19. Interacción con el Teclado
Implementar la entrada del teclado para controlar el movimiento o acciones de un Sprite.
- En la práctica anterior hemos visto cómo mover un
Node2D
incrementando su posiciónx
en el tiempo. ¿No sería genial ser nosotros a través de las flechas del teclado quien mueve el sprite a la izquierda o la derecha? Vamos a hacerlo. - Como la función
_process
se ejecuta constantemente podemos comprobar en ella si en ese preciso instante se está presionando una tecla y en caso afirmativo, mediante una condición, ejecutar el movimiento. - Este concepto de detectar el estado de las teclas, del teclado e inclusi un mando recibe el nombre de captura de
inputs
y representan entradas de información del exterior hacia el programa. - Cada
input
tiene su propio código y existen diferentes estados para ellos. Por ejemplo, de una tecla concreta podemos comprobar si se esta presionando, si se ha presionado o se se ha dejado de presionar. Cada uno de esos estados de la tecla tiene un código de entrada distinto. - Podemos configurar nuestros propios inputs desde
Proyecto > Configuración > Mapa de Entrada
añadiendo acciones o utilizar las que tiene por defecto Godot mostrando lasAcciones Integradas
. - Por ejemplo, la entrada con la clave
ui_accept
será verdadera cuando se presiona la teclaEnter
,Kp Enter
oSpace
del teclado. - O una dirección de movimiento
ui_left
para identificar una dirección a la izquierda está mapeada con la flecha izquierdaLeft
, el botón iquierdo de la cruceta del mando o el eje horizontal izquierdo del Joystick. Usar estosinput
es muy útil porque con un solo código podemos programar la entrada desde diferentes dispositivos. - En cualquier caso vamos a estar usando las acciones
ui_left
yui_right
, así que veamos como detectarlas:
var velocidad = 100 # en pixeles por segundo
# Detectamos si presionamos la acción derecha
if Input.is_action_pressed("ui_right"):
position.x += velocidad * delta
- Para programar el movimiento a la izquierda podemos hacer exactamente lo mismo, pero en lugar de sumar a
x
le restamos:
# Detectamos si presionamos la acción izquierda
if Input.is_action_pressed("ui_right"):
position.x -= velocidad * delta
- ¡Ya ya lo tenemos! Hemos creado nuestro primer controlador de movimiento, que también funcionará con un mando si tenéis uno conectado.
- Antes de acabar quiero enseñaros un concepto nuevos que os gustará.
- Se trata de exportar nuestras variables como propiedades en el inspector.
- De la misma forma que podemos modificar los valores de
position
si exportamos la variablevelocidad
para dar la posibilidad de editarla desde la interfaz gráfica. - Para ello el primer paso es sacar la variable al bloque principal del script, encima de todas las definiciones de funciones:
extends Sprite2D
var velocidad = 100 # en pixeles por segundo
- Una vez la tenemos ahí la exportaremos con la siguiente sintaxis:
extends Sprite2D
@export var velocidad: int = 100 # en pixeles por segundo
- ¡Listo! Ahora podemos modificar esta variable como una propiedad del nodo usando el inspector.
- Y si ejecutamos el programa podemos modificar el valor al vuelo, lo malo es que se quedará cambiado así que ojo al hacer experimentos.
- Para rizar el rizo yo recomendaría añadir el código de movimiento en una función a parte para tenerlo todo mejor organizado. Refactorizamos un poquito aquí y allá y listo:
extends Sprite2D
@export var velocidad: int = 100 # en pixeles por segundo
func inputs(delta: float) -> void:
# Detectamos si presionamos la acción derecha
if Input.is_action_pressed("ui_right"):
position.x += velocidad * delta
# Detectamos si presionamos la acción izquierda
if Input.is_action_pressed("ui_left"):
position.x -= velocidad * delta
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
DisplayServer.window_set_title(str(1/delta))
inputs(delta)
- Os reto a programar los movimientos arriba y abajo, buscad vosotros mismos las acciones
up
ydown
en el elMapa de Entrada
del proyecto para más información.
20. Detectar Clics del Mouse
Usar el input del mouse para mover un Sprite al lugar clicado.
- En este experimento vamos a seguir explorando los
inputs
, concretamente los del mouse. - Nuestro objetivo consiste en cambiar la posición del sprite justo donde hagamos clic.
- El primer paso es crear una acción para detectar el clic ya que ésta no se encuentra por defecto.
- En el
Mapa de Entrada
del proyecto desactivamos las acciones integradas. - Para crear una nueva acción escribiremos un nombre como por ejemplo
clic_izquierdo
y le daremos aenter
. - Una vez en la lista la seleccionaremos y presionaremos el botón
+
. - En esta subventana podemos hacer clic con el mouse para detectar automáticamente la acción a enlazar.
- La que nos interesa es "Botón Izquierdo del Mouse" y le damos a
Aceptar
. - Para detectar este clic lo podemos hacer igual que con las teclas:
# Detectamos si presionamos el clic izquierdo
if Input.is_action_pressed("clic_izquierdo"):
print("Clicado")
- La función
is_action_pressed
comprueba contínuamente si presionamos el botón, pero podemos comprobar dos momentos más. Si acabamos de hacer clicis_action_just_pressed
o si dejamos de hacer clicis_action_just_released
. Estas variantes se ejecutan una sola vez en lugar de en cada fotograma y son más óptimas:
# Detectamos si presionamos el clic izquierdo
if Input.is_action_just_pressed("clic_izquierdo"):
print("Recién clicado")
- Nuestro próximo objetivo consiste en consultar la posición del mouse sobre el
viewport
. Tenemos dos funciones a nuestra disposición para consultar la posición local y la posición global del cursor:
if Input.is_action_just_pressed("clic_izquierdo"):
print("Recién clicado")
prints("posición local", get_local_mouse_position())
prints("posición global", get_global_mouse_position())
- Fijaros que la posición local
(0,0)
concuerda exactamente en el centro del icono, mientras que la posición global(0,0)
se calcula en referencia a la esquina superior izquierda del viewport. - Como podéis suponer la que nos interesa es la posición global así que podemos simplemente asignarla a la posición del sprite:
if Input.is_action_just_pressed("clic_izquierdo"):
print("Recién clicado")
# Posicionamos el sprite en la posición global del ratón
position = get_global_mouse_position()
- Con esto ya lo tenemos, pero si queremos generar un efecto de arrastrar el sprite contínuamente podemos cambiar la función de
Input
ais_action_pressed
como la teníamos antes:
if Input.is_action_pressed("clic_izquierdo"):
# Posicionamos el sprite en la posición global del ratón
position = get_global_mouse_position()
- Ahora que ya sabemos mover sprites y detectar acciones desde dispositivos externos podemos pasar a la siguiente sección de
Experimentos Interactivos
, os espero.
21. Cambiar imágenes teclado
Mediante el teclado cambiar las imágenes de un sprite.
La lista de sprites son el logo de Godot modificado de color usando modulate
, si es que se puede al asignarlo.
extends Sprite
# Define una lista de rutas a las texturas que deseas usar
@export var textures : Array[Sprite2D]
var current_texture = 0
func _process(delta):
# Cambiar a la textura anterior con la flecha izquierda
if Input.is_action_just_pressed("ui_left"):
current_texture -= 1
if current_texture < 0:
current_texture = textures.size() - 1
update_texture()
# Cambiar a la siguiente textura con la flecha derecha
elif Input.is_action_just_pressed("ui_right"):
current_texture += 1
if current_texture >= textures.size():
current_texture = 0
update_texture()
func update_texture():
texture = load(textures[current_texture])
self.texture = texture
22. Rotación y escalado
Como funciona la rotación en grados y radianos y el escalado.
extends Sprite
# Variables para controlar la velocidad de rotación y escala
var rotation_speed_degrees = 90 # Grados por segundo
var rotation_speed_radians = PI # Radianes por segundo, PI radianes son 180 grados
var scaling_speed = 0.5 # Unidades de escala por segundo
func _ready():
# Configuración inicial opcional
pass
func _process(delta):
# Ejemplo de rotación en radianes
rotate(rotation_speed_radians * delta)
# Si prefieres rotar en grados, puedes usar la siguiente línea en su lugar
var degrees_to_radians = deg_to_rad(rotation_speed_degrees)
rotate(degrees_to_radians * delta)
# Ejemplo de escalado
scale = scale + Vector2(scaling_speed * delta, scaling_speed * delta)
# Para demostrar la conversión entre grados y radianes, podríamos imprimir los valores
var angle_in_degrees = rotation_degrees
var angle_in_radians = rotation
print("Ángulo en grados: ", angle_in_degrees, ", Ángulo en radianes: ", angle_in_radians)
# Nota: `rotation_degrees` y `rotation` son propiedades que obtienen o establecen la rotación del Sprite
# en grados o radianes, respectivamente.
23. Movimiento diagonal
Descubrir el bug del movimiento diagonal y rectificar la velocidad.
Cuando implementas movimiento diagonal en un videojuego utilizando los métodos comunes de sumar las velocidades horizontal y vertical, a menudo encuentras un "bug" no intencionado donde el movimiento diagonal es más rápido que el movimiento horizontal o vertical por sí solos. Esto se debe a que al moverse diagonalmente, en realidad estás sumando los vectores de movimiento horizontal y vertical, resultando en un vector con una magnitud mayor que los vectores individuales. Para "rectificar" este aumento de velocidad y hacer que el movimiento diagonal sea consistente con el movimiento en un solo eje, necesitas normalizar el vector de velocidad y luego multiplicarlo por la velocidad deseada.
extends Sprite2D
@export var speed : int = 200
func _process(delta):
var direction_pressed : Vector2
if Input.is_action_pressed("ui_right"):
direction_pressed.x += 1
if Input.is_action_pressed("ui_left"):
direction_pressed.x -= 1
if Input.is_action_pressed("ui_up"):
direction_pressed.y -= 1
if Input.is_action_pressed("ui_down"):
direction_pressed.y += 1
var direccion_normalizada = direction_pressed.normalized()
var longitud_direccion = sqrt(direccion_normalizada.x ** 2 + direccion_normalizada.y ** 2)
print(longitud_direccion)
position += direccion_normalizada * speed * delta
24. Cuerpos estáticos y rígidos
Aprender sobre los objetos físicos estáticos y rígidos, la gravedad y otros conceptos.
-
Durante las próximas lecciones vamos a aprender sobre el manejo de físicas.
-
Pero antes vamos a hacer limpieza guardando estos nodos como escenas en su propio subdirectorio
SandboxInteractivo
. -
En Godot 4 o superior, los nodos de "cuerpos"
StaticBody2D
,RigidBody2D
yCharacterBody2D
son elementos que interactúan con las físicas del juego. -
Sin embargo para que los cuerpos puedan detectar colisiones deben tener un
CollisionShape2D
oCollisionPolygon2D
que defina su forma física. -
Cuando dos formas físicas se intersecan en el espacio dan lugar a una colisión que se maneja automáticamente dando lugar a rebotes y bloqueos de movimiento.
-
Godot nos permite capturar y responder a estas colisiones utilizando señales como
on_body_entered
yon_body_exited
o sobrescribiendo el método_physics_process(delta)
para implementar lógica personalizada. -
Empecemos experimentando con los cuerpos estáticos
StaticBody2D
que sirven para representar objetos inamovibles a los que no les afectan las fuerzas externas, como por ejemplo suelos y paredes. -
Vamos a crear un suelo usando un nodo
StaticBody2D
con unSprite2D
que puede ser unPlaceHolderTexture
con un color establecido en elSelf Modulate
y unaCollisionShape2D
. -
Si ejecutamos el juego veremos que no ocurre nada, el cuerpo estático se queda quieto.
-
Por otra parte los cuerpos rígidos
RigidBody2D
son movidos por el sistema de simulación de físicas en función de sus propiedades especificas o en su defecto utilizando los valores de configuración del proyecto. -
Vamos a crear una caja con
RigidBody2D
, unSprite2D
yCollisionShape2D
. -
Si ejecutamos el juego, como por defecto a los
RigidBody2D
les afecta la gravedad pues cae sobre el piso. -
Al trabajar con simulaciones de físicas los cuerpos tienden a comportarte con en la realidad.
-
Podemos experimentar creando una pendiente y observando la diferencia entre objetos deslizándose por ella y cambiando sus masas.
-
También es interesante crear algunos cuerpos circulares en forma de bola para observar su comportamiento. Os adjunto un sprite con un icono redondo para hacer unas pruebas.
-
Es posible añadir materiales físicos para extender aún más sus comportamientos, por ejemplo para modificar su fricción
friction
o el reboterebound
. -
Por último observemos como afecta cambiar de forma general la gravedad de todos los cuerpos al modificarla desde los ajustes del proyectos en el apartado
Físicas > 2D
.
25. Cuerpo de personaje
-
El nodo
CharacterBody2D
es una adición introducida en Godot 4.0 que substituye los antiguosKinematicBody2D
. -
Es una abstracción de un
RigidBody2D
diseñada específicamente para facilitar la creación de personajes que interactúan con el entorno de manera física pero controladas a través de scripts de código. -
Ofrecen funcionalidades para implementar movimientos y comportamientos comunes de personajes, como caminar, saltar o colisionar con el entorno sin tener que manejar directamente las complejidades de la física.
-
Vamos a añadir un
CharacterBody2D
con unSprite2D
y unCollisionShape2D
. -
Por defecto a este cuerpo no le afecta nada, somos nosotros mediante código quienes debemos programar sus movimientos y colisiones.
-
Hagamos un ejemplo muy sencillo para entender cómo funciona.
-
Los cuerpos rígidos se mueven internamente procesando un vector llamado
velocity
. Éste no solo almacena la velocidad de movimiento, sino también la dirección. Como unCharacterBody2D
es una abstracción de unRigidBody2D
se mueve de la misma manera:
extends CharacterBody2D
func _ready(delta):
velocity = Vector2(50, 0) # Derecha 50 píxel por segundo
- Sin embargo no es suficiente con establecer el vector, también es necesario decirle que en cada ciclo de físicas Godot aplique la velocidad al cuerpo mediante la función
move_and_slice()
:
extends CharacterBody2D
func _ready():
velocity = Vector2(50, 0) # Derecha 50 píxel por segundo
func _physics_process(delta):
move_and_slide()
- Os habréis fijado que no estamos teniendo en cuenta
delta
para rectificar la velocidad, eso es porque ya se hace automáticamente en la funciónmove_and_slide()
, sin embargo hay una variante de esta función llamadamove_and_collide()
que requiere enviar el vector de velocidad que deseamos utilizar y sí requiere rectificarlo por delta:
extends CharacterBody2D
func _ready():
velocity = Vector2(50, 0) # Derecha 50 píxel por segundo
func _physics_process(delta):
# move_and_slide()
move_and_collide(velocity * delta)
- Algo que nos ofrecen ambas funciones es la posibilidad de detectar el nodo contra el que ocurre una colisión:
var collision = move_and_slide()
if collision:
print("Colisión detectada")
- Pero ya hablaremos más adelante de colisiones más adelante, por ahora hagamos un experimento.
- Veamos cómo mover el
CharacterBody2D
hacia la posición donde hacemos clic con el ratón:
extends CharacterBody2D
@export var speed = 200
var target = position
func _input(event):
if event.is_action_pressed("ui_click"):
target = get_global_mouse_position()
prints("Posición del clic", target)
func _physics_process(delta):
# Como conseguimos la dirección a la posición de clic?
velocity = ????
move_and_slide()
- Los
Vector2
tienen muchos métodos útiles, uno de ellos esdirection_to
y devuelve el vector de dirección desde el propio vector hacia otro vector:
func _physics_process(delta):
velocity = position.direction_to(target) * speed # new
move_and_slide()
- Listo, pero fijaros que cuando llega al destino se mueve constantemente.
- Esto es porque nunca llega al punto concreto, siempre se pasa de largo debido a la velocidad.
- Una forma de solucionarlo es comprobar si estamos suficiente cerca del destino y detener el movimiento:
func _physics_process(delta):
velocity = position.direction_to(target) * speed
if position.distance_to(target) > 10:
move_and_slide()
- Por último otra función que tenemos disponible es
look_at()
, ésta permite rotar el cuerpo en dirección a unVector2
:
func _physics_process(delta):
velocity = position.direction_to(target) * speed
if position.distance_to(target) > 10:
look_at(target) # new
move_and_slide()
- Claro, aquí se genera un efecto raro porque el sprite rota sobre sí mismo al moverse a la izquierda, si por defecto el icono mirase a la derecha quedaría mejor. Solo tenemos que cambiar la rotación del sprite a 90º.
26. Template cuerpo personaje
- En los nodos
CharacterBody2D
, Godot nos da la posibilidad de añadir un template de movimiento básico para un típico juego de plataformas 2D. - En esta práctica vamos a analizar el código de ese script para aprender más sobre el manejo de físicas:
extends CharacterBody2D
# Definir constantes para la velocidad de movimiento y la velocidad de salto.
const SPEED = 300.0
const JUMP_VELOCITY = -400.0
# Obtener la gravedad de las configuraciones del proyecto para sincronizarla con los nodos RigidBody.
var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")
func _physics_process(delta):
# Añadir gravedad si el personaje no está en el suelo.
if not is_on_floor():
velocity.y += gravity * delta
# Manejar el salto. Se activa al presionar el botón asignado a "ui_accept" (por defecto, es la tecla 'Espacio' o 'Enter')
# y solo si el personaje está tocando el suelo.
if Input.is_action_just_pressed("ui_accept") and is_on_floor():
velocity.y = JUMP_VELOCITY
# Obtener la dirección de entrada del usuario y manejar el movimiento/desaceleración.
var direction = Input.get_axis("ui_left", "ui_right")
if direction:
velocity.x = direction * SPEED
else:
velocity.x = move_toward(velocity.x, 0, SPEED)
# Mover el personaje y deslizar sobre el suelo. Esta función automáticamente maneja colisiones y rebotes.
move_and_slide()
27. Áreas de colisión
Entender la diferencia entre las colisiones de cuerpos y áreas.
- Anteriormente hemos visto cómo detectar colisiones entre cuerpos desde un
CharacterBody2D
. - ¿Pero y si queremos detectar colisiones sin influir en las físicas de los cuerpos? Pues para eso existen los nodos
Area2D
. - Una
Area2D
también necesita unCollisionShape2D
oCollisionPolygon2D
para establecer su espacio de detección. - Lo bueno es que pueden emitir señales como
body_entered(body: Node)
ybody_exited(body: Node)
para notificar la entrada y salida de cuerpos en su espacio. - Con ellas podemos disparar eventos como activar una trampa, abrir una puerta, cambiar el estado del juego o cualquier otro comportamiento sin influir en las físicas del cuerpo que interseca su espacio.
extends Area2D
func _ready():
connect("body_entered", _on_body_entered)
connect("body_exited", _on_body_exited)
func _on_body_entered(body: Node):
print("El área ha colisionado con el cuerpo: ", body.name)
func _on_body_exited(body: Node):
print("El área ha dejado de colisionar con el cuerpo: ", body.name)
- Las áreas no sirven solo para detectar colisiones contra cuerpos sino que pueden modificar sus propiedades físicas mientras estos se encuentran en ellos.
- Para ello podemos activar sus propiedades de
Space Override
aReplace
y cambiar el eje de gravedad vertical a-1
. - Cuando un cuerpo rígido entre en el área su gravedad se invertirá creando un efecto de rebotar en él:
- Si tenemos el origen del área centrado y activamos la opción
Point
la gravedad se calculará respecto a ese punto, cambiando el comportamiento a una especie de oscilización a su alrededor:
- Os animo a experimentar añadiendo más cuerpos a la escena y sobrescribiendo otras propiedades físicas.
28. Botón con sprites y áreas
En este experimento veremos otro uso que se le puede dar a las áreas.
- Ahora crearemos un
Area2D
llamadoBotonSprite
: - Añadiremos un
Sprite2D
con un circulo blanco y unCollisionShape2D
en su interior.
extends Area2D
enum Estados {NORMAL, HOVER, CLIC}
var estado : Estados
func _ready():
estado = Estados.NORMAL
func _process(delta):
match estado:
Estados.NORMAL:
modulate = Color.RED
Estados.HOVER:
modulate = Color.ORANGE
Estados.CLIC:
modulate = Color.GREEN
func _on_mouse_entered():
estado = Estados.HOVER
func _on_mouse_exited():
estado = Estados.NORMAL
func _on_input_event(viewport, event, shape_idx):
if event.is_action_pressed("ui_click"):
estado = Estados.CLIC
if event.is_action_released("ui_click"):
estado = Estados.HOVER
29. Cámaras de seguimiento
Configurar una cámara que siga al personaje y examinar sus propiedades.
Hasta ahora hemos trabajado siempre dentro del viewport por defecto, pero en muchas ocasiones nuestros juegos abarcarán un espacio mayor, como cuando nuestros personajes se mueven por un escenario.
Para estos casos necesitamos crear una cámara que siga al personaje, de esa forma se renderizará lo que ve en el viewport.
Para hacer que Camera2D
siga a un nodo en movimiento necesitamos un personaje y un escenario más grande que lo que abarca la ventana. Vamos a recuperar el escenario que teníamos anteriormente volviendo a activarlo en su Process
:
- Añadir el Nodo
Camera2D
: Coloca el nodoCamera2D
como hijo del nodo que deseas seguir. Esto podría ser, por ejemplo, tu personaje jugador. - Establecer como Cámara Actual: Activa la propiedad
Current
en el inspector de Godot para hacer de esta cámara la que se usa actualmente en la vista. - Ajustar el
Camera2D
: Coloca la cámara de manera que su posición inicial sea la deseada respecto al nodo padre.
Propiedades Importantes:
Drag Margin
: Estos márgenes definen cuánto puede moverse el nodo seguido dentro de la vista de la cámara antes de que la cámara comience a seguirlo. Por defecto, todos están configurados al 0.5 (50%), lo que significa que la cámara comenzará a moverse cuando el nodo se mueva fuera del centro de la pantalla. Ajusta estos valores para cambiar cuándo debería la cámara empezar a seguir al nodo.Smoothing
: La suavización permite que la cámara siga al nodo de manera más fluida en lugar de replicar cada movimiento de manera exacta e instantánea. Al aumentar el valor de Speed en Smoothing, harás que la cámara se atrase ligeramente detrás del nodo seguido, creando un efecto de seguimiento más suave.Limit
: Establece los límites de la cámara para evitar que muestre áreas fuera de tu mundo de juego (como el vacío fuera de los bordes del nivel). Estos límites restringen hasta dónde puede moverse la cámara en las direcciones X e Y.Zoom
: Esta propiedad te permite ajustar el nivel de zoom de la cámara. Un valor de Vector2(1, 1) significa que no hay zoom. Valores más bajos acercan la cámara, mientras que valores más altos la alejan.
Un addon muy útil que extiende el funcionamiento de las cámaras es Phantom Camera
. Éste implementa dos nodos PhantomCamera2D
y PhantomCamera3D
, podemos echar un vistazo a las escenas de pruebas para ver su funcionamiento e intentar usarla en nuestro personaje.
30. Cronómetro con timer
En esta lección vamos a introducir el uso de temprizadores, un tipo de Nodo muy útil para programar eventos en el tiempo.
Primero, añadimos un nodo Timer a la escena:
- Establece Wait Time a 1. Esto hará que el temporizador tenga un intervalo de un segundo.
- Asegúrate de que
One Shot
esté desactivado, para que el temporizador se repita. - Activa
Autostart
si deseas que el temporizador comience a contar automáticamente al iniciar la escena. - Configurar la señal
timeout
del temporizador:
extends Node
var segundos = 0 # Contador de segundos
# func _ready():
# var timer = $Timer # Asume que el nodo Timer es un hijo directo y se llama "Timer"
# timer.connect("timeout", self, "_on_Timer_timeout")
# timer.start() # Inicia el timer si no has activado Autostart
func _on_Timer_timeout():
segundos += 1
print("Tiempo transcurrido: ", segundos, " segundos")
31. Reproducción de audio
En esta lección vamos introducir la reproducción de audio al presionar el botón que creamos anteriormente.
extends Area2D
enum Estados {NORMAL, HOVER, CLIC}
var estado : Estados
# Pre-cargar tus archivos de sonido si aún no lo has hecho
var sonido = preload("res://alarm.ogg") # new
func _ready():
estado = Estados.NORMAL
$AudioStreamPlayer.stream = sound # new
func _on_input_event(viewport, event, shape_idx):
if event.is_action_pressed("ui_click"):
estado = Estados.CLIC
$AudioStreamPlayer.play(from_position=0.0) # new
if event.is_action_released("ui_click"):
estado = Estados.HOVER
$AudioStreamPlayer.stop() # new
32. Alarma de cuenta regresiva
En este experimento vamos a fusionar lo que sabemos sobre temprizadores y reproducción de audio para hacer sonar una alarma programando un temprizador.
- Añade un nodo
Timer
a tu escena: Este nodo será el encargado de llevar la cuenta regresiva. - Añade un nodo
AudioStreamPlayer
: Este nodo reproducirá el sonido de la alarma cuando el temporizador llegue a cero. - Coloca tu archivo de sonido
alarm.ogg
en el proyecto: Asegúrate de que el archivo de sonido esté dentro de la carpeta del proyecto de Godot para poder acceder a él desde el nodoAudioStreamPlayer
. - Selecciona el nodo
Timer
y ajusta las siguientes propiedades en el inspector:Wait Time
: Establece esto al número de segundos para la cuenta regresiva.One Shot
: Activa esta opción para que el temporizador solo cuente una vez y no se repita.
- Selecciona el nodo
AudioStreamPlayer
y en el inspector, bajo la propiedadStream
, asigna tu archivoalarm.ogg
. - Adjunta un nuevo script a tu nodo
Timer
o al nodo padre que contenga tanto elTimer
como elAudioStreamPlayer
. Asegúrate de exportar una variable para configurar la cuenta regresiva desde el inspector:
extends Node
# Exporta la variable para configurar los segundos de la cuenta regresiva desde el inspector.
@export var segundos_cuenta_regresiva : float = 5.0
# Referencia al nodo AudioStreamPlayer
@onready var alarma = $AudioStreamPlayer
func _ready():
# Configura el Timer con la duración deseada y lo inicia.
var timer = $Timer
timer.wait_time = segundos_cuenta_regresiva
timer.one_shot = true
timer.connect("timeout", self, "_on_Timer_timeout")
timer.start()
func _process(delta):
var timer = $Timer
# Obtiene el tiempo restante del Timer y actualiza el título de la ventana con este valor.
var tiempo_restante = timer.time_left
DisplayServer.window_set_title("Tiempo restante: " + str(tiempo_restante) + " segundos")
func _on_Timer_timeout():
# Reproduce el sonido de la alarma cuando el temporizador llega a cero.
alarma.play()
# Opcional: resetear el título de la ventana o establecer un mensaje final.
DisplayServer.set_window_title("¡Tiempo agotado!")
33. Efectos de sonido
Enseñar a utilizar el plugin gdfxr
para generar, guardar y reproducir sonidos.
- Instalar el paquete gdfxr, acitvarlo, crear varios sonidos y Guardarlos como.
- Añadir un
AudioStreamPlayer
y asignarle el efecto de sonido asignarles los efectos de sonido. - El código para la reproducción es muy simple:
$AudioStreamPlayer.play()
- Podemos ver la diferencia respecto al
AudioStreamPlayer2D
, que tiene posicionamiento. - Si el reproductor está situado en un nodo a la izquierda del viewport escucharemos el sonido por ese lado.
- Pero si está situado a la derecha lo escucharemos por el otro altavoz.
34. Cambio de escenas
Crear un script que cambie entre escenas al ocurrir algún evento concreto.
extends Node
func _ready():
# Esto asegura que el nodo no sea eliminado al cambiar de escena.
if is_inside_tree():
get_tree().root.add_child(self)
self.owner = get_tree().root
func _input(event):
if event is InputEventKey and event.pressed:
if event.scancode == KEY_F1:
change_scene("res://Scene1.tscn")
elif event.scancode == KEY_F2:
change_scene("res://Scene2.tscn")
elif event.scancode == KEY_F3:
change_scene("res://Scene3.tscn")
func change_scene(scene_path: String):
get_tree().change_scene(scene_path)
35. Animaciones con código
Configurar una animación de movimiento en diferentes direcciones con código.
Preparamos el fondo:
- Añadimos un
ParallaxBackground
con unaParallaxLayer
. - En la capa añadimos un
Sprite2D
de fondo, no centrado y en (0,0). - Configuramos el mirroring con el tamaño extra de a repetir (480,120).
Preparamos el personaje:
- Creamos un
CharacterBody2D
y le cambiamos el nombre aPersonaje
. - Le añadimos un
Sprite2D
con todo elspritesheet
yTexture
enNearest
al ser pixel art. - En el
Sprite2D
activamos laRegion
y activamos la cuadrícula para seleccionar los 16 sprites. - Nota: Si usamos la versión con ropa necesitaremos establecer un offset en el spritesheet:
- También activaremos la opción
Region
>Filter Clip Enabled
para evitar artefactos en los bordes. - Como la animación son 4 sprites establecemos el
Animation > HFrames
en 4 y ya lo tenemos listo. - Nos llevamos el
CharacterBody2D
al centro de la escena y escalamos la imagen a (4,4).
Preparamos el script de movimiento:
- Añadimos un script vacio al
CharacterBody2D
con el siguiente codigo:
extends CharacterBody2D
@export var speed = 200
func move():
# Recuperamos un vector normalizado en base a las cuatro direcciones
var direction := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
# Establecemos la velocidad y movemos al personaje
velocity = direction * speed
move_and_slide()
func _physics_process(delta: float):
move()
Preparamos la primera animación:
- Añadimos al personaje un
AnimationPlayer
. - Añadimos una nueva animación
walk_down
. - Este nodo permite animar cualquier propiedad, nosotros vamos a ir cambiando el sprite.
- Dentro del animador seleccionamos el
Sprite2D
y en elframe
hacemos clic en lallave
. - Le damos a que cree las pistas y se creará una barra de animación en la parte inferior.
- Como tenemos 4 animaciones vamos a crear 4 key frames cambiando los cuatro
frame
en el tiempo. - Podemos establecer el ajuste a
0.25
segundos para que la animación completa dure 1 segundo. - Muy importante 1: Podemos cambiar el orden Mover-Quieto-Mover-Quieto para más fluidez:
-
Muy importante 2: Activamos el MODO DE ACTUALIZACIÓN a
discreto
para evitar interpolaciones. -
Muy importante 3: Activamos el LOOP para que se repitan al finalizar.
-
Una vez tenemos la animación procedemos a controlarla mediante código:
extends CharacterBody2D
@export var speed = 200
@onready var animation_player: AnimationPlayer = $AnimationPlayer # conservar para evitar bugs
# Preparamos una máquina de estados
enum PlayerState {IDLE, WALK}
var state : PlayerState
func _physics_process(delta: float):
move()
animate()
func move():
# Recuperamos un vector normalizado en base a las cuatro direcciones
var direction := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
# Establecemos el estado de la máquina en base al vector de dirección
if direction == Vector2.ZERO: state = PlayerState.IDLE
else: state = PlayerState.WALK
# Establecemos la velocidad y movemos al personaje
velocity = direction * speed
move_and_slide()
func animate():
# Comprobamos el estado de la máquina
match state:
# Si estamos parados detenemos el animador
PlayerState.IDLE:
animation_player.stop()
# Si estamos andando reproducimos la animación correspondiente
PlayerState.WALK:
animation_player.play("walk_down")
Finalizamos las animaciones y adaptamos la máquina de estados:
- Repetimos el proceso para crear las animaciones
walk_up
,walk_right
ywalk_left
. - Recordemos que las animaciones de caminar duran 1 segundo con tramos de 0.25 para cada imagen.
func animate():
# Comprobamos el estado de la máquina
match state:
# Si estamos parados detenemos el animador
PlayerState.IDLE:
animation_player.stop()
# Si estamos andando reproducimos la animación correspondiente
PlayerState.WALK:
match velocity.normalized():
Vector2.UP: animation_player.play("walk_up")
Vector2.DOWN: animation_player.play("walk_down")
Vector2.LEFT: animation_player.play("walk_left")
Vector2.RIGHT: animation_player.play("walk_right")
- En este punto tenemos un personaje animado en cuatro direcciones.
36. Animaciones con árboles
Sustituir la animación por código con una máquina de estados en un AnimationTree.
Configurar un árbol de animación:
- Veamos una forma de manejar las animaciones automáticamente con árboles de animación.
- En lugar de programar una máquina de estados manualmente podemos hacerlo desde la interfaz.
- Dentro del
CharacterBody2D
añadimos unAnimationTree
. - Creamos un nuevo
Tree Root
de tipoAnimatioNodeStateMachine
. - Configuramos el
Anim Player
alAnimationPlayer
del personaje. - Si tenemos activado el
AnimationMixer
ya no podemos reproducir animaciones directamente. - En su lugar las vamos a manejar a través del código del propio árbol de animación.
Preparando la máquina de estados:
- Lo primero que debemos tener en cuenta es que ahora no podemos simplemente detener el animador.
- Eso significa que necesitamos crear 4 animaciones nuevas para las direcciones del estado
Idle
. - Podemos hacer clonando las que tenemos y dejando el primer sprite
idle_up
,idle_down
,idle_left
eidle_right
. - En este punto diferenciamos dos estados
Idle
yWalk
cada uno con 4 animaciones en base a una dirección. - Ambos serán de tipo
BlendSpace2D
, este espacio 2D permite determinar las animaciones automáticamente tomando un vector.
Configurando el estado Idle
:
- Por comodidad podemos establecer en la mezcla
x
para el eje horizontal ey
para el eje vertical. - Cuando
x=1
seráidle_right
,x=-1
seráidle_left
,y=1
seráidle_down
ey=-1
seráidle_up
. - Recordemos que cuando
y
es positiva la dirección apunta hacia abajo y si es positiva apunta hacia arriba. - Por último es muy importante cambiar la mezcla a discreta para evitar la interpolación automática.
- Haremos exactamente lo mismo para el estado caminar
Walk
, configurando las mismas direcciones y la mezcla a discreta.
Configurando el script:
- La nueva versión del script no requerirá una máquina de estados ya que ésta se gestionará desde el árbol de animación.
- Lo único que tenemos que hacer es establecer los valores de los vectores de mezcla y cambiar el estado actual dependiendo de si nos movemos o no:
extends CharacterBody2D
@export var speed = 200
@onready var animation_player: AnimationPlayer = $AnimationPlayer # conservar para evitar bugs
@onready var animation_tree: AnimationTree = $AnimationTree
func _physics_process(delta: float):
move()
func move():
# Recuperamos un vector normalizado en base a las cuatro direcciones
var direction := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
# Actualizamos los espacios de mezcla al detectar un movimiento
if direction != Vector2.ZERO:
animation_tree.set("parameters/Idle/blend_position", direction)
animation_tree.set("parameters/Walk/blend_position", direction)
# Cambiamos a la animación de caminar manualmente
animation_tree.get("parameters/playback").travel("Walk")
else:
# Cambiamos a la animación de parado manualmente
animation_tree.get("parameters/playback").travel("Idle")
# Establecemos la velocidad y movemos al personaje
velocity = direction * speed
move_and_slide()
- En la siguiente lección veremos como automatizar todavía más este sistema.
37. Animaciones condicionadas
Realizar el cambio de estados automáticamente usando condiciones.
- Anteriormente hemos visto cómo configurar un árbol de animaciones con una máquina de estados manejada mediante código.
- En esta lección os voy a enseñar cómo automatizar todavía más nuestra máquina de estados utilizando condicionantes.
- Al hacerlo no necesitaremos decirle explícitamente qué animación debe reproducir sino que lo determinará automáticamente.
Configurando las transiciones:
- Empezaremos creando una transición del estado
Start
aIdle
, ésta se ejecutará automáticamente al principio.
- A continuación vamos a crear otra transición de
Idle
aWalk
. - El modo será automático y la condición una expresión
is_walking
. - Añadiremos otra transición de vuelta a
Idle
con la expresiónnot is_walking
.
- A continuación estableceremos el
AdvancedExpresionBaseNode
como nuestroCharacterBody2D
. - Al hacerlo le diremos que compruebe ahí la variable
is_walking
. - Solo tenemos que crearla y cambiarla cuando caminemos o dejemos de caminar:directamente los :
@export var is_walking := false
# Actualizamos los espacios de mezcla al detectar un movimiento
if direction != Vector2.ZERO:
animation_tree.set("parameters/Idle/blend_position", direction)
animation_tree.set("parameters/Walk/blend_position", direction)
# Activamos la transición al estado caminar
is_walking = true
else:
# O la desactivamos para volver al estado parado
is_walking = false
- Esto incluso se puede simplificar en una sola línea:
# Actualizamos los espacios de mezcla al detectar un movimiento
if direction != Vector2.ZERO:
animation_tree.set("parameters/Idle/blend_position", direction)
animation_tree.set("parameters/Walk/blend_position", direction)
# Establecemos la transición dependiendo de la longitud del vector dirección
is_walking = direction.length()
- El resultado final es el mismo pero tiene una ventaja clave.
- En lugar de hacer código dependiente de los estados del árbol de animaciones, ahora es el árbol de animaciones quien depende de nuestro código y eso nos da un control total a la hora de manejar el sistema de animaciones.
38. Cuadrículas con Tilemaps
En este lección vamos a aprender a manejar el escenario usando cuadriculas con el nodo Tilemap.
- Añadimos un Tilemap en la parte superior del escenario, lo configuramos a 64x64.
- Si no se ve el preview hacer clic en la escena y ya se vera bien las líneas.
- Vamos a crearle un nodo hijo
TileMapDebug
para debugear diferentes situaciones. - Añadirle un script para determinar la posición de clic y la celda:
extends TileMap
func _input(event):
if event.is_action_pressed("ui_click"):
# Obtiene la posición del mouse relativa al TileMap
var mouse_pos = get_local_mouse_position()
# Convierte la posición del mundo a coordenadas del mapa (celda)
var clicked_cell = local_to_map(mouse_pos)
print("Posición clicada: ", mouse_pos)
print("Celda clicada: ", clicked_cell)
- Es difícil hacernos una idea de lo que esta ocurriendo asi que vamos a debugear la cuadrícula dibujándola:
@export var debug := false
func _draw():
if debug:
var viewport_size = get_viewport_rect().size
var tile_size = tile_set.tile_size
# Color de la cuadrícula, gris claro
var color = Color(0.3, 0.3, 0.3)
# Dibujar líneas verticales
for x in range(0, viewport_size.x, tile_size.x):
draw_line(Vector2(x, 0), Vector2(x, viewport_size.y), color)
# Dibujar líneas horizontales
for y in range(0, viewport_size.y, tile_size.y):
draw_line(Vector2(0, y), Vector2(viewport_size.x, y), color)
- Con el debug en marcha ya es más fácil hacernos una idea de qué está manejando el nodo Tilemap.
- Pero aún podemos ir más un paso más allá y debugear la coordenada exacta de cada celda en base a su columna y fila.
@export var debug_font : Font
func _input(event):
if event.is_action_pressed("ui_text_indent"):
debug = not debug
queue_redraw() # Llamar de nuevo al método draw al presionar tab
# ...
func _draw():
if debug:
# ...
# Color del texto negro y tamaño en píxeles
var text_color = Color(0, 0, 0)
var text_size = 16
# Dibujar el texto
for x in range(0, viewport_size.x, tile_size.x):
for y in range(0, viewport_size.y, tile_size.y):
# Centrar el texto en la celda
var cell_position = Vector2(x, y + tile_size.y/2 + text_size/2)
var column = x / tile_size.x
var row = y / tile_size.y
# Formatea los números como strings de dos dígitos
var text = "(%0d,%0d)" % [column, row]
draw_string(debug_font, cell_position, text, 1, tile_size.x, text_size, text_color)
39. Seguimiento de celdas
En esta lección continuaremos el ejemplo anterior para identificar y debugear celdas especificas.
- Lo último que quiero hacer es resaltar la celda actual sobre la que el ratón haga clic:
@export var draw_mouse_cell : bool
var selected_mouse_cell := Vector2()
func _input(event):
# ...
if event.is_action_pressed("ui_click"):
# ...
# Establecer celda que clicamos
selected_mouse_cell = clicked_cell
queue_redraw() # llamar de nuevo al método draw
func _draw():
if debug:
# ...
# Si queremos dibujar la celda seleccionada
if draw_mouse_cell:
# Convertir coordenadas de celda a posición local
var cell_position = map_to_local(selected_mouse_cell) - Vector2(tile_size.x/2, tile_size.y/2)
# Color rojo semitransparente para resaltar
var highlight_color = Color(1, 0, 0, 0.5)
# Dibuja un rectángulo sobre la celda actual
draw_rect(Rect2(cell_position, tile_size), highlight_color, true)
- Y aplicando esta lógica incluso podemos resaltar la celda sobre la que se esta moviendo el personaje en todo momento, solo tenemos que resaltar la celda en función de su posición en lugar de la posición del mouse:
@export var draw_character_cell : bool
@export var character : CharacterBody2D
var current_character_cell := Vector2()
func _process(delta):
current_character_cell = local_to_map(character.position)
queue_redraw() # llamar de nuevo al método draw
func _draw():
if debug:
# ...
# Si queremos dibujar la celda bajo el personaje
if draw_character_cell:
# Convertir coordenadas de celda a posición local
var cell_position = map_to_local(current_character_cell) - Vector2(tile_size.x/2, tile_size.y/2)
# Color amarillo semitransparente para resaltar
var highlight_color = Color(1, 1, 0, 0.5)
# Dibuja un rectángulo sobre la celda actual
draw_rect(Rect2(cell_position, tile_size), highlight_color, true)
- Refactorizar el código final para hacerlo más entendible y escalable:
extends TileMap
@export var debug := false
@export var debug_font : Font
@export var draw_mouse_cell : bool
@export var draw_character_cell : bool
@export var character : CharacterBody2D
var selected_mouse_cell := Vector2()
var current_character_cell := Vector2()
func _process(delta):
# Actualiza la celda actual bajo el personaje
current_character_cell = local_to_map(character.position)
queue_redraw() # Solicita redibujar el TileMap
func _input(event):
toggle_debug_mode(event) # Cambia el modo de depuración
check_mouse_click(event) # Verifica si se hizo clic y actualiza la celda seleccionada
func toggle_debug_mode(event):
# Alterna el modo de depuración con una tecla
if event.is_action_pressed("ui_text_indent"):
debug = not debug
queue_redraw() # Solicita redibujar para aplicar cambios de depuración
func check_mouse_click(event):
# Detecta clics y actualiza la celda seleccionada
if event.is_action_pressed("ui_click"):
var mouse_pos = get_local_mouse_position()
var clicked_cell = local_to_map(mouse_pos)
# Imprime información de la celda
print_cell_info(mouse_pos, clicked_cell) if debug else null
selected_mouse_cell = clicked_cell
queue_redraw()
func print_cell_info(mouse_pos, cell):
# Imprime información sobre la celda clicada
print("Posición clicada: ", mouse_pos)
print("Celda clicada", cell, " Columna: ", cell.x, " Fila: ", cell.y)
func _draw():
# Dibuja elementos de depuración y resalta celdas según corresponda
draw_debug_grid_and_text() if debug else null
highlight_cell(selected_mouse_cell, Color(1, 0, 0, 0.5)) if draw_mouse_cell else null
highlight_cell(current_character_cell, Color(1, 1, 0, 0.5)) if draw_character_cell else null
func draw_debug_grid_and_text():
# Dibuja la cuadrícula de depuración y etiquetas de celdas
var viewport_size = get_viewport_rect().size
var tile_size = tile_set.tile_size
draw_grid(viewport_size, tile_size, Color(0.3, 0.3, 0.3))
draw_grid_text(viewport_size, tile_size, debug_font, 16, Color(0, 0, 0))
func draw_grid(viewport_size, tile_size, color):
# Dibuja líneas de cuadrícula
for x in range(0, viewport_size.x, tile_size.x):
draw_line(Vector2(x, 0), Vector2(x, viewport_size.y), color)
for y in range(0, viewport_size.y, tile_size.y):
draw_line(Vector2(0, y), Vector2(viewport_size.x, y), color)
func draw_grid_text(viewport_size, tile_size, font, text_size, color):
# Dibuja coordenadas de celdas como texto
for x in range(0, viewport_size.x, tile_size.x):
for y in range(0, viewport_size.y, tile_size.y):
var cell_position = Vector2(x, y + tile_size.y/2 + text_size/2)
var text = "(%02d,%02d)" % [x / tile_size.x, y / tile_size.y]
draw_string(font, cell_position, text, 1, tile_size.x, text_size, color)
func highlight_cell(cell, color):
# Resalta una celda específica
var cell_position = map_to_local(cell) - Vector2(tile_set.tile_size.x/2, tile_set.tile_size.y/2)
draw_rect(Rect2(cell_position, tile_set.tile_size), color, true)
- Repasad a fuego este experimento porque lo que os he enseñado es la base para crear videojuegos basados en retículas cuadradas, isométricas y hexagonales.
40. Navegación con Agente
Mover el personaje a una posición de la escena al hacer clic teniendo en cuenta las colisiones.
- Añadir un
NavigationRegion2D
con elTileMap
en su interior. - Ahora sobre el viewport creamos los polígonos y le damos a
Crear polígono de Navegación
para que genere el área de movimiento del agente. Esto hay que repetirlo si modifamos el tamaño del escenario o añadimos colisiones en el Tilemap tal como veremos en un futuro experimento. - Añadir al personaje un nodo
NavigationAgent2D
- Añadir una variable para controlar si se alcanza el punto de destino:
@export var is_target_reached:= true
- Configurar las dos formas de movernos:
func _physics_process(delta: float):
if not is_target_reached:
move_agent()
else:
move()
- Cuando presionamos el raton buscar la posición a la que movernos y establecerla como destino del agente:
func _input(event):
if event.is_action_pressed("ui_click"):
# Mover al personaje donde se hace clic
var mouse_pos = get_global_mouse_position()
navigation_agent_2d.target_position = mouse_pos
is_target_reached = false
- Programar la función de movimiento del agente, que es prácticamente calcada a una función de movimiento normal:
func move_agent():
# Recuperamos un vector normalizado en base a la siguiente posición del agente
var direction := (navigation_agent_2d.get_next_path_position() - global_position).normalized()
# Actualizamos los espacios de mezcla al detectar un movimiento
if direction != Vector2.ZERO:
animation_tree.set("parameters/Idle/blend_position", direction)
animation_tree.set("parameters/Walk/blend_position", direction)
# Establecemos la animación a partir de la longitud del vector dirección
is_walking = direction.length()
# Establecemos la velocidad y movemos al personaje
velocity = direction * speed
move_and_slide()
- Configuramos una señal en el agente
target_reached
para establecer el fin del movimiento automático:
func _on_navigation_agent_2d_target_reached() -> void:
is_target_reached = true
- Podemos reducir el
Path Desired Distance
a5
para que no se pase de largo en la cantidad de movimiento y activar elDebug > Enabled
para visualizar una línea hasta el destino.
- Si queremos que el personaje se desplace hasta justo el centro de la celda podemos rectificar la posición recuperando la posición de la celda y sumando el offset hasta el centro:
func _input(event):
if event.is_action_pressed("ui_click"):
# Obtener la posición del clic del ratón en coordenadas del mundo
var mouse_pos = get_global_mouse_position()
# Convertir esa posición a coordenadas locales del TileMap
var local_mouse_pos = tile_map.to_local(mouse_pos)
# Obtener las coordenadas de la celda donde se hizo clic
var clicked_cell = tile_map.local_to_map(local_mouse_pos)
# Convertir las coordenadas de la celda de vuelta a coordenadas del mundo
var cell_center_world_pos = tile_map.map_to_local(clicked_cell)
# Mover el NavigationAgent2D al centro de la celda clicada
navigation_agent_2d.target_position = cell_center_world_pos
is_target_reached = false
- Lo que hemos hecho es un movimiento libre pero también es posible realizar un movimiento basado en cuadricula.
- Sin embargo para hacerlo se necesita implementar un algoritmo de navegación llamado AStarGrid2D. No voy a enseñarlo en esta serie porque su uso es más avanzado y requiere desarrollar mucha lógica personalizada, me lo reservo para un futuro curso centrado en algun videojuego basado en cuadrícula.
41. Escenarios con Tilemaps
Diseñar un escenario utilizando los tiles en Godot._
- Vamos a diseñar un escenario utilizando las herramientas que nos proveen los Tilemaps.
- Os voy a facilitar una imagen
overworld.png
que contiene un tileset de un mundo, descardlo abajo. - Lo primero es borrar el nodo
ParallaxBackground
, ya no lo necesitaremos, crearemos el diseño enTilemap
. - Seleccionamos el nodo
TileMap
y vamos a la secciónTileSet
de la barra inferior. - Le damos a
+
en la parte inferior y creamos unAtlas
a partir de la imagenoverworld.png
. - Configuramos el tamaño de la región a
64x64
y manteniendo la teclaControl
seleccionaremos elTileset
completo para que permita utilizar todos los cuadros como tiles:
- Ahora debemos empezar a dibujar las capas del escenario en el apartado
Layers
delTileMap
. - A la primera capa le daremos el nombre
Fondo
. - Vamos al apartado
TileMap
en barra inferior y utilizamos las herramientas de dibujo para crear un prado verde con algun lago en una esquina o lo que queráis, esto solo serán elementos de fondo. - Podemos hacerlo seleccionando el primer tile (cuadro verde), dibujando los cuadros uno a uno o trazando rectángulos.
- Una vez lo tengamos veremos que al ejecutar el juego ya no se debugea la información de nuestra cuadrícula, esto
es porque el tilemap se dibuja por encima, para solucionarlo podemos crear otro nodo de tipo
Node2D
como hijo delTileMap
con el nombreTileMapDebug
. - Nota (Este paso deberia haberlo arreglado con anterioridad): Ahora vamos a desconectar el script del
TileMap
y lo vamos a añadir alTileMapDebug
, cambiando elextends
a unNode2D
y creando una referencia a su nodo padreTileMap
, ejecutando de él las funciones como `local_to_map``, etc.
extends Node2D # <---
@export var debug := false
@export var debug_font : Font
@export var draw_mouse_cell : bool
@export var draw_character_cell : bool
@onready var character: CharacterBody2D = $"../../../CharacterBody2D" # <---
@onready var tile_map: TileMap = $".." # <---
var selected_mouse_cell := Vector2()
var current_character_cell := Vector2()
func _process(delta):
# Actualiza la celda actual bajo el personaje
current_character_cell = tile_map.local_to_map(character.position)
queue_redraw() # Solicita redibujar el TileMap
func _input(event):
toggle_debug_mode(event) # Cambia el modo de depuración
check_mouse_click(event) # Verifica si se hizo clic y actualiza la celda seleccionada
func toggle_debug_mode(event):
# Alterna el modo de depuración con una tecla
if event.is_action_pressed("ui_text_indent"):
debug = not debug
queue_redraw() # Solicita redibujar para aplicar cambios de depuración
func check_mouse_click(event):
# Detecta clics y actualiza la celda seleccionada
if event.is_action_pressed("ui_click"):
var mouse_pos = get_local_mouse_position()
var clicked_cell = tile_map.local_to_map(mouse_pos)
# Imprime información de la celda
print_cell_info(mouse_pos, clicked_cell) if debug else null
selected_mouse_cell = clicked_cell
queue_redraw()
func print_cell_info(mouse_pos, cell):
# Imprime información sobre la celda clicada
print("Posición clicada: ", mouse_pos)
print("Celda clicada", cell, " Columna: ", cell.x, " Fila: ", cell.y)
func _draw():
# Dibuja elementos de depuración y resalta celdas según corresponda
draw_debug_grid_and_text() if debug else null
highlight_cell(selected_mouse_cell, Color(1, 0, 0, 0.5)) if draw_mouse_cell else null
highlight_cell(current_character_cell, Color(1, 1, 0, 0.5)) if draw_character_cell else null
func draw_debug_grid_and_text():
# Dibuja la cuadrícula de depuración y etiquetas de celdas
var viewport_size = get_viewport_rect().size
var tile_size = tile_map.tile_set.tile_size
draw_grid(viewport_size, tile_size, Color(0.3, 0.3, 0.3))
draw_grid_text(viewport_size, tile_size, debug_font, 16, Color(0, 0, 0))
func draw_grid(viewport_size, tile_size, color):
# Dibuja líneas de cuadrícula
for x in range(0, viewport_size.x, tile_size.x):
draw_line(Vector2(x, 0), Vector2(x, viewport_size.y), color)
for y in range(0, viewport_size.y, tile_size.y):
draw_line(Vector2(0, y), Vector2(viewport_size.x, y), color)
func draw_grid_text(viewport_size, tile_size, font, text_size, color):
# Dibuja coordenadas de celdas como texto
for x in range(0, viewport_size.x, tile_size.x):
for y in range(0, viewport_size.y, tile_size.y):
var cell_position = Vector2(x, y + tile_size.y/2 + text_size/2)
var text = "(%02d,%02d)" % [x / tile_size.x, y / tile_size.y]
draw_string(font, cell_position, text, 1, tile_size.x, text_size, color)
func highlight_cell(cell, color):
# Resalta una celda específica
var cell_position = tile_map.map_to_local(cell) - Vector2(tile_map.tile_set.tile_size.x/2, tile_map.tile_set.tile_size.y/2)
draw_rect(Rect2(cell_position, tile_map.tile_set.tile_size), color, true)
- Una vez tengamos el
Debug
funcionando vamos a añadir más capas por encima de nuestroTileMap
, por ejemplo una capa llamadaFondo Adornos
que por defecto aparecerá por encima delFondo
. - Si volvemos al
TileMap
ahora podemos dibujar flores y pequeñas piedras selecionando dicha capa, añadid solo elementos que el personaje pueda pisar, dejad los márgenes de la escena libres por ahora.
- Ahora vamos a añadir una nueva capa llamada
Obstáculos
, esta capa la vamos a utilizar de límite para que el personaje no pueda cruzar la zona. Lo haremos con árboles, troncos y piedras grandes:
- Como véis la clave consiste en dibujar primero las capas del fondo y luego ir añadiendo más por encima.
- En la próxima práctica acabaremos de pulir el experimento añadiendo las colisiones.
42. Colisiones en Tilemaps
Implementar las colisiones entre el personaje, los obstáculos, edificios, etc..
- Para añadir colisiones a nuestro
TileMap
el primer paso es crear una capa de físicasPhysics Layers
en el propioTileSet
, para ello lo tenemos que desplegarlo. - Una vez añadida iremos al apartado
TileSet > Pintar
y enPropiedades de Pintura
seleccionaremosCapa de Fisica 0
. - Ahora podremos pintar sobre los tiles las áreas de colisión que queramos. Dibujad áreas a la piedra, al árbol y a la casa, pero dejad la puerta de la casa libre:
- Ahora añadimos una
CollisionShape2D
circular al personaje, de20px
justo en su centro, esto es importante:
- Y listo, las colisiones están implementadas y podemos movernos con las teclas por el escenario:
- Por defecto el
CharacterBody2D
detecta cuando hay alguna colisión cuerpos físicos en los tiles y ahí sucede la magia. - Sin embargo nuestro agente de navegación no funcionará correctamente en este punto.
- Para que funcione necesitamos que la
NavigationRegion2D
detecte los tiles con físicas y genere una malla de navegación específica. - Si presionamos el botón "Crear polígono de navegación" no funcionará porque éste solo detecta colisiones en la primera capa del
TileMap
. - En otras palabras, necesitamos establecer al principio de todo la capa
Obstáculos
para que concuerde con la primera posición:
- EL problema es que al hacerlo dejaremos de ver los obstáculos en la pantalla. Podemos arreglar esto dibujando la capa
Obstáculos
por encima de las otras aunque realmente sea la primera. - Esto lo conseguiremos cambiando el
Z-Index
de cada capa manualmente, por ejemploObstáculos - 2
,Fondo - 0
,Fondo Adornos - 1
:
- Dado que hemos cambiado la profundidad de dibujado de las capas vamos a configurar al personaje para que se dibuje por encima de ellas estableciendo al
CharacterBody2D
unOrdering > Z-Index
más alto, por ejemplo de10
. - En este punto si seleccionamos el
NavigationRegion2D
yRecreamos el polígono de navegación
veremos que se crea una región por la que se puede mover el personaje al hacer clic.
- Si probamos el juego nuestro personaje se quedará atorado en las esquinas de las colisiones:
- Para solucionarlo vamos a configurar el radio de los agentes de navegación del
NavigationRegion2D > Agents
a20px
, lo mismo que mide el círculo de nuestra colisión. - Volvemos a
recrear el polígono de navegación
, ahora se verá que queda menos espacio entre los tiles:
- ¡Y problema solucionado!
- Espero que os haya gustado esta introducción a los Tilemaps y la navegación con agentes.
- Con esto acabamos el capítulo de experimentos interactivos y damos paso a uno de experimentos con interfaces gráficas, os espero!