diff --git a/apps/client/src/scenes/game/input/joystick/hooks/useJoystickController.ts b/apps/client/src/scenes/game/input/joystick/hooks/useJoystickController.ts index 36664ac..d97b1d1 100644 --- a/apps/client/src/scenes/game/input/joystick/hooks/useJoystickController.ts +++ b/apps/client/src/scenes/game/input/joystick/hooks/useJoystickController.ts @@ -15,35 +15,6 @@ } from "../common"; import { useJoystickState } from "./useJoystickState"; -const isJoystickDebugEnabled = (): boolean => { - if (import.meta.env.DEV) { - try { - return window.localStorage.getItem("debug:joystick") !== "0"; - } catch { - return true; - } - } - - try { - return window.localStorage.getItem("debug:joystick") === "1"; - } catch { - return false; - } -}; - -const debugJoystick = (label: string, payload?: unknown): void => { - if (!isJoystickDebugEnabled()) { - return; - } - - if (payload === undefined) { - console.log(`[joystick-controller] ${label}`); - return; - } - - console.log(`[joystick-controller] ${label}`, payload); -}; - /** 入力イベントと通知処理を仲介するフック */ export const useJoystickController = ({ onInput, @@ -53,7 +24,6 @@ const emitInput = useCallback( (normalized: NormalizedInput) => { - debugJoystick("emit", normalized); onInput(normalized.x, normalized.y); }, [onInput], @@ -65,12 +35,6 @@ if (last) { const delta = Math.hypot(normalized.x - last.x, normalized.y - last.y); if (delta < JOYSTICK_MIN_MOVEMENT_DELTA) { - debugJoystick("skip-small-delta", { - normalized, - last, - delta, - threshold: JOYSTICK_MIN_MOVEMENT_DELTA, - }); return; } } diff --git a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts index a6beb7e..f5c90b3 100644 --- a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts +++ b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts @@ -73,7 +73,7 @@ roomId: domain.room.Room["roomId"], hurricanes: UpdateHurricanesPayload, ): void; - publishInitialHurricanesToRoom( + publishReliableHurricanesToRoom( roomId: domain.room.Room["roomId"], hurricanes: UpdateHurricanesPayload, ): void; @@ -129,7 +129,7 @@ GameOutputPort, | "publishUpdatePlayersToRoom" | "publishMapCellUpdatesToRoom" - | "publishInitialHurricanesToRoom" + | "publishReliableHurricanesToRoom" | "publishUpdateHurricanesToRoom" | "publishGameEndToRoom" | "publishGameResultToRoom" diff --git a/apps/server/src/domains/game/application/useCases/startGameUseCase.ts b/apps/server/src/domains/game/application/useCases/startGameUseCase.ts index 0ca5c95..3cd14ea 100644 --- a/apps/server/src/domains/game/application/useCases/startGameUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/startGameUseCase.ts @@ -42,6 +42,24 @@ output, tickData, }: TickUpdatePublishParams): void => { + if (tickData.hurricaneInitialSnapshot.length > 0) { + output.publishReliableHurricanesToRoom( + roomId, + tickData.hurricaneInitialSnapshot, + ); + } + + if (tickData.hurricaneReliableSnapshot.length > 0) { + output.publishReliableHurricanesToRoom( + roomId, + tickData.hurricaneReliableSnapshot, + ); + } + + if (tickData.hurricaneUpdates.length > 0) { + output.publishUpdateHurricanesToRoom(roomId, tickData.hurricaneUpdates); + } + if (tickData.playerUpdates.length > 0) { output.publishUpdatePlayersToRoom(roomId, tickData.playerUpdates); } @@ -49,17 +67,6 @@ if (tickData.cellUpdates.length > 0) { output.publishMapCellUpdatesToRoom(roomId, tickData.cellUpdates); } - - if (tickData.hurricaneInitialSnapshot.length > 0) { - output.publishInitialHurricanesToRoom( - roomId, - tickData.hurricaneInitialSnapshot, - ); - } - - if (tickData.hurricaneUpdates.length > 0) { - output.publishUpdateHurricanesToRoom(roomId, tickData.hurricaneUpdates); - } }; /** ゲームセッション開始とティック通知,終了通知を実行する */ diff --git a/apps/server/src/domains/game/loop/GameLoop.ts b/apps/server/src/domains/game/loop/GameLoop.ts index fb1755a..7df18ce 100644 --- a/apps/server/src/domains/game/loop/GameLoop.ts +++ b/apps/server/src/domains/game/loop/GameLoop.ts @@ -173,7 +173,7 @@ const gridColorsSnapshot = this.mapStore.getGridColorsSnapshot(); this.updateBotPlayers(wallClockNowMs, elapsedMs, gridColorsSnapshot); this.detectBotBombHits(elapsedMs, wallClockNowMs); - const tickData = this.buildTickData(); + const tickData = this.buildTickData(elapsedMs); this.callbacks.onTick(tickData); } @@ -259,17 +259,20 @@ this.disconnectedBotControlledPlayerIds.delete(playerId); } - private buildTickData(): domain.game.tick.TickData { + private buildTickData(elapsedMs: number): domain.game.tick.TickData { const activePlayerIds = new Set(); const playerUpdates = this.collectChangedPlayerUpdates(activePlayerIds); this.cleanupInactivePlayerSnapshots(activePlayerIds); const hurricaneInitialSnapshot = - this.hurricaneSystem.consumeInitialSyncPayload(); + this.hurricaneSystem.consumeInitialSyncPayload(elapsedMs); + const hurricaneReliableSnapshot = + this.hurricaneSystem.consumePeriodicReliableSyncPayload(elapsedMs); return { playerUpdates, cellUpdates: this.mapStore.getAndClearUpdates(), hurricaneInitialSnapshot, + hurricaneReliableSnapshot, hurricaneUpdates: this.hurricaneSystem.getUpdatePayload(), }; } diff --git a/apps/server/src/domains/game/loop/HurricaneSystem.ts b/apps/server/src/domains/game/loop/HurricaneSystem.ts index 9208e1d..99fd5ef 100644 --- a/apps/server/src/domains/game/loop/HurricaneSystem.ts +++ b/apps/server/src/domains/game/loop/HurricaneSystem.ts @@ -9,6 +9,7 @@ import { Player } from "../entities/player/Player.js"; const { checkBombHit } = domain.game.bombHit; +const HURRICANE_RELIABLE_RESYNC_INTERVAL_MS = 200; type HurricaneState = { id: string; @@ -76,6 +77,7 @@ private readonly mapSize: MapGridSize; private hasSpawned = false; private hasInitialSyncPending = false; + private lastReliableSyncElapsedMs = -1; private hurricanes: HurricaneState[] = []; private readonly lastHitAtMsByPlayerId = new Map(); private readonly lastSentSnapshotByHurricaneId = new Map< @@ -108,25 +110,39 @@ } /** 出現直後に1回だけ配信用の初期同期スナップショットを返す */ - public consumeInitialSyncPayload(): HurricaneStatePayload[] { + public consumeInitialSyncPayload(elapsedMs: number): HurricaneStatePayload[] { if (!this.hasInitialSyncPending) { return []; } this.hasInitialSyncPending = false; + this.lastReliableSyncElapsedMs = elapsedMs; - return this.hurricanes.map((hurricane) => { - const snapshot = toHurricaneSyncSnapshot(hurricane); - this.lastSentSnapshotByHurricaneId.set(hurricane.id, snapshot); + return this.buildSnapshotPayload(true); + } - return { - id: hurricane.id, - x: snapshot.x, - y: snapshot.y, - radius: snapshot.radius, - rotationRad: snapshot.rotationRad, - }; - }); + /** 欠損復旧のため一定間隔で再同期スナップショットを返す */ + public consumePeriodicReliableSyncPayload( + elapsedMs: number, + ): HurricaneStatePayload[] { + if (this.hurricanes.length === 0 || this.hasInitialSyncPending) { + return []; + } + + if (this.lastReliableSyncElapsedMs < 0) { + this.lastReliableSyncElapsedMs = elapsedMs; + return []; + } + + if ( + elapsedMs - this.lastReliableSyncElapsedMs + < HURRICANE_RELIABLE_RESYNC_INTERVAL_MS + ) { + return []; + } + + this.lastReliableSyncElapsedMs = elapsedMs; + return this.buildSnapshotPayload(true); } /** ハリケーンを直線移動させ,境界で反射させる */ @@ -237,6 +253,7 @@ public clear(): void { this.hasSpawned = false; this.hasInitialSyncPending = false; + this.lastReliableSyncElapsedMs = -1; this.hurricanes = []; this.lastHitAtMsByPlayerId.clear(); this.lastSentSnapshotByHurricaneId.clear(); @@ -264,4 +281,22 @@ private randomInRange(min: number, max: number): number { return min + Math.random() * Math.max(0, max - min); } + + /** 現在状態を量子化スナップショットへ変換する */ + private buildSnapshotPayload(rememberAsLastSent: boolean): HurricaneStatePayload[] { + return this.hurricanes.map((hurricane) => { + const snapshot = toHurricaneSyncSnapshot(hurricane); + if (rememberAsLastSent) { + this.lastSentSnapshotByHurricaneId.set(hurricane.id, snapshot); + } + + return { + id: hurricane.id, + x: snapshot.x, + y: snapshot.y, + radius: snapshot.radius, + rotationRad: snapshot.rotationRad, + }; + }); + } } diff --git a/apps/server/src/network/adapters/socketEmitters.ts b/apps/server/src/network/adapters/socketEmitters.ts index 4fb57bb..dfe1fc3 100644 --- a/apps/server/src/network/adapters/socketEmitters.ts +++ b/apps/server/src/network/adapters/socketEmitters.ts @@ -15,11 +15,6 @@ (roomId: string, event: TEvent, payload: ServerToClientPayloadOf): void; }; -type EmitToRoomVolatile = { - (roomId: string, event: TEvent): void; - (roomId: string, event: TEvent, payload: ServerToClientPayloadOf): void; -}; - type EmitToRoomExceptSocket = { (roomId: string, excludedSocketId: string, event: TEvent): void; (roomId: string, excludedSocketId: string, event: TEvent, payload: ServerToClientPayloadOf): void; @@ -61,17 +56,6 @@ }; }; -/** ルーム単位の volatile 送信関数を生成する */ -export const createEmitToRoomVolatile = (io: Server): EmitToRoomVolatile => { - return (roomId: string, event: SocketEventName, payload?: unknown) => { - emitWithOptionalPayload( - (eventName, body) => io.to(roomId).volatile.emit(eventName, body), - event, - payload, - ); - }; -}; - /** ルーム送信時に特定ソケットを除外する送信関数を生成する */ export const createEmitToRoomExceptSocket = (io: Server): EmitToRoomExceptSocket => { return (roomId: string, excludedSocketId: string, event: SocketEventName, payload?: unknown) => { diff --git a/apps/server/src/network/handlers/CommonHandler.ts b/apps/server/src/network/handlers/CommonHandler.ts index 2636c34..7613892 100644 --- a/apps/server/src/network/handlers/CommonHandler.ts +++ b/apps/server/src/network/handlers/CommonHandler.ts @@ -6,7 +6,6 @@ import { createEmitToAll, createEmitToRoom, - createEmitToRoomVolatile, createEmitToRoomExceptSocket, createEmitToSocket, createEmitToSocketById, @@ -21,15 +20,9 @@ emitToSocketById: ReturnType; }; -/** 鮮度を重視する高頻度送信関数群 */ -export type RealtimeEmitters = { - emitToRoom: ReturnType; -}; - /** ハンドラで共通利用する送信コンテキスト */ export type CommonHandlerContext = { reliable: ReliableEmitters; - realtime: RealtimeEmitters; }; /** 送信先別のエミッタをまとめた共通コンテキストを生成する */ @@ -45,12 +38,7 @@ emitToSocketById: createEmitToSocketById(io), }; - const realtime: RealtimeEmitters = { - emitToRoom: createEmitToRoomVolatile(io), - }; - return { reliable, - realtime, }; }; diff --git a/apps/server/src/network/handlers/game/createGameOutputAdapter.ts b/apps/server/src/network/handlers/game/createGameOutputAdapter.ts index 18d0370..ea9c04e 100644 --- a/apps/server/src/network/handlers/game/createGameOutputAdapter.ts +++ b/apps/server/src/network/handlers/game/createGameOutputAdapter.ts @@ -52,7 +52,7 @@ export const createGameOutputAdapter = ( common: CommonHandlerContext, ): GameOutputAdapter => { - const { reliable, realtime } = common; + const { reliable } = common; const realtimeRoomSyncState = createRealtimeRoomSyncStateStore(); return { @@ -73,7 +73,7 @@ return; } - realtime.emitToRoom( + reliable.emitToRoom( roomId, protocol.SocketEvents.UPDATE_PLAYERS, changedPlayers, @@ -94,13 +94,9 @@ roomId: RoomId, hurricanes: UpdateHurricanesPayload, ) => { - reliable.emitToRoom( - roomId, - protocol.SocketEvents.UPDATE_HURRICANES, - hurricanes, - ); + reliable.emitToRoom(roomId, protocol.SocketEvents.UPDATE_HURRICANES, hurricanes); }, - publishInitialHurricanesToRoom: ( + publishReliableHurricanesToRoom: ( roomId: RoomId, hurricanes: UpdateHurricanesPayload, ) => { diff --git a/packages/shared/src/domains/game/tick/tick.type.ts b/packages/shared/src/domains/game/tick/tick.type.ts index d19a27c..e5d42f1 100644 --- a/packages/shared/src/domains/game/tick/tick.type.ts +++ b/packages/shared/src/domains/game/tick/tick.type.ts @@ -15,5 +15,7 @@ cellUpdates: CellUpdate[]; /** 出現直後に1回だけ配信するハリケーン初期同期スナップショット */ hurricaneInitialSnapshot: HurricaneStatePayload[]; + /** 欠損復旧のため定期配信するハリケーン再同期スナップショット */ + hurricaneReliableSnapshot: HurricaneStatePayload[]; hurricaneUpdates: HurricaneStatePayload[]; }