Improve network stack (less traffic)

This commit is contained in:
Mike Müller 2026-03-08 20:56:28 +01:00
parent 43874d0f54
commit ddf68f316e

View File

@ -55,7 +55,8 @@ const POWERUPS = ["bomb", "flame", "speed"];
const MAX_PLAYERS = 8;
const NETWORK_PATH = "/ws";
const PLAYER_NAME_STORAGE_KEY = "gptBomberPlayerName";
const SNAPSHOT_RATE = 24;
const SNAPSHOT_RATE = 18;
const MAP_RESYNC_SECONDS = 1.2;
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
@ -142,6 +143,8 @@ const network = {
lastPingAt: 0,
lobbyPhase: "lobby",
hasReceivedSnapshot: false,
lastFullMapSnapshotAt: 0,
lastSentMapState: null,
};
let audio = null;
@ -351,6 +354,7 @@ function resetRoundSingle() {
state.multiplayerRoundKills = {};
state.multiplayerRoundDeathTimes = {};
state.multiplayerRoundTime = 0;
resetHostSnapshotMapState();
closeMenu();
}
@ -384,6 +388,7 @@ function resetRoundMultiplayer(roster) {
state.multiplayerRoundKills = {};
state.multiplayerRoundDeathTimes = {};
state.multiplayerRoundTime = 0;
resetHostSnapshotMapState();
closeMenu();
inputState.bombQueued = false;
network.remoteBombPrev.clear();
@ -858,6 +863,7 @@ function leaveLobbyInternals() {
network.inputStateAtClient = { dir: null, bomb: false, bombCell: null };
network.lastInputSentAt = 0;
network.hasReceivedSnapshot = false;
resetHostSnapshotMapState();
}
function leaveMultiplayerToMainMenu() {
@ -878,12 +884,68 @@ function hostStartMatchFromLobby() {
sendSocketMessage("lobby_start");
}
function serializeGameState() {
function resetHostSnapshotMapState() {
network.lastSentMapState = null;
network.lastFullMapSnapshotAt = 0;
}
function serializeMapTile(tile) {
return { type: tile.type, powerup: tile.powerup || null };
}
function mapTileSignature(tile) {
return tile.type + "|" + (tile.powerup || "");
}
function buildMapSignatureState() {
return state.map.map((row) => row.map((tile) => mapTileSignature(tile)));
}
function serializeMapPatch() {
if (!Array.isArray(network.lastSentMapState) || network.lastSentMapState.length !== state.map.length) {
network.lastSentMapState = buildMapSignatureState();
return [];
}
const patch = [];
for (let y = 0; y < state.map.length; y += 1) {
const row = state.map[y];
const previousRow = network.lastSentMapState[y];
for (let x = 0; x < row.length; x += 1) {
const tile = row[x];
const signature = mapTileSignature(tile);
if (!previousRow || previousRow[x] !== signature) {
patch.push({ x, y, type: tile.type, powerup: tile.powerup || null });
if (previousRow) {
previousRow[x] = signature;
}
}
}
}
return patch;
}
function serializeGameState(includeFullMap = false) {
let map;
let mapPatch;
if (includeFullMap || !Array.isArray(network.lastSentMapState)) {
map = state.map.map((row) => row.map((tile) => serializeMapTile(tile)));
network.lastSentMapState = buildMapSignatureState();
} else {
const patch = serializeMapPatch();
if (patch.length > 0) {
mapPatch = patch;
}
}
return {
status: state.status,
message: state.message,
time: state.time,
map: state.map.map((row) => row.map((tile) => ({ type: tile.type, powerup: tile.powerup || null }))),
map,
mapPatch,
players: state.players.map((player) => ({
id: player.id,
name: player.name,
@ -957,20 +1019,33 @@ function playSnapshotSfx(snapshot) {
}
let powerupCollected = false;
for (let y = 0; y < state.map.length; y += 1) {
for (let x = 0; x < state.map[y].length; x += 1) {
const prevTile = state.map[y][x];
const nextTile = snapshot.map[y]?.[x];
if (!prevTile || !nextTile) {
continue;
if (Array.isArray(snapshot.map)) {
for (let y = 0; y < state.map.length; y += 1) {
for (let x = 0; x < state.map[y].length; x += 1) {
const prevTile = state.map[y][x];
const nextTile = snapshot.map[y]?.[x];
if (!prevTile || !nextTile) {
continue;
}
if (prevTile.type === "floor" && prevTile.powerup && nextTile.type === "floor" && !nextTile.powerup) {
powerupCollected = true;
break;
}
}
if (prevTile.type === "floor" && prevTile.powerup && nextTile.type === "floor" && !nextTile.powerup) {
powerupCollected = true;
if (powerupCollected) {
break;
}
}
if (powerupCollected) {
break;
} else if (Array.isArray(snapshot.mapPatch)) {
for (const change of snapshot.mapPatch) {
const prevTile = state.map[change.y]?.[change.x];
if (!prevTile) {
continue;
}
if (prevTile.type === "floor" && prevTile.powerup && change.type === "floor" && !change.powerup) {
powerupCollected = true;
break;
}
}
}
@ -989,7 +1064,21 @@ function applySnapshot(snapshot) {
state.status = snapshot.status;
state.message = snapshot.message;
state.time = snapshot.time;
state.map = snapshot.map.map((row) => row.map((tile) => ({ type: tile.type, powerup: tile.powerup })));
if (Array.isArray(snapshot.map)) {
state.map = snapshot.map.map((row) => row.map((tile) => ({ type: tile.type, powerup: tile.powerup })));
} else if (Array.isArray(snapshot.mapPatch) && state.map.length > 0) {
for (const change of snapshot.mapPatch) {
if (!inBounds(change.x, change.y)) {
continue;
}
const row = state.map[change.y];
if (!row || !row[change.x]) {
continue;
}
row[change.x] = { type: change.type, powerup: change.powerup || null };
}
}
state.players = snapshot.players.map((player) => {
const key = player.ownerId || `id:${player.id}`;
const prev = previousPlayers.get(key);
@ -1031,12 +1120,23 @@ function broadcastSnapshot(force = false) {
if (!network.isHost || state.mode !== "multiplayer" || state.screen !== "game") {
return;
}
const t = now();
if (!force && t - network.lastSnapshotSentAt < 1 / SNAPSHOT_RATE) {
return;
}
const includeFullMap =
force ||
!Array.isArray(network.lastSentMapState) ||
t - network.lastFullMapSnapshotAt >= MAP_RESYNC_SECONDS;
if (includeFullMap) {
network.lastFullMapSnapshotAt = t;
}
network.lastSnapshotSentAt = t;
sendSocketMessage("game_snapshot", { snapshot: serializeGameState(), ts: t });
sendSocketMessage("game_snapshot", { snapshot: serializeGameState(includeFullMap), ts: t });
}
function updateLobbyNetwork(dt) {