← Volver al listado de tecnologías

Networking y Juegos Multijugador

Por: Artiko
defoldnetworkingmultijugadorwebsocketsreal-time

Networking y Juegos Multijugador

Los juegos multijugador requieren técnicas especializadas para sincronizar estados entre múltiples clientes y manejar la latencia de red. En esta lección aprenderás a crear experiencias multijugador robustas en Defold.

🌐 Fundamentos de Networking

Configuración WebSocket

-- network_manager.script
local json = require "main.json"

local MESSAGE_TYPES = {
    CONNECT = "connect",
    DISCONNECT = "disconnect",
    PLAYER_UPDATE = "player_update",
    GAME_STATE = "game_state",
    CHAT_MESSAGE = "chat_message",
    ROOM_JOIN = "room_join",
    ROOM_LEAVE = "room_leave"
}

function init(self)
    self.connection = nil
    self.player_id = nil
    self.room_id = nil
    self.connected = false
    self.reconnect_timer = 0
    self.reconnect_interval = 5.0
    self.message_queue = {}

    -- Buffer para mensajes offline
    self.offline_messages = {}
end

local function connect_to_server(self, server_url)
    local connection = websocket.connect(server_url, {
        timeout = 3000,
        headers = "User-Agent: DefoldGame/1.0\r\n"
    })

    if connection then
        self.connection = connection
        print("Conectando al servidor:", server_url)
    else
        print("Error: No se pudo crear conexión WebSocket")
    end
end

function on_message(self, message_id, message, sender)
    if message_id == hash("websocket_connected") then
        self.connected = true
        self.reconnect_timer = 0
        print("Conectado al servidor")

        -- Enviar mensaje de autenticación
        self:send_message(MESSAGE_TYPES.CONNECT, {
            client_version = sys.get_config("project.version"),
            platform = sys.get_sys_info().system_name
        })

        -- Enviar mensajes en cola
        self:flush_message_queue()

    elseif message_id == hash("websocket_disconnected") then
        self.connected = false
        print("Desconectado del servidor")

    elseif message_id == hash("websocket_message") then
        self:handle_server_message(message.message)

    elseif message_id == hash("websocket_error") then
        print("Error WebSocket:", message.message)
        self.connected = false
    end
end

local function send_message(self, type, data)
    if not self.connected then
        -- Guardar en cola para enviar cuando reconecte
        table.insert(self.offline_messages, {type = type, data = data})
        return false
    end

    local message = {
        type = type,
        timestamp = os.time(),
        player_id = self.player_id,
        data = data
    }

    local json_message = json.encode(message)
    websocket.send(self.connection, json_message)
    return true
end

Protocolo de Mensajes

-- message_protocol.script
local PROTOCOL_VERSION = "1.0"

local MESSAGE_SCHEMAS = {
    player_update = {
        position = "vector3",
        velocity = "vector3",
        animation = "string",
        health = "number",
        sequence = "number"
    },
    game_state = {
        players = "table",
        entities = "table",
        game_time = "number",
        round_state = "string"
    },
    input_command = {
        input_type = "string",
        value = "any",
        timestamp = "number",
        sequence = "number"
    }
}

local function validate_message(message_type, data)
    local schema = MESSAGE_SCHEMAS[message_type]
    if not schema then return false end

    for field, expected_type in pairs(schema) do
        if data[field] == nil then
            return false, "Campo requerido faltante: " .. field
        end

        local actual_type = type(data[field])
        if expected_type == "vector3" then
            if not (actual_type == "userdata" or
                   (actual_type == "table" and data[field].x and data[field].y and data[field].z)) then
                return false, "Campo " .. field .. " debe ser vector3"
            end
        elseif actual_type ~= expected_type then
            return false, string.format("Campo %s debe ser %s, recibido %s",
                                       field, expected_type, actual_type)
        end
    end

    return true
end

local function serialize_message(type, data, sequence)
    local message = {
        protocol_version = PROTOCOL_VERSION,
        type = type,
        sequence = sequence or 0,
        timestamp = socket.gettime() * 1000, -- milisegundos
        data = data
    }

    return json.encode(message)
