diff --git a/apps/client/src/scenes/game/application/network/handlers/PlayerSyncHandler.ts b/apps/client/src/scenes/game/application/network/handlers/PlayerSyncHandler.ts index fe4a42c..0368e0c 100644 --- a/apps/client/src/scenes/game/application/network/handlers/PlayerSyncHandler.ts +++ b/apps/client/src/scenes/game/application/network/handlers/PlayerSyncHandler.ts @@ -5,6 +5,8 @@ */ import { Container } from "pixi.js"; import type { + CurrentPlayerMetaPayload, + CurrentPlayerBootstrapPayload, CurrentPlayersPayload, NewPlayerPayload, RemovePlayerPayload, @@ -42,16 +44,16 @@ }); } - /** 初期プレイヤー一覧を生成して反映する */ + /** 初期プレイヤー一覧を受信し,座標付き要素のみ実体生成する */ public handleCurrentPlayers = (serverPlayers: CurrentPlayersPayload): void => { serverPlayers.forEach((player) => { - this.replacePlayerController(player.id, player); + this.upsertFromCurrentPlayerBootstrapPayload(player); }); }; /** 新規参加プレイヤーを生成して反映する */ public handleNewPlayer = (payload: NewPlayerPayload): void => { - this.replacePlayerController(payload.id, payload); + this.upsertFromNewPlayerPayload(payload); }; /** プレイヤー差分更新を反映する(自分自身は除外する) */ @@ -91,4 +93,33 @@ this.worldContainer.addChild(playerController.getDisplayObject()); this.playerRepository.upsert(playerId, playerController); } + + /** current-players要素から初期表示用の実体生成を行う */ + private upsertFromCurrentPlayerBootstrapPayload( + payload: CurrentPlayerBootstrapPayload, + ): void { + if (!this.hasBootstrapPosition(payload)) { + return; + } + + this.upsertFromNewPlayerPayload({ + id: payload.id, + name: payload.name, + teamId: payload.teamId, + x: payload.x, + y: payload.y, + }); + } + + /** current-players要素が初期表示座標を含むか判定する */ + private hasBootstrapPosition( + payload: CurrentPlayerBootstrapPayload, + ): payload is CurrentPlayerMetaPayload & Pick { + return "x" in payload && "y" in payload; + } + + /** new-player情報から実体生成を行う */ + private upsertFromNewPlayerPayload(payload: NewPlayerPayload): void { + this.replacePlayerController(payload.id, payload); + } } \ No newline at end of file diff --git a/apps/server/src/domains/game/application/services/currentPlayersBootstrapBuilder.ts b/apps/server/src/domains/game/application/services/currentPlayersBootstrapBuilder.ts new file mode 100644 index 0000000..49fa938 --- /dev/null +++ b/apps/server/src/domains/game/application/services/currentPlayersBootstrapBuilder.ts @@ -0,0 +1,72 @@ +/** + * currentPlayersBootstrapBuilder + * READY_FOR_GAME向けcurrent-playersペイロード生成を提供する + * 自分とAOI内プレイヤーのみ初期表示座標を同梱する + */ +import { domain, type CurrentPlayersPayload } from "@repo/shared"; +import { config } from "@server/config"; + +/** current-players初期表示ペイロードを生成する */ +export const buildCurrentPlayersBootstrapPayload = ( + socketId: string, + roomPlayers: domain.game.player.PlayerData[], +): CurrentPlayersPayload => { + const me = roomPlayers.find((player) => player.id === socketId); + const nearbyPlayerIds = new Set(); + + if (me) { + const centerCell = domain.game.aoi.resolveAoiCellFromPosition( + me.x, + me.y, + config.GAME_CONFIG.AOI_CELL_SIZE, + ); + const aoiWindow = domain.game.aoi.resolveAoiWindowFromCell( + centerCell, + config.GAME_CONFIG.AOI_WINDOW_COLS, + config.GAME_CONFIG.AOI_WINDOW_ROWS, + ); + + roomPlayers.forEach((player) => { + if (player.id === socketId) { + nearbyPlayerIds.add(player.id); + return; + } + + if ( + domain.game.aoi.isPositionInAoiWindow( + player.x, + player.y, + aoiWindow, + config.GAME_CONFIG.AOI_CELL_SIZE, + ) + ) { + nearbyPlayerIds.add(player.id); + } + }); + } + + return roomPlayers.map((player) => { + const includePosition = nearbyPlayerIds.has(player.id); + + if (!includePosition) { + return { + id: player.id, + name: player.name, + teamId: player.teamId, + }; + } + + const quantizedPosition = domain.game.player.quantizeMovePayload({ + x: player.x, + y: player.y, + }); + + return { + id: player.id, + name: player.name, + teamId: player.teamId, + x: quantizedPosition.x, + y: quantizedPosition.y, + }; + }); +}; diff --git a/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts b/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts index 1cb24f0..c8bac61 100644 --- a/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts @@ -8,6 +8,7 @@ import { logEvent } from "@server/logging/logger"; import { config as sharedConfig } from "@repo/shared"; import { gameUseCaseLogEvents, logResults, logScopes } from "@server/logging/index"; +import { buildCurrentPlayersBootstrapPayload } from "../services/currentPlayersBootstrapBuilder"; type ReadyForGameUseCaseParams = { socketId: string; @@ -34,7 +35,8 @@ } const roomPlayers = gameManager.getRoomPlayers(); - output.publishCurrentPlayersToSocket(roomPlayers); + const playerMetas = buildCurrentPlayersBootstrapPayload(socketId, roomPlayers); + output.publishCurrentPlayersToSocket(playerMetas); logEvent(logScopes.GAME_USE_CASE, { event: gameUseCaseLogEvents.READY_FOR_GAME, diff --git a/apps/server/src/domains/game/application/useCases/startGameUseCase.ts b/apps/server/src/domains/game/application/useCases/startGameUseCase.ts index 048ed13..f3a2788 100644 --- a/apps/server/src/domains/game/application/useCases/startGameUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/startGameUseCase.ts @@ -72,10 +72,6 @@ { key: "player", run: ({ roomId, output, tickData }) => { - if (tickData.playerUpdates.length === 0) { - return; - } - output.publishUpdatePlayersToRoom(roomId, tickData.playerUpdates); }, }, diff --git a/apps/server/src/network/handlers/game/services/playerSyncService.ts b/apps/server/src/network/handlers/game/services/playerSyncService.ts index 71b5b4f..b42f681 100644 --- a/apps/server/src/network/handlers/game/services/playerSyncService.ts +++ b/apps/server/src/network/handlers/game/services/playerSyncService.ts @@ -84,6 +84,23 @@ deps.realtimeRoomSyncState.replaceVisiblePlayerIds(roomId, viewerId, nextVisibleIds); }; + const buildVisibleSnapshotPlayers = ( + roomPlayers: domain.game.player.PlayerData[], + viewerId: SocketId, + aoiWindow: AoiWindow, + options: { + includeSelf: boolean; + }, + ): domain.game.player.PlayerData[] => { + return roomPlayers.filter((player) => { + if (player.id === viewerId) { + return options.includeSelf; + } + + return isInViewerAoi(player, aoiWindow); + }); + }; + return { publishUpdatePlayersToRoom: (roomId, players) => { const activeBombs = getActiveBombSnapshotsInRoom(deps.runtimeDeps, roomId); @@ -92,51 +109,53 @@ runtimeDeps: deps.runtimeDeps, roomId, run: ({ viewerId, viewer, roomPlayers }) => { - deps.bombSyncService.syncVisibleBombsByViewer( - roomId, - viewerId, - viewer, - activeBombs, - ); + deps.bombSyncService.syncVisibleBombsByViewer( + roomId, + viewerId, + viewer, + activeBombs, + ); - deps.updateViewerAoiCellCache(roomId, viewerId, viewer); - const aoiWindow = resolveViewerAoiWindow(viewer); + deps.updateViewerAoiCellCache(roomId, viewerId, viewer); + const aoiWindow = resolveViewerAoiWindow(viewer); - const visibleSnapshotPlayers = roomPlayers.filter((player) => { - if (player.id === viewerId) { - return false; + const visibleSnapshotPlayers = buildVisibleSnapshotPlayers( + roomPlayers, + viewerId, + aoiWindow, + { + // ローカルプレイヤー実体生成のため,自分自身を初回同期対象に含める + includeSelf: true, + }, + ); + syncVisiblePlayersByViewer(roomId, viewerId, visibleSnapshotPlayers); + + const visibleDeltaPlayers = quantizedPlayers.filter((player) => { + if (player.id === viewerId) { + return false; + } + + return isInViewerAoi(player, aoiWindow); + }); + + const viewerPositionCache = deps.realtimeRoomSyncState.getPlayerPositionCache( + roomId, + viewerId, + ); + const changedPlayers = collectChangedUpdatePlayersPayload( + visibleDeltaPlayers, + viewerPositionCache, + ); + + if (changedPlayers.length === 0) { + return; } - return isInViewerAoi(player, aoiWindow); - }); - syncVisiblePlayersByViewer(roomId, viewerId, visibleSnapshotPlayers); - - const visibleDeltaPlayers = quantizedPlayers.filter((player) => { - if (player.id === viewerId) { - return false; - } - - return isInViewerAoi(player, aoiWindow); - }); - - const viewerPositionCache = deps.realtimeRoomSyncState.getPlayerPositionCache( - roomId, - viewerId, - ); - const changedPlayers = collectChangedUpdatePlayersPayload( - visibleDeltaPlayers, - viewerPositionCache, - ); - - if (changedPlayers.length === 0) { - return; - } - - deps.reliable.emitToSocketById( - viewerId, - protocol.SocketEvents.UPDATE_PLAYERS, - changedPlayers, - ); + deps.reliable.emitToSocketById( + viewerId, + protocol.SocketEvents.UPDATE_PLAYERS, + changedPlayers, + ); }, }); }, diff --git a/packages/shared/src/protocol/eventPayloads.ts b/packages/shared/src/protocol/eventPayloads.ts index e74b3d3..ac0ca32 100644 --- a/packages/shared/src/protocol/eventPayloads.ts +++ b/packages/shared/src/protocol/eventPayloads.ts @@ -17,6 +17,11 @@ /** ゲームイベントのペイロード型を再公開する */ export type { PlayerSnapshotPayload, + CurrentPlayerMetaPayload, + CurrentPlayerBootstrapPayload, + CurrentPlayerBootstrapListPayload, + PlayerMetaPayload, + PlayerMetaListPayload, PlayerDeltaPayload, InitialPlayerSyncPayload, DeltaPlayerSyncPayload, diff --git a/packages/shared/src/protocol/payloads/gamePayloads.ts b/packages/shared/src/protocol/payloads/gamePayloads.ts index b36357c..b23d8ff 100644 --- a/packages/shared/src/protocol/payloads/gamePayloads.ts +++ b/packages/shared/src/protocol/payloads/gamePayloads.ts @@ -42,9 +42,26 @@ playerStats?: PlayerGameStats[]; }; -/** current-players で配信するプレイヤー全体スナップショット */ +/** current-players で配信するプレイヤー全体スナップショット(旧互換) */ export type PlayerSnapshotPayload = PlayerData[]; +/** current-players で配信する不変プレイヤー情報 */ +export type CurrentPlayerMetaPayload = Pick; + +/** current-players で配信する初期表示用プレイヤー情報 */ +export type CurrentPlayerBootstrapPayload = + | CurrentPlayerMetaPayload + | (CurrentPlayerMetaPayload & Pick); + +/** current-players で配信する初期表示用プレイヤー情報配列 */ +export type CurrentPlayerBootstrapListPayload = CurrentPlayerBootstrapPayload[]; + +/** current-players で配信する不変プレイヤー情報(互換名) */ +export type PlayerMetaPayload = CurrentPlayerMetaPayload; + +/** current-players で配信する不変プレイヤー情報配列(互換名) */ +export type PlayerMetaListPayload = CurrentPlayerMetaPayload[]; + /** * update-players で配信するプレイヤー差分配列 * 帯域最適化のため teamId は含めず,id/x/y のみを配信する @@ -66,8 +83,11 @@ /** update-players イベントで送受信するプレイヤー差分配列 */ export type UpdatePlayersPayload = PlayerDeltaPayload; -/** current-players イベントで送受信するプレイヤー一覧 */ -export type CurrentPlayersPayload = PlayerSnapshotPayload; +/** + * current-players イベントで送受信するプレイヤー一覧 + * 全プレイヤーのメタ情報を含み,自分と周辺プレイヤーのみ初期表示座標を含む + */ +export type CurrentPlayersPayload = CurrentPlayerBootstrapListPayload; /** update-map-cells イベントで送受信するグループ化マップ差分 */ export type UpdateMapCellsPayload = GroupedCellUpdates;