Game Objects y Components en Defold
Game Objects y Components en Defold
En esta lección profundizaremos en la arquitectura fundamental de Defold: el sistema de Game Objects y Components. Esta arquitectura única te permitirá crear juegos escalables y bien organizados.
Arquitectura Basada en Entidades
¿Qué es un Game Object?
Un Game Object (GO) es como un contenedor invisible en el espacio 3D que tiene:
- Posición (x, y, z)
- Rotación (quaternion)
- Escala (x, y, z)
- ID único en la collection
-- Obtener propiedades de transformación
local pos = go.get_position()
local rot = go.get_rotation()
local scale = go.get_scale()
-- Modificar transformación
go.set_position(vmath.vector3(100, 200, 0))
go.set_rotation(vmath.quat_rotation_z(math.rad(45)))
go.set_scale(vmath.vector3(2, 2, 1))
Components: La Funcionalidad
Los Components dan funcionalidad a los Game Objects:
| Component | Función | Ejemplo de Uso |
|---|---|---|
| Sprite | Renderizar imagen 2D | Personajes, UI, fondos |
| Script | Lógica en Lua | IA, gameplay, controllers |
| Sound | Audio y música | SFX, música ambiente |
| Factory | Crear objetos dinámicamente | Balas, enemigos, power-ups |
| Collection Factory | Cargar escenas completas | Niveles, menús |
| Collision Object | Física y colisiones | Hitboxes, triggers |
Creando un Sistema de Enemigos
Vamos a crear un sistema completo de enemigos para entender mejor esta arquitectura.
Paso 1: Enemy Game Object
- Click derecho en
main/→ New → Game Object - Nombra:
enemy.go
Paso 2: Sprite Component
- Click derecho en
enemy.go→ Add Component → Sprite - Configura:
- Image:
/builtins/graphics/particle_blob.png - Default Animation:
anim - Tint: Rojo (1.0, 0.2, 0.2, 1.0)
- Image:
Paso 3: Enemy Script
- Add Component → Script
- Nombra:
enemy.script
-- enemy.script
local SPEED = 100 -- Velocidad constante hacia abajo
function init(self)
-- Configuración inicial del enemigo
self.health = 3
self.score_value = 10
-- Posición aleatoria en la parte superior
local screen_width = 960
local random_x = math.random(50, screen_width - 50)
go.set_position(vmath.vector3(random_x, 700, 0))
print("Enemigo creado en x:", random_x)
end
function update(self, dt)
-- Movimiento hacia abajo
local pos = go.get_position()
pos.y = pos.y - SPEED * dt
go.set_position(pos)
-- Destruir si sale de pantalla
if pos.y < -50 then
go.delete()
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("hit") then
self.health = self.health - 1
-- Efecto visual de daño
go.animate(".", "tint", go.PLAYBACK_ONCE_FORWARD,
vmath.vector4(1, 1, 1, 1), go.EASING_OUTQUAD, 0.1,
0, function()
go.animate(".", "tint", go.PLAYBACK_ONCE_FORWARD,
vmath.vector4(1, 0.2, 0.2, 1), go.EASING_OUTQUAD, 0.1)
end)
if self.health <= 0 then
-- Notificar muerte y destruir
msg.post("/main", "enemy_killed", {score = self.score_value})
go.delete()
end
end
end
Paso 4: Factory Component
Para crear enemigos dinámicamente, usaremos un Factory Component.
- Abre
main/main.collection - Click derecho → Add Game Object
- Nombra:
game_manager - Add Component → Factory al game_manager
- Configura Factory:
- Id:
enemy_factory - Prototype:
main/enemy.go
- Id:
Paso 5: Game Manager Script
- Add Component → Script al
game_manager - Nombra:
game_manager.script
-- game_manager.script
local SPAWN_INTERVAL = 2.0 -- Segundos entre spawns
function init(self)
self.score = 0
self.spawn_timer = 0
self.game_active = true
print("Game Manager iniciado")
end
function update(self, dt)
if not self.game_active then
return
end
-- Timer para spawn de enemigos
self.spawn_timer = self.spawn_timer + dt
if self.spawn_timer >= SPAWN_INTERVAL then
self.spawn_timer = 0
spawn_enemy(self)
end
end
function spawn_enemy(self)
-- Crear enemigo usando factory
local enemy_id = factory.create("#enemy_factory")
print("Enemigo spawneado:", enemy_id)
end
function on_message(self, message_id, message, sender)
if message_id == hash("enemy_killed") then
self.score = self.score + message.score
print("Score:", self.score)
-- Aumentar dificultad gradualmente
if self.score % 50 == 0 then
SPAWN_INTERVAL = math.max(0.5, SPAWN_INTERVAL - 0.1)
print("¡Dificultad aumentada! Intervalo:", SPAWN_INTERVAL)
end
end
end
Sistema de Comunicación por Mensajes
Tipos de Mensajes
Built-in Messages:
-- Lifecycle messages
"init" -- Cuando se crea el objeto
"final" -- Antes de destruirse
"update" -- Cada frame
"on_input" -- Input del usuario
"on_message" -- Mensajes personalizados
Custom Messages:
-- Enviar mensaje personalizado
msg.post("enemy", "take_damage", {amount = 1, type = "fire"})
-- Recibir mensaje
function on_message(self, message_id, message, sender)
if message_id == hash("take_damage") then
print("Daño recibido:", message.amount, "tipo:", message.type)
print("Remitente:", sender)
end
end
Addressing System
Defold usa un sistema de direcciones único:
-- Direcciones absolutas
msg.post("/main/player", "jump") -- Específico
msg.post("/main", "level_complete") -- Collection
msg.post(".", "local_message") -- Mismo objeto
msg.post("#sprite", "play_animation") -- Component específico
-- Direcciones relativas
msg.post("../manager", "player_died") -- Objeto padre
msg.post("enemy_1", "attack") -- Por ID
Collections: Organizando tu Juego
¿Qué son las Collections?
Las Collections son grupos de Game Objects que forman una escena completa:
- main.collection - Escena principal
- menu.collection - Menú principal
- level1.collection - Primer nivel
- ui.collection - Interfaz de usuario
Creando una Collection para UI
- New → Collection →
ui.collection - Agregar Game Objects para UI:
-- ui.collection estructura:
ui.collection
├── score_display.go
│ ├── sprite (background)
│ ├── label (text)
│ └── ui_script.script
└── health_bar.go
├── sprite (bar_bg)
├── sprite (bar_fill)
└── health_script.script
Collection Proxy para Carga Dinámica
Para cargar collections dinámicamente:
- Add Component → Collection Proxy
- Configura:
- Collection:
/levels/level1.collection - Async Loading: ✓
- Collection:
-- Cargar collection dinámicamente
function load_level(level_name)
msg.post("#level_proxy", "load")
end
function on_message(self, message_id, message, sender)
if message_id == hash("proxy_loaded") then
msg.post("#level_proxy", "init")
msg.post("#level_proxy", "enable")
end
end
Propiedades y Configuración
Properties en Scripts
Las properties permiten configurar scripts desde el editor:
-- script_properties.script
go.property("speed", 200)
go.property("max_health", 100)
go.property("player_name", "Hero")
go.property("is_invincible", false)
go.property("damage_color", vmath.vector4(1, 0, 0, 1))
function init(self)
self.current_health = self.max_health
print("Jugador:", self.player_name, "Velocidad:", self.speed)
end
En el editor, estas properties aparecen como campos editables.
URL System
Defold usa URLs para referenciar objetos y components:
-- Diferentes formas de crear URLs
local sprite_url = msg.url(".", "sprite", "")
local sound_url = msg.url("enemy_1", "sound", "")
local script_url = msg.url("/main/player", "script", "")
-- Usar URLs para enviar mensajes
msg.post(sprite_url, "play_animation", {id = hash("walk")})
Ejemplo Completo: Sistema de Power-ups
Vamos a crear un sistema completo de power-ups para demostrar todos los conceptos:
PowerUp Game Object
-- powerup.script
go.property("powerup_type", "speed") -- "speed", "health", "damage"
go.property("duration", 5.0)
local FALL_SPEED = 80
function init(self)
-- Configurar sprite según tipo
local tint = vmath.vector4(1, 1, 1, 1)
if self.powerup_type == "speed" then
tint = vmath.vector4(0, 1, 0, 1) -- Verde
elseif self.powerup_type == "health" then
tint = vmath.vector4(1, 0, 1, 1) -- Magenta
elseif self.powerup_type == "damage" then
tint = vmath.vector4(1, 1, 0, 1) -- Amarillo
end
go.set("#sprite", "tint", tint)
-- Animación de rotación
go.animate(".", "rotation.z", go.PLAYBACK_LOOP_FORWARD,
math.rad(360), go.EASING_LINEAR, 2.0)
end
function update(self, dt)
-- Caer hacia abajo
local pos = go.get_position()
pos.y = pos.y - FALL_SPEED * dt
go.set_position(pos)
if pos.y < -50 then
go.delete()
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("collected") then
-- Notificar al jugador
msg.post("/main/player", "powerup_collected", {
type = self.powerup_type,
duration = self.duration
})
-- Efecto visual antes de destruir
go.animate(".", "scale", go.PLAYBACK_ONCE_FORWARD,
vmath.vector3(2, 2, 2), go.EASING_OUTBACK, 0.3,
0, function()
go.delete()
end)
end
end
Player con Power-ups
-- player.script (versión extendida)
go.property("base_speed", 200)
function init(self)
self.speed = self.base_speed
self.powerup_timers = {}
msg.post(".", "acquire_input_focus")
end
function update(self, dt)
-- Actualizar timers de power-ups
for powerup_type, remaining_time in pairs(self.powerup_timers) do
remaining_time = remaining_time - dt
if remaining_time <= 0 then
remove_powerup(self, powerup_type)
self.powerup_timers[powerup_type] = nil
else
self.powerup_timers[powerup_type] = remaining_time
end
end
-- Movimiento normal...
handle_movement(self, dt)
end
function on_message(self, message_id, message, sender)
if message_id == hash("powerup_collected") then
apply_powerup(self, message.type, message.duration)
end
end
function apply_powerup(self, powerup_type, duration)
self.powerup_timers[powerup_type] = duration
if powerup_type == "speed" then
self.speed = self.base_speed * 2
print("¡Speed boost activado!")
elseif powerup_type == "health" then
-- Restaurar salud (implementar sistema de salud)
print("¡Salud restaurada!")
elseif powerup_type == "damage" then
-- Aumentar daño (implementar sistema de combate)
print("¡Daño aumentado!")
end
end
function remove_powerup(self, powerup_type)
if powerup_type == "speed" then
self.speed = self.base_speed
print("Speed boost expirado")
end
-- Remover otros efectos...
end
Buenas Prácticas
1. Organización de Archivos
project/
├── main/
│ ├── main.collection # Escena principal
│ └── main.script # Manager principal
├── player/
│ ├── player.go # Game object del jugador
│ ├── player.script # Lógica del jugador
│ └── player_controller.script # Input handling
├── enemies/
│ ├── basic_enemy.go
│ ├── boss_enemy.go
│ └── enemy_ai.script
└── powerups/
├── powerup.go
└── powerup_types.script
2. Naming Conventions
-- IDs y archivos: snake_case
main_menu.collection
enemy_spawner.go
player_controller.script
-- Variables Lua: snake_case
local move_speed = 200
local is_jumping = false
-- Constants: UPPER_CASE
local MAX_HEALTH = 100
local GRAVITY = -800
3. Message Design
-- Mensajes descriptivos con datos estructurados
msg.post("player", "take_damage", {
amount = 25,
damage_type = "fire",
source_position = enemy_pos,
knockback_force = 300
})
Ejercicios Prácticos
Ejercicio 1: Sistema de Inventario
Crea un sistema donde el jugador pueda recoger y usar diferentes objetos:
- Crear diferentes tipos de items
- Script de inventario con límite de slots
- UI para mostrar inventario
- Sistema de uso de items
Ejercicio 2: AI Básica
Implementa diferentes tipos de enemigos:
- Follower: Sigue al jugador
- Patroller: Se mueve en patrón
- Shooter: Dispara proyectiles
- Guard: Se activa cuando el jugador está cerca
Ejercicio 3: Sistema de Niveles
Crea un sistema completo de niveles:
- Collection por cada nivel
- Sistema de transición entre niveles
- Datos persistentes entre niveles
- Sistema de checkpoint
Próximos Pasos
En la siguiente lección crearemos nuestro primer juego completo: un Space Shooter clásico que pondrá en práctica todos estos conceptos de manera integrada.
⬅️ Anterior: Introducción a Defold | Siguiente: Primer Juego - Space Shooter ➡️
¡Excelente! Ya dominas la arquitectura fundamental de Defold. Estos conceptos son la base para crear cualquier tipo de juego profesional.