end

local function deserialize_message(json_str)
    local success, message = pcall(json.decode, json_str)
    if not success then
        return nil, "JSON inválido"
    end

    if message.protocol_version ~= PROTOCOL_VERSION then
        return nil, "Versión de protocolo incompatible"
    end

    local valid, error_msg = validate_message(message.type, message.data)
    if not valid then
        return nil, error_msg
    end

    return message
end

🎮 Sincronización de Estados

Client-Side Prediction

-- client_prediction.script
local PREDICTION_BUFFER_SIZE = 60 -- 1 segundo a 60 FPS

function init(self)
    self.input_sequence = 0
    self.last_server_sequence = 0
    self.prediction_buffer = {}
    self.server_states = {}

    -- Estado local del jugador
    self.predicted_state = {
        position = vmath.vector3(),
        velocity = vmath.vector3(),
        health = 100
    }
end

local function apply_input(state, input, dt)
    local new_state = {
        position = vmath.vector3(state.position),
        velocity = vmath.vector3(state.velocity),
        health = state.health
    }

    -- Aplicar física básica
    if input.move_x then
        new_state.velocity.x = input.move_x * 200 -- velocidad de movimiento
    end
    if input.move_y then
        new_state.velocity.y = input.move_y * 200
    end

    -- Aplicar fricción
    new_state.velocity = new_state.velocity * 0.9

    -- Actualizar posición
    new_state.position = new_state.position + new_state.velocity * dt

    return new_state
end

local function store_prediction(self, input, state)
    self.input_sequence = self.input_sequence + 1

    local prediction = {
        sequence = self.input_sequence,
        input = input,
        state = state,
        timestamp = socket.gettime()
    }

    -- Guardar en buffer circular
    local index = (self.input_sequence % PREDICTION_BUFFER_SIZE) + 1
    self.prediction_buffer[index] = prediction

    return self.input_sequence
end

local function reconcile_with_server(self, server_state, server_sequence)
    if server_sequence <= self.last_server_sequence then
        return -- Estado más antiguo, ignorar
    end

    self.last_server_sequence = server_sequence

    -- Encontrar la predicción correspondiente
    local prediction_index = (server_sequence % PREDICTION_BUFFER_SIZE) + 1
    local prediction = self.prediction_buffer[prediction_index]

    if not prediction or prediction.sequence ~= server_sequence then
        -- No encontramos la predicción, confiar en el servidor
        self.predicted_state = server_state
        return
    end

    -- Verificar si hay diferencia significativa
    local position_diff = vmath.length(prediction.state.position - server_state.position)
    local threshold = 5.0 -- 5 pixels de tolerancia

    if position_diff > threshold then
        -- Corrección necesaria
        print("Reconciliando estados - diferencia:", position_diff)

        -- Empezar desde el estado del servidor
        local corrected_state = server_state

        -- Re-aplicar inputs posteriores
        for i = server_sequence + 1, self.input_sequence do
            local idx = (i % PREDICTION_BUFFER_SIZE) + 1
            local stored_prediction = self.prediction_buffer[idx]

            if stored_prediction and stored_prediction.sequence == i then
                corrected_state = apply_input(corrected_state, stored_prediction.input, 1/60)
            end
        end

        self.predicted_state = corrected_state
        go.set_position(self.predicted_state.position)
    end
end

Server Reconciliation

