← Volver al listado de tecnologías
Juego de Plataformas Completo
Juego de Plataformas Completo
En esta lección crearemos un juego de plataformas completo y pulido que incluye todas las mecánicas clásicas del género. Aprenderás técnicas avanzadas de control de personajes, diseño de niveles con tilemaps, y sistemas de progresión.
Características del Juego
Nuestro platformer incluirá:
- 🏃♂️ Personaje con físicas precisas - Movimiento fluido y responsivo
- 🎯 Sistema de salto avanzado - Coyote time, jump buffering, variable height
- 🗺️ Niveles con tilemaps - Construcción visual de niveles
- 💎 Collectibles y power-ups - Gemas, vidas extra, habilidades
- 👹 Enemigos con IA - Patrullaje, detección de jugador
- 🚪 Sistema de checkpoints - Respawn inteligente
- 🏆 Progresión de niveles - Desbloqueo secuencial
Configuración del Proyecto
Estructura de Archivos
platformer_game/
├── player/
│ ├── player.go
│ ├── player.script
│ └── player_animations.script
├── enemies/
│ ├── goomba.go
│ ├── goomba.script
│ ├── flying_enemy.go
│ └── enemy_ai.script
├── levels/
│ ├── level_01.collection
│ ├── level_01.tilemap
│ ├── level_manager.script
│ └── checkpoint.go
├── collectibles/
│ ├── gem.go
│ ├── powerup.go
│ └── collectible.script
├── ui/
│ ├── game_ui.gui
│ └── level_complete.gui
└── assets/
├── sprites/
├── tilemaps/
└── sounds/
Configuración de Física
Primero configuremos el sistema de física en game.project:
[physics]
type = 2D
gravity_y = -800
debug = 0
max_collisions = 64
max_contacts = 32
velocity_iterations = 6
position_iterations = 2
Parte 1: Personaje del Jugador
Player Game Object y Físicas
-- player/player.script
go.property("max_speed", 200)
go.property("acceleration", 800)
go.property("jump_power", 450)
go.property("friction", 0.85)
function init(self)
-- Estado del personaje
self.velocity = vmath.vector3(0, 0, 0)
self.grounded = false
self.ground_contact_count = 0
-- Mecánicas avanzadas de salto
self.coyote_time = 0.15 -- Tiempo para saltar después de dejar plataforma
self.coyote_timer = 0
self.jump_buffer_time = 0.1 -- Buffer para input de salto temprano
self.jump_buffer_timer = 0
self.variable_jump_time = 0.3 -- Tiempo para controlar altura del salto
self.jump_timer = 0
self.jumping = false
-- Animaciones
self.current_animation = ""
self.facing_right = true
-- Control
self.input = {
left = false,
right = false,
jump = false,
jump_pressed = false
}
msg.post(".", "acquire_input_focus")
print("Player inicializado")
end
function update(self, dt)
-- Actualizar timers
update_timers(self, dt)
-- Detectar estado de suelo
update_ground_state(self)
-- Procesar input
handle_horizontal_movement(self, dt)
handle_jumping(self, dt)
-- Aplicar velocidad con límites
apply_velocity_limits(self)
-- Actualizar animaciones
update_animations(self)
-- Aplicar fricción
apply_friction(self, dt)
end
function update_timers(self, dt)
-- Coyote time (tiempo de gracia para saltar)
if self.grounded then
self.coyote_timer = self.coyote_time
else
self.coyote_timer = math.max(0, self.coyote_timer - dt)
end
-- Jump buffer (buffer para input de salto)
if self.jump_buffer_timer > 0 then
self.jump_buffer_timer = self.jump_buffer_timer - dt
end
-- Variable jump timer
if self.jump_timer > 0 then
self.jump_timer = self.jump_timer - dt
end
end
function update_ground_state(self)
self.grounded = self.ground_contact_count > 0
-- Debug visual
if self.grounded then
sprite.set_constant("#sprite", "tint", vmath.vector4(1, 1, 1, 1))
else
sprite.set_constant("#sprite", "tint", vmath.vector4(1, 0.8, 0.8, 1))
end
end
function handle_horizontal_movement(self, dt)
local input_force = 0
if self.input.left then
input_force = -self.acceleration
self.facing_right = false
elseif self.input.right then
input_force = self.acceleration
self.facing_right = true
end
-- Aplicar fuerza horizontal
if input_force ~= 0 then
self.velocity.x = self.velocity.x + input_force * dt
end
end
function handle_jumping(self, dt)
-- Procesar input de salto
if self.input.jump_pressed then
self.jump_buffer_timer = self.jump_buffer_time
self.input.jump_pressed = false
end
-- Iniciar salto si es posible
if self.jump_buffer_timer > 0 and (self.grounded or self.coyote_timer > 0) then
start_jump(self)
self.jump_buffer_timer = 0
self.coyote_timer = 0
end
-- Variable jump height
if self.jumping and self.input.jump and self.jump_timer > 0 then
-- Mantener fuerza de salto mientras se presiona
physics.apply_force(".", vmath.vector3(0, self.jump_power * 0.3, 0))
else
self.jumping = false
end
end
function start_jump(self)
self.velocity.y = self.jump_power
self.jumping = true
self.jump_timer = self.variable_jump_time
-- Efecto de sonido
msg.post("#jump_sound", "play_sound")
-- Efecto visual
create_jump_effect(self)
print("¡Salto!")
end
function apply_velocity_limits(self)
-- Limitar velocidad horizontal
self.velocity.x = math.max(-self.max_speed, math.min(self.max_speed, self.velocity.x))
-- Limitar velocidad vertical (terminal velocity)
self.velocity.y = math.max(-600, self.velocity.y)
-- Aplicar velocidad al motor de física
physics.set_velocity(".", self.velocity)
end
function apply_friction(self, dt)
if self.grounded and not (self.input.left or self.input.right) then
self.velocity.x = self.velocity.x * self.friction
end
end
function update_animations(self)
local target_animation = determine_animation(self)
if target_animation ~= self.current_animation then
play_animation(self, target_animation)
self.current_animation = target_animation
end
-- Voltear sprite según dirección
local scale = go.get_scale()
scale.x = self.facing_right and math.abs(scale.x) or -math.abs(scale.x)
go.set_scale(scale)
end
function determine_animation(self)
if not self.grounded then
if self.velocity.y > 50 then
return "jump"
else
return "fall"
end
elseif math.abs(self.velocity.x) > 20 then
return "run"
else
return "idle"
end
end
function play_animation(self, animation_name)
sprite.play_flipbook("#sprite", animation_name)
print("Animación:", animation_name)
end
function on_input(self, action_id, action)
if action_id == hash("left") then
self.input.left = action.pressed or action.repeated
elseif action_id == hash("right") then
self.input.right = action.pressed or action.repeated
elseif action_id == hash("jump") then
self.input.jump = action.pressed or action.repeated
if action.pressed then
self.input.jump_pressed = true
end
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("contact_point_response") then
handle_collision(self, message)
elseif message_id == hash("trigger_response") then
handle_trigger(self, message)
elseif message_id == hash("take_damage") then
take_damage(self, message.amount or 1)
end
end
function handle_collision(self, message)
local other_group = message.other_group
if other_group == hash("ground") then
if message.normal.y > 0.7 then -- Superficie mayormente horizontal
if not self.grounded then
self.ground_contact_count = self.ground_contact_count + 1
land_on_ground(self)
end
elseif math.abs(message.normal.x) > 0.7 then -- Pared
-- Cancelar velocidad horizontal si golpea pared
if (message.normal.x > 0 and self.velocity.x < 0) or
(message.normal.x < 0 and self.velocity.x > 0) then
self.velocity.x = 0
end
end
elseif other_group == hash("enemy") then
-- Verificar si salta sobre enemigo
if message.normal.y > 0.7 and self.velocity.y < 0 then
bounce_off_enemy(self)
msg.post(message.other_id, "stomped")
else
take_damage(self, 1)
end
end
end
function land_on_ground(self)
if self.velocity.y < -100 then -- Caída significativa
create_landing_effect(self)
msg.post("#land_sound", "play_sound")
end
end
function bounce_off_enemy(self)
self.velocity.y = self.jump_power * 0.7 -- Rebote menor que salto normal
create_stomp_effect(self)
msg.post("#stomp_sound", "play_sound")
end
Sistema de Efectos Visuales
-- player/player_effects.script
function create_jump_effect(player_script)
local pos = go.get_position()
pos.y = pos.y - 20 -- Efecto en los pies
-- Crear partículas de polvo
factory.create("/effects#dust_factory", pos, nil, {
particle_count = 5,
direction = vmath.vector3(0, -1, 0)
})
end
function create_landing_effect(player_script)
local pos = go.get_position()
pos.y = pos.y - 20
factory.create("/effects#dust_factory", pos, nil, {
particle_count = 8,
direction = vmath.vector3(0, -1, 0)
})
-- Screen shake sutil
msg.post("/camera", "shake", {intensity = 2, duration = 0.1})
end
function create_stomp_effect(player_script)
local pos = go.get_position()
factory.create("/effects#stomp_factory", pos, nil, {
particle_count = 10,
color = vmath.vector4(1, 1, 0, 1)
})
end
Parte 2: Construcción de Niveles con Tilemaps
Configurar Tilemap
- Assets → New → Tile Source →
level_tiles.tilesource - Configurar:
- Image:
/assets/tilemap_sheet.png - Tile Width/Height: 32
- Collision Groups:
ground,platform,spike
- Image:
Crear Nivel con Tilemap
-- levels/level_01.collection estructura:
level_01/
├── background (Game Object)
│ └── tilemap (Tilemap)
├── foreground (Game Object)
│ └── tilemap (Tilemap)
├── collision_layer (Game Object)
│ └── tilemap (Tilemap) - Solo colisión
├── player_spawn (Game Object)
├── checkpoints/ (Collection)
└── collectibles/ (Collection)
Level Manager
-- levels/level_manager.script
function init(self)
-- Estado del nivel
self.gems_collected = 0
self.total_gems = 0
self.checkpoints_activated = {}
self.current_checkpoint = "/level/player_spawn"
-- Configuración de cámara
self.camera_bounds = {
left = 0,
right = 2048, -- Ancho del nivel
top = 640,
bottom = 0
}
-- Contar collectibles
count_level_collectibles(self)
print("Level Manager inicializado")
print("Gemas en el nivel:", self.total_gems)
end
function count_level_collectibles(self)
-- Contar todos los collectibles en el nivel
self.total_gems = #go.get_children("/level/collectibles")
end
function update(self, dt)
-- Actualizar cámara para seguir al jugador
update_camera_position(self)
-- Verificar condiciones de victoria
check_level_completion(self)
end
function update_camera_position(self)
local player_pos = go.get_position("/level/player")
local camera_pos = go.get_position("/camera")
-- Calcular nueva posición de cámara
local target_x = math.max(self.camera_bounds.left + 480,
math.min(self.camera_bounds.right - 480, player_pos.x))
local target_y = math.max(self.camera_bounds.bottom + 320,
math.min(self.camera_bounds.top - 320, player_pos.y))
-- Movimiento suave de cámara (lerp)
local lerp_speed = 5
camera_pos.x = camera_pos.x + (target_x - camera_pos.x) * lerp_speed * (1/60)
camera_pos.y = camera_pos.y + (target_y - camera_pos.y) * lerp_speed * (1/60)
go.set_position(camera_pos, "/camera")
end
function check_level_completion(self)
if self.gems_collected >= self.total_gems then
if not self.level_completed then
complete_level(self)
end
end
end
function complete_level(self)
self.level_completed = true
print("¡Nivel completado!")
-- Mostrar UI de nivel completo
msg.post("/ui#level_complete", "show", {
gems_collected = self.gems_collected,
total_gems = self.total_gems,
time_taken = self.level_time
})
-- Desbloquear siguiente nivel
msg.post("/game_manager", "unlock_next_level")
end
function on_message(self, message_id, message, sender)
if message_id == hash("gem_collected") then
self.gems_collected = self.gems_collected + 1
print("Gemas:", self.gems_collected .. "/" .. self.total_gems)
-- Actualizar UI
msg.post("/ui#game_hud", "update_gems", {
collected = self.gems_collected,
total = self.total_gems
})
elseif message_id == hash("checkpoint_activated") then
activate_checkpoint(self, sender)
elseif message_id == hash("player_died") then
respawn_player(self)
elseif message_id == hash("player_fell") then
-- Jugador cayó del mapa
msg.post("/level/player", "take_damage", {amount = 1})
end
end
function activate_checkpoint(self, checkpoint_id)
if not self.checkpoints_activated[checkpoint_id] then
self.checkpoints_activated[checkpoint_id] = true
self.current_checkpoint = checkpoint_id
print("Checkpoint activado:", checkpoint_id)
-- Efecto visual y sonido
msg.post(checkpoint_id, "activate_visual")
msg.post("#checkpoint_sound", "play_sound")
end
end
function respawn_player(self)
local checkpoint_pos = go.get_position(self.current_checkpoint)
go.set_position(checkpoint_pos, "/level/player")
-- Resetear velocidad del jugador
physics.set_velocity("/level/player", vmath.vector3(0, 0, 0))
print("Jugador respawneado en:", checkpoint_pos)
end
Parte 3: Enemigos con IA
Goomba Básico
-- enemies/goomba.script
go.property("patrol_distance", 100)
go.property("move_speed", 50)
go.property("detection_range", 80)
function init(self)
self.state = "patrol" -- "patrol", "chase", "stunned"
self.direction = 1 -- 1 = derecha, -1 = izquierda
self.start_pos = go.get_position()
self.patrol_left = self.start_pos.x - self.patrol_distance/2
self.patrol_right = self.start_pos.x + self.patrol_distance/2
self.health = 1
self.stunned_timer = 0
print("Goomba inicializado en:", self.start_pos)
end
function update(self, dt)
if self.state == "stunned" then
self.stunned_timer = self.stunned_timer - dt
if self.stunned_timer <= 0 then
self.state = "patrol"
end
return
end
local player_pos = go.get_position("/level/player")
local my_pos = go.get_position()
local distance_to_player = vmath.length(player_pos - my_pos)
-- Decidir comportamiento basado en distancia al jugador
if distance_to_player < self.detection_range then
self.state = "chase"
chase_player(self, player_pos, my_pos, dt)
else
self.state = "patrol"
patrol_behavior(self, my_pos, dt)
end
-- Actualizar animación
update_enemy_animation(self)
end
function patrol_behavior(self, pos, dt)
-- Patrullar entre dos puntos
local next_x = pos.x + self.direction * self.move_speed * dt
if next_x > self.patrol_right then
self.direction = -1
next_x = self.patrol_right
elseif next_x < self.patrol_left then
self.direction = 1
next_x = self.patrol_left
end
pos.x = next_x
go.set_position(pos)
end
function chase_player(self, player_pos, my_pos, dt)
-- Moverse hacia el jugador
local direction_to_player = player_pos.x > my_pos.x and 1 or -1
self.direction = direction_to_player
local next_x = my_pos.x + direction_to_player * self.move_speed * 1.5 * dt
my_pos.x = next_x
go.set_position(my_pos)
end
function update_enemy_animation(self)
-- Voltear sprite según dirección
local scale = go.get_scale()
scale.x = self.direction > 0 and math.abs(scale.x) or -math.abs(scale.x)
go.set_scale(scale)
-- Cambiar animación según estado
if self.state == "stunned" then
sprite.play_flipbook("#sprite", "stunned")
else
sprite.play_flipbook("#sprite", "walk")
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("stomped") then
get_stomped(self)
elseif message_id == hash("contact_point_response") then
handle_collision(self, message)
end
end
function get_stomped(self)
self.health = self.health - 1
if self.health <= 0 then
die(self)
else
self.state = "stunned"
self.stunned_timer = 1.0
-- Efecto visual de stun
sprite.set_constant("#sprite", "tint", vmath.vector4(1, 1, 0.5, 1))
end
end
function die(self)
-- Efecto de muerte
create_death_effect(go.get_position())
msg.post("#death_sound", "play_sound")
-- Dar puntos al jugador
msg.post("/level_manager", "enemy_killed", {points = 100})
go.delete()
end
function handle_collision(self, message)
if message.other_group == hash("ground") then
-- Voltear dirección si choca con pared
if math.abs(message.normal.x) > 0.7 then
self.direction = -self.direction
end
end
end
Enemigo Volador
-- enemies/flying_enemy.script
go.property("fly_height", 150)
go.property("fly_speed", 80)
go.property("dive_speed", 200)
function init(self)
self.state = "fly" -- "fly", "dive", "return"
self.start_pos = go.get_position()
self.target_height = self.start_pos.y + self.fly_height
self.direction = 1
self.dive_start_pos = nil
self.dive_target_pos = nil
print("Flying enemy inicializado")
end
function update(self, dt)
local player_pos = go.get_position("/level/player")
local my_pos = go.get_position()
if self.state == "fly" then
fly_behavior(self, player_pos, my_pos, dt)
elseif self.state == "dive" then
dive_behavior(self, my_pos, dt)
elseif self.state == "return" then
return_behavior(self, my_pos, dt)
end
end
function fly_behavior(self, player_pos, my_pos, dt)
-- Volar horizontalmente a altura fija
my_pos.x = my_pos.x + self.direction * self.fly_speed * dt
my_pos.y = self.target_height
-- Detectar si está sobre el jugador para atacar
local horizontal_distance = math.abs(player_pos.x - my_pos.x)
if horizontal_distance < 30 and player_pos.y < my_pos.y - 50 then
start_dive(self, player_pos)
end
go.set_position(my_pos)
end
function start_dive(self, player_pos)
self.state = "dive"
self.dive_start_pos = go.get_position()
self.dive_target_pos = player_pos
print("Flying enemy iniciando picada")
end
function dive_behavior(self, my_pos, dt)
-- Moverse hacia el objetivo de la picada
local direction = vmath.normalize(self.dive_target_pos - my_pos)
my_pos = my_pos + direction * self.dive_speed * dt
go.set_position(my_pos)
-- Si llegó al suelo o muy cerca del objetivo, regresar
if my_pos.y <= self.dive_target_pos.y + 20 then
self.state = "return"
end
end
function return_behavior(self, my_pos, dt)
-- Regresar a posición de vuelo
local direction = vmath.normalize(self.dive_start_pos - my_pos)
my_pos = my_pos + direction * self.fly_speed * dt
go.set_position(my_pos)
-- Si regresó a la altura de vuelo, continuar patrullaje
if my_pos.y >= self.target_height - 10 then
self.state = "fly"
end
end
Parte 4: Collectibles y Power-ups
Sistema de Gemas
-- collectibles/gem.script
go.property("gem_value", 10)
go.property("bounce_height", 20)
function init(self)
self.collected = false
self.bounce_timer = 0
-- Animación de flotación
animate_floating(self)
print("Gema creada con valor:", self.gem_value)
end
function update(self, dt)
if not self.collected then
self.bounce_timer = self.bounce_timer + dt
local bounce_offset = math.sin(self.bounce_timer * 3) * self.bounce_height
local base_pos = go.get_position()
base_pos.y = base_pos.y + bounce_offset * 0.1
go.set_position(base_pos)
end
end
function animate_floating(self)
-- Rotación continua
go.animate(".", "rotation.z", go.PLAYBACK_LOOP_FORWARD,
math.rad(360), go.EASING_LINEAR, 2.0)
-- Escala pulsante
go.animate(".", "scale", go.PLAYBACK_LOOP_PINGPONG,
vmath.vector3(1.2, 1.2, 1), go.EASING_INOUTQUAD, 1.0)
end
function on_message(self, message_id, message, sender)
if message_id == hash("trigger_response") then
if message.enter and message.other_group == hash("player") then
collect_gem(self)
end
end
end
function collect_gem(self)
if self.collected then return end
self.collected = true
-- Notificar al level manager
msg.post("/level_manager", "gem_collected", {value = self.gem_value})
-- Efecto visual de recolección
create_collect_effect(self)
-- Sonido
msg.post("#collect_sound", "play_sound")
-- Animación de recolección hacia UI
animate_to_ui(self)
end
function create_collect_effect(self)
local pos = go.get_position()
-- Partículas brillantes
factory.create("/effects#sparkle_factory", pos)
-- Texto flotante con puntos
factory.create("/effects#score_text_factory", pos, nil, {
text = "+" .. self.gem_value,
color = vmath.vector4(1, 1, 0, 1)
})
end
function animate_to_ui(self)
-- Animar hacia el contador de gemas en la UI
local ui_pos = vmath.vector3(100, 580, 0) -- Posición del contador
go.animate(".", "position", go.PLAYBACK_ONCE_FORWARD,
ui_pos, go.EASING_OUTQUAD, 0.8)
go.animate(".", "scale", go.PLAYBACK_ONCE_FORWARD,
vmath.vector3(0.3, 0.3, 1), go.EASING_OUTQUAD, 0.8,
0, function() go.delete() end)
end
Power-ups Temporales
-- collectibles/powerup.script
go.property("powerup_type", "speed") -- "speed", "jump", "invincible"
go.property("duration", 10.0)
local POWERUP_CONFIGS = {
speed = {
color = vmath.vector4(0, 1, 0, 1),
effect_multiplier = 1.5
},
jump = {
color = vmath.vector4(0, 0.5, 1, 1),
effect_multiplier = 1.3
},
invincible = {
color = vmath.vector4(1, 1, 0, 1),
effect_multiplier = 1.0
}
}
function init(self)
self.collected = false
local config = POWERUP_CONFIGS[self.powerup_type]
-- Configurar apariencia según tipo
sprite.set_constant("#sprite", "tint", config.color)
-- Animación de power-up
animate_powerup(self)
print("Power-up creado:", self.powerup_type)
end
function animate_powerup(self)
-- Rotación y escala dinámica
go.animate(".", "rotation.z", go.PLAYBACK_LOOP_FORWARD,
math.rad(180), go.EASING_LINEAR, 1.0)
go.animate(".", "scale", go.PLAYBACK_LOOP_PINGPONG,
vmath.vector3(1.3, 1.3, 1), go.EASING_INOUTSIN, 0.8)
end
function on_message(self, message_id, message, sender)
if message_id == hash("trigger_response") then
if message.enter and message.other_group == hash("player") then
activate_powerup(self)
end
end
end
function activate_powerup(self)
if self.collected then return end
self.collected = true
-- Aplicar power-up al jugador
msg.post("/level/player", "apply_powerup", {
type = self.powerup_type,
duration = self.duration,
multiplier = POWERUP_CONFIGS[self.powerup_type].effect_multiplier
})
-- Efecto visual
create_powerup_effect(self)
-- Sonido especial
msg.post("#powerup_sound", "play_sound")
go.delete()
end
function create_powerup_effect(self)
local pos = go.get_position()
local config = POWERUP_CONFIGS[self.powerup_type]
-- Efecto de explosión colorida
factory.create("/effects#powerup_explosion_factory", pos, nil, {
color = config.color,
particle_count = 20
})
end
Parte 5: Sistema de Checkpoints
Checkpoint Game Object
-- levels/checkpoint.script
function init(self)
self.activated = false
self.animation_playing = false
print("Checkpoint creado")
end
function on_message(self, message_id, message, sender)
if message_id == hash("trigger_response") then
if message.enter and message.other_group == hash("player") then
if not self.activated then
activate_checkpoint(self)
end
end
elseif message_id == hash("activate_visual") then
play_activation_animation(self)
end
end
function activate_checkpoint(self)
self.activated = true
-- Notificar al level manager
msg.post("/level_manager", "checkpoint_activated", {checkpoint_id = go.get_id()})
-- Activar animación visual
play_activation_animation(self)
end
function play_activation_animation(self)
if self.animation_playing then return end
self.animation_playing = true
-- Cambiar a sprite activado
sprite.play_flipbook("#sprite", "checkpoint_active")
-- Efecto de luz
go.animate(".", "scale", go.PLAYBACK_ONCE_FORWARD,
vmath.vector3(1.5, 1.5, 1), go.EASING_OUTBACK, 0.3,
0, function()
go.animate(".", "scale", go.PLAYBACK_ONCE_FORWARD,
vmath.vector3(1, 1, 1), go.EASING_OUTQUAD, 0.2,
0, function()
self.animation_playing = false
end)
end)
-- Efecto de partículas
local pos = go.get_position()
factory.create("/effects#checkpoint_factory", pos)
end
Ejercicios de Expansión
Ejercicio 1: Mecánicas Avanzadas
- Wall Jump: Saltar en paredes
- Dash: Movimiento rápido horizontal
- Double Jump: Segundo salto en el aire
Ejercicio 2: Elementos de Nivel
- Plataformas móviles: Que se mueven en patrones
- Interruptores: Que activan/desactivan plataformas
- Trampolines: Que impulsan al jugador
Ejercicio 3: Boss Fight
- Jefe de nivel: Con múltiples fases
- Patrones de ataque: Proyectiles y ataques físicos
- Mecánicas únicas: Uso del entorno
Próximos Pasos
En la siguiente lección aprenderemos a crear efectos visuales y partículas profesionales que darán vida y polish a nuestros juegos.
⬅️ Anterior: GUI y Menús | Siguiente: Efectos y Partículas ➡️
¡Excelente! Ahora tienes un juego de plataformas completo con todas las mecánicas esenciales del género. Este proyecto te servirá como base sólida para crear platformers más complejos.