← Volver al listado de tecnologías

Física y Colisiones en Defold

Por: Artiko
defoldfisicacolisionescollision-objectsfisica-2d

Física y Colisiones en Defold

El sistema de física de Defold está basado en Box2D (2D) y Bullet Physics (3D), proporcionando simulaciones físicas de alto rendimiento optimizadas para juegos móviles. En esta lección aprenderás a crear interacciones físicas complejas y realistas.

Fundamentos del Sistema de Física

Tipos de Collision Objects

Defold maneja tres tipos principales de objetos de colisión:

TipoDescripciónUso Típico
STATICNo se mueve, no afectado por físicaParedes, plataformas, terreno
KINEMATICControlado por código, detecta colisionesJugador, proyectiles, triggers
DYNAMICControlado por motor físicoObjetos que caen, vehículos

Grupos y Máscaras de Colisión

El sistema de grupos y máscaras determina qué objetos pueden colisionar entre sí:

-- Configuración típica de grupos
player_group = "player"     -- Máscara: enemy, collectible, wall
enemy_group = "enemy"       -- Máscara: player, bullet, wall
bullet_group = "bullet"     -- Máscara: enemy, wall
wall_group = "wall"         -- Máscara: player, enemy, bullet
collectible_group = "item"  -- Máscara: player

Proyecto Práctico: Física Realista

Vamos a crear un proyecto que demuestre todos los aspectos del sistema de física.

Configuración Inicial

  1. Crear nuevo proyecto: physics_demo
  2. Estructura de carpetas:
physics_demo/
├── world/
│   ├── ground.go
│   ├── walls.go
│   └── platforms.go
├── objects/
│   ├── ball.go
│   ├── box.go
│   └── player.go
├── triggers/
│   ├── switch.go
│   └── portal.go
└── main/
    ├── main.collection
    └── physics_manager.script

Parte 1: Objetos Estáticos (STATIC)

Crear el Suelo

-- world/ground.script
function init(self)
    -- El suelo es estático, no necesita update
    print("Suelo creado")
end

function on_message(self, message_id, message, sender)
    if message_id == hash("collision_response") then
        local other_group = message.other_group

        if other_group == hash("dynamic_object") then
            print("Objeto impactó el suelo")
            -- Crear efecto de polvo o sonido
            create_dust_effect(message.contact_point_world)
        end
    end
end

function create_dust_effect(position)
    -- Crear partículas de polvo (implementar más adelante)
    print("Efecto de polvo en:", position)
end

Configuración del Ground Collision Object

En ground.go:

  1. Add Component → Collision Object
  2. Configuración:
    • Type: STATIC
    • Group: wall
    • Mask: dynamic_object, player
  3. Add Shape → Box:
    • Position: (0, -320, 0)
    • Dimensions: (960, 20, 10)

Crear Plataformas

-- world/platforms.script
go.property("platform_width", 200)
go.property("platform_height", 20)

function init(self)
    -- Configurar tamaño de plataforma dinámicamente
    local shape = msg.url(".", "collisionobject", "shape")
    physics.set_shape(shape, physics.SHAPE_TYPE_BOX,
                     vmath.vector3(self.platform_width/2, self.platform_height/2, 5))

    print("Plataforma creada:", self.platform_width, "x", self.platform_height)
end

Parte 2: Objetos Dinámicos (DYNAMIC)

Pelota con Física Realista

-- objects/ball.script
go.property("bounce_factor", 0.8)
go.property("friction", 0.3)

function init(self)
    -- Configurar propiedades físicas
    msg.post(".", "set_parent", {parent_id = hash("/world")})

    -- Aplicar propiedades físicas al collision object
    local co_url = msg.url(".", "collisionobject", "")
    physics.set_friction(co_url, self.friction)
    physics.set_restitution(co_url, self.bounce_factor)

    print("Pelota creada con bounce:", self.bounce_factor)
end

function update(self, dt)
    -- Limitar velocidad máxima para evitar comportamientos erráticos
    local velocity = physics.get_velocity(".")
    local max_speed = 800

    if vmath.length(velocity) > max_speed then
        velocity = vmath.normalize(velocity) * max_speed
        physics.set_velocity(".", velocity)
    end
end

function on_input(self, action_id, action)
    if action_id == hash("click") and action.pressed then
        -- Aplicar impulso hacia el cursor
        local pos = go.get_position()
        local target = vmath.vector3(action.x, action.y, 0)
        local direction = vmath.normalize(target - pos)
        local force = direction * 500

        physics.apply_force(".", force)
        print("Impulso aplicado:", force)
    end
end

