diff --git a/apps/client/src/scenes/game/GameManager.ts b/apps/client/src/scenes/game/GameManager.ts index e560ca8..9a1b14e 100644 --- a/apps/client/src/scenes/game/GameManager.ts +++ b/apps/client/src/scenes/game/GameManager.ts @@ -5,13 +5,11 @@ */ import { Application, Container, Ticker } from "pixi.js"; import { socketManager } from "@client/network/SocketManager"; -import { config } from "@repo/shared"; import { GameMapController } from "./entities/map/GameMapController"; -import { BombController } from "./entities/bomb/BombController"; -import { LocalPlayerController } from "./entities/player/PlayerController"; import { GameTimer } from "./application/GameTimer"; import { GameNetworkSync } from "./application/GameNetworkSync"; import { GameLoop } from "./application/GameLoop"; +import { BombManager } from "./application/BombManager"; import type { GamePlayers } from "./application/game.types"; /** ゲームシーンの実行ライフサイクルを管理するマネージャー */ @@ -23,8 +21,7 @@ private container: HTMLDivElement; private gameMap!: GameMapController; private timer = new GameTimer(); - private bombs: BombController[] = []; - private lastBombPlacedElapsedMs = Number.NEGATIVE_INFINITY; + private bombManager: BombManager | null = null; private networkSync: GameNetworkSync | null = null; private gameLoop: GameLoop | null = null; @@ -39,27 +36,7 @@ } public placeBomb() { - const me = this.players[this.myId]; - if (!me || !(me instanceof LocalPlayerController)) return; - - const elapsedMs = this.timer.getElapsedMs(); - const { BOMB_COOLDOWN_MS, BOMB_FUSE_MS, BOMB_RADIUS_GRID } = config.GAME_CONFIG; - if (elapsedMs - this.lastBombPlacedElapsedMs < BOMB_COOLDOWN_MS) { - return; - } - - const position = me.getPosition(); - const bomb = new BombController({ - x: position.x, - y: position.y, - radiusGrid: BOMB_RADIUS_GRID, - placedAtElapsedMs: elapsedMs, - explodeAtElapsedMs: elapsedMs + BOMB_FUSE_MS, - }); - - this.bombs.push(bomb); - this.worldContainer.addChild(bomb.getDisplayObject()); - this.lastBombPlacedElapsedMs = elapsedMs; + this.bombManager?.placeBomb(); } // 入力と状態管理 @@ -112,6 +89,13 @@ getJoystickInput: () => this.joystickInput, }); + this.bombManager = new BombManager({ + worldContainer: this.worldContainer, + players: this.players, + myId: this.myId, + getElapsedMs: () => this.timer.getElapsedMs(), + }); + // サーバーへゲーム準備完了を通知 socketManager.game.readyForGame(); @@ -132,28 +116,9 @@ */ private tick = (ticker: Ticker) => { this.gameLoop?.tick(ticker); - this.updateBombs(); + this.bombManager?.tick(); }; - private updateBombs() { - const elapsedMs = this.timer.getElapsedMs(); - - const nextBombs: BombController[] = []; - this.bombs.forEach((bomb) => { - bomb.tick(elapsedMs); - - if (bomb.isFinished()) { - this.worldContainer.removeChild(bomb.getDisplayObject()); - bomb.destroy(); - return; - } - - nextBombs.push(bomb); - }); - - this.bombs = nextBombs; - } - /** * クリーンアップ処理(コンポーネントアンマウント時) */ @@ -162,8 +127,8 @@ if (this.isInitialized) { this.app.destroy(true, { children: true }); } - this.bombs.forEach((bomb) => bomb.destroy()); - this.bombs = []; + this.bombManager?.destroy(); + this.bombManager = null; this.players = {}; // イベント購読の解除 diff --git a/apps/client/src/scenes/game/application/BombManager.ts b/apps/client/src/scenes/game/application/BombManager.ts new file mode 100644 index 0000000..0b6ef4d --- /dev/null +++ b/apps/client/src/scenes/game/application/BombManager.ts @@ -0,0 +1,84 @@ +/** + * BombManager + * 爆弾エンティティの生成,更新,破棄を管理する + * クールダウンと設置位置解決をまとめて扱う + */ +import type { Container } from "pixi.js"; +import { config } from "@repo/shared"; +import { LocalPlayerController } from "@client/scenes/game/entities/player/PlayerController"; +import { BombController } from "@client/scenes/game/entities/bomb/BombController"; +import type { GamePlayers } from "./game.types"; + +type BombManagerOptions = { + worldContainer: Container; + players: GamePlayers; + myId: string; + getElapsedMs: () => number; +}; + +/** 爆弾エンティティのライフサイクルを管理する */ +export class BombManager { + private worldContainer: Container; + private players: GamePlayers; + private myId: string; + private getElapsedMs: () => number; + private bombs: BombController[] = []; + private lastBombPlacedElapsedMs = Number.NEGATIVE_INFINITY; + + constructor({ worldContainer, players, myId, getElapsedMs }: BombManagerOptions) { + this.worldContainer = worldContainer; + this.players = players; + this.myId = myId; + this.getElapsedMs = getElapsedMs; + } + + /** 自プレイヤー位置に爆弾を設置する */ + public placeBomb(): void { + const me = this.players[this.myId]; + if (!me || !(me instanceof LocalPlayerController)) return; + + const elapsedMs = this.getElapsedMs(); + const { BOMB_COOLDOWN_MS, BOMB_FUSE_MS, BOMB_RADIUS_GRID } = config.GAME_CONFIG; + if (elapsedMs - this.lastBombPlacedElapsedMs < BOMB_COOLDOWN_MS) { + return; + } + + const position = me.getPosition(); + const bomb = new BombController({ + x: position.x, + y: position.y, + radiusGrid: BOMB_RADIUS_GRID, + explodeAtElapsedMs: elapsedMs + BOMB_FUSE_MS, + }); + + this.bombs.push(bomb); + this.worldContainer.addChild(bomb.getDisplayObject()); + this.lastBombPlacedElapsedMs = elapsedMs; + } + + /** 爆弾状態を更新し終了済みを破棄する */ + public tick(): void { + const elapsedMs = this.getElapsedMs(); + + const nextBombs: BombController[] = []; + this.bombs.forEach((bomb) => { + bomb.tick(elapsedMs); + + if (bomb.isFinished()) { + this.worldContainer.removeChild(bomb.getDisplayObject()); + bomb.destroy(); + return; + } + + nextBombs.push(bomb); + }); + + this.bombs = nextBombs; + } + + /** 管理中の爆弾をすべて破棄する */ + public destroy(): void { + this.bombs.forEach((bomb) => bomb.destroy()); + this.bombs = []; + } +} diff --git a/apps/client/src/scenes/game/entities/bomb/BombController.ts b/apps/client/src/scenes/game/entities/bomb/BombController.ts index fe2cf19..5ef5991 100644 --- a/apps/client/src/scenes/game/entities/bomb/BombController.ts +++ b/apps/client/src/scenes/game/entities/bomb/BombController.ts @@ -10,7 +10,6 @@ x: number; y: number; radiusGrid: number; - placedAtElapsedMs: number; explodeAtElapsedMs: number; }; @@ -19,8 +18,8 @@ private readonly model: BombModel; private readonly view: BombView; - constructor({ x, y, radiusGrid, placedAtElapsedMs, explodeAtElapsedMs }: BombControllerOptions) { - this.model = new BombModel({ x, y, radiusGrid, placedAtElapsedMs, explodeAtElapsedMs }); + constructor({ x, y, radiusGrid, explodeAtElapsedMs }: BombControllerOptions) { + this.model = new BombModel({ x, y, radiusGrid, explodeAtElapsedMs }); this.view = new BombView(); const pos = this.model.getPosition(); diff --git a/apps/client/src/scenes/game/entities/bomb/BombModel.ts b/apps/client/src/scenes/game/entities/bomb/BombModel.ts index 82768f0..339b124 100644 --- a/apps/client/src/scenes/game/entities/bomb/BombModel.ts +++ b/apps/client/src/scenes/game/entities/bomb/BombModel.ts @@ -3,6 +3,7 @@ * 爆弾の状態遷移と時間管理を担うモデル * 設置中 → 爆発中 → 終了 のライフサイクルを管理する */ +/** 爆弾の状態を表す型 */ export type BombState = "armed" | "exploded" | "finished"; const EXPLOSION_VISIBLE_MS = 250; @@ -11,7 +12,6 @@ x: number; y: number; radiusGrid: number; - placedAtElapsedMs: number; explodeAtElapsedMs: number; }; @@ -20,15 +20,13 @@ private x: number; private y: number; private radiusGrid: number; - private placedAtElapsedMs: number; private explodeAtElapsedMs: number; private state: BombState = "armed"; - constructor({ x, y, radiusGrid, placedAtElapsedMs, explodeAtElapsedMs }: BombModelOptions) { + constructor({ x, y, radiusGrid, explodeAtElapsedMs }: BombModelOptions) { this.x = x; this.y = y; this.radiusGrid = radiusGrid; - this.placedAtElapsedMs = placedAtElapsedMs; this.explodeAtElapsedMs = explodeAtElapsedMs; } @@ -61,10 +59,6 @@ return this.state === "finished"; } - public getPlacedAtElapsedMs(): number { - return this.placedAtElapsedMs; - } - public getExplodeAtElapsedMs(): number { return this.explodeAtElapsedMs; } diff --git a/apps/client/src/scenes/game/entities/bomb/BombView.ts b/apps/client/src/scenes/game/entities/bomb/BombView.ts index d88d673..7021e18 100644 --- a/apps/client/src/scenes/game/entities/bomb/BombView.ts +++ b/apps/client/src/scenes/game/entities/bomb/BombView.ts @@ -13,6 +13,8 @@ private bombGraphic: Graphics; private explosionGraphic: Graphics; + private lastRenderedState: BombState | null = null; + private lastRenderedRadiusGrid: number | null = null; constructor() { this.displayObject = new Container(); @@ -31,10 +33,17 @@ } public renderState(state: BombState, radiusGrid: number): void { + if (this.lastRenderedState === state && this.lastRenderedRadiusGrid === radiusGrid) { + return; + } + const { GRID_CELL_SIZE } = config.GAME_CONFIG; const bombRadiusPx = GRID_CELL_SIZE * 0.2; const explosionRadiusPx = radiusGrid * GRID_CELL_SIZE; + this.lastRenderedState = state; + this.lastRenderedRadiusGrid = radiusGrid; + this.bombGraphic.clear(); this.explosionGraphic.clear(); diff --git a/packages/shared/src/config/gameConfig.ts b/packages/shared/src/config/gameConfig.ts index b4b821d..b73e6ca 100644 --- a/packages/shared/src/config/gameConfig.ts +++ b/packages/shared/src/config/gameConfig.ts @@ -1,3 +1,9 @@ +/** + * gameConfig + * ゲーム進行,描画,入力に関する共有設定値を定義する + * クライアントとサーバーで参照する定数群を提供する + */ +/** ゲーム全体で利用する共有設定値 */ export const GAME_CONFIG = { // ゲーム設定 MAX_PLAYERS_PER_ROOM: 100, // ルーム収容人数設定