← Volver al listado de tecnologías

Game Objects y Components en Defold

Por: Artiko
defoldgameobjectscomponentscollectionsarquitectura

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:

-- 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:

ComponentFunciónEjemplo de Uso
SpriteRenderizar imagen 2DPersonajes, UI, fondos
ScriptLógica en LuaIA, gameplay, controllers
SoundAudio y músicaSFX, música ambiente
FactoryCrear objetos dinámicamenteBalas, enemigos, power-ups
Collection FactoryCargar escenas completasNiveles, menús
Collision ObjectFísica y colisionesHitboxes, triggers

Creando un Sistema de Enemigos

Vamos a crear un sistema completo de enemigos para entender mejor esta arquitectura.

Paso 1: Enemy Game Object

  1. Click derecho en main/New → Game Object
  2. Nombra: enemy.go

Paso 2: Sprite Component

  1. Click derecho en enemy.goAdd Component → Sprite
  2. Configura:
    • Image: /builtins/graphics/particle_blob.png
    • Default Animation: anim
    • Tint: Rojo (1.0, 0.2, 0.2, 1.0)

Paso 3: Enemy Script

  1. Add Component → Script
  2. 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.

  1. Abre main/main.collection
  2. Click derechoAdd Game Object
  3. Nombra: game_manager
  4. Add Component → Factory al game_manager
  5. Configura Factory:
    • Id: enemy_factory
    • Prototype: main/enemy.go

Paso 5: Game Manager Script

  1. Add Component → Script al game_manager
  2. 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:

Creando una Collection para UI

  1. New → Collectionui.collection
  2. 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:

  1. Add Component → Collection Proxy
  2. Configura:
    • Collection: /levels/level1.collection
    • Async Loading:
-- 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:

  1. Crear diferentes tipos de items
  2. Script de inventario con límite de slots
  3. UI para mostrar inventario
  4. Sistema de uso de items

Ejercicio 2: AI Básica

Implementa diferentes tipos de enemigos:

  1. Follower: Sigue al jugador
  2. Patroller: Se mueve en patrón
  3. Shooter: Dispara proyectiles
  4. Guard: Se activa cuando el jugador está cerca

Ejercicio 3: Sistema de Niveles

Crea un sistema completo de niveles:

  1. Collection por cada nivel
  2. Sistema de transición entre niveles
  3. Datos persistentes entre niveles
  4. 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.