function on_message(self, message_id, message, sender)
    if message_id == hash("collision_response") then
        local other_group = message.other_group
        local impulse_magnitude = vmath.length(message.distance)

        if other_group == hash("wall") and impulse_magnitude > 50 then
            -- Sonido de rebote proporcional al impacto
            local volume = math.min(impulse_magnitude / 200, 1.0)
            msg.post("#sound", "play_sound", {volume = volume})
        end
    end
end

Configuración del Ball Collision Object

-- En ball.go
-- Collision Object:
-- Type: DYNAMIC
-- Group: dynamic_object
-- Mask: wall, player, trigger
-- Mass: 1.0
-- Linear Damping: 0.1
-- Angular Damping: 0.1

-- Shape: Sphere
-- Radius: 15

Caja con Física de Cuerpo Rígido

-- objects/box.script
go.property("density", 1.0)
go.property("can_rotate", true)

function init(self)
    -- Configurar densidad del material
    local shape_url = msg.url(".", "collisionobject", "shape")
    physics.set_density(shape_url, self.density)

    if not self.can_rotate then
        -- Congelar rotación
        physics.set_angular_velocity(".", vmath.vector3(0, 0, 0))
        physics.set_locked_rotation(".", true)
    end

    print("Caja creada - Densidad:", self.density, "Puede rotar:", self.can_rotate)
end

function on_message(self, message_id, message, sender)
    if message_id == hash("collision_response") then
        local other_group = message.other_group

        if other_group == hash("player") then
            -- La caja es empujada por el jugador
            local push_force = message.normal * -200
            physics.apply_force(".", push_force)
        end
    elseif message_id == hash("apply_explosion") then
        -- Aplicar fuerza de explosión
        local explosion_pos = message.position
        local my_pos = go.get_position()
        local direction = vmath.normalize(my_pos - explosion_pos)
        local distance = vmath.length(my_pos - explosion_pos)
        local force_magnitude = math.max(0, 1000 - distance * 2)

        physics.apply_force(".", direction * force_magnitude)
    end
end

Parte 3: Objetos Kinematic (Jugador)

Player con Control Físico

-- objects/player.script
go.property("move_force", 400)
go.property("jump_force", 600)
go.property("max_speed", 300)

function init(self)
    self.ground_contact = 0  -- Contador de contactos con suelo
    self.is_grounded = false

    msg.post(".", "acquire_input_focus")
    print("Player físico inicializado")
end

function update(self, dt)
    -- Verificar si está en el suelo
    self.is_grounded = self.ground_contact > 0

    -- Controlar velocidad máxima horizontal
    local velocity = physics.get_velocity(".")
    if math.abs(velocity.x) > self.max_speed then
        velocity.x = velocity.x > 0 and self.max_speed or -self.max_speed
        physics.set_velocity(".", velocity)
    end
end

function on_input(self, action_id, action)
    if action_id == hash("left") and action.pressed or action.repeated then
        apply_horizontal_force(self, -self.move_force)
    elseif action_id == hash("right") and action.pressed or action.repeated then
        apply_horizontal_force(self, self.move_force)
    elseif action_id == hash("jump") and action.pressed and self.is_grounded then
        jump(self)
    end
end

function apply_horizontal_force(self, force)
    local velocity = physics.get_velocity(".")
    -- Solo aplicar fuerza si no hemos alcanzado velocidad máxima
    if (force > 0 and velocity.x < self.max_speed) or
       (force < 0 and velocity.x > -self.max_speed) then
        physics.apply_force(".", vmath.vector3(force, 0, 0))
    end
end

function jump(self)
    -- Cancelar velocidad vertical previa y aplicar salto
    local velocity = physics.get_velocity(".")
    velocity.y = 0
    physics.set_velocity(".", velocity)
    physics.apply_force(".", vmath.vector3(0, self.jump_force, 0))

    print("¡Salto!")
end

function on_message(self, message_id, message, sender)
    if message_id == hash("contact_point_response") then
        -- Detectar contacto con el suelo
        if message.other_group == hash("wall") then
            -- Verificar si el contacto es desde arriba (pisando)
            if message.normal.y > 0.7 then  -- Ángulo casi vertical
                self.ground_contact = self.ground_contact + 1
            end
        end
    elseif message_id == hash("contact_point_event") then
        if message.event == physics.CONTACT_POINT_EVENT_SEPARATED then
            if message.other_group == hash("wall") then
                self.ground_contact = math.max(0, self.ground_contact - 1)
            end
        end
    end
end

Parte 4: Triggers y Sensores

Switch Activable

