tutoriales.com

Creando un Sistema de Detección de Colisiones Avanzado en Godot 4: Precisión y Respuesta

Este tutorial te guiará a través de la implementación de un sistema de detección de colisiones robusto y avanzado en Godot 4 para juegos 2D. Aprenderás a utilizar los nodos clave como Area2D, StaticBody2D, RigidBody2D, y a configurar sus CollisionShapes para una interacción precisa. Cubriremos cómo manejar señales de colisión, grupos de colisión, capas de máscara y a crear lógica de juego compleja basada en estas interacciones.

Intermedio18 min de lectura28 views
Reportar error

La detección de colisiones es la columna vertebral de casi cualquier juego. Sin ella, tus personajes no podrían interactuar con el mundo, los proyectiles pasarían a través de los enemigos, y los objetos caerían infinitamente al vacío. En Godot 4, el sistema de físicas y colisiones es increíblemente potente y flexible, permitiéndote crear interacciones complejas con gran precisión.

Este tutorial te sumergirá en el corazón de la detección de colisiones en 2D, explorando los diferentes nodos de cuerpo de física, sus propiedades, y cómo puedes utilizarlos para dar vida a tus mecánicas de juego. ¡Prepárate para hacer que tus objetos interactúen de maneras sorprendentes y controladas!


🚀 Entendiendo los Nodos de Colisión 2D en Godot 4

Godot 4 proporciona varios nodos clave para manejar colisiones y físicas en un entorno 2D. Cada uno tiene un propósito específico y es crucial entender sus diferencias para utilizarlos eficazmente.

StaticBody2D: El Terreno Inamovible

El StaticBody2D es ideal para objetos que no se mueven ni son afectados por la física, como paredes, suelos, o cualquier parte del entorno que sea estática. No responde a la gravedad ni a las fuerzas externas, pero otros cuerpos pueden colisionar con él y ser afectados por su presencia. Piensa en ellos como la base sólida de tu mundo.

💡 Consejo: Usa StaticBody2D para optimizar el rendimiento. Al ser estáticos, el motor de físicas no necesita recalcular su posición o aplicar fuerzas en cada frame, lo que reduce la carga de CPU.

RigidBody2D: La Física Dinámica

El RigidBody2D es el nodo por excelencia para objetos que deben comportarse de forma realista bajo las leyes de la física. Responden a la gravedad, fuerzas, torques y colisiones con otros cuerpos. Puedes controlar su modo (Rígido, Carácter, Coche, o Congelado) para ajustar su comportamiento. Son perfectos para cajas que caen, enemigos que son empujados, o proyectiles.

Cuando un RigidBody2D colisiona con otro objeto, las fuerzas de la colisión se calculan y aplican automáticamente, afectando la velocidad y rotación de ambos cuerpos.

Propiedad ComúnDescripción
------
modeDefine cómo se comporta el cuerpo (Rigid, Character, Vehicle, Kinematic).
massLa masa del cuerpo. Afecta la inercia y la respuesta a las fuerzas.
------
frictionFricción de la superficie del cuerpo.
bounceCoeficiente de restitución (rebote) del cuerpo.
------
gravity_scaleEscala la cantidad de gravedad que afecta a este cuerpo.

CharacterBody2D: El Control Preciso de tu Jugador

Originalmente conocido como KinematicBody2D en versiones anteriores, el CharacterBody2D es el nodo preferido para controlar personajes. A diferencia del RigidBody2D, su movimiento no es impulsado por el motor de físicas, sino que tú lo controlas directamente a través de código. Sin embargo, aún interactúa con el mundo físico, y Godot te proporciona funciones como move_and_slide() para manejar las colisiones y deslizamientos de forma robusta y sin complicaciones. Es la elección ideal para tu jugador o NPCs complejos.

🔥 Importante: La principal diferencia entre RigidBody2D y CharacterBody2D radica en quién mueve el cuerpo. RigidBody2D es movido por la física; CharacterBody2D es movido por tu código.

Area2D: Detectando Sin Interactuar Físicamente

El Area2D es un tipo especial de nodo de colisión que detecta la superposición con otros cuerpos o áreas, pero no interactúa físicamente con ellos. Es decir, no aplicará fuerzas ni impedirá el movimiento de otros objetos. Son perfectos para disparadores (triggers), zonas de daño, recolectables, o áreas de detección de enemigos.

