Introducción a la física

En el desarrollo de videojuegos, muchas veces necesitarás saber cuando dos objetos se superponen o entran en contacto. Esto es conocido como detección de colisiones. Cuando una colisión es detectada, normalmente es deseable que algo suceda, esto es llamado respuesta a la colisión.

Godot ofrece varios objetos de colisión en 2D y 3D para proveer tanto detección como respuesta a una colisión. Tratar de decidir cuál utilizar para tu proyecto puede ser confuso. Puedes evitar problemas y simplificar el desarrollo si entiendes como funciona cada uno y cuáles son sus ventajas y desventajas.

En esta guía aprenderás:

  • Los cuatro tipos de objetos de colisión en Godot

  • Cómo funciona cada tipo de objeto de colisión

  • Cuándo y por qué seleccionar un tipo en lugar de otro

Nota

Los ejemplos de este documento utilizarán objetos 2D. Cada objeto 2D, físico y forma de colisión (CollisionShape) tiene su equivalente 3D y en la mayoría de los casos funcionan del mismo modo.

Objetos de colisión

Godot ofrece cuatro tipos de objetos físicos, que extienden CollisionObject2D:

  • Area2D

    Los nodos Area2D proveen detección e Influencia. Estos pueden detectar cuando objetos se superponen a ellos y emitir señales cuando cuerpos físicos entran o salen. Un Area2D también puede usarse para superponer o reemplazar propiedades físicas como gravedad o resistencia en una zona definida.

Los otros tres cuerpos físicos extienden de PhysicsBody2D:

  • StaticBody2D

    Un cuerpo estático es uno que no es movido por el motor de física. Participa en detección de colisiones pero no se mueve en respuesta a la colisión. Son normalmente utilizados para objetos que son parte del entorno y no necesitan ningún comportamiento dinámico.

  • RigidBody2D

    Este es el nodo que implementa simulación de física 2D. Tú no controlas un RigidBody2D directamente, en lugar de ello, aplicas fuerzas (gravedad, impulsos, etc.) y el motor de física calcula el movimiento resultante. Para más información sobre el uso cuerpos rígidos, ver aquí.

  • KinematicBody2D

    Es un cuerpo que provee detección de colisiones, pero no física. Todo movimiento y respuestas a colisiones debe ser implementado en código.

Material de físicas

Los cuerpos estáticos y rígidos se pueden configurar para usar un: ref: physics material <class_PhysicsMaterial>. Esto permite ajustar la fricción y el rebote de un objeto, y establecer si es absorbente y / o áspero.

Figuras de Colisión

Un cuerpo físico puede contener cuaquier número de objetos Shape2D como hijos. Estas figuras o shapes, son usadas para definir los límites de colisión y detectar contactos con otros objetos físicos.

Nota

Para detectar colisiones, al menos un recurso derivado de Shape2d debe ser asignado al objeto.

El modo más común de asignar un "shape" es agregando un CollisionShape2D o CollisionPolygon2D como hijo del objeto. Esos nodos permiten dibujar la forma directamente en el espacio de trabajo.

Importante

Ten cuidado de nunca escalar las formas de colisión (CollisionShape) en el editor. La propiedad "scale" en el inspector debe mantenerse (1,1). Cuando se cambia el tamaño del nodo correspondiente a la figura de colisión, siempre se deben utilizar los controles para cambiar tamaño, no los controles de Node2D para escala. Escalar la forma de colisión puede resultar en un comportamiento no esperado.

../../_images/player_coll_shape1.png

Llamada interna para procesamiento de física

El motor de física puede generar múltiples hilos para mejorar el rendimiento, entonces puede utilizar más de un frame para procesar la física. A raíz de esto, el valor del estado de un cuerpo como position o linear_velocity puede no ser preciso para el frame actual.

Para evitar esta inexactitud, cualquier código que necesite acceder a las propiedades de un cuerpo debe ser ejecutado en la llamada ref:Node._physics_process() <class_Node_method__physics_process>, que se llama antes de cada paso de la física a una velocidad de cuadro constante (60 veces por segundo por defecto). A este método se le pasará un parámetro delta, que es un número de punto flotante igual al tiempo transcurrido en segundos desde el último paso. Cuando se utiliza la tasa de actualización de la física de 60 Hz por defecto, será comúnmente igual a 0.01666... (pero no siempre, ver más abajo).

