Patrones de Diseño para Juegos
Patrones de Diseño para Juegos
Los patrones de diseño son soluciones probadas a problemas comunes en el desarrollo de software. En el desarrollo de juegos, ciertos patrones son especialmente útiles para manejar la complejidad inherente de los sistemas interactivos. Esta lección te enseñará los patrones más importantes aplicados a Defold.
🎯 State Machine Pattern
Finite State Machine Básica
-- state_machine.lua
local StateMachine = {}
StateMachine.__index = StateMachine
function StateMachine.new()
local self = setmetatable({}, StateMachine)
self.states = {}
self.current_state = nil
self.previous_state = nil
self.state_data = {}
return self
end
function StateMachine:add_state(name, state_obj)
self.states[name] = state_obj
state_obj.name = name
state_obj.machine = self
end
function StateMachine:change_state(new_state_name, ...)
local new_state = self.states[new_state_name]
if not new_state then
error("State '" .. new_state_name .. "' does not exist")
end
-- Exit current state
if self.current_state and self.current_state.on_exit then
self.current_state:on_exit()
end
-- Change state
self.previous_state = self.current_state
self.current_state = new_state
-- Enter new state
if self.current_state.on_enter then
self.current_state:on_enter(...)
end
print("State changed to:", new_state_name)
end
function StateMachine:update(dt)
if self.current_state and self.current_state.update then
self.current_state:update(dt)
end
end
function StateMachine:on_message(message_id, message, sender)
if self.current_state and self.current_state.on_message then
self.current_state:on_message(message_id, message, sender)
end
end
function StateMachine:get_current_state_name()
return self.current_state and self.current_state.name or nil
end
return StateMachine
Game State Manager
-- game_state_manager.script
local StateMachine = require "main.state_machine"
-- Estados del juego
local MenuState = {}
local GameplayState = {}
local PauseState = {}
local GameOverState = {}
function init(self)
self.state_machine = StateMachine.new()
-- Configurar estados
self.state_machine:add_state("menu", MenuState)
self.state_machine:add_state("gameplay", GameplayState)
self.state_machine:add_state("pause", PauseState)
self.state_machine:add_state("game_over", GameOverState)
-- Estado inicial
self.state_machine:change_state("menu")
-- Data compartida entre estados
self.game_data = {
score = 0,
lives = 3,
level = 1,
settings = {}
}
end
-- === MENU STATE ===
function MenuState:on_enter()
msg.post("menu_gui", "show")
msg.post("background_music", "play", {track = "menu_theme"})
end
function MenuState:on_exit()
msg.post("menu_gui", "hide")
end
function MenuState:on_message(message_id, message, sender)
if message_id == hash("start_game") then
self.machine:change_state("gameplay")
elseif message_id == hash("show_settings") then
msg.post("settings_gui", "show")
end
end
-- === GAMEPLAY STATE ===
function GameplayState:on_enter()
msg.post("game_gui", "show")
msg.post("player", "enable")
msg.post("enemy_spawner", "start_spawning")
msg.post("background_music", "play", {track = "gameplay_theme"})
-- Inicializar datos de gameplay
self.machine.state_data.gameplay = {
start_time = socket.gettime(),
enemies_killed = 0,
powerups_collected = 0
}
end
function GameplayState:on_exit()
msg.post("player", "disable")
msg.post("enemy_spawner", "stop_spawning")
end
function GameplayState:update(dt)
-- Lógica específica del gameplay
local data = self.machine.state_data.gameplay
if data then
data.play_time = socket.gettime() - data.start_time
end
end
function GameplayState:on_message(message_id, message, sender)
if message_id == hash("pause_game") then
self.machine:change_state("pause")
elseif message_id == hash("player_died") then
local game_data = self.machine.state_data
game_data.lives = game_data.lives - 1
if game_data.lives <= 0 then
self.machine:change_state("game_over")
else
-- Respawn player
msg.post("player", "respawn")
end
elseif message_id == hash("level_completed") then
self.machine.state_data.level = self.machine.state_data.level + 1
-- Transition to next level or victory screen
end
end
-- === PAUSE STATE ===
function PauseState:on_enter()
msg.post("pause_gui", "show")
msg.post(".", "set_time_step", {factor = 0, mode = 1}) -- Pausar tiempo
msg.post("background_music", "set_gain", {gain = 0.3}) -- Reducir volumen
end
function PauseState:on_exit()
msg.post("pause_gui", "hide")
msg.post(".", "set_time_step", {factor = 1, mode = 1}) -- Reanudar tiempo
msg.post("background_music", "set_gain", {gain = 1.0}) -- Restaurar volumen
end
function PauseState:on_message(message_id, message, sender)
if message_id == hash("resume_game") then
self.machine:change_state("gameplay")
elseif message_id == hash("quit_to_menu") then
self.machine:change_state("menu")
end
end
-- === GAME OVER STATE ===
function GameOverState:on_enter()
msg.post("game_over_gui", "show")
msg.post("background_music", "play", {track = "game_over_theme"})
-- Calcular estadísticas finales
local gameplay_data = self.machine.state_data.gameplay or {}
local final_stats = {
final_score = self.machine.state_data.score,
play_time = gameplay_data.play_time or 0,
enemies_killed = gameplay_data.enemies_killed or 0,
level_reached = self.machine.state_data.level
}
msg.post("game_over_gui", "show_stats", final_stats)
-- Guardar high score
self:save_high_score(final_stats.final_score)
end
function GameOverState:save_high_score(score)
local high_score = sys.load("high_score") or 0
if score > high_score then
sys.save("high_score", score)
msg.post("game_over_gui", "show_new_record")
end
end
function GameOverState:on_message(message_id, message, sender)
if message_id == hash("restart_game") then
-- Reset game data
self.machine.state_data = {
score = 0,
lives = 3,
level = 1
}
self.machine:change_state("gameplay")
elseif message_id == hash("return_to_menu") then
self.machine:change_state("menu")
end
end
function update(self, dt)
self.state_machine:update(dt)
end
function on_message(self, message_id, message, sender)
self.state_machine:on_message(message_id, message, sender)
end
👁️ Observer Pattern
Event System
-- event_system.lua
local EventSystem = {}
EventSystem.__index = EventSystem
function EventSystem.new()
local self = setmetatable({}, EventSystem)
self.observers = {}
self.event_queue = {}
self.processing_events = false
return self
end
function EventSystem:subscribe(event_type, observer, callback)
if not self.observers[event_type] then
self.observers[event_type] = {}
end
local subscription = {
observer = observer,
callback = callback,
id = tostring(observer) .. "_" .. tostring(callback)
}
table.insert(self.observers[event_type], subscription)
return subscription.id
end
function EventSystem:unsubscribe(event_type, subscription_id)
local observers_list = self.observers[event_type]
if not observers_list then return false end
for i, subscription in ipairs(observers_list) do
if subscription.id == subscription_id then
table.remove(observers_list, i)
return true
end
end
return false
end
function EventSystem:unsubscribe_all(observer)
for event_type, observers_list in pairs(self.observers) do
for i = #observers_list, 1, -1 do
if observers_list[i].observer == observer then
table.remove(observers_list, i)
end
end
end
end
function EventSystem:emit(event_type, event_data)
local event = {
type = event_type,
data = event_data or {},
timestamp = socket.gettime()
}
-- Si estamos procesando eventos, añadir a la cola para evitar recursión
if self.processing_events then
table.insert(self.event_queue, event)
return
end
self:process_event(event)
end
function EventSystem:process_event(event)
local observers_list = self.observers[event.type]
if not observers_list then return end
self.processing_events = true
-- Crear copia de la lista para evitar problemas si se modifican durante la iteración
local observers_copy = {}
for i, observer in ipairs(observers_list) do
observers_copy[i] = observer
end
for _, subscription in ipairs(observers_copy) do
local success, error_msg = pcall(subscription.callback, subscription.observer, event)
if not success then
print("Error in event callback:", error_msg)
end
end
self.processing_events = false
-- Procesar eventos que se añadieron durante el procesamiento
if #self.event_queue > 0 then
local queued_event = table.remove(self.event_queue, 1)
self:process_event(queued_event)
end
end
function EventSystem:clear_all()
self.observers = {}
self.event_queue = {}
end
return EventSystem
Game Events Implementation
-- game_events.script
local EventSystem = require "main.event_system"
-- Singleton del sistema de eventos
local events = EventSystem.new()
function init(self)
-- Hacer el sistema de eventos global
_G.game_events = events
-- Ejemplos de eventos del juego
self:setup_game_events()
end
local function setup_game_events(self)
-- Evento: Player Score Changed
events:subscribe("player_score_changed", self, function(observer, event)
local new_score = event.data.score
local old_score = event.data.old_score
local difference = new_score - old_score
msg.post("hud", "update_score", {score = new_score})
-- Efectos especiales para puntuaciones altas
if difference >= 1000 then
msg.post("effects_manager", "show_score_burst", {amount = difference})
end
end)
-- Evento: Enemy Defeated
events:subscribe("enemy_defeated", self, function(observer, event)
local enemy_type = event.data.enemy_type
local position = event.data.position
local score_value = event.data.score_value
-- Crear efectos visuales
msg.post("effects_manager", "create_explosion", {
position = position,
type = enemy_type
})
-- Reproducir sonido
msg.post("audio_manager", "play_sound", {
sound = "enemy_death_" .. enemy_type
})
-- Actualizar estadísticas
msg.post("stats_manager", "increment_stat", {
stat = "enemies_killed",
value = 1
})
-- Otorgar puntos
if score_value > 0 then
events:emit("player_score_changed", {
score = game.score + score_value,
old_score = game.score
})
game.score = game.score + score_value
end
end)
-- Evento: Power-up Collected
events:subscribe("powerup_collected", self, function(observer, event)
local powerup_type = event.data.type
local player = event.data.player
if powerup_type == "health" then
msg.post(player, "heal", {amount = 25})
events:emit("player_healed", {amount = 25})
elseif powerup_type == "speed_boost" then
msg.post(player, "apply_effect", {
effect = "speed_boost",
duration = 10,
multiplier = 1.5
})
elseif powerup_type == "double_damage" then
msg.post(player, "apply_effect", {
effect = "double_damage",
duration = 15
})
elseif powerup_type == "shield" then
msg.post(player, "activate_shield", {duration = 20})
end
-- Efecto visual universal
msg.post("effects_manager", "create_powerup_effect", {
type = powerup_type,
position = go.get_position(player)
})
end)
-- Evento: Level Completed
events:subscribe("level_completed", self, function(observer, event)
local level = event.data.level
local completion_time = event.data.time
local bonus_score = event.data.bonus
-- Calcular bonus por tiempo
local time_bonus = math.max(0, 10000 - completion_time * 10)
events:emit("player_score_changed", {
score = game.score + bonus_score + time_bonus,
old_score = game.score
})
-- Mostrar pantalla de completado
msg.post("level_complete_gui", "show", {
level = level,
time = completion_time,
bonus = bonus_score + time_bonus
})
-- Guardar progreso
msg.post("save_manager", "save_progress", {
level = level + 1,
score = game.score
})
end)
end
-- Funciones helper para emitir eventos comunes
function emit_player_damaged(damage, source)
events:emit("player_damaged", {
damage = damage,
source = source,
player_health = game.player_health - damage
})
end
function emit_item_pickup(item_type, position)
events:emit("item_picked_up", {
type = item_type,
position = position,
timestamp = socket.gettime()
})
end
function emit_achievement_unlocked(achievement_id)
events:emit("achievement_unlocked", {
id = achievement_id,
timestamp = socket.gettime()
})
end
🧩 Component System Pattern
Entity Component System
-- ecs.lua
local ECS = {}
-- Entity Manager
local EntityManager = {}
EntityManager.__index = EntityManager
function EntityManager.new()
local self = setmetatable({}, EntityManager)
self.next_id = 1
self.entities = {}
return self
end
function EntityManager:create_entity()
local entity_id = self.next_id
self.next_id = self.next_id + 1
self.entities[entity_id] = {
id = entity_id,
components = {},
active = true
}
return entity_id
end
function EntityManager:destroy_entity(entity_id)
self.entities[entity_id] = nil
end
function EntityManager:is_active(entity_id)
local entity = self.entities[entity_id]
return entity and entity.active
end
-- Component Manager
local ComponentManager = {}
ComponentManager.__index = ComponentManager
function ComponentManager.new()
local self = setmetatable({}, ComponentManager)
self.components = {} -- [component_type][entity_id] = component_data
self.component_types = {}
return self
end
function ComponentManager:register_component_type(component_type, component_class)
self.component_types[component_type] = component_class
if not self.components[component_type] then
self.components[component_type] = {}
end
end
function ComponentManager:add_component(entity_id, component_type, component_data)
if not self.components[component_type] then
self.components[component_type] = {}
end
local component_class = self.component_types[component_type]
local component = component_class and component_class.new(component_data) or component_data
self.components[component_type][entity_id] = component
return component
end
function ComponentManager:get_component(entity_id, component_type)
return self.components[component_type] and self.components[component_type][entity_id]
end
function ComponentManager:remove_component(entity_id, component_type)
if self.components[component_type] then
self.components[component_type][entity_id] = nil
end
end
function ComponentManager:get_entities_with_component(component_type)
local entities = {}
if self.components[component_type] then
for entity_id, _ in pairs(self.components[component_type]) do
table.insert(entities, entity_id)
end
end
return entities
end
-- System Manager
local SystemManager = {}
SystemManager.__index = SystemManager
function SystemManager.new(entity_manager, component_manager)
local self = setmetatable({}, SystemManager)
self.systems = {}
self.entity_manager = entity_manager
self.component_manager = component_manager
return self
end
function SystemManager:add_system(system)
system.entity_manager = self.entity_manager
system.component_manager = self.component_manager
table.insert(self.systems, system)
if system.init then
system:init()
end
end
function SystemManager:update(dt)
for _, system in ipairs(self.systems) do
if system.update then
system:update(dt)
end
end
end
-- ECS World
function ECS.create_world()
local entity_manager = EntityManager.new()
local component_manager = ComponentManager.new()
local system_manager = SystemManager.new(entity_manager, component_manager)
return {
entity_manager = entity_manager,
component_manager = component_manager,
system_manager = system_manager
}
end
return ECS
Game Components
-- game_components.lua
-- Transform Component
local TransformComponent = {}
TransformComponent.__index = TransformComponent
function TransformComponent.new(data)
data = data or {}
local self = setmetatable({}, TransformComponent)
self.position = data.position or vmath.vector3()
self.rotation = data.rotation or vmath.quat()
self.scale = data.scale or vmath.vector3(1, 1, 1)
return self
end
-- Health Component
local HealthComponent = {}
HealthComponent.__index = HealthComponent
function HealthComponent.new(data)
data = data or {}
local self = setmetatable({}, HealthComponent)
self.current_health = data.current_health or data.max_health or 100
self.max_health = data.max_health or 100
self.regeneration_rate = data.regeneration_rate or 0
self.invulnerable = false
self.invulnerability_time = 0
return self
end
function HealthComponent:take_damage(amount)
if self.invulnerable then return false end
self.current_health = math.max(0, self.current_health - amount)
self.invulnerable = true
self.invulnerability_time = 0.5 -- 500ms de invulnerabilidad
return true
end
function HealthComponent:heal(amount)
self.current_health = math.min(self.max_health, self.current_health + amount)
end
function HealthComponent:is_alive()
return self.current_health > 0
end
-- Velocity Component
local VelocityComponent = {}
VelocityComponent.__index = VelocityComponent
function VelocityComponent.new(data)
data = data or {}
local self = setmetatable({}, VelocityComponent)
self.velocity = data.velocity or vmath.vector3()
self.max_speed = data.max_speed or 200
self.friction = data.friction or 0.9
return self
end
-- Weapon Component
local WeaponComponent = {}
WeaponComponent.__index = WeaponComponent
function WeaponComponent.new(data)
data = data or {}
local self = setmetatable({}, WeaponComponent)
self.damage = data.damage or 10
self.fire_rate = data.fire_rate or 1.0 -- disparos por segundo
self.last_shot_time = 0
self.ammo = data.ammo or -1 -- -1 = munición infinita
self.max_ammo = data.max_ammo or 30
self.projectile_speed = data.projectile_speed or 500
return self
end
function WeaponComponent:can_shoot()
local current_time = socket.gettime()
local time_since_last_shot = current_time - self.last_shot_time
local shot_cooldown = 1.0 / self.fire_rate
return time_since_last_shot >= shot_cooldown and
(self.ammo > 0 or self.ammo == -1)
end
function WeaponComponent:shoot()
if not self:can_shoot() then return false end
self.last_shot_time = socket.gettime()
if self.ammo > 0 then
self.ammo = self.ammo - 1
end
return true
end
-- AI Component
local AIComponent = {}
AIComponent.__index = AIComponent
function AIComponent.new(data)
data = data or {}
local self = setmetatable({}, AIComponent)
self.state = data.initial_state or "idle"
self.target = nil
self.detection_range = data.detection_range or 100
self.attack_range = data.attack_range or 50
self.patrol_points = data.patrol_points or {}
self.current_patrol_index = 1
self.state_timer = 0
return self
end
return {
TransformComponent = TransformComponent,
HealthComponent = HealthComponent,
VelocityComponent = VelocityComponent,
WeaponComponent = WeaponComponent,
AIComponent = AIComponent
}
🔄 Object Pool Pattern
Generic Object Pool
-- object_pool.lua
local ObjectPool = {}
ObjectPool.__index = ObjectPool
function ObjectPool.new(factory_function, reset_function, initial_size)
local self = setmetatable({}, ObjectPool)
self.factory_function = factory_function
self.reset_function = reset_function
self.available_objects = {}
self.used_objects = {}
self.total_created = 0
-- Pre-crear objetos iniciales
initial_size = initial_size or 10
for i = 1, initial_size do
local obj = self.factory_function()
table.insert(self.available_objects, obj)
self.total_created = self.total_created + 1
end
return self
end
function ObjectPool:acquire()
local obj = nil
if #self.available_objects > 0 then
obj = table.remove(self.available_objects)
else
-- Pool vacío, crear nuevo objeto
obj = self.factory_function()
self.total_created = self.total_created + 1
end
self.used_objects[obj] = true
return obj
end
function ObjectPool:release(obj)
if not self.used_objects[obj] then
return false -- Objeto no pertenece a este pool
end
-- Reset del objeto
if self.reset_function then
self.reset_function(obj)
end
-- Devolver al pool
self.used_objects[obj] = nil
table.insert(self.available_objects, obj)
return true
end
function ObjectPool:get_stats()
return {
total_created = self.total_created,
available = #self.available_objects,
used = self:count_used_objects(),
efficiency = self:calculate_efficiency()
}
end
function ObjectPool:count_used_objects()
local count = 0
for _ in pairs(self.used_objects) do
count = count + 1
end
return count
end
function ObjectPool:calculate_efficiency()
local used_count = self:count_used_objects()
return used_count / self.total_created
end
return ObjectPool
Game Object Pools
-- game_object_pools.script
local ObjectPool = require "main.object_pool"
function init(self)
self.pools = {}
-- Pool de balas
self.pools.bullets = ObjectPool.new(
function() -- Factory function
return factory.create("#bullet_factory")
end,
function(bullet) -- Reset function
go.set_position(vmath.vector3(-1000, -1000, 0), bullet)
go.set_rotation(vmath.quat(), bullet)
msg.post(bullet, "reset")
end,
50 -- Initial size
)
-- Pool de enemigos
self.pools.enemies = ObjectPool.new(
function()
return factory.create("#enemy_factory")
end,
function(enemy)
go.set_position(vmath.vector3(-1000, -1000, 0), enemy)
msg.post(enemy, "reset")
end,
20
)
-- Pool de efectos de partículas
self.pools.particles = ObjectPool.new(
function()
return factory.create("#particle_factory")
end,
function(particle)
go.set_position(vmath.vector3(-1000, -1000, 0), particle)
particlefx.stop(particle)
end,
30
)
-- Pool de power-ups
self.pools.powerups = ObjectPool.new(
function()
return factory.create("#powerup_factory")
end,
function(powerup)
go.set_position(vmath.vector3(-1000, -1000, 0), powerup)
msg.post(powerup, "reset")
end,
15
)
end
-- Funciones helper para usar los pools
function spawn_bullet(position, direction, speed)
local bullet = self.pools.bullets:acquire()
if bullet then
go.set_position(position, bullet)
msg.post(bullet, "initialize", {
direction = direction,
speed = speed
})
end
return bullet
end
function spawn_enemy(position, enemy_type)
local enemy = self.pools.enemies:acquire()
if enemy then
go.set_position(position, enemy)
msg.post(enemy, "initialize", {
type = enemy_type
})
end
return enemy
end
function spawn_explosion(position, explosion_type)
local particle = self.pools.particles:acquire()
if particle then
go.set_position(position, particle)
msg.post(particle, "play_explosion", {
type = explosion_type
})
-- Auto-return to pool después de la animación
timer.delay(2.0, false, function()
self.pools.particles:release(particle)
end)
end
return particle
end
function destroy_bullet(bullet)
if self.pools.bullets:release(bullet) then
-- Crear efecto de impacto antes de devolver al pool
local pos = go.get_position(bullet)
spawn_explosion(pos, "bullet_impact")
end
end
function destroy_enemy(enemy)
if self.pools.enemies:release(enemy) then
local pos = go.get_position(enemy)
spawn_explosion(pos, "enemy_death")
-- Posibilidad de drop de power-up
if math.random() < 0.3 then -- 30% chance
spawn_powerup(pos, "random")
end
end
end
function spawn_powerup(position, powerup_type)
local powerup = self.pools.powerups:acquire()
if powerup then
go.set_position(position, powerup)
msg.post(powerup, "initialize", {
type = powerup_type == "random" and get_random_powerup_type() or powerup_type
})
end
return powerup
end
function get_random_powerup_type()
local types = {"health", "speed_boost", "double_damage", "shield"}
return types[math.random(#types)]
end
-- Debug y estadísticas
function on_message(self, message_id, message, sender)
if message_id == hash("show_pool_stats") then
for pool_name, pool in pairs(self.pools) do
local stats = pool:get_stats()
print(string.format("%s Pool - Created: %d, Available: %d, Used: %d, Efficiency: %.2f",
pool_name, stats.total_created, stats.available,
stats.used, stats.efficiency))
end
end
end
⚡ Command Pattern
Command System
-- command_system.lua
local CommandSystem = {}
CommandSystem.__index = CommandSystem
function CommandSystem.new()
local self = setmetatable({}, CommandSystem)
self.command_history = {}
self.undo_stack = {}
self.redo_stack = {}
self.max_history = 100
return self
end
-- Comando base
local Command = {}
Command.__index = Command
function Command.new()
return setmetatable({}, Command)
end
function Command:execute()
error("execute() must be implemented by subclass")
end
function Command:undo()
error("undo() must be implemented by subclass")
end
function Command:can_undo()
return true
end
-- Ejecutar comando
function CommandSystem:execute_command(command)
-- Ejecutar comando
local success, result = pcall(command.execute, command)
if success then
-- Añadir al historial
table.insert(self.command_history, {
command = command,
timestamp = socket.gettime(),
result = result
})
-- Añadir a pila de undo si es posible
if command:can_undo() then
table.insert(self.undo_stack, command)
self.redo_stack = {} -- Limpiar redo stack
end
-- Mantener historial limitado
if #self.command_history > self.max_history then
table.remove(self.command_history, 1)
end
return true, result
else
print("Command execution failed:", result)
return false, result
end
end
function CommandSystem:undo()
if #self.undo_stack == 0 then
return false, "Nothing to undo"
end
local command = table.remove(self.undo_stack)
local success, result = pcall(command.undo, command)
if success then
table.insert(self.redo_stack, command)
return true, result
else
-- Devolver comando a la pila si falló el undo
table.insert(self.undo_stack, command)
return false, result
end
end
function CommandSystem:redo()
if #self.redo_stack == 0 then
return false, "Nothing to redo"
end
local command = table.remove(self.redo_stack)
return self:execute_command(command)
end
function CommandSystem:clear_history()
self.command_history = {}
self.undo_stack = {}
self.redo_stack = {}
end
CommandSystem.Command = Command
return CommandSystem
Game Commands Implementation
-- game_commands.script
local CommandSystem = require "main.command_system"
function init(self)
self.command_system = CommandSystem.new()
-- Hacer el sistema de comandos global
_G.game_commands = self.command_system
-- Registrar input para undo/redo
msg.post(".", "acquire_input_focus")
end
-- === MOVE COMMAND ===
local MoveCommand = setmetatable({}, {__index = CommandSystem.Command})
MoveCommand.__index = MoveCommand
function MoveCommand.new(entity, from_position, to_position)
local self = setmetatable(CommandSystem.Command.new(), MoveCommand)
self.entity = entity
self.from_position = from_position
self.to_position = to_position
return self
end
function MoveCommand:execute()
go.set_position(self.to_position, self.entity)
return "Moved to " .. tostring(self.to_position)
end
function MoveCommand:undo()
go.set_position(self.from_position, self.entity)
return "Moved back to " .. tostring(self.from_position)
end
-- === DAMAGE COMMAND ===
local DamageCommand = setmetatable({}, {__index = CommandSystem.Command})
DamageCommand.__index = DamageCommand
function DamageCommand.new(target, damage_amount)
local self = setmetatable(CommandSystem.Command.new(), DamageCommand)
self.target = target
self.damage_amount = damage_amount
self.previous_health = nil
return self
end
function DamageCommand:execute()
-- Guardar salud anterior
self.previous_health = go.get(self.target, "health")
-- Aplicar daño
local new_health = math.max(0, self.previous_health - self.damage_amount)
go.set(self.target, "health", new_health)
msg.post(self.target, "damaged", {
damage = self.damage_amount,
new_health = new_health
})
return string.format("Dealt %d damage", self.damage_amount)
end
function DamageCommand:undo()
go.set(self.target, "health", self.previous_health)
msg.post(self.target, "healed", {
amount = self.damage_amount,
new_health = self.previous_health
})
return string.format("Restored %d health", self.damage_amount)
end
-- === SPAWN COMMAND ===
local SpawnCommand = setmetatable({}, {__index = CommandSystem.Command})
SpawnCommand.__index = SpawnCommand
function SpawnCommand.new(factory_url, position, properties)
local self = setmetatable(CommandSystem.Command.new(), SpawnCommand)
self.factory_url = factory_url
self.position = position
self.properties = properties or {}
self.spawned_object = nil
return self
end
function SpawnCommand:execute()
self.spawned_object = factory.create(self.factory_url, self.position, nil, self.properties)
return "Spawned object: " .. tostring(self.spawned_object)
end
function SpawnCommand:undo()
if self.spawned_object then
go.delete(self.spawned_object)
self.spawned_object = nil
end
return "Destroyed spawned object"
end
-- === COMPOSITE COMMAND ===
local CompositeCommand = setmetatable({}, {__index = CommandSystem.Command})
CompositeCommand.__index = CompositeCommand
function CompositeCommand.new(commands)
local self = setmetatable(CommandSystem.Command.new(), CompositeCommand)
self.commands = commands or {}
return self
end
function CompositeCommand:add_command(command)
table.insert(self.commands, command)
end
function CompositeCommand:execute()
local results = {}
for i, command in ipairs(self.commands) do
local success, result = pcall(command.execute, command)
if not success then
-- Undo comandos ya ejecutados
for j = i - 1, 1, -1 do
pcall(self.commands[j].undo, self.commands[j])
end
error("Composite command failed at step " .. i .. ": " .. result)
end
table.insert(results, result)
end
return results
end
function CompositeCommand:undo()
local results = {}
-- Undo en orden inverso
for i = #self.commands, 1, -1 do
local success, result = pcall(self.commands[i].undo, self.commands[i])
if success then
table.insert(results, result)
end
end
return results
end
-- Funciones helper para crear comandos
function create_move_command(entity, to_position)
local from_position = go.get_position(entity)
return MoveCommand.new(entity, from_position, to_position)
end
function create_damage_command(target, damage)
return DamageCommand.new(target, damage)
end
function create_spawn_command(factory_url, position, properties)
return SpawnCommand.new(factory_url, position, properties)
end
function create_attack_sequence(attacker, target, damage)
local composite = CompositeCommand.new()
-- Mover atacante hacia objetivo
local attacker_pos = go.get_position(attacker)
local target_pos = go.get_position(target)
local attack_pos = target_pos + vmath.normalize(attacker_pos - target_pos) * 50
composite:add_command(create_move_command(attacker, attack_pos))
composite:add_command(create_damage_command(target, damage))
composite:add_command(create_move_command(attacker, attacker_pos))
return composite
end
-- Input handling
function on_input(self, action_id, action)
if action_id == hash("undo") and action.pressed then
local success, result = self.command_system:undo()
if success then
print("Undo:", result)
else
print("Cannot undo:", result)
end
elseif action_id == hash("redo") and action.pressed then
local success, result = self.command_system:redo()
if success then
print("Redo:", result)
else
print("Cannot redo:", result)
end
end
end
📚 Recursos y Referencias
Patrones de Diseño Clásicos
- Gang of Four - Design Patterns book
- Game Programming Patterns - Robert Nystrom
- Real-Time Rendering - Akenine-Möller
Arquitecturas de Juegos
- Entity Component System - Para juegos complejos
- Model-View-Controller - Para interfaces
- Publish-Subscribe - Para eventos
- Data-Driven Design - Para flexibilidad
Links Útiles
🎯 Ejercicios Propuestos
-
Dialogue System: Crea un sistema de diálogos usando State Machine y Command Pattern.
-
Inventory System: Implementa un inventario completo usando Component System.
-
Quest System: Diseña un sistema de misiones con Observer Pattern para eventos.
-
Save/Load System: Usa Command Pattern para crear un sistema de guardado que soporte undo.
-
Skill System: Crea un sistema de habilidades con cooldowns usando múltiples patrones.
Los patrones de diseño son herramientas fundamentales para crear código mantenible y escalable. Dominar estos patrones te permitirá abordar problemas complejos de manera elegante y crear arquitecturas robustas que puedan evolucionar con tu juego.