Cuando un Area2D superpone a otro cuerpo (StaticBody2D, RigidBody2D, CharacterBody2D) o a otra Area2D, emite señales que puedes conectar y utilizar para ejecutar lógica de juego. Esto es increíblemente potente para crear interacciones sin simulación física.


📐 CollisionShape2D y CollisionPolygon2D: Definiendo la Forma

Todos los nodos de cuerpo de física y Area2D necesitan un nodo hijo de forma para definir su área de colisión. Sin una forma, el cuerpo no puede detectar ni responder a colisiones. Los más comunes son CollisionShape2D y CollisionPolygon2D.

CollisionShape2D

Este nodo te permite asignar una de las formas primitivas predefinidas:

  • RectangleShape2D: Un rectángulo. Ideal para la mayoría de los objetos cuadrados o rectangulares.
  • CircleShape2D: Un círculo. Perfecto para personajes redondos, proyectiles o radios de efecto.
  • CapsuleShape2D: Una cápsula. Muy útil para personajes humanoides, ya que permite un movimiento suave al deslizarse por paredes.
  • ConvexPolygonShape2D: Para formas convexas más complejas. Puedes dibujarla manualmente.
  • ConcavePolygonShape2D: Similar al anterior pero para formas cóncavas. Ten cuidado, ya que puede ser más costoso en rendimiento.
📌 Nota: Puedes tener múltiples CollisionShape2D hijos en un mismo cuerpo para crear formas de colisión complejas. Sin embargo, para rendimiento, intenta usar la forma más simple posible que se ajuste a tus necesidades.

CollisionPolygon2D

Si necesitas una forma de colisión muy específica que no se ajuste a las primitivas, CollisionPolygon2D te permite dibujar un polígono a mano. Es excelente para terrenos irregulares, plataformas con ángulos extraños o enemigos con formas únicas. Simplemente selecciona el nodo y usa las herramientas del editor 2D para dibujar los vértices de tu polígono.


🛠️ Configurando Capas y Máscaras de Colisión (Collision Layers & Masks)

Las capas y máscaras de colisión son un concepto fundamental en Godot para controlar quién colisiona con quién. Imagina que tienes diferentes tipos de objetos en tu juego (jugador, enemigos, proyectiles, objetos recolectables) y no todos deben interactuar entre sí. Las capas y máscaras te permiten especificar estas interacciones de manera granular.

¿Qué son las Capas (Layers)?

Cada cuerpo de colisión se puede asignar a una o varias capas. Piensa en las capas como categorías a las que pertenece un objeto. Por defecto, todos los objetos están en la capa 1. Puedes nombrar las capas en Project Settings -> Layer Names -> 2D Physics para hacerlas más legibles.

Por ejemplo:

  • Capa 1: "Jugador"
  • Capa 2: "Enemigos"
  • Capa 3: "Proyectiles"
  • Capa 4: "Entorno"

¿Qué son las Máscaras (Masks)?

La máscara de un cuerpo de colisión define con qué otras capas colisionará o detectará. Si la capa de un objeto coincide con una de las máscaras de otro objeto, se producirá una colisión (o detección).

En otras palabras:

  • Un objeto está en una o varias capas. (¿Quién soy?)
  • Un objeto mira a una o varias máscaras. (¿Con quién quiero colisionar/detectar?)

Ejemplo Práctico:

Supongamos que tenemos:

  • Jugador: Capa "Jugador" (1), Máscara "Enemigos" (2) y "Entorno" (4).
  • Enemigo: Capa "Enemigos" (2), Máscara "Jugador" (1) y "Proyectiles" (3).
  • Proyectil: Capa "Proyectiles" (3), Máscara "Enemigos" (2).
  • Muro (StaticBody2D): Capa "Entorno" (4), Máscara "Jugador" (1), "Enemigos" (2) y "Proyectiles" (3).

Con esta configuración:

  • El Jugador colisionará con Enemigos y Entorno.
  • El Enemigo colisionará con el Jugador y será afectado por los Proyectiles.
  • El Proyectil solo colisionará con los Enemigos (lo atravesaría todo lo demás).
  • El Muro será un obstáculo para el Jugador, los Enemigos y los Proyectiles.
