diff --git a/apps/server/src/domains/game/loop/GameLoop.ts b/apps/server/src/domains/game/loop/GameLoop.ts index c3a4372..9673e75 100644 --- a/apps/server/src/domains/game/loop/GameLoop.ts +++ b/apps/server/src/domains/game/loop/GameLoop.ts @@ -18,7 +18,7 @@ private endMonotonicTimeMs: number = 0; private nextTickAtMs: number = 0; private readonly maxCatchUpTicks: number = 3; - private lastSentPlayers: Map = new Map(); + private lastSentPlayers: Map = new Map(); constructor( private roomId: string, @@ -93,9 +93,11 @@ private processSingleTick(): void { const changedPlayers: gameTypes.TickData["playerUpdates"] = []; + const activePlayerIds = new Set(); // 1. 各プレイヤーの座標処理とマス塗りの判定 this.players.forEach((player) => { + activePlayerIds.add(player.id); const gridIndex = getPlayerGridIndex(player); if (gridIndex !== null) { @@ -103,19 +105,17 @@ } // 送信用のプレイヤーデータを構築 - const playerData = { + const playerData: gameTypes.PlayerPositionUpdate = { id: player.id, x: player.x, y: player.y, - teamId: player.teamId, }; const lastSentPlayer = this.lastSentPlayers.get(player.id); const isChanged = !lastSentPlayer || lastSentPlayer.x !== playerData.x || - lastSentPlayer.y !== playerData.y || - lastSentPlayer.teamId !== playerData.teamId; + lastSentPlayer.y !== playerData.y; if (isChanged) { changedPlayers.push(playerData); @@ -125,7 +125,7 @@ // ルームから離脱したプレイヤーの送信状態をクリーンアップする Array.from(this.lastSentPlayers.keys()).forEach((playerId) => { - if (!this.players.has(playerId)) { + if (!activePlayerIds.has(playerId)) { this.lastSentPlayers.delete(playerId); } }); diff --git a/apps/server/src/network/adapters/gamePayloadSanitizers.ts b/apps/server/src/network/adapters/gamePayloadSanitizers.ts new file mode 100644 index 0000000..35fffec --- /dev/null +++ b/apps/server/src/network/adapters/gamePayloadSanitizers.ts @@ -0,0 +1,12 @@ +/** + * gamePayloadSanitizers + * ゲーム関連の送信ペイロードを境界で正規化する + */ +import type { UpdatePlayersPayload } from "@repo/shared"; + +/** UPDATE_PLAYERS の送信値を座標差分のみへ正規化する */ +export const sanitizeUpdatePlayersPayload = ( + players: UpdatePlayersPayload +): UpdatePlayersPayload => { + return players.map(({ id, x, y }) => ({ id, x, y })); +}; diff --git a/apps/server/src/network/handlers/game/createGameOutputAdapter.ts b/apps/server/src/network/handlers/game/createGameOutputAdapter.ts index 78fa94a..a0416cf 100644 --- a/apps/server/src/network/handlers/game/createGameOutputAdapter.ts +++ b/apps/server/src/network/handlers/game/createGameOutputAdapter.ts @@ -20,6 +20,7 @@ BombOutputPort, GameOutputPort, } from "@server/domains/game/application/ports/gameUseCasePorts"; +import { sanitizeUpdatePlayersPayload } from "@server/network/adapters/gamePayloadSanitizers"; import { createEmitToRoom } from "@server/network/adapters/socketEmitters"; import type { CommonHandlerContext } from "../CommonHandler"; @@ -38,7 +39,8 @@ common.emitToSocket(protocol.SocketEvents.PONG, payload); }, publishUpdatePlayersToSocket: (socketId: string, players: UpdatePlayersPayload) => { - common.emitToSocketById(socketId, protocol.SocketEvents.UPDATE_PLAYERS, players); + const sanitizedPlayers = sanitizeUpdatePlayersPayload(players); + common.emitToSocketById(socketId, protocol.SocketEvents.UPDATE_PLAYERS, sanitizedPlayers); }, publishMapCellUpdatesToRoom: (roomId: RoomId, cellUpdates: UpdateMapCellsPayload) => { common.emitToRoom(roomId, protocol.SocketEvents.UPDATE_MAP_CELLS, cellUpdates); diff --git "a/docs/02_Guide/GUIDE_05_\343\203\227\343\203\255\343\203\210\343\202\263\343\203\253\350\277\275\345\212\240\346\211\213\351\240\206.txt" "b/docs/02_Guide/GUIDE_05_\343\203\227\343\203\255\343\203\210\343\202\263\343\203\253\350\277\275\345\212\240\346\211\213\351\240\206.txt" index 30ab458..56c5839 100644 --- "a/docs/02_Guide/GUIDE_05_\343\203\227\343\203\255\343\203\210\343\202\263\343\203\253\350\277\275\345\212\240\346\211\213\351\240\206.txt" +++ "b/docs/02_Guide/GUIDE_05_\343\203\227\343\203\255\343\203\210\343\202\263\343\203\253\350\277\275\345\212\240\346\211\213\351\240\206.txt" @@ -73,6 +73,8 @@ ・events.ts 再公開が過不足ないか ・client/server で型エラーがないか ・既存イベントの型互換を壊していないか +・player 座標差分イベント(UPDATE_PLAYERS)に teamId を含めていないか +・初期同期イベント(CURRENT_PLAYERS / NEW_PLAYER)に teamId を含めているか 6. 典型ミスと対策 (Common Pitfalls) diff --git a/packages/shared/src/domains/game/game.type.ts b/packages/shared/src/domains/game/game.type.ts index ed51d9f..1f880a3 100644 --- a/packages/shared/src/domains/game/game.type.ts +++ b/packages/shared/src/domains/game/game.type.ts @@ -5,8 +5,11 @@ import type { CellUpdate } from "../gridMap/gridMap.type"; import type { PlayerData } from "../player/player.type"; +/** 1ティックで配信するプレイヤー座標差分 */ +export type PlayerPositionUpdate = Pick; + /** 1ティック分のプレイヤー差分更新とマップ差分を表す共有データ */ export interface TickData { - playerUpdates: PlayerData[]; + playerUpdates: PlayerPositionUpdate[]; cellUpdates: CellUpdate[]; } diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 49f47b1..76566e7 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -29,6 +29,8 @@ ClientToServerPayloadOf, ServerToClientEventPayloadMap, ServerToClientPayloadOf, + InitialPlayerSyncPayload, + DeltaPlayerSyncPayload, CurrentPlayersPayload, BombPlacedPayload, BombPlacedAckPayload, diff --git a/packages/shared/src/protocol/eventPayloads.ts b/packages/shared/src/protocol/eventPayloads.ts index cf0976f..4abd536 100644 --- a/packages/shared/src/protocol/eventPayloads.ts +++ b/packages/shared/src/protocol/eventPayloads.ts @@ -16,6 +16,8 @@ /** ゲームイベントのペイロード型を再公開する */ export type { + InitialPlayerSyncPayload, + DeltaPlayerSyncPayload, UpdatePlayersPayload, CurrentPlayersPayload, UpdateMapCellsPayload, diff --git a/packages/shared/src/protocol/events.ts b/packages/shared/src/protocol/events.ts index cfef91b..14c1e4b 100644 --- a/packages/shared/src/protocol/events.ts +++ b/packages/shared/src/protocol/events.ts @@ -15,6 +15,8 @@ /** 基本ペイロード型を再公開する */ export type { + InitialPlayerSyncPayload, + DeltaPlayerSyncPayload, UpdatePlayersPayload, CurrentPlayersPayload, UpdateMapCellsPayload, diff --git a/packages/shared/src/protocol/payloads/gamePayloads.ts b/packages/shared/src/protocol/payloads/gamePayloads.ts index f28a522..65c859b 100644 --- a/packages/shared/src/protocol/payloads/gamePayloads.ts +++ b/packages/shared/src/protocol/payloads/gamePayloads.ts @@ -3,7 +3,8 @@ * ゲーム進行イベントで利用するペイロード型を定義する * プレイヤー差分,マップ差分,開始終了系の契約を集約する */ -import type { TickData } from "../../domains/game/game.type"; +import type { PlayerPositionUpdate } from "../../domains/game/game.type"; +import type { CellUpdate } from "../../domains/gridMap/gridMap.type"; import type { MovePayload as PlayerMovePayload, PlayerData } from "../../domains/player/player.type"; /** GAME_RESULT イベントで送受信するランキング1行 */ @@ -19,16 +20,31 @@ rankings: GameResultRanking[]; }; +/** + * 初期同期(CURRENT_PLAYERS)で利用するプレイヤー一覧 + * 初期同期用のため teamId を含む完全な PlayerData を配信する + */ +export type InitialPlayerSyncPayload = PlayerData[]; + +/** + * 差分同期(UPDATE_PLAYERS)で利用するプレイヤー差分配列 + * 帯域最適化のため teamId は含めず,id/x/y のみを配信する + */ +export type DeltaPlayerSyncPayload = PlayerPositionUpdate[]; + /** UPDATE_PLAYERS イベントで送受信するプレイヤー差分配列 */ -export type UpdatePlayersPayload = TickData["playerUpdates"]; +export type UpdatePlayersPayload = DeltaPlayerSyncPayload; /** CURRENT_PLAYERS イベントで送受信するプレイヤー一覧 */ -export type CurrentPlayersPayload = TickData["playerUpdates"]; +export type CurrentPlayersPayload = InitialPlayerSyncPayload; /** UPDATE_MAP_CELLS イベントで送受信するマップ差分配列 */ -export type UpdateMapCellsPayload = TickData["cellUpdates"]; +export type UpdateMapCellsPayload = CellUpdate[]; -/** NEW_PLAYER イベントで送受信するプレイヤー情報 */ +/** + * NEW_PLAYER イベントで送受信するプレイヤー情報 + * 初回参加通知のため teamId を含む完全な PlayerData を配信する + */ export type NewPlayerPayload = PlayerData; /** REMOVE_PLAYER イベントで送受信するプレイヤーID */