← Volver al listado de tecnologías

Primer Juego: Space Shooter Completo

Por: Artiko
defoldspace-shooterprimer-juegotutorialgamedev

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:

Configuración Inicial del Proyecto

Crear Nuevo Proyecto

  1. File → New Project
  2. Selecciona “Desktop Game” template
  3. Nombra: space_shooter
  4. 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

  1. Click derecho en main/New → Folderplayer
  2. En player/New → Game Objectplayer.go

Configurar Sprite del Player

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

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

  1. Abre input/game.input_binding
  2. En Key Triggers:
    • KEY_LEFTleft
    • KEY_RIGHTright
    • KEY_UPup
    • KEY_DOWNdown
    • KEY_SPACEfire
  3. En Mouse Triggers:
    • MOUSE_BUTTON_LEFTtouch

Parte 2: Sistema de Balas

Crear Bullet Game Object

  1. En main/New → Folderbullets
  2. En bullets/New → Game Objectbullet.go

Configurar Bullet Sprite

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

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

  1. Abre main/main.collection
  2. Click derechoAdd Game Objectfactories
  3. Add Component → Factory:
    • Id: bullet_factory
    • Prototype: /bullets/bullet.go

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

  1. En player.goAdd Component → Collision Object

  2. Configura:

    • Type: KINEMATIC
    • Group: player
    • Mask: enemy
  3. 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

  1. En main/New → Folderui
  2. En ui/New → Guihud.gui

Configurar HUD Elements

En el editor GUI:

  1. Add → Textscore_text

    • Position: (50, 600)
    • Text: “Score: 0”
    • Font: System font
  2. Add → Textlives_text

    • Position: (50, 570)
    • Text: “Lives: 3”
  3. Add → Textlevel_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

  1. En main/main.collection
  2. Add Game Objectui
  3. Add Component → GUI al objeto ui:
    • Gui: /ui/hud.gui
    • Id: hud

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

  1. Project → Build (Ctrl+B)
  2. 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:

Ejercicio 2: Efectos Visuales

Ejercicio 3: Audio

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.