-- server_simulation.script (para servidor Node.js/Python)
--[[
class GameState {
    constructor() {
        this.players = new Map();
        this.entities = new Map();
        this.tickRate = 60;
        this.currentTick = 0;
        this.inputBuffer = new Map(); // buffer de inputs por jugador
    }

    processInputs(playerId, inputs) {
        const player = this.players.get(playerId);
        if (!player) return;

        inputs.forEach(input => {
            // Validar input en el servidor
            if (this.isValidInput(input)) {
                // Aplicar input al estado del jugador
                this.applyInput(player, input);

                // Guardar para reconciliación
                const playerInputs = this.inputBuffer.get(playerId) || [];
                playerInputs.push({
                    sequence: input.sequence,
                    input: input,
                    resultState: { ...player.state }
                });

                // Mantener solo últimos 120 inputs (2 segundos)
                if (playerInputs.length > 120) {
                    playerInputs.shift();
                }

                this.inputBuffer.set(playerId, playerInputs);
            }
        });
    }

    broadcastState() {
        const gameState = {
            tick: this.currentTick,
            players: Array.from(this.players.values()).map(p => ({
                id: p.id,
                position: p.state.position,
                velocity: p.state.velocity,
                health: p.state.health,
                lastInputSequence: p.lastInputSequence
            })),
            timestamp: Date.now()
        };

        this.broadcast('game_state', gameState);
    }
}
--]]

-- Lado cliente: manejo de estado del servidor
local function handle_server_state(self, state_data)
    -- Actualizar otros jugadores (sin predicción)
    for _, player_data in ipairs(state_data.players) do
        if player_data.id ~= self.player_id then
            self:update_remote_player(player_data)
        else
            -- Reconciliar nuestro propio estado
            reconcile_with_server(self, player_data.state, player_data.lastInputSequence)
        end
    end

    -- Actualizar entidades del juego
    for _, entity_data in ipairs(state_data.entities or {}) do
        self:update_entity(entity_data)
    end
end

🕰️ Lag Compensation

Interpolación de Estados

-- interpolation.script
local INTERPOLATION_DELAY = 100 -- 100ms de buffer

function init(self)
    self.remote_players = {}
    self.state_buffer = {}
end

local function add_state_to_buffer(self, player_id, state, timestamp)
    if not self.state_buffer[player_id] then
        self.state_buffer[player_id] = {}
    end

    table.insert(self.state_buffer[player_id], {
        state = state,
        timestamp = timestamp
    })

    -- Mantener solo estados recientes
    local buffer = self.state_buffer[player_id]
    local current_time = socket.gettime() * 1000

    while #buffer > 0 and current_time - buffer[1].timestamp > 1000 do
        table.remove(buffer, 1)
    end
end