Nota

Se recomienda utilizar siempre el parámetro delta cuando sea relevante en tus cálculos de físicas, para que así el juego se comporte correctamente si cambias la tasa de actualización de las físicas o si el dispositivo del jugador no puede mantenerlo.

Capas y Máscaras de Colisión

Una de las características más poderosas, aunque frecuentemente malentendidas, de las colisiones es el sistema de capa de colisión. Este sistema permite crear interacciones complejas entre una variedad de objetos. Los conceptos clave son layers (capas) y masks (mascaras). Cada CollisionObject2D tiene 20 capas físicas diferentes con las que se puede interactuar.

Veamos cada una de las propiedades:

  • collision_layer

    Esta describe las capas en las que el objeto aparece. Por defecto, todos los cuerpos están en la capa 1.

  • collision_mask

    Esto describe en qué capas el cuerpo buscará colisiones. Si un objeto no está en una de las máscaras, el cuerpo lo ignorará. Por defectos, todos los cuerpos buscan la capa 1.

Estas propiedades pueden ser configuradas por código o editadas mediante el Inspector.

Mantener un seguimiento de cada capa usada puede ser complicado, así que puede ser útil asignar nombres a cada una de las capas utilizadas. Los nombres pueden ser asignados en Ajustes del Proyecto -> Layer Names.

../../_images/physics_layer_names.png

Ejemplo de GUI

Tienes 4 tipos de nodos en tu juego: Walls (Paredes), Player (Jugador), Enemy (Enemigo) y Coin (Monedas). Player y Enemy deben colisionar con Wall. El nodo Player debe detectar colisiones tanto de Enemy como de Coin, pero Enemy y coin deben ignorarse mutuamente.

Comienza nombrando las capas 1 a 4 "walls", "player", "enemies", y "coins" y coloca cada nodo en su respectiva propiedad "layer". Luego asigna cada propiedad "mask" seleccionando las capas con las que debe interactuar. Por ejemplo Player debería verse así:

../../_images/player_collision_layers.png ../../_images/player_collision_mask.png

Ejemplo de código

En las llamadas de función, las capas se especifican como una máscara de bits. Cuando una función habilita todas las capas por defecto, la máscara de capa se dará como 0x7fffffff. Tu código puede utilizar la notación binaria, hexadecimal o decimal para las máscaras de capa, dependiendo de tu preferencia.

El código equivalente del ejemplo anterior en el que se habilitaron las capas 1, 3 y 4 sería el siguiente:

# Example: Setting mask value for enabling layers 1, 3 and 4

# Binary - set the bit corresponding to the layers you want to enable (1, 3, and 4) to 1, set all other bits to 0.
# Note: Layer 20 is the first bit, layer 1 is the last. The mask for layers 4,3 and 1 is therefore
0b00000000000000001101
# (This can be shortened to 0b1101)

# Hexadecimal equivalent (1101 binary converted to hexadecimal)
0x000d
# (This value can be shortened to 0xd)

# Decimal - Add the results of 2 to the power of (layer to be enabled - 1).
# (2^(1-1)) + (2^(3-1)) + (2^(4-1)) = 1 + 4 + 8 = 13
pow(2, 1) + pow(2, 3) + pow(2, 4)

Area2D

Los nodos de área (Area) proporcionan detección y influencia. Pueden detectar cuando los objetos se superponen y emitir señales cuando los cuerpos entran o salen. Las áreas también pueden utilizarse para anular propiedades físicas, como la gravedad o la amortiguación, en un área definida.

Estos son los tres tipos de usos principales de Area2D:

  • Sobreescribir los parámetros físicos (como la gravedad) en una región determinada.

  • Detectar cuando otros cuerpos entran o salen de una región o qué cuerpos están actualmente en una región.

  • Revisar la superposición con otras áreas.

Por defecto, los nodos tipo Area también reciben entradas (input) de ratón y táctiles.

StaticBody2D