-- triggers/switch.script
go.property("requires_weight", 50)  -- Peso mínimo para activar

function init(self)
    self.is_activated = false
    self.current_weight = 0
    self.objects_on_switch = {}
end

function on_message(self, message_id, message, sender)
    if message_id == hash("trigger_response") then
        local other_id = message.other_id
        local other_group = message.other_group

        if message.enter then
            -- Objeto entra en el trigger
            if other_group == hash("dynamic_object") or other_group == hash("player") then
                add_object_to_switch(self, other_id)
            end
        else
            -- Objeto sale del trigger
            remove_object_from_switch(self, other_id)
        end

        check_activation(self)
    end
end

function add_object_to_switch(self, object_id)
    if not self.objects_on_switch[object_id] then
        -- Obtener masa del objeto (simulada)
        local mass = get_object_mass(object_id)
        self.objects_on_switch[object_id] = mass
        self.current_weight = self.current_weight + mass

        print("Objeto en switch. Peso total:", self.current_weight)
    end
end

function remove_object_from_switch(self, object_id)
    if self.objects_on_switch[object_id] then
        self.current_weight = self.current_weight - self.objects_on_switch[object_id]
        self.objects_on_switch[object_id] = nil

        print("Objeto removido. Peso total:", self.current_weight)
    end
end

function check_activation(self)
    local should_activate = self.current_weight >= self.requires_weight

    if should_activate and not self.is_activated then
        activate_switch(self)
    elseif not should_activate and self.is_activated then
        deactivate_switch(self)
    end
end

function activate_switch(self)
    self.is_activated = true
    print("¡Switch activado!")

    -- Cambiar sprite a activado
    sprite.play_flipbook("#sprite", "switch_on")

    -- Notificar a objetos conectados
    msg.post("/world/door", "open")
    msg.post("/world/bridge", "extend")
end

function deactivate_switch(self)
    self.is_activated = false
    print("Switch desactivado")

    sprite.play_flipbook("#sprite", "switch_off")

    msg.post("/world/door", "close")
    msg.post("/world/bridge", "retract")
end

function get_object_mass(object_id)
    -- Simulación de masa basada en el tipo de objeto
    local url_string = tostring(object_id)

    if string.find(url_string, "player") then
        return 70  -- Masa del jugador
    elseif string.find(url_string, "box") then
        return 30  -- Masa de caja
    elseif string.find(url_string, "ball") then
        return 10  -- Masa de pelota
    else
        return 20  -- Masa por defecto
    end
end

Configuración del Switch Trigger

-- En switch.go
-- Collision Object:
-- Type: KINEMATIC (para triggers)
-- Group: trigger
-- Mask: dynamic_object, player
-- Shape: Box (40, 10, 10)

Portal de Teletransporte

-- triggers/portal.script
go.property("destination_portal", hash(""))
go.property("teleport_cooldown", 1.0)

function init(self)
    self.teleport_timer = 0
    self.active = true
end

function update(self, dt)
    if self.teleport_timer > 0 then
        self.teleport_timer = self.teleport_timer - dt
        if self.teleport_timer <= 0 then
            self.active = true
            -- Reactivar efecto visual
            sprite.set_constant("#sprite", "tint", vmath.vector4(1, 1, 1, 1))
        end
    end
end

function on_message(self, message_id, message, sender)
    if message_id == hash("trigger_response") then
        if message.enter and self.active then
            local other_group = message.other_group

            if other_group == hash("player") or other_group == hash("dynamic_object") then
                teleport_object(self, message.other_id)
            end
        end
    end
end

function teleport_object(self, object_id)
    if self.destination_portal == hash("") then
        print("Portal sin destino configurado")
        return
    end

    -- Obtener posición del portal destino
    local dest_pos = go.get_position(self.destination_portal)
    dest_pos.y = dest_pos.y + 50  -- Aparecer ligeramente arriba

    -- Teletransportar objeto
    go.set_position(dest_pos, object_id)

    -- Si es un objeto dinámico, cancelar velocidad
    physics.set_velocity(object_id, vmath.vector3(0, 0, 0))

    -- Efecto visual y sonoro
    create_teleport_effect(self, go.get_position())
    create_teleport_effect(self, dest_pos)

    -- Desactivar portal temporalmente
    self.active = false
    self.teleport_timer = self.teleport_cooldown

    -- Efecto visual de cooldown
    sprite.set_constant("#sprite", "tint", vmath.vector4(0.5, 0.5, 0.5, 1))

    print("Objeto teletransportado a:", dest_pos)
end

function create_teleport_effect(self, position)
    -- Crear efecto de partículas (implementar más adelante)
    print("Efecto de teletransporte en:", position)
