diff --git a/apps/client/src/app.tsx b/apps/client/src/app.tsx index d9d20ac..e4393d1 100644 --- a/apps/client/src/app.tsx +++ b/apps/client/src/app.tsx @@ -1,17 +1,10 @@ -import { useEffect, useState, useRef } from "react"; -import { Application, Container } from "pixi.js"; - -// ネットワーク・入力 -import { socketClient, type PlayerData } from "./network/SocketClient"; -import { VirtualJoystick, MAX_DIST } from "./input/VirtualJoystick"; +import { useEffect, useState } from "react"; +import { socketClient } from "./network/SocketClient"; // シーン(画面)コンポーネント import { TitleScene } from "./scenes/TitleScene"; import { LobbyScene } from "./scenes/LobbyScene"; - -// ゲームオブジェクト(純粋なPixiJSクラス) -import { GameMap } from "./entities/GameMap"; -import { Player } from "./entities/Player"; +import { GameScene } from "./scenes/GameScene"; // 👈 追加 import type { Room } from "@repo/shared/src/types/room"; @@ -20,10 +13,6 @@ const [room, setRoom] = useState(null); const [myId, setMyId] = useState(null); - const pixiContainerRef = useRef(null); - const joystickInputRef = useRef({ x: 0, y: 0 }); - const playersRef = useRef>({}); - useEffect(() => { socketClient.onConnect((id) => setMyId(id)); socketClient.onRoomUpdate((updatedRoom) => { @@ -33,119 +22,15 @@ socketClient.onGameStart(() => setGameState("playing")); }, []); - /** - * ゲーム本体(PixiJSの実行環境) - */ - useEffect(() => { - if (gameState !== "playing" || !pixiContainerRef.current) return; + // レンダリング分岐 + if (gameState === "title") { + return socketClient.joinRoom(roomId, playerName)} />; + } + + if (gameState === "lobby") { + return socketClient.startGame()} />; + } - let isCancelled = false; - const app = new Application(); - 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[]; - playersArray.forEach((p) => { - const isMe = p.id === myId; - const playerSprite = new Player(p.color, isMe); - playerSprite.position.set(p.x, p.y); - worldContainer.addChild(playerSprite); - playersRef.current[p.id] = playerSprite; - }); - }); - - 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; - }); - - 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; - } - }); - - socketClient.onRemovePlayer((id: string) => { - const target = playersRef.current[id]; - if (target) { - worldContainer.removeChild(target); - target.destroy(); - delete playersRef.current[id]; - } - }); - - // データの受け取り口を作った後で、ゆっくりPixiを初期化する - await app.init({ resizeTo: window, backgroundColor: 0x111111, antialias: true }); - if (isCancelled) return; - - pixiContainerRef.current?.appendChild(app.canvas); - app.stage.addChild(worldContainer); - - // 🌟 【新規追加】 画面も受信設定もすべて完了したので、サーバーに「データちょうだい!」と要求する - socketClient.readyForGame(); - - // ゲームループ(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); - } - - // カメラ追従 - worldContainer.position.set( - -(me.x - window.innerWidth / 2), - -(me.y - window.innerHeight / 2) - ); - }); - }; - - initPixi(); - - // 🚨 【修正ポイント2】 クリーンアップ処理を追加 - return () => { - isCancelled = true; - 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"); - }; - }, [gameState, myId]); - - // レンダリング分岐(省略:TitleScene / LobbyScene) - if (gameState === "title") return socketClient.joinRoom(roomId, playerName)} />; - if (gameState === "lobby") return socketClient.startGame()} />; - - return ( -
-
-
- {/* ジョイスティックはReact UIとして管理 */} - { joystickInputRef.current = { x, y }; }} /> -
-
- ); + // playing 状態なら GameScene をレンダリング + return ; } \ No newline at end of file diff --git a/apps/client/src/scenes/GameScene.tsx b/apps/client/src/scenes/GameScene.tsx new file mode 100644 index 0000000..a09d602 --- /dev/null +++ b/apps/client/src/scenes/GameScene.tsx @@ -0,0 +1,138 @@ +import { useEffect, useRef } from "react"; +import { Application, Container } from "pixi.js"; + +// ネットワーク・入力 +import { socketClient, type PlayerData } from "../network/SocketClient"; +import { VirtualJoystick, MAX_DIST } from "../input/VirtualJoystick"; + +// ゲームオブジェクト +import { GameMap } from "../entities/GameMap"; +import { Player } from "../entities/Player"; + +interface GameSceneProps { + myId: string | null; +} + +export function GameScene({ myId }: GameSceneProps) { + const pixiContainerRef = useRef(null); + const joystickInputRef = useRef({ x: 0, y: 0 }); + const playersRef = useRef>({}); + + useEffect(() => { + if (!pixiContainerRef.current) return; + + let isCancelled = false; + let isInitialized = false; + const app = new Application(); + 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[]; + playersArray.forEach((p) => { + const isMe = p.id === myId; + const playerSprite = new Player(p.color, isMe); + playerSprite.position.set(p.x, p.y); + worldContainer.addChild(playerSprite); + playersRef.current[p.id] = playerSprite; + }); + }); + + 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; + }); + + 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; + } + }); + + socketClient.onRemovePlayer((id: string) => { + const target = playersRef.current[id]; + if (target) { + worldContainer.removeChild(target); + target.destroy(); + delete playersRef.current[id]; + } + }); + + // PixiJSの初期化 + await app.init({ resizeTo: window, backgroundColor: 0x111111, antialias: true }); + isInitialized = true; + + // 🚨 もし初期化を待っている間にReactがアンマウントされていたら、ここで破棄して終了する + if (isCancelled) { + app.destroy(true, { children: true }); + return; + } + + pixiContainerRef.current?.appendChild(app.canvas); + app.stage.addChild(worldContainer); + + // 受信設定完了後、サーバーにデータを要求 + socketClient.readyForGame(); + + // ゲームループ(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); + } + + // カメラ追従 + worldContainer.position.set( + -(me.x - window.innerWidth / 2), + -(me.y - window.innerHeight / 2) + ); + }); + }; + + 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 を依存配列に設定 + + return ( +
+
+
+ { joystickInputRef.current = { x, y }; }} /> +
+
+ ); +} \ No newline at end of file