⚠️ Advertencia: Una confusión común es pensar que la capa del objeto *debe* coincidir con la capa del otro objeto. No es así. La detección ocurre si la *máscara* de un objeto tiene activada la *capa* del otro objeto.
Cuerpo A Capa 1 Máscara 2 Cuerpo B Capa 2 Máscara 1 Cuerpo C Capa 3 Máscara 1 Máscara A cubre Capa B Máscara B cubre Capa A Sin interacciones externas

✨ Manejo de Señales de Colisión

Las señales son el mecanismo principal en Godot para reaccionar a eventos, y las colisiones no son una excepción. Los nodos de cuerpo de física y Area2D emiten señales cuando ocurren interacciones. Conectar estas señales a tus scripts te permite implementar la lógica de juego necesaria.

Señales de Area2D

El Area2D es el más versátil para la detección sin física. Emite las siguientes señales:

  • body_entered(body: Node2D): Se emite cuando un StaticBody2D, RigidBody2D o CharacterBody2D entra en la Area2D.
  • body_exited(body: Node2D): Se emite cuando un cuerpo sale de la Area2D.
  • area_entered(area: Area2D): Se emite cuando otra Area2D entra en esta Area2D.
  • area_exited(area: Area2D): Se emite cuando otra Area2D sale de esta Area2D.

Ejemplo con Area2D y body_entered

Imagina que tienes una moneda (Area2D) que tu jugador (CharacterBody2D) puede recolectar. El script de la moneda (Coin.gd) podría ser así:

# Coin.gd
extends Area2D

func _on_body_entered(body: Node2D) -> void:
    if body.name == "Player": # Asegúrate de que solo el jugador la recolecte
        print("Moneda recolectada por el jugador!")
        # Aquí iría la lógica para sumar puntos al jugador
        queue_free() # Elimina la moneda del juego

Para que esto funcione, en el editor de Godot, selecciona tu nodo Area2D (la moneda), ve a la pestaña Node, selecciona la señal body_entered, haz doble clic y conéctala a tu script Coin.gd.

Señales de RigidBody2D y CharacterBody2D

RigidBody2D emite la señal body_shape_entered (y su contraparte exited) y body_entered (y exited). CharacterBody2D también tiene señales similares, pero la forma más común de manejar colisiones con un CharacterBody2D es a través de sus funciones de movimiento.

Ejemplo con CharacterBody2D y move_and_slide()

Cuando usas move_and_slide() en un CharacterBody2D, esta función no solo mueve el cuerpo y lo desliza por las colisiones, sino que también devuelve un KinematicCollision2D por cada colisión ocurrida durante el movimiento. Puedes iterar sobre estas colisiones para obtener información detallada.

# Player.gd (para un CharacterBody2D)
extends CharacterBody2D

const SPEED = 100.0
const JUMP_VELOCITY = -200.0

# ... (gravedad, etc.)

func _physics_process(delta: float) -> void:
    var direction = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
    velocity.x = direction.x * SPEED

    # Manejo de salto y gravedad
    if not is_on_floor():
        velocity.y += gravity * delta

    if Input.is_action_just_pressed("ui_accept") and is_on_floor():
        velocity.y = JUMP_VELOCITY

    # Mover y deslizar, obteniendo información de colisiones
    move_and_slide()

    # Iterar sobre las colisiones ocurridas durante el movimiento
    for i in get_slide_collision_count():
        var collision = get_slide_collision(i)
        print("Colisionó con: ", collision.get_collider().name)
        print("Normal de la colisión: ", collision.get_normal())
        if collision.get_collider().is_in_group("Enemies"): # Reacciona si colisiona con un enemigo
            print("¡El jugador ha chocado con un enemigo!")
            # Aquí iría la lógica de daño al jugador, empuje, etc.

Este método te da un control muy fino sobre cómo reaccionar a las colisiones, ya que tienes acceso al objeto con el que colisionaste, la normal de la colisión (útil para rebotar o determinar la superficie), y más.


🎯 Grupos de Colisión: Una Forma Sencilla de Clasificar Objetos

