Física y Colisiones en Defold
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:
| Tipo | Descripción | Uso Típico |
|---|---|---|
| STATIC | No se mueve, no afectado por física | Paredes, plataformas, terreno |
| KINEMATIC | Controlado por código, detecta colisiones | Jugador, proyectiles, triggers |
| DYNAMIC | Controlado por motor físico | Objetos 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
- Crear nuevo proyecto:
physics_demo - 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:
- Add Component → Collision Object
- Configuración:
- Type:
STATIC - Group:
wall - Mask:
dynamic_object,player
- Type:
- 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:
- Objeto fijo en el techo
- Objeto pesado conectado por distance joint
- Respuesta realista a empujones
Ejercicio 2: Catapulta
Construye una catapulta funcional:
- Brazo con revolute joint
- Sistema de tensión con springs
- Proyectil que se lanza con física realista
Ejercicio 3: Puzzle Físico
Diseña un puzzle que requiera:
- Apilar objetos para alcanzar altura
- Usar palancas con joints
- Activar switches con peso
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.