Networking y Juegos Multijugador
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
websocket.connect()- Conectar a servidor WebSocketwebsocket.send()- Enviar mensaje al servidorhttp.request()- Realizar peticiones HTTPsocket.gettime()- Obtener timestamp preciso
Herramientas de Servidor
- Node.js + Socket.io - Servidor WebSocket fácil
- Python + WebSockets - Alternativa ligera
- Colyseus - Framework para juegos multijugador
- Photon - Solución comercial robusta
Links Útiles
🎯 Ejercicios Propuestos
-
Chat System: Implementa un sistema de chat con comandos y filtros.
-
Leaderboards: Crea tablas de puntuación globales y por temporada.
-
Spectator Mode: Permite a jugadores observar partidas en curso.
-
Tournament System: Sistema de torneos con brackets eliminatorios.
-
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.