local function interpolate_player_state(self, player_id)
    local buffer = self.state_buffer[player_id]
    if not buffer or #buffer < 2 then return nil end

    local target_time = socket.gettime() * 1000 - INTERPOLATION_DELAY
    local state1, state2 = nil, nil

    -- Encontrar los dos estados para interpolar
    for i = 1, #buffer - 1 do
        if buffer[i].timestamp <= target_time and buffer[i + 1].timestamp >= target_time then
            state1 = buffer[i]
            state2 = buffer[i + 1]
            break
        end
    end

    if not state1 or not state2 then
        -- Usar el estado más reciente
        return buffer[#buffer].state
    end

    -- Calcular factor de interpolación
    local time_diff = state2.timestamp - state1.timestamp
    local elapsed = target_time - state1.timestamp
    local t = elapsed / time_diff

    -- Interpolar posición y rotación
    local interpolated_state = {
        position = vmath.lerp(t, state1.state.position, state2.state.position),
        rotation = vmath.slerp(t, state1.state.rotation, state2.state.rotation),
        velocity = vmath.lerp(t, state1.state.velocity, state2.state.velocity),
        health = state2.state.health -- No interpolar valores discretos
    }

    return interpolated_state
end

function update(self, dt)
    -- Actualizar posiciones interpoladas de jugadores remotos
    for player_id, player_go in pairs(self.remote_players) do
        local interpolated_state = interpolate_player_state(self, player_id)
        if interpolated_state then
            go.set_position(interpolated_state.position, player_go)
            go.set_rotation(interpolated_state.rotation, player_go)
        end
    end
end

Rollback Networking

-- rollback_system.script
local ROLLBACK_FRAMES = 8 -- Máximo 8 frames de rollback

function init(self)
    self.frame_data = {} -- Historial de estados por frame
    self.current_frame = 0
    self.confirmed_frame = 0
    self.input_history = {}
end

local function save_frame_state(self, frame)
    local state = {
        players = {},
        entities = {},
        game_state = {}
    }

    -- Guardar estado de todos los objetos importantes
    for player_id, player in pairs(game.players) do
        state.players[player_id] = {
            position = go.get_position(player.game_object),
            velocity = player.velocity,
            health = player.health,
            animation_state = player.animation_state
        }
    end

    self.frame_data[frame] = state

    -- Limpiar frames muy antiguos
    local oldest_frame = frame - ROLLBACK_FRAMES - 10
    if self.frame_data[oldest_frame] then
        self.frame_data[oldest_frame] = nil
    end
end

local function rollback_to_frame(self, target_frame)
    if not self.frame_data[target_frame] then
        print("Error: No hay datos para frame", target_frame)
        return false
    end

    print("Rollback desde frame", self.current_frame, "hasta frame", target_frame)

    -- Restaurar estado del frame objetivo
    local state = self.frame_data[target_frame]

    for player_id, player_state in pairs(state.players) do
        local player = game.players[player_id]
        if player then
            go.set_position(player_state.position, player.game_object)
            player.velocity = player_state.velocity
            player.health = player_state.health
            player.animation_state = player_state.animation_state
        end
    end

    -- Re-simular frames hasta el presente
    for frame = target_frame + 1, self.current_frame do
        self:simulate_frame(frame)
    end

    return true
end

local function handle_input_correction(self, corrected_inputs)
    local earliest_frame = math.huge

    -- Encontrar el frame más temprano que necesita corrección
    for frame, inputs in pairs(corrected_inputs) do
        if frame < earliest_frame then
            earliest_frame = frame
        end

        -- Actualizar historial de inputs
        self.input_history[frame] = inputs
    end

    -- Rollback si es necesario
    if earliest_frame <= self.current_frame then
        rollback_to_frame(self, earliest_frame - 1)
    end
end

🏠 Sistema de Salas (Rooms)

Room Manager

-- room_manager.script
local MAX_PLAYERS_PER_ROOM = 4
local ROOM_TIMEOUT = 300 -- 5 minutos sin actividad

function init(self)
    self.rooms = {}
    self.player_room_map = {} -- player_id -> room_id
    self.room_counter = 0
end

local function create_room(self, room_settings)
    self.room_counter = self.room_counter + 1
    local room_id = "room_" .. self.room_counter

    local room = {
        id = room_id,
        players = {},
        max_players = room_settings.max_players or MAX_PLAYERS_PER_ROOM,
        game_mode = room_settings.game_mode or "deathmatch",
        map = room_settings.map or "default",
        created_at = os.time(),
        last_activity = os.time(),
        state = "waiting", -- waiting, playing, finished
        settings = room_settings
    }

    self.rooms[room_id] = room
    return room_id
end

local function join_room(self, player_id, room_id)
    local room = self.rooms[room_id]
    if not room then
        return false, "Sala no existe"
    end

    if #room.players >= room.max_players then
        return false, "Sala llena"
    end

    if room.state ~= "waiting" then
        return false, "Partida en progreso"
    end

    -- Remover jugador de sala anterior si existe
    local old_room_id = self.player_room_map[player_id]
    if old_room_id then
        self:leave_room(player_id)
    end

    -- Añadir a nueva sala
    table.insert(room.players, player_id)
    self.player_room_map[player_id] = room_id
    room.last_activity = os.time()

    -- Notificar a todos los jugadores
    self:broadcast_to_room(room_id, "player_joined", {
        player_id = player_id,
        players_count = #room.players
    })

    -- Iniciar partida si está llena
    if #room.players >= room.max_players then
        self:start_game(room_id)
    end

    return true
end

local function matchmake_player(self, player_id, preferences)
    -- Buscar sala compatible
    for room_id, room in pairs(self.rooms) do
        if room.state == "waiting" and
           #room.players < room.max_players and
           room.game_mode == (preferences.game_mode or "deathmatch") then

            local success = join_room(self, player_id, room_id)
            if success then
                return room_id
            end
        end
    end

    -- Crear nueva sala si no se encontró ninguna
    local room_settings = {
        max_players = preferences.max_players or MAX_PLAYERS_PER_ROOM,
        game_mode = preferences.game_mode or "deathmatch",
        map = preferences.map or "default"
    }

    local room_id = create_room(self, room_settings)
    join_room(self, player_id, room_id)

    return room_id
end

Game Modes

-- game_modes.script
local GAME_MODES = {
    deathmatch = {
        score_limit = 10,
        time_limit = 300, -- 5 minutos
        respawn_time = 3,
        friendly_fire = false
    },
    team_deathmatch = {
        score_limit = 50,
        time_limit = 600, -- 10 minutos
        team_count = 2,
        friendly_fire = false
    },
    capture_flag = {
        capture_limit = 3,
        time_limit = 900, -- 15 minutos
        team_count = 2,
        flag_return_time = 30
    },
    battle_royale = {
        shrink_interval = 60, -- Zona se reduce cada minuto
        initial_area = 2000,
        final_area = 100,
        max_players = 16
    }
}

local function init_game_mode(room, mode_name)
    local mode_config = GAME_MODES[mode_name]
    if not mode_config then return false end

    room.game_state = {
        mode = mode_name,
        config = mode_config,
        scores = {},
        start_time = os.time(),
        time_remaining = mode_config.time_limit,
        round_number = 1
    }

    -- Configuración específica por modo
    if mode_name == "team_deathmatch" or mode_name == "capture_flag" then
        room.game_state.teams = {}
        for i = 1, mode_config.team_count do
            room.game_state.teams[i] = {
                players = {},
                score = 0
            }
        end

        -- Asignar jugadores a equipos
        for i, player_id in ipairs(room.players) do
            local team = ((i - 1) % mode_config.team_count) + 1
            table.insert(room.game_state.teams[team].players, player_id)
        end

    elseif mode_name == "battle_royale" then
        room.game_state.safe_zone = {
            center = vmath.vector3(0, 0, 0),
            radius = mode_config.initial_area
        }
        room.game_state.next_shrink = os.time() + mode_config.shrink_interval
    end

    return true
end

local function update_game_mode(room, dt)
    local mode = room.game_state.mode

    if mode == "deathmatch" then
        -- Verificar condiciones de victoria
        for player_id, score in pairs(room.game_state.scores) do
            if score >= room.game_state.config.score_limit then
                end_game(room, {winner = player_id, reason = "score_limit"})
                return
            end
        end

    elseif mode == "battle_royale" then
        -- Reducir zona segura
        if os.time() >= room.game_state.next_shrink then
            shrink_safe_zone(room)
            room.game_state.next_shrink = os.time() + room.game_state.config.shrink_interval
        end

        -- Aplicar daño fuera de zona
        apply_zone_damage(room)
    end

    -- Verificar límite de tiempo
    room.game_state.time_remaining = room.game_state.time_remaining - dt
    if room.game_state.time_remaining <= 0 then
        end_game(room, {reason = "time_limit"})
    end
end

🔐 Seguridad y Anti-Cheat

Validación Server-Side

-- server_validation.script (conceptual para servidor)
--[[
class AntiCheat {
    constructor() {
        this.playerStats = new Map();
        this.suspiciousEvents = new Map();
        this.movementValidator = new MovementValidator();
        this.actionValidator = new ActionValidator();
    }

    validatePlayerUpdate(playerId, update) {
        const player = this.playerStats.get(playerId);
        if (!player) return false;

        // Validar movimiento
        if (!this.movementValidator.isValidMovement(player.lastPosition, update.position, update.deltaTime)) {
            this.flagSuspiciousActivity(playerId, 'impossible_movement', update);
            return false;
        }

        // Validar velocidad máxima
        const maxSpeed = 300; // pixels por segundo
        const distance = Math.sqrt(
            Math.pow(update.position.x - player.lastPosition.x, 2) +
            Math.pow(update.position.y - player.lastPosition.y, 2)
        );
        const speed = distance / update.deltaTime;

        if (speed > maxSpeed * 1.1) { // 10% de tolerancia
            this.flagSuspiciousActivity(playerId, 'speed_hack', {speed, maxSpeed});
            return false;
        }

        // Validar rate limiting
        if (!this.validateUpdateRate(playerId)) {
            return false;
        }

        return true;
    }

    validateAction(playerId, action) {
        const player = this.playerStats.get(playerId);

        // Validar cooldowns
        if (action.type === 'attack') {
            const lastAttack = player.lastAttackTime || 0;
            const minCooldown = 500; // 500ms entre ataques

            if (Date.now() - lastAttack < minCooldown) {
                this.flagSuspiciousActivity(playerId, 'attack_spam', action);
                return false;
            }
        }

        // Validar recursos
        if (action.type === 'use_ability' && action.cost > player.mana) {
            this.flagSuspiciousActivity(playerId, 'insufficient_resources', action);
            return false;
        }

        return true;
    }
}
--]]

-- Cliente: envío seguro de datos
local function send_secure_update(self, update_data)
    -- Añadir checksum para verificar integridad
    local checksum = self:calculate_checksum(update_data)

    local secure_update = {
        data = update_data,
        checksum = checksum,
        client_time = socket.gettime() * 1000,
        sequence = self.update_sequence
    }

    self.update_sequence = self.update_sequence + 1
    msg.post("network_manager", "send_message", {
        type = "player_update",
        data = secure_update
    })
end

🎮 Proyecto Práctico: Battle Royale Multijugador

Vamos a crear un juego Battle Royale multijugador completo:

1. Game Manager

-- battle_royale_manager.script
local GAME_PHASES = {
    LOBBY = "lobby",
    DEPLOYING = "deploying",
    PLAYING = "playing",
    ENDING = "ending"
}

function init(self)
    self.current_phase = GAME_PHASES.LOBBY
    self.players = {}
    self.alive_players = {}
    self.safe_zone = {
        center = vmath.vector3(0, 0, 0),
        radius = 2000,
        next_radius = 1500,
        shrink_time = 60
    }
    self.phase_timer = 0
    self.match_stats = {}
end

local function start_deployment_phase(self)
    self.current_phase = GAME_PHASES.DEPLOYING
    self.phase_timer = 30 -- 30 segundos para deploy

    -- Spawn airplane
    local airplane_path = self:create_airplane_path()
    msg.post("airplane_manager", "start_flight", {path = airplane_path})

    -- Notificar a todos los jugadores
    msg.post("network_manager", "broadcast_message", {
        type = "phase_change",
        data = {
            phase = GAME_PHASES.DEPLOYING,
            airplane_path = airplane_path,
            time_limit = 30
        }
    })
end

local function update_safe_zone(self, dt)
    if self.safe_zone.shrink_time > 0 then
        self.safe_zone.shrink_time = self.safe_zone.shrink_time - dt

        -- Interpolar radio de zona segura
        local progress = 1 - (self.safe_zone.shrink_time / 60)
        local current_radius = vmath.lerp(progress, self.safe_zone.radius, self.safe_zone.next_radius)

        -- Actualizar visual de zona
        msg.post("safe_zone_visual", "update_radius", {radius = current_radius})

        if self.safe_zone.shrink_time <= 0 then
            -- Zona terminó de reducirse
            self.safe_zone.radius = self.safe_zone.next_radius
            self:plan_next_shrink()
        end
    end
end

function update(self, dt)
    if self.current_phase == GAME_PHASES.DEPLOYING then
        self.phase_timer = self.phase_timer - dt
        if self.phase_timer <= 0 then
            -- Forzar deploy de jugadores restantes
            self:force_deploy_remaining_players()
            self.current_phase = GAME_PHASES.PLAYING
        end

    elseif self.current_phase == GAME_PHASES.PLAYING then
        update_safe_zone(self, dt)
        self:check_victory_condition()
        self:apply_zone_damage()
    end
end

2. Loot System

-- loot_system.script
local LOOT_TABLES = {
    common = {
        {item = "bandage", weight = 30, min = 2, max = 5},
        {item = "ammo_pistol", weight = 25, min = 15, max = 30},
        {item = "scope_1x", weight = 20},
        {item = "armor_level1", weight = 15},
        {item = "pistol", weight = 10}
    },
    rare = {
        {item = "medkit", weight = 25, min = 1, max = 2},
        {item = "ammo_rifle", weight = 20, min = 30, max = 60},
        {item = "scope_2x", weight = 20},
        {item = "armor_level2", weight = 15},
        {item = "assault_rifle", weight = 10},
        {item = "smoke_grenade", weight = 10, min = 1, max = 3}
    },
    legendary = {
        {item = "armor_level3", weight = 30},
        {item = "scope_8x", weight = 25},
        {item = "sniper_rifle", weight = 20},
        {item = "frag_grenade", weight = 15, min = 2, max = 4},
        {item = "adrenaline", weight = 10, min = 1, max = 2}
    }
}

local function generate_loot_spawn(self, position, loot_tier)
    local loot_table = LOOT_TABLES[loot_tier] or LOOT_TABLES.common
    local total_weight = 0

    -- Calcular peso total
    for _, item in ipairs(loot_table) do
        total_weight = total_weight + item.weight
    end

    -- Seleccionar items aleatorios
    local generated_items = {}
    local item_count = math.random(2, 4) -- 2-4 items por spawn

    for i = 1, item_count do
        local random_weight = math.random() * total_weight
        local current_weight = 0

        for _, item in ipairs(loot_table) do
            current_weight = current_weight + item.weight
            if random_weight <= current_weight then
                local quantity = 1
                if item.min and item.max then
                    quantity = math.random(item.min, item.max)
                end

                table.insert(generated_items, {
                    type = item.item,
                    quantity = quantity,
                    position = position + vmath.vector3(
                        math.random(-20, 20),
                        math.random(-20, 20),
                        0
                    )
                })
                break
            end
        end
    end

    return generated_items
end

local function spawn_loot_containers(self)
    -- Spawns de alta calidad (pocas cantidades)
    local legendary_spawns = {
        vmath.vector3(500, 500, 0),
        vmath.vector3(-500, -500, 0),
        vmath.vector3(0, 800, 0)
    }

    for _, pos in ipairs(legendary_spawns) do
        local items = generate_loot_spawn(self, pos, "legendary")
        self:create_loot_container(pos, items, "military_crate")
    end

    -- Spawns medios (cantidad media)
    for i = 1, 15 do
        local pos = vmath.vector3(
            math.random(-1000, 1000),
            math.random(-1000, 1000),
            0
        )
        local items = generate_loot_spawn(self, pos, "rare")
        self:create_loot_container(pos, items, "supply_box")
    end

    -- Spawns comunes (muchas cantidades)
    for i = 1, 40 do
        local pos = vmath.vector3(
            math.random(-1500, 1500),
            math.random(-1500, 1500),
            0
        )
        local items = generate_loot_spawn(self, pos, "common")
        self:create_loot_container(pos, items, "wooden_crate")
    end
end

📚 Recursos y Referencias

APIs de Networking en Defold

Herramientas de Servidor

🎯 Ejercicios Propuestos

  1. Chat System: Implementa un sistema de chat con comandos y filtros.

  2. Leaderboards: Crea tablas de puntuación globales y por temporada.

  3. Spectator Mode: Permite a jugadores observar partidas en curso.

  4. Tournament System: Sistema de torneos con brackets eliminatorios.

  5. P2P Networking: Implementa networking peer-to-peer para partidas privadas.

El networking multijugador es uno de los aspectos más desafiantes del desarrollo de juegos, pero dominar estas técnicas te permitirá crear experiencias inolvidables que conecten jugadores de todo el mundo.