Un cuerpo estático (static body) es uno que no es movido por el motor de física. Participa en detección de colisiones pero no se mueve en respuesta a ellas. Sin embargo, pueden provocar movimientos o rotaciones a un cuerpo que colisiona como si fuesen movidas cuando utilizan sus propiedades constant_linear_velocity (velocidad linear constante) y constant_angular_velocity (velocidad angular constante).

Los nodos StaticBody2D son usados mayormente par aobjetos que son parte del entorno o que no necesitan ningún comportamiento dinámico.

Ejemplos de usos para StaticBody2D:

  • Plataformas (incluyendo plataformas móviles)

  • Cintas transportadoras

  • Paredes y otros obstáculos sólidos

RigidBody2D

Este es el nodo que implementa la simulación de física 2D. No se puede controlar un RigidBody2D directamente. En su lugar, se le aplican fuerzas y el motor de física calcula el movimiento resultante, incluyendo colisiones con otros cuerpos, y respuestas a colisiones, como rebotes, rotación, etc.

Puedes modificar el comportamiento de un rigid body (cuerpo rígido) mediante propiedades como "Mass" (masa), "Friction"(fricción), o "Bounce" (rebote/elasticidad), estas pueden configurar en el Inspector.

El comportamiento de un cuerpo de este tipo también es afectado por las propiedades del mundo, como las asignadas en Ajustes del Proyecto -> Physics o pro entrar en un Area2D que está reemplazando las propiedades físicas globales.

Cuando un cuerpo rígido está inmóvil y permanece inmóvil por un tiempo, entra en reposo. Un cuerpo en reposo actúa como un cuerpo estático, y sus fuerzas no son calculadas por el motor de física. El cuerpo se activará cuando se ejerzan fuerzas, ya sea por una colisión o mediante código.

Modos de cuerpos rígidos

Un cuerpo rígido puede estar en uno de cuatro modos:

  • Rigid - El cuerpo se comporta como un objeto físico. Colisiona con otros cuerpos y responde a las fuerzas aplicadas sobre él. Este es el modo asignado por defecto.

  • Static - El cuerpo se comporta como un StaticBody2D y no se mueve.

  • Character - Similar al modo "Rigid", pero el cuerpo no puede rotar.

  • Kinematic - El cuerpo se comporta como un KinematicBody2D y debe ser movido por código.

Usando RigidBody2D

Uno de los beneficios de utilizar un cuerpo rígido (rigid body) es que mucho de comportamiento se puede obtener "gratis", sin escribir una sola línea de código, si estás haciendo un juego tipo "Angry Birds", con bloques que caen, sólo necesitarás crear nodos RigidBody2D y ajustar sus propiedades. Apilar, hacer caer y rebotar será manejado por el motor de física.

Sin embargo, si lo que quieres es tener algo de control sobre el cuerpo, debes tener cuidado - modificar position (posición), linear_velocity (velocidad lineal) u otras propiedades físicas de un cuerpo rígido puede resultar en un comportamiento inesperado. Si necesitas alterar cualquier propiedad relacionada a la física, deberás usar la función interna o callback _integrate_forces() en lugar de _physics_process(). En este callback, tienes acceso al Physics2DDirectBodyState (representación del estado físico) del cuerpo, lo que permite cambiar propiedades de manera segura y sincronizarlas con el motor de física.

Por ejemplo, este es el código para una nave tipo "Asteroids":

extends RigidBody2D

var thrust = Vector2(0, 250)
var torque = 20000

func _integrate_forces(state):
    if Input.is_action_pressed("ui_up"):
        applied_force = thrust.rotated(rotation)
    else:
        applied_force = Vector2()
    var rotation_dir = 0
    if Input.is_action_pressed("ui_right"):
        rotation_dir += 1
    if Input.is_action_pressed("ui_left"):
        rotation_dir -= 1
    applied_torque = rotation_dir * torque

Note que no estamos asignando linear_velocity o angular_velocity directamente, en su lugar aplicamos fuerzas (thrust - propulsión - y torque) al cuerpo y dejamos que el motor de física calcule el movimiento resultante.

Nota

