Primer Juego: Space Shooter Completo
Primer Juego: Space Shooter Completo
¡Es hora de crear tu primer juego completo en Defold! En esta lección construiremos un Space Shooter clásico desde cero, aplicando todos los conceptos aprendidos. Al final tendrás un juego totalmente funcional con:
- 🚀 Nave espacial controlable
- 👾 Enemigos con diferentes patrones
- 💥 Sistema de disparos y colisiones
- ⭐ Power-ups y mejoras
- 🏆 Puntuación y vidas
- 🎮 Estados de juego (menú, juego, game over)
Configuración Inicial del Proyecto
Crear Nuevo Proyecto
- File → New Project
- Selecciona “Desktop Game” template
- Nombra:
space_shooter - Create New Project
Estructura de Carpetas
Organicemos el proyecto desde el inicio:
space_shooter/
├── main/
│ ├── main.collection
│ └── main.script
├── player/
│ ├── player.go
│ └── player.script
├── enemies/
│ ├── basic_enemy.go
│ └── enemy.script
├── bullets/
│ ├── bullet.go
│ └── bullet.script
├── ui/
│ ├── hud.gui
│ └── hud_script.gui_script
└── sounds/
└── (archivos de audio)
Parte 1: La Nave del Jugador
Crear el Player Game Object
- Click derecho en
main/→ New → Folder →player - En
player/→ New → Game Object →player.go
Configurar Sprite del Player
- Click derecho en
player.go→ Add Component → Sprite - Configura:
- Image:
/builtins/graphics/particle_blob.png - Default Animation:
anim - Tint: Azul (0.2, 0.5, 1.0, 1.0)
- Image:
Script del Player
-- player/player.script
go.property("speed", 400)
go.property("fire_rate", 0.2) -- Tiempo entre disparos
function init(self)
-- Configuración inicial
self.fire_timer = 0
self.screen_width = 960
self.screen_height = 640
-- Posición inicial en la parte inferior
go.set_position(vmath.vector3(self.screen_width/2, 100, 0))
-- Habilitar input
msg.post(".", "acquire_input_focus")
print("Player inicializado")
end
function update(self, dt)
-- Actualizar timer de disparo
if self.fire_timer > 0 then
self.fire_timer = self.fire_timer - dt
end
-- Obtener posición actual
local pos = go.get_position()
-- Mantener dentro de pantalla
pos.x = math.max(30, math.min(self.screen_width - 30, pos.x))
pos.y = math.max(30, math.min(self.screen_height - 30, pos.y))
go.set_position(pos)
end
function on_input(self, action_id, action)
local pos = go.get_position()
-- Movimiento con teclado
if action_id == hash("left") and action.pressed then
pos.x = pos.x - self.speed * (1/60) -- Aprox 60fps
go.set_position(pos)
elseif action_id == hash("right") and action.pressed then
pos.x = pos.x + self.speed * (1/60)
go.set_position(pos)
elseif action_id == hash("up") and action.pressed then
pos.y = pos.y + self.speed * (1/60)
go.set_position(pos)
elseif action_id == hash("down") and action.pressed then
pos.y = pos.y - self.speed * (1/60)
go.set_position(pos)
end
-- Disparo automático con espacio
if action_id == hash("fire") and action.pressed then
if self.fire_timer <= 0 then
fire_bullet(self)
self.fire_timer = self.fire_rate
end
end
-- Movimiento táctil/mouse
if action_id == hash("touch") and (action.pressed or action.repeated) then
local target_pos = vmath.vector3(action.x, action.y, 0)
go.animate(".", "position", go.PLAYBACK_ONCE_FORWARD,
target_pos, go.EASING_OUTQUAD, 0.1)
end
end
function fire_bullet(self)
local pos = go.get_position()
-- Crear bala ligeramente adelante de la nave
pos.y = pos.y + 30
factory.create("/main#bullet_factory", pos)
end
function on_message(self, message_id, message, sender)
if message_id == hash("hit") then
-- Player recibe daño
print("¡Player golpeado!")
msg.post("/main#game_manager", "player_hit")
-- Efecto visual de daño
go.animate(".", "tint", go.PLAYBACK_ONCE_FORWARD,
vmath.vector4(1, 0.2, 0.2, 1), go.EASING_OUTQUAD, 0.1,
0, function()
go.animate(".", "tint", go.PLAYBACK_ONCE_FORWARD,
vmath.vector4(0.2, 0.5, 1, 1), go.EASING_OUTQUAD, 0.1)
end)
end
end
Configurar Input Bindings
- Abre
input/game.input_binding - En Key Triggers:
KEY_LEFT→leftKEY_RIGHT→rightKEY_UP→upKEY_DOWN→downKEY_SPACE→fire
- En Mouse Triggers:
MOUSE_BUTTON_LEFT→touch
Parte 2: Sistema de Balas
Crear Bullet Game Object
- En
main/→ New → Folder →bullets - En
bullets/→ New → Game Object →bullet.go
Configurar Bullet Sprite
- Add Component → Sprite
- Configura:
- Image:
/builtins/graphics/particle_blob.png - Default Animation:
anim - Tint: Amarillo (1.0, 1.0, 0.2, 1.0)
- Size: (8, 16, 0) - Hacer más pequeña
- Image:
Script de las Balas
-- bullets/bullet.script
local SPEED = 600 -- Velocidad hacia arriba
function init(self)
-- Destruir automáticamente después de 3 segundos
timer.delay(3.0, false, function()
go.delete()
end)
end
function update(self, dt)
-- Mover hacia arriba
local pos = go.get_position()
pos.y = pos.y + SPEED * dt
go.set_position(pos)
-- Destruir si sale de pantalla
if pos.y > 700 then
go.delete()
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("hit_enemy") then
-- La bala impactó un enemigo
go.delete()
end
end
Factory para Balas
- Abre
main/main.collection - Click derecho → Add Game Object →
factories - Add Component → Factory:
- Id:
bullet_factory - Prototype:
/bullets/bullet.go
- Id:
Parte 3: Sistema de Enemigos
Crear Enemy Game Object
-- enemies/enemy.script
go.property("enemy_type", "basic") -- "basic", "fast", "tank"
go.property("health", 1)
go.property("speed", 150)
go.property("score_value", 10)
function init(self)
-- Configurar según tipo de enemigo
setup_enemy_type(self)
-- 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))
-- Animación de entrada
go.animate(".", "scale", go.PLAYBACK_ONCE_FORWARD,
vmath.vector3(1, 1, 1), go.EASING_OUTBACK, 0.3)
end
function setup_enemy_type(self)
local tint = vmath.vector4(1, 1, 1, 1)
if self.enemy_type == "basic" then
self.health = 1
self.speed = 150
self.score_value = 10
tint = vmath.vector4(1, 0.3, 0.3, 1) -- Rojo
elseif self.enemy_type == "fast" then
self.health = 1
self.speed = 300
self.score_value = 20
tint = vmath.vector4(1, 1, 0.3, 1) -- Amarillo
elseif self.enemy_type == "tank" then
self.health = 3
self.speed = 80
self.score_value = 50
tint = vmath.vector4(0.5, 0.5, 0.5, 1) -- Gris
go.set_scale(vmath.vector3(1.5, 1.5, 1)) -- Más grande
end
go.set("#sprite", "tint", tint)
end
function update(self, dt)
-- Movimiento hacia abajo con zigzag para algunos tipos
local pos = go.get_position()
if self.enemy_type == "basic" then
pos.y = pos.y - self.speed * dt
elseif self.enemy_type == "fast" then
pos.y = pos.y - self.speed * dt
-- Movimiento zigzag
pos.x = pos.x + math.sin(socket.gettime() * 5) * 100 * dt
elseif self.enemy_type == "tank" then
pos.y = pos.y - self.speed * dt
end
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
take_damage(self, 1)
end
end
function take_damage(self, damage)
self.health = self.health - damage
-- 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()
if self.health > 0 then
-- Restaurar color original
setup_enemy_type(self)
end
end)
if self.health <= 0 then
-- Enemigo destruido
destroy_enemy(self)
end
end
function destroy_enemy(self)
-- Notificar puntuación
msg.post("/main#game_manager", "enemy_destroyed", {
score = self.score_value,
position = go.get_position()
})
-- Efecto de explosión
go.animate(".", "scale", go.PLAYBACK_ONCE_FORWARD,
vmath.vector3(2, 2, 2), go.EASING_OUTQUAD, 0.2)
go.animate(".", "tint.w", go.PLAYBACK_ONCE_FORWARD,
0, go.EASING_OUTQUAD, 0.2,
0, function()
go.delete()
end)
end
Factory para Enemigos
En main/main.collection, agregar más factories:
-- Factories en main.collection
├── bullet_factory (ya creado)
├── enemy_basic_factory
├── enemy_fast_factory
└── enemy_tank_factory
Parte 4: Game Manager
Script Principal del Juego
-- main/main.script
local SPAWN_INTERVALS = {
basic = 2.0,
fast = 4.0,
tank = 8.0
}
function init(self)
-- Estado del juego
self.score = 0
self.lives = 3
self.level = 1
self.game_state = "playing" -- "menu", "playing", "paused", "game_over"
-- Timers para spawn
self.spawn_timers = {
basic = 0,
fast = 0,
tank = 0
}
-- Configuración de dificultad
self.difficulty_multiplier = 1.0
print("Space Shooter iniciado!")
print("Vidas:", self.lives, "Score:", self.score)
end
function update(self, dt)
if self.game_state ~= "playing" then
return
end
-- Actualizar spawn timers
for enemy_type, timer in pairs(self.spawn_timers) do
timer = timer + dt
local spawn_interval = SPAWN_INTERVALS[enemy_type] / self.difficulty_multiplier
if timer >= spawn_interval then
spawn_enemy(self, enemy_type)
self.spawn_timers[enemy_type] = 0
else
self.spawn_timers[enemy_type] = timer
end
end
-- Aumentar dificultad progresivamente
update_difficulty(self)
end
function spawn_enemy(self, enemy_type)
local factory_id = "#enemy_" .. enemy_type .. "_factory"
factory.create(factory_id)
end
function update_difficulty(self)
-- Aumentar dificultad cada 200 puntos
local new_level = math.floor(self.score / 200) + 1
if new_level > self.level then
self.level = new_level
self.difficulty_multiplier = 1.0 + (self.level - 1) * 0.3
print("¡Nivel", self.level, "! Dificultad:", self.difficulty_multiplier)
-- Mensaje visual de nivel up
msg.post("/ui#hud", "show_level_up", {level = self.level})
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("enemy_destroyed") then
self.score = self.score + message.score
print("Score:", self.score)
-- Actualizar UI
msg.post("/ui#hud", "update_score", {score = self.score})
elseif message_id == hash("player_hit") then
self.lives = self.lives - 1
print("Vidas restantes:", self.lives)
-- Actualizar UI
msg.post("/ui#hud", "update_lives", {lives = self.lives})
if self.lives <= 0 then
game_over(self)
end
end
end
function game_over(self)
self.game_state = "game_over"
print("¡GAME OVER! Score final:", self.score)
-- Mostrar pantalla de game over
msg.post("/ui#hud", "show_game_over", {final_score = self.score})
-- Pausar spawn de enemigos
for enemy_type, _ in pairs(self.spawn_timers) do
self.spawn_timers[enemy_type] = 0
end
end
Parte 5: Sistema de Colisiones
Para detectar colisiones entre balas y enemigos, necesitamos Collision Objects.
Agregar Colisiones al Player
-
En
player.go→ Add Component → Collision Object -
Configura:
- Type:
KINEMATIC - Group:
player - Mask:
enemy
- Type:
-
Add Shape → Box:
- Count: 1
- Dimensions: (20, 20, 10)
Agregar Colisiones a Bullets
-- En bullet.go
-- Collision Object:
-- Type: KINEMATIC
-- Group: bullet
-- Mask: enemy
-- Shape: Box (6, 12, 10)
Agregar Colisiones a Enemies
-- En enemy.go
-- Collision Object:
-- Type: KINEMATIC
-- Group: enemy
-- Mask: player, bullet
-- Shape: Box (20, 20, 10)
Script de Colisiones
-- Agregar a bullet.script
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("enemy") then
-- Notificar al enemigo
msg.post(message.other_id, "hit")
-- Destruir bala
go.delete()
end
end
end
-- Agregar a enemy.script
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("bullet") then
take_damage(self, 1)
elseif other_group == hash("player") then
-- Dañar al jugador
msg.post(message.other_id, "hit")
take_damage(self, 999) -- Destruir enemigo también
end
elseif message_id == hash("hit") then
take_damage(self, 1)
end
end
Parte 6: Interfaz de Usuario (HUD)
Crear GUI para HUD
- En
main/→ New → Folder →ui - En
ui/→ New → Gui →hud.gui
Configurar HUD Elements
En el editor GUI:
-
Add → Text →
score_text- Position: (50, 600)
- Text: “Score: 0”
- Font: System font
-
Add → Text →
lives_text- Position: (50, 570)
- Text: “Lives: 3”
-
Add → Text →
level_text- Position: (50, 540)
- Text: “Level: 1”
Script del HUD
-- ui/hud_script.gui_script
function init(self)
-- Referencias a elementos GUI
self.score_text = gui.get_node("score_text")
self.lives_text = gui.get_node("lives_text")
self.level_text = gui.get_node("level_text")
print("HUD inicializado")
end
function on_message(self, message_id, message, sender)
if message_id == hash("update_score") then
gui.set_text(self.score_text, "Score: " .. message.score)
elseif message_id == hash("update_lives") then
gui.set_text(self.lives_text, "Lives: " .. message.lives)
elseif message_id == hash("show_level_up") then
gui.set_text(self.level_text, "Level: " .. message.level)
-- Animación de level up
gui.animate(self.level_text, gui.PROP_SCALE, vmath.vector3(1.5, 1.5, 1),
gui.EASING_OUTBACK, 0.3, 0, function()
gui.animate(self.level_text, gui.PROP_SCALE, vmath.vector3(1, 1, 1),
gui.EASING_OUTQUAD, 0.2)
end)
elseif message_id == hash("show_game_over") then
-- Mostrar pantalla de game over
show_game_over_screen(self, message.final_score)
end
end
function show_game_over_screen(self, final_score)
-- Crear elementos de game over dinámicamente
-- (En un proyecto real, estos estarían predefinidos)
print("GAME OVER - Score final:", final_score)
end
Agregar HUD a la Escena
- En
main/main.collection - Add Game Object →
ui - Add Component → GUI al objeto
ui:- Gui:
/ui/hud.gui - Id:
hud
- Gui:
Parte 7: Prueba y Refinamiento
Balanceo del Juego
Ajusta estos valores según la experiencia de juego:
-- Velocidades
player.speed = 400 -- Movimiento jugador
bullet.speed = 600 -- Velocidad balas
enemy.speed = 150 -- Velocidad enemigos
-- Tiempo entre disparos
player.fire_rate = 0.15 -- Más rápido = más disparos
-- Spawn de enemigos
SPAWN_INTERVALS = {
basic = 1.5, -- Más frecuente
fast = 3.0, -- Intermedio
tank = 6.0 -- Menos frecuente
}
-- Puntuación
basic_enemy.score = 10
fast_enemy.score = 25
tank_enemy.score = 100
Testing y Debug
-- Agregar debug info en game_manager
function update(self, dt)
-- Debug en pantalla
if DEBUG then
msg.post("@render:", "draw_debug_text", {
text = "Score: " .. self.score .. " Lives: " .. self.lives,
position = vmath.vector3(10, 30, 0)
})
end
end
Compilar y Ejecutar
- Project → Build (
Ctrl+B) - Project → Run (
F5)
¡Felicidades! Ya tienes un Space Shooter completamente funcional.
Ejercicios de Mejora
Ejercicio 1: Power-ups
Agregar power-ups que caen de enemigos destruidos:
- Rapid Fire: Disparo más rápido
- Spread Shot: Múltiples balas
- Shield: Protección temporal
Ejercicio 2: Efectos Visuales
- Partículas para explosiones
- Screen shake cuando el player es golpeado
- Background estrellado animado
Ejercicio 3: Audio
- Sonidos de disparo
- Explosiones
- Música de fondo
- Efectos de power-up
Próximos Pasos
En la siguiente lección profundizaremos en el sistema de física y colisiones de Defold, aprendiendo técnicas más avanzadas para crear interacciones complejas.
⬅️ Anterior: Game Objects y Components | Siguiente: Física y Colisiones ➡️
¡Excelente trabajo! Has creado tu primer juego completo en Defold. Este proyecto te servirá como base para entender todos los sistemas fundamentales del motor.