diff --git a/apps/client/src/app.tsx b/apps/client/src/app.tsx index 844e855..3e30c73 100644 --- a/apps/client/src/app.tsx +++ b/apps/client/src/app.tsx @@ -1,52 +1,78 @@ import { useEffect, useState, useRef } from "react"; import { Stage, Container } from "@pixi/react"; -// 👇 インポートパスを src ディレクトリ基準(./)に修正 +// ネットワーク・エンティティ・UIコンポーネントのインポート import { socketClient } from "./network/SocketClient"; -import { GameMap, MAP_SIZE } from "./entities/GameMap"; +import { GameMap } from "./entities/GameMap"; import { PlayerSprite } from "./entities/PlayerSprite"; import { VirtualJoystick, MAX_DIST } from "./input/VirtualJoystick"; +// シーン(画面)コンポーネント import { TitleScene } from "./scenes/TitleScene"; import { LobbyScene } from "./scenes/LobbyScene"; -// 👇 共有パッケージへのパスを src/app.tsx からの相対パスに修正 +// 共有設定(マップサイズやプレイヤーの当たり判定サイズなど) +import { GAME_CONFIG } from "@repo/shared/src/config/gameConfig"; import type { Room } from "@repo/shared/src/types/room"; +const { MAP_WIDTH, MAP_HEIGHT, PLAYER_RADIUS } = GAME_CONFIG; + +// プレイヤーの型定義 type Player = { id: string; x: number; y: number; color: string; }; -// 👇 変更: export default function App() に戻しました export default function App() { + /** + * 1. 状態管理 (State) + */ + // ゲームの現在のフェーズ(タイトル、ロビー、プレイ中) const [gameState, setGameState] = useState<"title" | "lobby" | "playing">("title"); const [room, setRoom] = useState(null); - const [myId, setMyId] = useState(null); const [players, setPlayers] = useState>({}); const [viewport, setViewport] = useState({ w: window.innerWidth, h: window.innerHeight }); - const myPosRef = useRef({ x: MAP_SIZE / 2, y: MAP_SIZE / 2 }); + /** + * 2. 参照管理 (Refs) + * 頻繁に更新される値(座標や入力)をStateに入れると再レンダリングが走りすぎるため、 + * useRef で保持して requestAnimationFrame 内で処理します。 + */ + const myPosRef = useRef({ x: MAP_WIDTH / 2, y: MAP_HEIGHT / 2 }); const joystickInputRef = useRef({ x: 0, y: 0 }); + // ジョイスティックからの入力をRefに保存 const handleJoystickMove = (moveX: number, moveY: number) => { joystickInputRef.current = { x: moveX, y: moveY }; }; + /** + * 3. ゲームループ (Game Loop) + * プレイ中のみ動作し、自身の移動計算とサーバーへの送信を行います。 + */ useEffect(() => { if (gameState !== "playing") return; let animationFrameId: number; const tick = () => { const { x: dx, y: dy } = joystickInputRef.current; + + // 入力がある場合のみ移動処理を実行 if (myId && (dx !== 0 || dy !== 0)) { const speed = 5.0; + // 入力値(0〜MAX_DIST)を正規化して座標を計算 let nextX = myPosRef.current.x + (dx / MAX_DIST) * speed; let nextY = myPosRef.current.y + (dy / MAX_DIST) * speed; - nextX = Math.max(20, Math.min(MAP_SIZE - 20, nextX)); - nextY = Math.max(20, Math.min(MAP_SIZE - 20, nextY)); + // マップの境界線から出ないように制限(クランプ処理) + nextX = Math.max(PLAYER_RADIUS, Math.min(MAP_WIDTH - PLAYER_RADIUS, nextX)); + nextY = Math.max(PLAYER_RADIUS, Math.min(MAP_HEIGHT - PLAYER_RADIUS, nextY)); + + // ローカルの座標情報を更新 myPosRef.current = { x: nextX, y: nextY }; + + // サーバーに最新の座標を送信 socketClient.sendMove(nextX, nextY); + // 自分の描画位置を即座に反映 setPlayers((prev) => { if (!prev[myId]) return prev; return { ...prev, [myId]: { ...prev[myId], x: nextX, y: nextY } }; @@ -59,25 +85,36 @@ return () => cancelAnimationFrame(animationFrameId); }, [myId, gameState]); + /** + * 4. ネットワークイベントの購読 + */ useEffect(() => { + // 接続時に自分のIDを保持 socketClient.onConnect((id) => setMyId(id)); + // ルビーの状態更新を受け取ったらロビー画面へ socketClient.onRoomUpdate((updatedRoom) => { setRoom(updatedRoom); setGameState("lobby"); }); + // ホストがゲームを開始した際の通知 socketClient.onGameStart(() => { setGameState("playing"); }); + // 初期プレイヤーリストの同期 socketClient.onCurrentPlayers((serverPlayers: any) => { const pMap: Record = {}; if (Array.isArray(serverPlayers)) serverPlayers.forEach((p) => (pMap[p.id] = p)); else Object.assign(pMap, serverPlayers); setPlayers(pMap); }); + + // 他プレイヤーの入室 socketClient.onNewPlayer((p: Player) => setPlayers((prev) => ({ ...prev, [p.id]: p }))); + + // 他プレイヤーの移動反映(自分自身の更新は Game Loop で行うため除外) socketClient.onUpdatePlayer((data: any) => { if (data.id === socketClient.socket.id) return; setPlayers((prev) => { @@ -85,6 +122,8 @@ return { ...prev, [data.id]: { ...prev[data.id], ...data } }; }); }); + + // 他プレイヤーの退室 socketClient.onRemovePlayer((id: string) => { setPlayers((prev) => { const next = { ...prev }; @@ -93,35 +132,51 @@ }); }); + // ウィンドウサイズ変更への対応 const handleResize = () => setViewport({ w: window.innerWidth, h: window.innerHeight }); window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, []); + /** + * 5. レンダリング分岐(シーン切り替え) + */ + // タイトル画面 if (gameState === "title") { return socketClient.joinRoom(roomId, playerName)} />; } + // ロビー画面 if (gameState === "lobby") { return socketClient.startGame()} />; } + /** + * 6. ゲーム本編の描画 + */ const me = myId ? players[myId] : null; - const camX = (me?.x || MAP_SIZE / 2) - viewport.w / 2; - const camY = (me?.y || MAP_SIZE / 2) - viewport.h / 2; + // カメラの座標計算:自分を中心に据えるよう計算(マップ端でも追従) + const camX = (me?.x ?? MAP_WIDTH / 2) - viewport.w / 2; + const camY = (me?.y ?? MAP_HEIGHT / 2) - viewport.h / 2; return (
+ {/* PixiJS の描画エリア */}
+ {/* コンテナをカメラの逆方向に動かすことで「カメラ追従」を実現 */} + {/* 背景マップ */} + {/* 全プレイヤーの描画 */} {Object.values(players).map((p) => ( ))}
+ + {/* UIレイヤー:ジョイスティック(Canvasの上に重ねて配置) */}
); diff --git a/apps/client/src/entities/GameMap.tsx b/apps/client/src/entities/GameMap.tsx index 1b9adf0..1462420 100644 --- a/apps/client/src/entities/GameMap.tsx +++ b/apps/client/src/entities/GameMap.tsx @@ -1,26 +1,31 @@ import { useCallback } from "react"; import { Graphics } from "@pixi/react"; - -// マップの広さ(他のファイルでも使えるように export する) -export const MAP_SIZE = 2000; +import { GAME_CONFIG } from "@repo/shared/src/config/gameConfig"; export const GameMap = () => { const draw = useCallback((g: any) => { + const { MAP_WIDTH, MAP_HEIGHT } = GAME_CONFIG; + g.clear(); + // 背景 g.beginFill(0x111111); - g.drawRect(0, 0, MAP_SIZE, MAP_SIZE); + g.drawRect(0, 0, MAP_WIDTH, MAP_HEIGHT); g.endFill(); // グリッド線 g.lineStyle(1, 0x333333); - for (let i = 0; i <= MAP_SIZE; i += 100) { - g.moveTo(i, 0).lineTo(i, MAP_SIZE); - g.moveTo(0, i).lineTo(MAP_SIZE, i); + // 垂直線 + for (let x = 0; x <= MAP_WIDTH; x += 100) { + g.moveTo(x, 0).lineTo(x, MAP_HEIGHT); + } + // 水平線 + for (let y = 0; y <= MAP_HEIGHT; y += 100) { + g.moveTo(0, y).lineTo(MAP_WIDTH, y); } // 外枠 g.lineStyle(5, 0xff4444); - g.drawRect(0, 0, MAP_SIZE, MAP_SIZE); + g.drawRect(0, 0, MAP_WIDTH, MAP_HEIGHT); }, []); return ;