Cuando un cuerpo rígido se duerme, la función _integrate_forces no es llamada. Para evitar este comportamiento debemos mantener el cuerpo despierto creando una colisión, aplicando una fuerza sobre él o desactivando la propiedad can_sleep (puede dormirse). Ten en cuenta que esto puede impactar negativamente en el desempeño.

Reporte de contactos

Por defecto, los cuerpos rígidos no mantienen registro de los contactos porque esto requiere gran cantidad de memoria si hay muchos en la escena. Para habilitar el reporte de contactos, asigna un valor mayor a cero a la propiedad contacts_reported. Los contactos se pueden obtener mediante la función Physics2DDirectBodyState.get_contact_count() y otras funciones relacionadas.

El monitoreo de contactos mediante señales se puede activar mediante la propiedad contact_monitor. Ver RigidBody2D para conocer la lista de señales disponibles.

KinematicBody2D

Los cuerpos tipo KinematicBody2D detectan colisiones con otros cuerpos pero no son afectados por propiedades físicas como gravedad o fricción, deben ser controlados por el usuario mediante código. El motor de física no moverá un cuerpo cinemático.

Al mover un cuerpo cinemático, no se debe establecer su position directamente. En su lugar, se utilizan los métodos move_and_collide() o move_and_slide(). Estos métodos mueven el cuerpo a lo largo de un vector determinado, y se detendrá inmediatamente si se detecta una colisión con otro cuerpo. Después de que el cuerpo haya colisionado, cualquier respuesta de colisión debe ser programada manualmente.

Respuesta de colisión cinemática

Después de una colisión, puedes querer que un cuerpo rebote, para que se deslice a lo largo de una superficie o para alterar las propiedades del objeto golpeado. El modo de manejar una respuesta de colisión depende del método usado para mover el KinematicBody2D.

move_and_collide

Cuando se utiliza move_and_collide() (mover y colisionar), la función retorna un objeto del tipo KinematicCollision2D, el cual contiene información sobre la colisión y el cuerpo golpeado. Puedes usar esta información para determinar la respuesta.

Por ejemplo, si quieres obtener el punto en el espacio donde ocurrió una colisión:

extends KinematicBody2D

var velocity = Vector2(250, 250)

func _physics_process(delta):
    var collision_info = move_and_collide(velocity * delta)
    if collision_info:
        var collision_point = collision_info.position

O para rebotar del objeto colisionado:

extends KinematicBody2D

var velocity = Vector2(250, 250)

func _physics_process(delta):
    var collision_info = move_and_collide(velocity * delta)
    if collision_info:
        velocity = velocity.bounce(collision_info.normal)

move_and_slide

Resbalar o "slide" es una respuesta común a colisiones. Imagina un jugador moviéndose a lo largo de las paredes en un juego de vista superior o top-down, o corriendo hacia arriba o hacia abajo en las pendientes de un jeugo de plataformas. Es posible codificar la respuesta para este tipo de colisiones utilizando move_and_collide(), pero move_and_slide() provee un modo conveniente de implementar el efecto de resbalar sobre superficies con mucho menos código.

Advertencia

move_and_slide() incluye automáticamente el "timestep" (paso temporal) en su cálculo, por lo que no se debe multiplicar el vector de velocidad por delta.

Por ejemplo, usa el siguiente código para hacer que un personaje pueda caminar por el piso (incluyendo pendientes) y salte cuando está en el piso:

extends KinematicBody2D

var run_speed = 350
var jump_speed = -1000
var gravity = 2500

var velocity = Vector2()

func get_input():
    velocity.x = 0
    var right = Input.is_action_pressed('ui_right')
    var left = Input.is_action_pressed('ui_left')
    var jump = Input.is_action_just_pressed('ui_select')

    if is_on_floor() and jump:
        velocity.y = jump_speed
    if right:
        velocity.x += run_speed
    if left:
        velocity.x -= run_speed

func _physics_process(delta):
    velocity.y += gravity * delta
    get_input()
    velocity = move_and_slide(velocity, Vector2(0, -1))

Ver Personaje cinemático (2D) para más detalles sobre el uso de move_and_slide(), incluyendo proyecto de demostración con código detallado.