end

Parte 5: Física Avanzada

Joints y Conexiones

-- Crear joint entre dos objetos
function create_joint(self, object_a, object_b, joint_type)
    local joint_props = {
        type = physics.JOINT_TYPE_WELD,  -- O REVOLUTE, DISTANCE, etc.
        collide_connected = false,
        max_length = 100  -- Para distance joints
    }

    if joint_type == "rope" then
        joint_props.type = physics.JOINT_TYPE_DISTANCE
        joint_props.max_length = 150
        joint_props.frequency = 2.0
        joint_props.damping = 0.5
    elseif joint_type == "hinge" then
        joint_props.type = physics.JOINT_TYPE_REVOLUTE
        joint_props.lower_angle = -math.pi/4
        joint_props.upper_angle = math.pi/4
        joint_props.enable_limit = true
    end

    local joint_id = physics.create_joint(object_a, "joint", joint_props, object_b)
    return joint_id
end

Raycast para Detección

-- Sistema de raycast para line of sight
function cast_ray(self, start_pos, end_pos, group_mask)
    local result = physics.raycast(start_pos, end_pos, group_mask)

    if result then
        print("Raycast hit:", result.id, "at", result.position)
        print("Normal:", result.normal)
        print("Fracción:", result.fraction)

        -- Crear efecto visual en punto de impacto
        create_impact_effect(result.position, result.normal)

        return result
    else
        print("Raycast no impactó nada")
        return nil
    end
end

-- Usar raycast para detectar suelo
function check_ground_below(self)
    local pos = go.get_position()
    local start_pos = pos
    local end_pos = pos - vmath.vector3(0, 50, 0)

    local result = physics.raycast(start_pos, end_pos, {"wall"})
    return result ~= nil
end

Fuerzas y Campos

-- Campo gravitacional personalizado
function apply_gravity_field(self, dt)
    local field_center = go.get_position("/world/gravity_well")
    local field_strength = 500
    local max_distance = 200

    -- Aplicar a todos los objetos dinámicos
    for _, object_id in pairs(self.dynamic_objects) do
        local obj_pos = go.get_position(object_id)
        local distance_vec = field_center - obj_pos
        local distance = vmath.length(distance_vec)

        if distance < max_distance and distance > 10 then
            local direction = vmath.normalize(distance_vec)
            local force_magnitude = field_strength * (1 - distance / max_distance)
            local force = direction * force_magnitude * dt

            physics.apply_force(object_id, force)
        end
    end
end

-- Viento o corrientes
function apply_wind_force(self, dt)
    local wind_direction = vmath.vector3(1, 0, 0)  -- Viento hacia la derecha
    local wind_strength = 100
    local wind_force = wind_direction * wind_strength * dt

    for _, object_id in pairs(self.wind_affected_objects) do
        physics.apply_force(object_id, wind_force)
    end
end

Optimización de Física

Configuración de Rendimiento

-- En game.project
[physics]
type = 2D              # 2D para mejor rendimiento
gravity_y = -800       # Gravedad realista
debug = 0              # Desactivar en release
max_collisions = 256   # Ajustar según necesidades
max_contacts = 128     # Número de contactos simultáneos
velocity_iterations = 8 # Precisión vs rendimiento
position_iterations = 3 # Precisión vs rendimiento

Sleeping y Activación

-- Controlar cuándo objetos entran en sleep mode
function manage_physics_sleep(self)
    for object_id, last_velocity in pairs(self.object_velocities) do
        local current_velocity = physics.get_velocity(object_id)
        local speed = vmath.length(current_velocity)

        if speed < 10 then  -- Muy lento
            -- Permitir que entre en sleep
            physics.set_awake(object_id, false)
        elseif speed > 50 then  -- Moviéndose rápido
            -- Mantener despierto
            physics.set_awake(object_id, true)
        end

        self.object_velocities[object_id] = current_velocity
    end
end

Ejercicios Prácticos

Ejercicio 1: Péndulo Físico

Crea un péndulo usando joints:

Ejercicio 2: Catapulta

Construye una catapulta funcional:

Ejercicio 3: Puzzle Físico

Diseña un puzzle que requiera:

Próximos Pasos

En la siguiente lección aprenderemos a crear interfaces de usuario y menús profesionales usando el sistema GUI nativo de Defold.


⬅️ Anterior: Primer Juego - Space Shooter | Siguiente: GUI y Menús ➡️

¡Perfecto! Ahora dominas el sistema de física de Defold y puedes crear interacciones complejas y realistas en tus juegos.