Los grupos de colisión son una manera simple y efectiva de categorizar tus nodos en el juego. Aunque las capas y máscaras son para la interacción de físicas, los grupos son más para la lógica de juego. Puedes añadir cualquier nodo a uno o varios grupos y luego verificar si un nodo pertenece a un grupo determinado desde el código.

Cómo Usar Grupos

  1. En el Editor: Selecciona un nodo en la escena, ve a la pestaña Node en el Inspector, luego a Groups. Escribe el nombre del grupo y haz clic en Add.
  2. Por Código: Puedes añadir o eliminar nodos de grupos en tiempo de ejecución usando add_to_group("nombre_grupo") y remove_from_group("nombre_grupo").

Ventajas de los Grupos:

  • Fácil Verificación: node.is_in_group("nombre_grupo") es muy útil para comprobar el tipo de objeto con el que colisionaste.
  • Iteración Sencilla: Puedes obtener todos los nodos de un grupo usando get_tree().get_nodes_in_group("nombre_grupo"). Esto es útil para aplicar efectos a todos los enemigos en pantalla, por ejemplo.

Ejemplo: Detección de Colisión con Grupos

Volviendo al ejemplo del proyectil que daña a los enemigos. Podrías tener un Area2D para el proyectil que detecta cuerpos. En lugar de verificar body.name == "Enemy" (lo cual es frágil si cambias nombres), puedes usar grupos:

# Bullet.gd (Un Area2D)
extends Area2D

var damage = 10

func _on_body_entered(body: Node2D) -> void:
    if body.is_in_group("Enemies"):
        print("¡Impacto en enemigo!")
        # Suponiendo que tus enemigos tienen un método 'take_damage'
        body.take_damage(damage)
        queue_free() # El proyectil se destruye al impactar

Asegúrate de que tus nodos de enemigo estén añadidos al grupo "Enemies" en el editor.

💡 Consejo: Usa una combinación de capas/máscaras para filtrar las colisiones a nivel físico y grupos para la lógica de juego específica. Esto es más eficiente que solo usar grupos para todo.

🚶‍♂️ Creando un Personaje Jugable con CharacterBody2D y Colisiones

Vamos a poner en práctica lo aprendido creando un personaje simple que se mueve y salta, interactuando con el entorno.

Estructura de la Escena del Jugador

  1. Crea una nueva escena 2D.
  2. Añade un nodo CharacterBody2D y renómbralo a Player.
  3. Añade un nodo Sprite2D como hijo de Player y asigna una textura (un icono de Godot o cualquier imagen temporal).
  4. Añade un nodo CollisionShape2D como hijo de Player. En el Inspector, selecciona New CapsuleShape2D para Shape. Ajusta el tamaño de la cápsula para que se adapte a tu Sprite2D.

Script Player.gd

# Player.gd
extends CharacterBody2D

@export var speed: float = 150.0 # Velocidad de movimiento del jugador
@export var jump_velocity: float = -300.0 # Fuerza de salto
@export var gravity: float = 800.0 # Gravedad del mundo

func _physics_process(delta: float) -> void:
    # Aplicar gravedad
    if not is_on_floor():
        velocity.y += gravity * delta

    # Manejar salto
    if Input.is_action_just_pressed("jump") and is_on_floor():
        velocity.y = jump_velocity

    # Obtener entrada de movimiento horizontal
    var input_direction_x = Input.get_axis("move_left", "move_right")
    if input_direction_x != 0:
        velocity.x = input_direction_x * speed
    else:
        velocity.x = move_toward(velocity.x, 0, speed * delta * 5) # Deceleración suave

    # Mover el personaje y manejar colisiones
    move_and_slide()

    # Procesar colisiones ocurridas durante el movimiento
    for i in get_slide_collision_count():
        var collision = get_slide_collision(i)
        # Aquí puedes añadir lógica específica para cada tipo de colisión
        # Por ejemplo, si colisiona con un objeto en el grupo "DamageDealer"
        if collision.get_collider().is_in_group("DamageDealer"):
            print("¡El jugador ha recibido daño!")
            # Lógica para reducir la vida del jugador, aplicar empuje, etc.
        elif collision.get_collider().is_in_group("Collectible"):
            print("¡Jugador recolectó algo!")
            # collision.get_collider().collect() # Llamar a un método del objeto recolectable
