diff --git a/apps/client/src/scenes/game/application/GameLoop.ts b/apps/client/src/scenes/game/application/GameLoop.ts index 74d807f..4fc4139 100644 --- a/apps/client/src/scenes/game/application/GameLoop.ts +++ b/apps/client/src/scenes/game/application/GameLoop.ts @@ -8,6 +8,7 @@ import { LocalPlayerController } from "@client/scenes/game/entities/player/PlayerController"; import { BombManager } from "@client/scenes/game/entities/bomb/BombManager"; import type { GamePlayers } from "./game.types"; +import { PlayerRepository } from "./player/PlayerRepository"; import { InputStep } from "./loopSteps/InputStep"; import { SimulationStep } from "./loopSteps/SimulationStep"; import { CameraStep } from "./loopSteps/CameraStep"; @@ -30,7 +31,7 @@ export class GameLoop { private app: Application; private worldContainer: Container; - private players: GamePlayers; + private playerRepository: PlayerRepository; private myId: string; private inputStep: InputStep; private simulationStep: SimulationStep; @@ -41,7 +42,7 @@ constructor({ app, worldContainer, players, myId, getJoystickInput, bombManager, moveSender }: GameLoopOptions) { this.app = app; this.worldContainer = worldContainer; - this.players = players; + this.playerRepository = new PlayerRepository(players); this.myId = myId; this.inputStep = new InputStep({ getJoystickInput }); this.simulationStep = new SimulationStep({ @@ -58,7 +59,7 @@ } public tick = (ticker: Ticker) => { - const me = this.players[this.myId]; + const me = this.playerRepository.getById(this.myId); if (!me || !(me instanceof LocalPlayerController)) return; const { deltaSeconds } = resolveFrameDelta( @@ -68,7 +69,7 @@ const frameContext: LoopFrameContext = { app: this.app, worldContainer: this.worldContainer, - players: this.players, + playerRepository: this.playerRepository, me, deltaSeconds, isMoving: false, diff --git a/apps/client/src/scenes/game/application/GameNetworkSync.ts b/apps/client/src/scenes/game/application/GameNetworkSync.ts index 198f43d..bc3318f 100644 --- a/apps/client/src/scenes/game/application/GameNetworkSync.ts +++ b/apps/client/src/scenes/game/application/GameNetworkSync.ts @@ -22,6 +22,7 @@ toRemoteBombPlacedPayload, toRemotePlayerDeadPayload, } from "./network/adapters/GameNetworkEventAdapter"; +import { PlayerRepository } from "./player/PlayerRepository"; import { PlayerSyncHandler } from "./network/handlers/PlayerSyncHandler"; import { MapSyncHandler } from "./network/handlers/MapSyncHandler"; import { CombatSyncHandler } from "./network/handlers/CombatSyncHandler"; @@ -44,6 +45,7 @@ /** ゲーム中のネットワークイベント購読と同期処理を管理する */ export class GameNetworkSync { + private readonly playerRepository: PlayerRepository; private playerSyncHandler: PlayerSyncHandler; private mapSyncHandler: MapSyncHandler; private combatSyncHandler: CombatSyncHandler; @@ -84,9 +86,10 @@ onBombPlacementAcknowledged, onRemotePlayerDead, }: GameNetworkSyncOptions) { + this.playerRepository = new PlayerRepository(players); this.playerSyncHandler = new PlayerSyncHandler({ worldContainer, - players, + playerRepository: this.playerRepository, myId, appearanceResolver, }); diff --git a/apps/client/src/scenes/game/application/loopSteps/LoopStep.ts b/apps/client/src/scenes/game/application/loopSteps/LoopStep.ts index f94e9c4..2e3c883 100644 --- a/apps/client/src/scenes/game/application/loopSteps/LoopStep.ts +++ b/apps/client/src/scenes/game/application/loopSteps/LoopStep.ts @@ -5,13 +5,13 @@ */ import type { Application, Container } from "pixi.js"; import type { LocalPlayerController } from "@client/scenes/game/entities/player/PlayerController"; -import type { GamePlayers } from "@client/scenes/game/application/game.types"; +import type { PlayerRepository } from "@client/scenes/game/application/player/PlayerRepository"; /** 1フレーム分の更新文脈を表す型 */ export type LoopFrameContext = { app: Application; worldContainer: Container; - players: GamePlayers; + playerRepository: PlayerRepository; me: LocalPlayerController; deltaSeconds: number; isMoving: boolean; diff --git a/apps/client/src/scenes/game/application/loopSteps/SimulationStep.ts b/apps/client/src/scenes/game/application/loopSteps/SimulationStep.ts index 2f5a34c..59beed9 100644 --- a/apps/client/src/scenes/game/application/loopSteps/SimulationStep.ts +++ b/apps/client/src/scenes/game/application/loopSteps/SimulationStep.ts @@ -6,7 +6,7 @@ import { config } from "@client/config"; import { LocalPlayerController, RemotePlayerController } from "@client/scenes/game/entities/player/PlayerController"; import type { MoveSender } from "@client/scenes/game/application/network/PlayerMoveSender"; -import type { GamePlayers } from "../game.types"; +import type { PlayerRepository } from "../player/PlayerRepository"; import type { LoopFrameContext, LoopStep } from "./LoopStep"; /** SimulationStep の初期化入力 */ @@ -17,7 +17,7 @@ type SimulationStepParams = { me: LocalPlayerController; - players: GamePlayers; + playerRepository: PlayerRepository; deltaSeconds: number; isMoving: boolean; }; @@ -38,13 +38,16 @@ public run(context: LoopFrameContext): void { const params: SimulationStepParams = { me: context.me, - players: context.players, + playerRepository: context.playerRepository, deltaSeconds: context.deltaSeconds, isMoving: context.isMoving, }; this.runLocalSimulation({ me: params.me, isMoving: params.isMoving }); - this.runRemoteSimulation({ players: params.players, deltaSeconds: params.deltaSeconds }); + this.runRemoteSimulation({ + playerRepository: params.playerRepository, + deltaSeconds: params.deltaSeconds, + }); } private runLocalSimulation({ me, isMoving }: Pick) { @@ -68,8 +71,8 @@ this.wasMoving = isMoving; } - private runRemoteSimulation({ players, deltaSeconds }: Pick) { - Object.values(players).forEach((player) => { + private runRemoteSimulation({ playerRepository, deltaSeconds }: Pick) { + playerRepository.values().forEach((player) => { if (player instanceof RemotePlayerController) { player.tick(deltaSeconds); } 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 a1556db..084fe8d 100644 --- a/apps/client/src/scenes/game/application/network/handlers/PlayerSyncHandler.ts +++ b/apps/client/src/scenes/game/application/network/handlers/PlayerSyncHandler.ts @@ -15,12 +15,12 @@ RemotePlayerController, } from "@client/scenes/game/entities/player/PlayerController"; import { AppearanceResolver } from "@client/scenes/game/application/AppearanceResolver"; -import type { GamePlayers } from "@client/scenes/game/application/game.types"; +import { PlayerRepository } from "@client/scenes/game/application/player/PlayerRepository"; /** PlayerSyncHandler の初期化入力 */ export type PlayerSyncHandlerOptions = { worldContainer: Container; - players: GamePlayers; + playerRepository: PlayerRepository; myId: string; appearanceResolver: AppearanceResolver; }; @@ -28,13 +28,13 @@ /** プレイヤー関連の同期イベント適用を担当する */ export class PlayerSyncHandler { private readonly worldContainer: Container; - private readonly players: GamePlayers; + private readonly playerRepository: PlayerRepository; private readonly myId: string; private readonly appearanceResolver: AppearanceResolver; - constructor({ worldContainer, players, myId, appearanceResolver }: PlayerSyncHandlerOptions) { + constructor({ worldContainer, playerRepository, myId, appearanceResolver }: PlayerSyncHandlerOptions) { this.worldContainer = worldContainer; - this.players = players; + this.playerRepository = playerRepository; this.myId = myId; this.appearanceResolver = appearanceResolver; } @@ -46,7 +46,7 @@ ? new LocalPlayerController(player, this.appearanceResolver) : new RemotePlayerController(player, this.appearanceResolver); this.worldContainer.addChild(playerController.getDisplayObject()); - this.players[player.id] = playerController; + this.playerRepository.upsert(player.id, playerController); }); }; @@ -54,13 +54,13 @@ public handleNewPlayer = (payload: NewPlayerPayload): void => { const playerController = new RemotePlayerController(payload, this.appearanceResolver); this.worldContainer.addChild(playerController.getDisplayObject()); - this.players[payload.id] = playerController; + this.playerRepository.upsert(payload.id, playerController); }; /** プレイヤー差分更新を反映する */ public handlePlayerUpdates = (changedPlayers: UpdatePlayersPayload): void => { changedPlayers.forEach((playerData) => { - const target = this.players[playerData.id]; + const target = this.playerRepository.getById(playerData.id); if (target && target instanceof RemotePlayerController) { target.applyRemoteUpdate({ x: playerData.x, y: playerData.y }); } @@ -69,13 +69,12 @@ /** 退出プレイヤーを削除する */ public handleRemovePlayer = (id: RemovePlayerPayload): void => { - const target = this.players[id]; + const target = this.playerRepository.remove(id); if (!target) { return; } this.worldContainer.removeChild(target.getDisplayObject()); target.destroy(); - delete this.players[id]; }; } \ No newline at end of file diff --git a/apps/client/src/scenes/game/application/player/PlayerRepository.ts b/apps/client/src/scenes/game/application/player/PlayerRepository.ts new file mode 100644 index 0000000..cb51025 --- /dev/null +++ b/apps/client/src/scenes/game/application/player/PlayerRepository.ts @@ -0,0 +1,49 @@ +/** + * PlayerRepository + * プレイヤー集合の更新窓口を提供する + * 参照と更新を集約して副作用の追跡を容易にする + */ +import type { + GamePlayerController, + GamePlayers, +} from "@client/scenes/game/application/game.types"; + +/** プレイヤー集合の更新と参照を管理するリポジトリ */ +export class PlayerRepository { + private readonly players: GamePlayers; + + constructor(players: GamePlayers = {}) { + this.players = players; + } + + /** プレイヤーIDでコントローラーを取得する */ + public getById(playerId: string): GamePlayerController | undefined { + return this.players[playerId]; + } + + /** プレイヤーを追加または更新する */ + public upsert(playerId: string, controller: GamePlayerController): void { + this.players[playerId] = controller; + } + + /** プレイヤーを削除して削除対象を返す */ + public remove(playerId: string): GamePlayerController | undefined { + const target = this.players[playerId]; + if (!target) { + return undefined; + } + + delete this.players[playerId]; + return target; + } + + /** 管理中プレイヤー配列を返す */ + public values(): GamePlayerController[] { + return Object.values(this.players); + } + + /** 内部で保持するプレイヤー集合を返す */ + public toRecord(): GamePlayers { + return this.players; + } +} \ No newline at end of file