Tilemaps y Diseño de Niveles Avanzado
Tilemaps y Diseño de Niveles Avanzado
Los tilemaps son una herramienta fundamental para crear mundos de juego eficientes y visualmente atractivos. En esta lección aprenderás técnicas avanzadas para el diseño de niveles profesionales.
🗺️ Conceptos Fundamentales de Tilemaps
Componente Tilemap
Un tilemap en Defold es un componente que permite renderizar grandes áreas usando tiles (azulejos) de manera eficiente:
-- tilemap_controller.script
local TILE_SIZE = 32
local MAP_WIDTH = 100
local MAP_HEIGHT = 50
function init(self)
self.tilemap_url = msg.url("#tilemap")
self.tile_source = resource.get_text("/assets/tilesets/main_tileset.tilesource")
-- Configurar el tilemap
tilemap.set_tile(self.tilemap_url, "background", 0, 0, 1)
end
Configuración de Tile Sources
Crea un tile source (.tilesource) optimizado:
// main_tileset.tilesource
{
"tile_width": 32,
"tile_height": 32,
"tile_margin": 1,
"tile_spacing": 2,
"collision": "/assets/collision/tiles.convexshape",
"material": "/builtins/materials/tile_map.material",
"texture": "/assets/sprites/tileset_atlas.png"
}
🏗️ Sistema de Múltiples Capas
Estructura de Capas
Organiza tu nivel en múltiples capas para mayor flexibilidad:
-- level_manager.script
local LAYERS = {
BACKGROUND = "background",
MIDGROUND = "midground",
FOREGROUND = "foreground",
COLLISION = "collision",
DECORATION = "decoration"
}
local function setup_layers(self)
for layer_name, layer_id in pairs(LAYERS) do
tilemap.set_visible(msg.url("#tilemap"), layer_id, true)
end
end
local function set_tile_multiple_layers(self, x, y, tiles_by_layer)
for layer, tile_id in pairs(tiles_by_layer) do
if tile_id > 0 then
tilemap.set_tile(self.tilemap_url, layer, x, y, tile_id)
end
end
end
Generador de Terrain Procedural
-- terrain_generator.script
local noise = require "main.noise"
local TERRAIN_RULES = {
GRASS = { min_height = 0.3, max_height = 0.7, tile_id = 1 },
STONE = { min_height = 0.7, max_height = 1.0, tile_id = 2 },
WATER = { min_height = 0.0, max_height = 0.3, tile_id = 3 }
}
local function generate_terrain(self, width, height, seed)
math.randomseed(seed or os.time())
for x = 0, width - 1 do
for y = 0, height - 1 do
local noise_value = noise.perlin(x * 0.1, y * 0.1, 0)
local height_value = (noise_value + 1) * 0.5 -- Normalizar 0-1
for terrain_type, rule in pairs(TERRAIN_RULES) do
if height_value >= rule.min_height and height_value < rule.max_height then
tilemap.set_tile(self.tilemap_url, "background", x, y, rule.tile_id)
break
end
end
end
end
end
function init(self)
self.tilemap_url = msg.url("#tilemap")
generate_terrain(self, 200, 100, 12345)
end
🌄 Sistema de Parallax Scrolling
Parallax Manager
-- parallax_manager.script
local PARALLAX_LAYERS = {
{ name = "sky", speed = 0.1, tilemap = "#sky_tilemap" },
{ name = "mountains", speed = 0.3, tilemap = "#mountains_tilemap" },
{ name = "trees", speed = 0.6, tilemap = "#trees_tilemap" },
{ name = "ground", speed = 1.0, tilemap = "#ground_tilemap" }
}
function init(self)
self.camera_pos = vmath.vector3(0)
self.last_camera_pos = vmath.vector3(0)
end
function update(self, dt)
-- Obtener posición de la cámara
local camera_pos = go.get_position("main:/camera")
local delta = camera_pos - self.last_camera_pos
-- Aplicar parallax a cada capa
for _, layer in ipairs(PARALLAX_LAYERS) do
local current_pos = go.get_position(layer.tilemap)
local new_pos = current_pos - delta * layer.speed
go.set_position(new_pos, layer.tilemap)
end
self.last_camera_pos = camera_pos
end
Parallax Infinito
-- infinite_parallax.script
local function wrap_tilemap(tilemap_url, world_width)
local pos = go.get_position(tilemap_url)
if pos.x < -world_width then
pos.x = pos.x + world_width * 2
go.set_position(pos, tilemap_url)
elseif pos.x > world_width then
pos.x = pos.x - world_width * 2
go.set_position(pos, tilemap_url)
end
end
function update(self, dt)
-- Aplicar wrapping a capas de fondo
wrap_tilemap("#background_1", 2048)
wrap_tilemap("#background_2", 2048)
end
🎯 Colisiones Avanzadas con Tilemaps
Collision Shapes Personalizadas
-- tilemap_collision.script
local function setup_custom_collision(self)
-- Definir shapes personalizadas para diferentes tiles
local collision_shapes = {
[1] = { type = "box", size = vmath.vector3(32, 32, 0) },
[2] = { type = "polygon", points = {
vmath.vector3(0, 0, 0),
vmath.vector3(32, 0, 0),
vmath.vector3(16, 32, 0)
}},
[3] = { type = "circle", radius = 16 }
}
return collision_shapes
end
local function update_collision_layer(self)
local w, h = tilemap.get_bounds(self.tilemap_url)
for x = 0, w-1 do
for y = 0, h-1 do
local tile_id = tilemap.get_tile(self.tilemap_url, "collision", x, y)
if tile_id > 0 then
-- Crear collision object dinámicamente
local pos = vmath.vector3(x * 32 + 16, y * 32 + 16, 0)
physics.set_shape(self.collision_url, tile_id, pos)
end
end
end
end
Detección de Tiles Inteligente
-- tile_detection.script
local function get_tile_at_world_position(tilemap_url, world_pos, layer)
local tile_x = math.floor(world_pos.x / 32)
local tile_y = math.floor(world_pos.y / 32)
return tilemap.get_tile(tilemap_url, layer, tile_x, tile_y)
end
local function get_surrounding_tiles(tilemap_url, world_pos, layer, radius)
local center_x = math.floor(world_pos.x / 32)
local center_y = math.floor(world_pos.y / 32)
local tiles = {}
for x = center_x - radius, center_x + radius do
for y = center_y - radius, center_y + radius do
local tile_id = tilemap.get_tile(tilemap_url, layer, x, y)
if tile_id > 0 then
table.insert(tiles, {
x = x, y = y,
tile_id = tile_id,
world_pos = vmath.vector3(x * 32 + 16, y * 32 + 16, 0)
})
end
end
end
return tiles
end
🔧 Herramientas de Level Design
Editor de Niveles en Runtime
-- level_editor.script
local EDIT_MODES = {
PAINT = 1,
ERASE = 2,
EYEDROPPER = 3
}
function init(self)
self.edit_mode = EDIT_MODES.PAINT
self.selected_tile = 1
self.current_layer = "foreground"
self.tilemap_url = msg.url("#tilemap")
end
function on_input(self, action_id, action)
if action_id == hash("touch") and action.pressed then
local world_pos = camera.screen_to_world("main:/camera", vmath.vector3(action.x, action.y, 0))
local tile_x = math.floor(world_pos.x / 32)
local tile_y = math.floor(world_pos.y / 32)
if self.edit_mode == EDIT_MODES.PAINT then
tilemap.set_tile(self.tilemap_url, self.current_layer, tile_x, tile_y, self.selected_tile)
elseif self.edit_mode == EDIT_MODES.ERASE then
tilemap.set_tile(self.tilemap_url, self.current_layer, tile_x, tile_y, 0)
elseif self.edit_mode == EDIT_MODES.EYEDROPPER then
self.selected_tile = tilemap.get_tile(self.tilemap_url, self.current_layer, tile_x, tile_y)
end
end
end
Sistema de Chunks para Mundos Grandes
-- chunk_manager.script
local CHUNK_SIZE = 32
local LOAD_DISTANCE = 3
local function get_chunk_key(chunk_x, chunk_y)
return tostring(chunk_x) .. "," .. tostring(chunk_y)
end
local function load_chunk(self, chunk_x, chunk_y)
local chunk_key = get_chunk_key(chunk_x, chunk_y)
if not self.loaded_chunks[chunk_key] then
-- Cargar datos del chunk desde archivo o generar proceduralmente
local chunk_data = load_chunk_data(chunk_x, chunk_y)
for x = 0, CHUNK_SIZE - 1 do
for y = 0, CHUNK_SIZE - 1 do
local world_x = chunk_x * CHUNK_SIZE + x
local world_y = chunk_y * CHUNK_SIZE + y
local tile_id = chunk_data[x][y]
tilemap.set_tile(self.tilemap_url, "background", world_x, world_y, tile_id)
end
end
self.loaded_chunks[chunk_key] = true
end
end
local function unload_chunk(self, chunk_x, chunk_y)
local chunk_key = get_chunk_key(chunk_x, chunk_y)
if self.loaded_chunks[chunk_key] then
-- Limpiar tiles del chunk
for x = 0, CHUNK_SIZE - 1 do
for y = 0, CHUNK_SIZE - 1 do
local world_x = chunk_x * CHUNK_SIZE + x
local world_y = chunk_y * CHUNK_SIZE + y
tilemap.set_tile(self.tilemap_url, "background", world_x, world_y, 0)
end
end
self.loaded_chunks[chunk_key] = nil
end
end
function update(self, dt)
local camera_pos = go.get_position("main:/camera")
local camera_chunk_x = math.floor(camera_pos.x / (CHUNK_SIZE * 32))
local camera_chunk_y = math.floor(camera_pos.y / (CHUNK_SIZE * 32))
-- Cargar chunks cercanos
for x = camera_chunk_x - LOAD_DISTANCE, camera_chunk_x + LOAD_DISTANCE do
for y = camera_chunk_y - LOAD_DISTANCE, camera_chunk_y + LOAD_DISTANCE do
load_chunk(self, x, y)
end
end
-- Descargar chunks lejanos
for chunk_key, _ in pairs(self.loaded_chunks) do
local chunk_x, chunk_y = chunk_key:match("([^,]+),([^,]+)")
chunk_x, chunk_y = tonumber(chunk_x), tonumber(chunk_y)
local distance = math.max(
math.abs(chunk_x - camera_chunk_x),
math.abs(chunk_y - camera_chunk_y)
)
if distance > LOAD_DISTANCE then
unload_chunk(self, chunk_x, chunk_y)
end
end
end
📊 Optimización de Rendimiento
Tile Pooling
-- tile_pool.script
local function create_tile_pool(self, tile_count)
self.tile_pool = {}
for i = 1, tile_count do
local tile_go = factory.create("#tile_factory")
go.set_position(vmath.vector3(-1000, -1000, 0), tile_go)
table.insert(self.tile_pool, tile_go)
end
end
local function get_tile_from_pool(self)
if #self.tile_pool > 0 then
return table.remove(self.tile_pool)
else
return factory.create("#tile_factory")
end
end
local function return_tile_to_pool(self, tile_go)
go.set_position(vmath.vector3(-1000, -1000, 0), tile_go)
table.insert(self.tile_pool, tile_go)
end
LOD (Level of Detail) para Tilemaps
-- tilemap_lod.script
local LOD_DISTANCES = {
HIGH = 500,
MEDIUM = 1000,
LOW = 2000
}
local function update_lod(self, camera_pos)
local w, h = tilemap.get_bounds(self.tilemap_url)
for x = 0, w-1, 4 do -- Sampling cada 4 tiles para performance
for y = 0, h-1, 4 do
local tile_world_pos = vmath.vector3(x * 32, y * 32, 0)
local distance = vmath.length(camera_pos - tile_world_pos)
local visible = true
local detail_level = "HIGH"
if distance > LOD_DISTANCES.LOW then
visible = false
elseif distance > LOD_DISTANCES.MEDIUM then
detail_level = "LOW"
elseif distance > LOD_DISTANCES.HIGH then
detail_level = "MEDIUM"
end
-- Aplicar LOD
self:apply_lod_to_tile_area(x, y, detail_level, visible)
end
end
end
🎮 Proyecto Práctico: Metroidvania Level System
Vamos a crear un sistema completo de niveles para un juego estilo Metroidvania:
1. Room Manager
-- room_manager.script
local ROOM_SIZE = 20 * 32 -- 20 tiles = 640 pixels
function init(self)
self.current_room = { x = 0, y = 0 }
self.rooms = {}
self.tilemap_url = msg.url("#tilemap")
-- Cargar datos de rooms
self:load_room_data()
end
local function load_room_data(self)
-- Cargar desde JSON o definir aquí
self.rooms = {
["0,0"] = {
background = "room_start.json",
collision = "room_start_collision.json",
entities = { "player_spawn", "save_point" }
},
["1,0"] = {
background = "room_corridor.json",
collision = "room_corridor_collision.json",
entities = { "enemy_goomba", "collectible_health" }
}
}
end
local function transition_to_room(self, room_x, room_y)
local room_key = tostring(room_x) .. "," .. tostring(room_y)
local room_data = self.rooms[room_key]
if room_data then
-- Limpiar room actual
self:clear_current_room()
-- Cargar nueva room
self:load_room_tiles(room_data.background, "background")
self:load_room_tiles(room_data.collision, "collision")
self:spawn_room_entities(room_data.entities)
self.current_room = { x = room_x, y = room_y }
-- Mover cámara
local camera_pos = vmath.vector3(room_x * ROOM_SIZE, room_y * ROOM_SIZE, 0)
go.animate("main:/camera", "position", go.PLAYBACK_ONCE_FORWARD, camera_pos, go.EASING_OUTSINE, 1.0)
end
end
2. Room Transitions
-- room_transition.script
function on_message(self, message_id, message, sender)
if message_id == hash("trigger_response") then
if message.other_group == hash("player") then
local trigger_name = go.get("#")[2] -- Obtener nombre del trigger
if trigger_name == "exit_right" then
msg.post("main:/room_manager", "transition_room", { x = 1, y = 0 })
elseif trigger_name == "exit_left" then
msg.post("main:/room_manager", "transition_room", { x = -1, y = 0 })
elseif trigger_name == "exit_up" then
msg.post("main:/room_manager", "transition_room", { x = 0, y = 1 })
elseif trigger_name == "exit_down" then
msg.post("main:/room_manager", "transition_room", { x = 0, y = -1 })
end
end
end
end
📚 Recursos y Referencias
API de Tilemap en Defold
tilemap.get_bounds()- Obtener dimensiones del tilemaptilemap.set_tile()- Establecer tile específicotilemap.get_tile()- Obtener ID de tile específicotilemap.set_visible()- Controlar visibilidad de capas
Herramientas Recomendadas
- Tiled Map Editor - Para diseñar niveles visualmente
- Pyxel Edit - Para crear tilesets
- Aseprite - Para tiles animados
Links Útiles
🎯 Ejercicios Propuestos
-
Sistema de Biomas: Crea un generador procedural que genere diferentes biomas (bosque, desierto, montaña) con reglas específicas.
-
Dynamic Loading: Implementa un sistema que cargue niveles desde archivos JSON dinámicamente.
-
Destructible Terrain: Crea tiles que se puedan destruir y regenerar en runtime.
-
Weather System: Añade efectos climáticos que afecten la apariencia de los tiles.
-
Mini-mapa: Crea un mini-mapa que muestre la estructura del nivel usando los datos del tilemap.
Los tilemaps son la base para crear mundos de juego ricos y detallados. Con estas técnicas avanzadas podrás crear niveles profesionales que cautiven a los jugadores y optimicen el rendimiento de tu juego.