📌 Nota: Para las acciones `jump`, `move_left`, `move_right`, debes configurarlas en `Project Settings -> Input Map`.

Creando el Entorno

Para probar el jugador, crea una escena Main y añade:

  1. Un Node2D como raíz.
  2. Tu escena Player instanciada.
  3. Varios StaticBody2D con RectangleShape2D como suelos y paredes para que el jugador pueda moverse y saltar sobre ellos.

⚔️ Creando Interacciones Más Complejas: Proyectiles y Daño

Ahora, llevemos esto un paso más allá. Implementaremos un proyectil que el jugador puede disparar y que, al colisionar con un enemigo, le causa daño.

Escena del Proyectil (Bullet.tscn)

  1. Crea una nueva escena 2D para el proyectil.
  2. Añade un nodo Area2D y renómbralo a Bullet.
  3. Añade un Sprite2D como hijo y asígnale una textura (un pequeño círculo o cuadrado).
  4. Añade un CollisionShape2D como hijo de Bullet. Dale una RectangleShape2D pequeña o CircleShape2D.
  5. Asegúrate de que la Area2D del proyectil esté en la Capa "Proyectiles" y su Máscara detecte la Capa "Enemigos".
  6. Conecta la señal body_entered de Bullet a un nuevo script Bullet.gd.

Script Bullet.gd

# Bullet.gd
extends Area2D

@export var speed: float = 300.0
@export var damage: int = 20
var direction: Vector2 = Vector2.RIGHT # Dirección inicial, se puede cambiar al instanciar

func _physics_process(delta: float) -> void:
    position += direction * speed * delta

func _on_body_entered(body: Node2D) -> void:
    if body.is_in_group("Enemies"):
        print("¡Proyectil golpeó a un enemigo!")
        # Llama a una función de daño en el enemigo
        if body.has_method("take_damage"):
            body.take_damage(damage)
        queue_free() # Destruye el proyectil al impactar
    elif body.is_in_group("Environment"):
        # Si el proyectil golpea el entorno, también se destruye
        queue_free()

Modificando el Jugador para Disparar (Player.gd)

Vamos a añadir la capacidad de disparar al jugador. Necesitamos precargar la escena del proyectil y crear una posición de disparo.

# Player.gd (continuación)

# ... (variables existentes)

@export var shoot_cooldown: float = 0.5 # Tiempo entre disparos
var can_shoot: bool = true

# Precarga la escena del proyectil
const BULLET_SCENE = preload("res://Bullet.tscn")

# Añade una posición de disparo (Node2D) a la escena del Player en el editor
# y arrástralo aquí o usa get_node("BulletSpawnPoint")
@onready var bullet_spawn_point: Node2D = $BulletSpawnPoint

func _physics_process(delta: float) -> void:
    # ... (código de movimiento y salto existente)

    # Manejar disparo
    if Input.is_action_just_pressed("shoot") and can_shoot:
        shoot()
        can_shoot = false
        # Iniciar temporizador para el cooldown
        get_tree().create_timer(shoot_cooldown).timeout.connect(func(): can_shoot = true)

    # Mover y deslizar
    move_and_slide()

    # ... (procesar colisiones)

func shoot() -> void:
    var bullet_instance = BULLET_SCENE.instantiate() as Area2D
    get_parent().add_child(bullet_instance) # Añadir bala a la escena principal
    bullet_instance.global_position = bullet_spawn_point.global_position
    bullet_instance.direction = Vector2(1,0) # Asigna la dirección (ej. hacia la derecha)
    # Si tu jugador tiene facing_right o alguna variable similar, úsala para la dirección
    # bullet_instance.direction = Vector2(scale.x, 0).normalized()

Para que el bullet_spawn_point funcione, añade un nodo Node2D como hijo de tu Player en la escena y renómbralo a BulletSpawnPoint. Arrástralo a la posición donde quieres que salgan los proyectiles.

