diff --git a/apps/client/src/scenes/GameScene.tsx b/apps/client/src/scenes/GameScene.tsx index a09d602..e6a3c6e 100644 --- a/apps/client/src/scenes/GameScene.tsx +++ b/apps/client/src/scenes/GameScene.tsx @@ -4,6 +4,7 @@ // ネットワーク・入力 import { socketClient, type PlayerData } from "../network/SocketClient"; import { VirtualJoystick, MAX_DIST } from "../input/VirtualJoystick"; +import { GAME_CONFIG } from "@repo/shared/src/config/gameConfig"; // ゲームオブジェクト import { GameMap } from "../entities/GameMap"; @@ -13,25 +14,33 @@ myId: string | null; } +/** + * メインのゲーム画面コンポーネント + * PixiJSの初期化、エンティティの管理、ソケット通信の同期を行う + */ export function GameScene({ myId }: GameSceneProps) { const pixiContainerRef = useRef(null); - const joystickInputRef = useRef({ x: 0, y: 0 }); - const playersRef = useRef>({}); + const joystickInputRef = useRef({ x: 0, y: 0 }); // ジョイスティックの入力値を保持 + const playersRef = useRef>({}); // 全プレイヤーのスプライト参照 + const lastPositionSentTimeRef = useRef(0); // サーバーへの位置送信タイミングを制御 + const targetPositionsRef = useRef>({}); // 他プレイヤーの目標位置(補完用) useEffect(() => { if (!pixiContainerRef.current) return; - let isCancelled = false; + let isCancelled = false; // 非同期処理中のクリーンアップ判定用 let isInitialized = false; const app = new Application(); - const worldContainer = new Container(); + const worldContainer = new Container(); // カメラ移動を実現するための親コンテナ const initPixi = async () => { // マップを一番下のレイヤー(背景)として追加 const gameMap = new GameMap(); worldContainer.addChild(gameMap); - // サーバーからのデータ受信設定 + // --- ソケットイベントリスナーの設定 --- + + // 接続時:既存の全プレイヤーを生成 socketClient.onCurrentPlayers((serverPlayers: PlayerData[] | Record) => { console.log("🔥 プレイヤー一覧を受信:", serverPlayers); const playersArray = (Array.isArray(serverPlayers) ? serverPlayers : Object.values(serverPlayers)) as PlayerData[]; @@ -41,36 +50,43 @@ playerSprite.position.set(p.x, p.y); worldContainer.addChild(playerSprite); playersRef.current[p.id] = playerSprite; + targetPositionsRef.current[p.id] = { x: p.x, y: p.y }; }); }); + // 新規参加:新しいプレイヤーを画面に追加 socketClient.onNewPlayer((p: PlayerData) => { console.log("🔥 新規プレイヤー参加:", p); const playerSprite = new Player(p.color, false); playerSprite.position.set(p.x, p.y); worldContainer.addChild(playerSprite); playersRef.current[p.id] = playerSprite; + targetPositionsRef.current[p.id] = { x: p.x, y: p.y }; }); + // 更新:他プレイヤーの移動目標地点を更新 socketClient.onUpdatePlayer((data: Partial & { id: string }) => { if (data.id === myId) return; - const target = playersRef.current[data.id]; - if (target) { - if (data.x !== undefined) target.x = data.x; - if (data.y !== undefined) target.y = data.y; + + const targetPos = targetPositionsRef.current[data.id]; + if (targetPos) { + if (data.x !== undefined) targetPos.x = data.x; + if (data.y !== undefined) targetPos.y = data.y; } }); + // 退出:プレイヤーを削除してメモリを解放 socketClient.onRemovePlayer((id: string) => { const target = playersRef.current[id]; if (target) { worldContainer.removeChild(target); target.destroy(); delete playersRef.current[id]; + delete targetPositionsRef.current[id]; } }); - // PixiJSの初期化 + // PixiJS本体の初期化 await app.init({ resizeTo: window, backgroundColor: 0x111111, antialias: true }); isInitialized = true; @@ -83,23 +99,40 @@ pixiContainerRef.current?.appendChild(app.canvas); app.stage.addChild(worldContainer); - // 受信設定完了後、サーバーにデータを要求 + // 全ての準備が整ったことをサーバーに通知 socketClient.readyForGame(); - // ゲームループ(Ticker) + // --- メインゲームループ(Ticker) --- app.ticker.add((ticker) => { if (!myId) return; const me = playersRef.current[myId]; if (!me) return; + // 自分の移動処理 const { x: dx, y: dy } = joystickInputRef.current; - if (dx !== 0 || dy !== 0) { me.move(dx / MAX_DIST, dy / MAX_DIST, ticker.deltaTime); - socketClient.sendMove(me.x, me.y); + + // 通信負荷軽減のため、一定間隔(20Hz等)でのみサーバーへ位置を送信 + const now = performance.now(); + if (now - lastPositionSentTimeRef.current >= GAME_CONFIG.PLAYER_POSITION_UPDATE_MS) { + socketClient.sendMove(me.x, me.y); + lastPositionSentTimeRef.current = now; + } } - // カメラ追従 + // 他プレイヤーの線形補間(Lerp)処理:カクつきを抑えて滑らかに移動させる + Object.entries(playersRef.current).forEach(([id, player]) => { + if (id === myId) return; + + const targetPos = targetPositionsRef.current[id]; + if (targetPos) { + player.x += (targetPos.x - player.x) * GAME_CONFIG.PLAYER_LERP_SMOOTHNESS * ticker.deltaTime; + player.y += (targetPos.y - player.y) * GAME_CONFIG.PLAYER_LERP_SMOOTHNESS * ticker.deltaTime; + } + }); + + // カメラ追従:自分を中心に世界を逆方向にずらす worldContainer.position.set( -(me.x - window.innerWidth / 2), -(me.y - window.innerHeight / 2) @@ -109,27 +142,30 @@ initPixi(); - // クリーンアップ処理 (アンマウント時に実行) + // クリーンアップ処理 (コンポーネントのアンマウント時に実行) return () => { isCancelled = true; if (isInitialized) { app.destroy(true, { children: true }); } - + playersRef.current = {}; - // ソケットの受信設定を解除 + // メモリリーク防止のためソケットのイベント登録を解除 socketClient.socket.off("current_players"); socketClient.socket.off("new_player"); socketClient.socket.off("update_player"); socketClient.socket.off("remove_player"); }; - }, [myId]); // myId を依存配列に設定 + }, [myId]); // 自分のIDが確定したタイミングで再実行 return (
+ {/* PixiJSのCanvasが挿入される要素 */}
+ + {/* UIレイヤー:ジョイスティック等 */}
{ joystickInputRef.current = { x, y }; }} />