diff --git a/apps/client/src/app.tsx b/apps/client/src/app.tsx index f0e529a..d4a324b 100644 --- a/apps/client/src/app.tsx +++ b/apps/client/src/app.tsx @@ -1,4 +1,4 @@ -import { socketClient } from "./network/SocketClient"; +import { socketManager } from "./network/SocketManager"; import { useAppFlow } from "./hooks/useAppFlow"; // 画面遷移先シーンコンポーネント群 @@ -13,12 +13,12 @@ // タイトル画面分岐 if (scenePhase === appConsts.ScenePhase.TITLE) { - return socketClient.joinRoom(payload)} />; + return socketManager.room.joinRoom(payload)} />; } // ロビー画面分岐 if (scenePhase === appConsts.ScenePhase.LOBBY) { - return socketClient.startGame()} />; + return socketManager.room.startGame()} />; } // プレイ画面分岐 diff --git a/apps/client/src/hooks/useAppFlow.ts b/apps/client/src/hooks/useAppFlow.ts index 7280ba7..71baf36 100644 --- a/apps/client/src/hooks/useAppFlow.ts +++ b/apps/client/src/hooks/useAppFlow.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { socketClient } from "../network/SocketClient"; +import { socketManager } from "../network/SocketManager"; import { appConsts } from "@repo/shared"; import type { appTypes, roomTypes } from "@repo/shared"; @@ -15,12 +15,12 @@ const [myId, setMyId] = useState(null); useEffect(() => { - socketClient.onConnect((id) => setMyId(id)); - socketClient.onRoomUpdate((updatedRoom) => { + socketManager.connection.onConnect((id) => setMyId(id)); + socketManager.room.onRoomUpdate((updatedRoom) => { setRoom(updatedRoom); setScenePhase(appConsts.ScenePhase.LOBBY); }); - socketClient.onGameStart(() => setScenePhase(appConsts.ScenePhase.PLAYING)); + socketManager.game.onGameStart(() => setScenePhase(appConsts.ScenePhase.PLAYING)); }, []); return { scenePhase, room, myId }; diff --git a/apps/client/src/network/SocketClient.ts b/apps/client/src/network/SocketClient.ts deleted file mode 100644 index f29309b..0000000 --- a/apps/client/src/network/SocketClient.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { io, Socket } from "socket.io-client"; -import { protocol, config } from "@repo/shared"; -import type { playerTypes, gridMapTypes, roomTypes } from "@repo/shared"; - -/** - * サーバー WebSocket 通信管理クラス - */ -export class SocketClient { - public socket: Socket; - - constructor() { - // サーバー(バックエンド)のURLを直接指定する - // 本番環境(Render)のURLを指定することで、プロキシなしで直接通信させます - const SERVER_URL = import.meta.env.PROD - ? config.NETWORK_CONFIG.PROD_SERVER_URL - : config.NETWORK_CONFIG.DEV_SERVER_URL; - - this.socket = io(SERVER_URL, { - transports: [...config.NETWORK_CONFIG.SOCKET_TRANSPORTS], // 接続の安定性を高める - withCredentials: true - }); - } - - /** - * 接続完了イベント購読 - * @param callback 接続済みソケットID受け取りコールバック - */ - onConnect(callback: (id: string) => void) { - // 接続済み状態での即時通知 - if (this.socket.connected) { - callback(this.socket.id || ""); - } - - // 初回接続・再接続イベント購読 - this.socket.on(protocol.SocketEvents.CONNECT, () => { - callback(this.socket.id || ""); - }); - } - - /** - * 初期プレイヤー一覧受信イベント購読 - */ - onCurrentPlayers(callback: (players: playerTypes.PlayerData[] | Record) => void) { - this.socket.on(protocol.SocketEvents.CURRENT_PLAYERS, callback); - } - - /** - * 新規プレイヤー参加イベント購読 - */ - onNewPlayer(callback: (player: playerTypes.PlayerData) => void) { - this.socket.on(protocol.SocketEvents.NEW_PLAYER, callback); - } - - /** - * 他プレイヤー状態更新イベント購読 - */ - onUpdatePlayer(callback: (data: Partial & { id: string }) => void) { - this.socket.on(protocol.SocketEvents.UPDATE_PLAYER, callback); - } - - /** - * プレイヤー退出イベント購読 - */ - onRemovePlayer(callback: (id: string) => void) { - this.socket.on(protocol.SocketEvents.REMOVE_PLAYER, callback); - } - - /** - * 自身移動データ送信 - * @param x 現在のX座標 - * @param y 現在のY座標 - */ - sendMove(x: number, y: number) { - const payload: playerTypes.MovePayload = { x, y }; - this.socket.emit(protocol.SocketEvents.MOVE, payload); - } - - /** - * マス目の差分更新イベント購読 - */ - onUpdateMapCells(callback: (updates: gridMapTypes.CellUpdate[]) => void) { - this.socket.on(protocol.SocketEvents.UPDATE_MAP_CELLS, callback); - } - - /** - * ルーム入室リクエスト送信 - * @param payload 入室ペイロード - */ - joinRoom(payload: roomTypes.JoinRoomPayload) { - this.socket.emit(protocol.SocketEvents.JOIN_ROOM, payload); - } - - /** - * ルーム情報更新イベント購読 - */ - onRoomUpdate(callback: (room: roomTypes.Room) => void) { - this.socket.on(protocol.SocketEvents.ROOM_UPDATE, callback); - } - - /** - * ゲーム開始通知イベント購読 - */ - onGameStart(callback: (data: { startTime: number }) => void) { - this.socket.on(protocol.SocketEvents.GAME_START, callback); - } - - /** - * ゲーム開始リクエスト送信 - */ - startGame() { - this.socket.emit(protocol.SocketEvents.START_GAME); - } - - /** - * ゲーム画面準備完了通知 - */ - readyForGame() { - this.socket.emit(protocol.SocketEvents.READY_FOR_GAME); - } - - /** - * 全てのゲームプレイ関連イベントを解除(一括解除用) - */ - removeAllListeners() { - this.socket.off(protocol.SocketEvents.CURRENT_PLAYERS); - this.socket.off(protocol.SocketEvents.NEW_PLAYER); - this.socket.off(protocol.SocketEvents.UPDATE_PLAYER); - this.socket.off(protocol.SocketEvents.REMOVE_PLAYER); - this.socket.off(protocol.SocketEvents.UPDATE_MAP_CELLS); - } -} - -// シングルトン利用向け共有インスタンス -export const socketClient = new SocketClient(); \ No newline at end of file diff --git a/apps/client/src/network/SocketManager.ts b/apps/client/src/network/SocketManager.ts new file mode 100644 index 0000000..4c4048f --- /dev/null +++ b/apps/client/src/network/SocketManager.ts @@ -0,0 +1,29 @@ +import { io, Socket } from "socket.io-client"; +import { config } from "@repo/shared"; +import { createConnectionHandler, type ConnectionHandler } from "./handlers/ConnectionHandler"; +import { createRoomHandler, type RoomHandler } from "./handlers/RoomHandler"; +import { createGameHandler, type GameHandler } from "./handlers/GameHandler"; + +export class SocketManager { + public socket: Socket; + public connection: ConnectionHandler; + public room: RoomHandler; + public game: GameHandler; + + constructor() { + const serverUrl = import.meta.env.PROD + ? config.NETWORK_CONFIG.PROD_SERVER_URL + : config.NETWORK_CONFIG.DEV_SERVER_URL; + + this.socket = io(serverUrl, { + transports: [...config.NETWORK_CONFIG.SOCKET_TRANSPORTS], + withCredentials: true + }); + + this.connection = createConnectionHandler(this.socket); + this.room = createRoomHandler(this.socket); + this.game = createGameHandler(this.socket); + } +} + +export const socketManager = new SocketManager(); diff --git a/apps/client/src/network/handlers/ConnectionHandler.ts b/apps/client/src/network/handlers/ConnectionHandler.ts new file mode 100644 index 0000000..1fbe917 --- /dev/null +++ b/apps/client/src/network/handlers/ConnectionHandler.ts @@ -0,0 +1,22 @@ +import type { Socket } from "socket.io-client"; +import { protocol } from "@repo/shared"; + +type ConnectionHandler = { + onConnect: (callback: (id: string) => void) => void; +}; + +export const createConnectionHandler = (socket: Socket): ConnectionHandler => { + return { + onConnect: (callback: (id: string) => void) => { + if (socket.connected) { + callback(socket.id || ""); + } + + socket.on(protocol.SocketEvents.CONNECT, () => { + callback(socket.id || ""); + }); + } + }; +}; + +export type { ConnectionHandler }; diff --git a/apps/client/src/network/handlers/GameHandler.ts b/apps/client/src/network/handlers/GameHandler.ts new file mode 100644 index 0000000..77abe3d --- /dev/null +++ b/apps/client/src/network/handlers/GameHandler.ts @@ -0,0 +1,54 @@ +import type { Socket } from "socket.io-client"; +import { protocol } from "@repo/shared"; +import type { playerTypes, gridMapTypes } from "@repo/shared"; + +type GameHandler = { + onCurrentPlayers: (callback: (players: playerTypes.PlayerData[] | Record) => void) => void; + onNewPlayer: (callback: (player: playerTypes.PlayerData) => void) => void; + onUpdatePlayer: (callback: (data: Partial & { id: string }) => void) => void; + onRemovePlayer: (callback: (id: string) => void) => void; + onUpdateMapCells: (callback: (updates: gridMapTypes.CellUpdate[]) => void) => void; + onGameStart: (callback: (data: { startTime: number }) => void) => void; + sendMove: (x: number, y: number) => void; + readyForGame: () => void; + removeAllListeners: () => void; +}; + +export const createGameHandler = (socket: Socket): GameHandler => { + return { + onCurrentPlayers: (callback) => { + socket.on(protocol.SocketEvents.CURRENT_PLAYERS, callback); + }, + onNewPlayer: (callback) => { + socket.on(protocol.SocketEvents.NEW_PLAYER, callback); + }, + onUpdatePlayer: (callback) => { + socket.on(protocol.SocketEvents.UPDATE_PLAYER, callback); + }, + onRemovePlayer: (callback) => { + socket.on(protocol.SocketEvents.REMOVE_PLAYER, callback); + }, + onUpdateMapCells: (callback) => { + socket.on(protocol.SocketEvents.UPDATE_MAP_CELLS, callback); + }, + onGameStart: (callback) => { + socket.on(protocol.SocketEvents.GAME_START, callback); + }, + sendMove: (x, y) => { + const payload: playerTypes.MovePayload = { x, y }; + socket.emit(protocol.SocketEvents.MOVE, payload); + }, + readyForGame: () => { + socket.emit(protocol.SocketEvents.READY_FOR_GAME); + }, + removeAllListeners: () => { + socket.off(protocol.SocketEvents.CURRENT_PLAYERS); + socket.off(protocol.SocketEvents.NEW_PLAYER); + socket.off(protocol.SocketEvents.UPDATE_PLAYER); + socket.off(protocol.SocketEvents.REMOVE_PLAYER); + socket.off(protocol.SocketEvents.UPDATE_MAP_CELLS); + } + }; +}; + +export type { GameHandler }; diff --git a/apps/client/src/network/handlers/RoomHandler.ts b/apps/client/src/network/handlers/RoomHandler.ts new file mode 100644 index 0000000..4210616 --- /dev/null +++ b/apps/client/src/network/handlers/RoomHandler.ts @@ -0,0 +1,25 @@ +import type { Socket } from "socket.io-client"; +import { protocol } from "@repo/shared"; +import type { roomTypes } from "@repo/shared"; + +type RoomHandler = { + joinRoom: (payload: roomTypes.JoinRoomPayload) => void; + onRoomUpdate: (callback: (room: roomTypes.Room) => void) => void; + startGame: () => void; +}; + +export const createRoomHandler = (socket: Socket): RoomHandler => { + return { + joinRoom: (payload: roomTypes.JoinRoomPayload) => { + socket.emit(protocol.SocketEvents.JOIN_ROOM, payload); + }, + onRoomUpdate: (callback: (room: roomTypes.Room) => void) => { + socket.on(protocol.SocketEvents.ROOM_UPDATE, callback); + }, + startGame: () => { + socket.emit(protocol.SocketEvents.START_GAME); + } + }; +}; + +export type { RoomHandler }; diff --git a/apps/client/src/scenes/game/GameManager.ts b/apps/client/src/scenes/game/GameManager.ts index e8b881d..0a31e31 100644 --- a/apps/client/src/scenes/game/GameManager.ts +++ b/apps/client/src/scenes/game/GameManager.ts @@ -1,5 +1,5 @@ import { Application, Container, Ticker } from "pixi.js"; -import { socketClient } from "../../network/SocketClient"; +import { socketManager } from "../../network/SocketManager"; import { config } from "@repo/shared"; import type { playerTypes } from "@repo/shared"; import { BasePlayer, LocalPlayer, RemotePlayer } from "./Player"; @@ -70,7 +70,7 @@ this.setupSocketListeners(); // サーバーへゲーム準備完了を通知 - socketClient.readyForGame(); + socketManager.game.readyForGame(); // メインループの登録 this.app.ticker.add(this.tick.bind(this)); @@ -88,7 +88,7 @@ * ソケットイベントの登録 */ private setupSocketListeners() { - socketClient.onCurrentPlayers((serverPlayers: playerTypes.PlayerData[] | Record) => { + socketManager.game.onCurrentPlayers((serverPlayers: playerTypes.PlayerData[] | Record) => { const playersArray = (Array.isArray(serverPlayers) ? serverPlayers : Object.values(serverPlayers)) as playerTypes.PlayerData[]; playersArray.forEach((p) => { const playerSprite = p.id === this.myId ? new LocalPlayer(p) : new RemotePlayer(p); @@ -97,21 +97,21 @@ }); }); - socketClient.onNewPlayer((p: playerTypes.PlayerData) => { + socketManager.game.onNewPlayer((p: playerTypes.PlayerData) => { const playerSprite = new RemotePlayer(p); this.worldContainer.addChild(playerSprite); this.players[p.id] = playerSprite; }); // サーバーからの GAME_START を検知して開始時刻をセットする - socketClient.onGameStart((data) => { + socketManager.game.onGameStart((data) => { if (data && data.startTime) { this.setGameStart(data.startTime); console.log(`[GameManager] ゲーム開始時刻同期完了: ${data.startTime}`); } }); - socketClient.onUpdatePlayer((data: Partial & { id: string }) => { + socketManager.game.onUpdatePlayer((data: Partial & { id: string }) => { if (data.id === this.myId) return; const target = this.players[data.id]; if (target && target instanceof RemotePlayer) { @@ -119,7 +119,7 @@ } }); - socketClient.onRemovePlayer((id: string) => { + socketManager.game.onRemovePlayer((id: string) => { const target = this.players[id]; if (target) { this.worldContainer.removeChild(target); @@ -128,7 +128,7 @@ } }); - socketClient.onUpdateMapCells((updates) => { + socketManager.game.onUpdateMapCells((updates) => { this.gameMap.updateCells(updates); }); } @@ -151,11 +151,11 @@ const now = performance.now(); if (now - this.lastPositionSentTime >= config.GAME_CONFIG.PLAYER_POSITION_UPDATE_MS) { - socketClient.sendMove(me.gridX, me.gridY); + socketManager.game.sendMove(me.gridX, me.gridY); this.lastPositionSentTime = now; } } else if (this.wasMoving) { - socketClient.sendMove(me.gridX, me.gridY); + socketManager.game.sendMove(me.gridX, me.gridY); } this.wasMoving = isMoving; @@ -182,6 +182,6 @@ this.players = {}; // イベント購読の解除 - socketClient.removeAllListeners(); + socketManager.game.removeAllListeners(); } } \ No newline at end of file