Creando un Enemigo (Enemy.tscn)

  1. Crea una nueva escena 2D para el enemigo.
  2. Añade un CharacterBody2D (o RigidBody2D si quieres que sea empujado) y renómbralo a Enemy.
  3. Añade un Sprite2D y una textura.
  4. Añade un CollisionShape2D y ajusta su forma.
  5. IMPORTANTE: Asegúrate de que el nodo Enemy esté en el grupo "Enemies" (en la pestaña Node del Inspector) y en la Capa "Enemigos". Su Máscara debe detectar la Capa "Jugador" y "Proyectiles" si quieres que interactúe con ellos.
  6. Añade un script Enemy.gd.

Script Enemy.gd

# Enemy.gd
extends CharacterBody2D # O RigidBody2D si lo configuraste así

@export var max_health: int = 100
var current_health: int

func _ready() -> void:
    current_health = max_health

func take_damage(amount: int) -> void:
    current_health -= amount
    print("Enemigo recibió ", amount, " de daño. Salud restante: ", current_health)
    if current_health <= 0:
        die()

func die() -> void:
    print("¡El enemigo ha sido derrotado!")
    queue_free()

# ... (aquí puedes añadir lógica de movimiento para el enemigo)

func _physics_process(delta: float) -> void:
    if not is_on_floor(): # Si es CharacterBody2D
        velocity.y += 800 * delta # Gravedad de ejemplo
    move_and_slide() # Si es CharacterBody2D

Ahora, cuando el jugador dispare, los proyectiles colisionarán con los enemigos, les harán daño y se destruirán. ¡Has creado un sistema de combate básico usando colisiones!


🔍 Debugging y Visualización de Colisiones

Trabajar con colisiones puede ser complicado si no puedes ver lo que está pasando. Godot tiene excelentes herramientas de depuración.

Visualizar Formas de Colisión

En la barra de herramientas del editor 2D, hay un icono con dos cuadros superpuestos (Generalmente Debug -> Visible Collision Shapes). Haz clic en él para activar la visualización de las formas de colisión. Verás los contornos de tus CollisionShape2D y CollisionPolygon2D directamente en la ventana de juego, lo que es invaluable para ajustar su tamaño y posición.

Godot Engine - Vista 2D Escena ● Jugador (Kinematic) └ CollisionShape2D ● Suelo (StaticBody2D) └ CollisionShape2D CapsuleShape2D RectangleShape2D Inspector Transform Posición: (270, 140) Rotación: 0° Collision Capa: 1 Máscara: 1 Jugador (2D)

Inspector de Nodos Remoto

Mientras el juego está en ejecución, puedes usar el Inspector de Nodos Remoto (debajo de la pestaña Scene en el editor) para ver el estado actual de tus nodos en la jerarquía del juego. Puedes inspeccionar las propiedades de los cuerpos de física en tiempo real, lo que te ayuda a entender por qué algo no se mueve o colisiona como esperas.

¿Por qué mi objeto no colisiona? Posibles razones:
  • No tiene un `CollisionShape2D` o `CollisionPolygon2D` hijo.
  • Sus capas y máscaras no están configuradas correctamente.
  • El nodo padre no es el tipo de cuerpo de física adecuado para la interacción deseada (ej. un `Node2D` no colisionará).
  • Está usando un `Area2D` y espera una interacción física (recuerda, `Area2D` solo detecta, no impide el movimiento).
  • El `CollisionShape2D` es demasiado pequeño o está desplazado. Activa la visualización de colisiones para verificarlo.
Dominando Colisiones

🌟 Conclusión

La detección de colisiones es un aspecto fundamental del desarrollo de juegos en Godot. Al comprender la diferencia entre StaticBody2D, RigidBody2D, CharacterBody2D y Area2D, y al dominar la configuración de CollisionShape2D, capas y máscaras, y el manejo de señales y grupos, tienes las herramientas para crear interacciones complejas y un mundo de juego dinámico.

Experimenta con las propiedades de rebote, fricción y gravedad para ver cómo afectan el comportamiento de tus objetos. No subestimes el poder de las señales y la información que move_and_slide() puede proporcionarte. Con práctica, serás un maestro en la creación de mundos interactivos y responsivos.

¡Ahora tienes una base sólida para llevar tus juegos 2D en Godot 4 a un nuevo nivel de interactividad y realismo!

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!