diff --git a/apps/client/src/scenes/game/application/network/PlayerMoveSender.ts b/apps/client/src/scenes/game/application/network/PlayerMoveSender.ts index 9efa0c6..df0c394 100644 --- a/apps/client/src/scenes/game/application/network/PlayerMoveSender.ts +++ b/apps/client/src/scenes/game/application/network/PlayerMoveSender.ts @@ -3,6 +3,7 @@ * ローカルプレイヤー移動の送信責務を提供する * シミュレーション層から通信実装を分離する */ +import { config } from "@client/config"; import { socketManager } from "@client/network/SocketManager"; /** 移動送信のインターフェース型 */ @@ -10,10 +11,45 @@ sendMove: (x: number, y: number) => void; }; +type QuantizedPosition = { + x: number; + y: number; +}; + /** ソケット経由で移動送信を行う実装 */ export class SocketPlayerMoveSender implements MoveSender { + private lastSentPosition: QuantizedPosition | null = null; + /** 指定座標をサーバーへ送信する */ public sendMove(x: number, y: number): void { - socketManager.game.sendMove(x, y); + const quantizedPosition = this.quantizePosition(x, y); + const lastPosition = this.lastSentPosition; + + if ( + lastPosition + && lastPosition.x === quantizedPosition.x + && lastPosition.y === quantizedPosition.y + ) { + return; + } + + this.lastSentPosition = quantizedPosition; + socketManager.game.sendMove(quantizedPosition.x, quantizedPosition.y); + } + + private quantizePosition(x: number, y: number): QuantizedPosition { + return { + x: this.quantizeAxis(x), + y: this.quantizeAxis(y), + }; + } + + private quantizeAxis(value: number): number { + if (!Number.isFinite(value)) { + return 0; + } + + const scale = config.GAME_CONFIG.POSITION_QUANTIZE_SCALE; + return Math.round(value * scale) / scale; } } \ No newline at end of file diff --git a/apps/server/src/domains/game/application/useCases/startGameUseCase.ts b/apps/server/src/domains/game/application/useCases/startGameUseCase.ts index 0163655..ed6fa07 100644 --- a/apps/server/src/domains/game/application/useCases/startGameUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/startGameUseCase.ts @@ -58,7 +58,9 @@ output.publishMapCellUpdatesToRoom(roomId, tickData.cellUpdates); } - output.publishUpdateHurricanesToRoom(roomId, tickData.hurricaneUpdates); + if (tickData.hurricaneUpdates.length > 0) { + output.publishUpdateHurricanesToRoom(roomId, tickData.hurricaneUpdates); + } }, onGameEnd: (resultPayload) => { logEvent(logScopes.GAME_USE_CASE, { diff --git a/apps/server/src/network/adapters/socketEmitters.ts b/apps/server/src/network/adapters/socketEmitters.ts index dfe1fc3..4fb57bb 100644 --- a/apps/server/src/network/adapters/socketEmitters.ts +++ b/apps/server/src/network/adapters/socketEmitters.ts @@ -15,6 +15,11 @@ (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; @@ -56,6 +61,17 @@ }; }; +/** ルーム単位の 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 6348c9d..493e951 100644 --- a/apps/server/src/network/handlers/CommonHandler.ts +++ b/apps/server/src/network/handlers/CommonHandler.ts @@ -6,6 +6,7 @@ import { createEmitToAll, createEmitToRoom, + createEmitToRoomVolatile, createEmitToRoomExceptSocket, createEmitToSocket, createEmitToSocketById, @@ -15,6 +16,7 @@ export type CommonHandlerContext = { emitToAll: ReturnType; emitToRoom: ReturnType; + emitToRoomVolatile: ReturnType; emitToRoomExceptSocket: ReturnType; emitToSocket: ReturnType; emitToSocketById: ReturnType; @@ -28,6 +30,7 @@ return { emitToAll: createEmitToAll(io), emitToRoom: createEmitToRoom(io), + emitToRoomVolatile: createEmitToRoomVolatile(io), emitToRoomExceptSocket: createEmitToRoomExceptSocket(io), emitToSocket: createEmitToSocket(socket), emitToSocketById: createEmitToSocketById(io), diff --git a/apps/server/src/network/handlers/game/createGameOutputAdapter.ts b/apps/server/src/network/handlers/game/createGameOutputAdapter.ts index e0ee30f..0fb931b 100644 --- a/apps/server/src/network/handlers/game/createGameOutputAdapter.ts +++ b/apps/server/src/network/handlers/game/createGameOutputAdapter.ts @@ -57,7 +57,7 @@ players: UpdatePlayersPayload, ) => { const sanitizedPlayers = sanitizeUpdatePlayersPayload(players); - common.emitToRoom( + common.emitToRoomVolatile( roomId, protocol.SocketEvents.UPDATE_PLAYERS, sanitizedPlayers, @@ -78,7 +78,7 @@ roomId: RoomId, hurricanes: UpdateHurricanesPayload, ) => { - common.emitToRoom( + common.emitToRoomVolatile( roomId, protocol.SocketEvents.UPDATE_HURRICANES, hurricanes, diff --git a/packages/shared/src/config/gameConfig.ts b/packages/shared/src/config/gameConfig.ts index 6226512..aaa29d8 100644 --- a/packages/shared/src/config/gameConfig.ts +++ b/packages/shared/src/config/gameConfig.ts @@ -11,6 +11,7 @@ // ネットワーク同期設定(クライアント/サーバー契約) PLAYER_POSITION_UPDATE_MS: 50, // 座標送信間隔(20Hz) + POSITION_QUANTIZE_SCALE: 100, // 送信座標の量子化スケール(100なら小数第2位まで保持) // グリッド(マス)設定(クライアント/サーバー契約) GRID_COLS: 40, // 横のマス数(グリッド単位)