diff --git a/apps/client/src/app.tsx b/apps/client/src/app.tsx index 4bcc634..3a5e6d4 100644 --- a/apps/client/src/app.tsx +++ b/apps/client/src/app.tsx @@ -1,10 +1,11 @@ -import { useEffect, useState, useRef, useCallback } from "react"; -import { io, Socket } from "socket.io-client"; -// ✅ v7 (安定版) の正しいインポート -import { Stage, Container, Graphics } from "@pixi/react"; +import { useEffect, useState, useRef } from "react"; +import { Stage, Container } from "@pixi/react"; -const MAP_SIZE = 2000; -const MAX_DIST = 60; // ジョイスティックの可動範囲 +import { socketClient } from "./network/SocketClient"; +import { GameMap, MAP_SIZE } from "./entities/GameMap"; +import { PlayerSprite } from "./entities/PlayerSprite"; +// ✅ 追加: 切り出したジョイスティックを読み込む +import { VirtualJoystick, MAX_DIST } from "./input/VirtualJoystick"; type Player = { id: string; @@ -13,62 +14,6 @@ color: string; }; -// 🎨 プレイヤー描画(v7対応) -const PlayerCharacter = ({ - x, - y, - color, - isMe, -}: { - x: number; - y: number; - color: string; - isMe: boolean; -}) => { - const draw = useCallback( - (g: any) => { - g.clear(); - const hexColor = parseInt(color.replace("#", "0x"), 16) || 0xff0000; - - // v7の書き方: beginFill を使う - g.beginFill(hexColor); - g.drawCircle(0, 0, 10); - g.endFill(); - - if (isMe) { - g.lineStyle(3, 0xffff00); - g.drawCircle(0, 0, 10); - } - }, - [color, isMe], - ); - - return ; -}; - -// 🗺️ 背景マップ描画(v7対応) -const GameMap = () => { - const draw = useCallback((g: any) => { - g.clear(); - g.beginFill(0x111111); - g.drawRect(0, 0, MAP_SIZE, MAP_SIZE); - 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); - } - - // 外枠 - g.lineStyle(5, 0xff4444); - g.drawRect(0, 0, MAP_SIZE, MAP_SIZE); - }, []); - - return ; -}; - export default function App() { const [myId, setMyId] = useState(null); const [players, setPlayers] = useState>({}); @@ -77,90 +22,53 @@ h: window.innerHeight, }); - // 自作ジョイスティックの状態 - const [isMoving, setIsMoving] = useState(false); - const [basePos, setBasePos] = useState({ x: 0, y: 0 }); - const [stickPos, setStickPos] = useState({ x: 0, y: 0 }); - const myPosRef = useRef({ x: MAP_SIZE / 2, y: MAP_SIZE / 2 }); - const socketRef = useRef(null); - // 🕹️ 操作開始 - const handleStart = (e: React.TouchEvent | React.MouseEvent) => { - const clientX = - "touches" in e ? e.touches[0].clientX : (e as React.MouseEvent).clientX; - const clientY = - "touches" in e ? e.touches[0].clientY : (e as React.MouseEvent).clientY; - setBasePos({ x: clientX, y: clientY }); - setStickPos({ x: 0, y: 0 }); - setIsMoving(true); - }; + // 🕹️ ジョイスティックから受け取った移動量で計算するだけ + const handleJoystickMove = (moveX: number, moveY: number) => { + if (!myId) return; + const speed = 5.0; + let nextX = myPosRef.current.x + (moveX / MAX_DIST) * speed; + let nextY = myPosRef.current.y + (moveY / MAX_DIST) * speed; - // 🕹️ 操作中 - const handleMove = (e: React.TouchEvent | React.MouseEvent) => { - if (!isMoving) return; - const clientX = - "touches" in e ? e.touches[0].clientX : (e as React.MouseEvent).clientX; - const clientY = - "touches" in e ? e.touches[0].clientY : (e as React.MouseEvent).clientY; + nextX = Math.max(20, Math.min(MAP_SIZE - 20, nextX)); + nextY = Math.max(20, Math.min(MAP_SIZE - 20, nextY)); - const dx = clientX - basePos.x; - const dy = clientY - basePos.y; - const dist = Math.sqrt(dx * dx + dy * dy); - const angle = Math.atan2(dy, dx); + myPosRef.current = { x: nextX, y: nextY }; + + // サーバーへ送信 + socketClient.sendMove(nextX, nextY); - const limitedDist = Math.min(dist, MAX_DIST); - const moveX = Math.cos(angle) * limitedDist; - const moveY = Math.sin(angle) * limitedDist; - - setStickPos({ x: moveX, y: moveY }); - - if (socketRef.current && myId) { - const speed = 5.0; - let nextX = myPosRef.current.x + (moveX / MAX_DIST) * speed; - let nextY = myPosRef.current.y + (moveY / MAX_DIST) * speed; - - nextX = Math.max(20, Math.min(MAP_SIZE - 20, nextX)); - nextY = Math.max(20, Math.min(MAP_SIZE - 20, nextY)); - - myPosRef.current = { x: nextX, y: nextY }; - - socketRef.current.emit("move", { x: nextX, y: nextY }); - setPlayers((prev) => { - if (!prev[myId]) return prev; - return { ...prev, [myId]: { ...prev[myId], x: nextX, y: nextY } }; - }); - } - }; - - const handleEnd = () => { - setIsMoving(false); - setStickPos({ x: 0, y: 0 }); + setPlayers((prev) => { + if (!prev[myId]) return prev; + return { ...prev, [myId]: { ...prev[myId], x: nextX, y: nextY } }; + }); }; useEffect(() => { - const socket = io(); - socketRef.current = socket; + socketClient.onConnect((id) => setMyId(id)); - socket.on("connect", () => setMyId(socket.id || null)); - socket.on("current_players", (serverPlayers: any) => { + 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); }); - socket.on("new_player", (p: Player) => - setPlayers((prev) => ({ ...prev, [p.id]: p })), - ); - socket.on("update_player", (data: any) => { - if (data.id === socket.id) return; + + socketClient.onNewPlayer((p: Player) => { + setPlayers((prev) => ({ ...prev, [p.id]: p })); + }); + + socketClient.onUpdatePlayer((data: any) => { + if (data.id === socketClient.socket.id) return; setPlayers((prev) => { if (!prev[data.id]) return prev; return { ...prev, [data.id]: { ...prev[data.id], ...data } }; }); }); - socket.on("remove_player", (id: string) => { + + socketClient.onRemovePlayer((id: string) => { setPlayers((prev) => { const next = { ...prev }; delete next[id]; @@ -168,13 +76,10 @@ }); }); - const handleResize = () => - setViewport({ w: window.innerWidth, h: window.innerHeight }); + const handleResize = () => setViewport({ w: window.innerWidth, h: window.innerHeight }); window.addEventListener("resize", handleResize); - return () => { - socket.disconnect(); - window.removeEventListener("resize", handleResize); - }; + + return () => window.removeEventListener("resize", handleResize); }, []); const me = myId ? players[myId] : null; @@ -183,23 +88,14 @@ return (
- {/* 🚀 Layer 1: PixiJS描画 (v7) */}
{Object.values(players).map((p) => ( -
- {/* 🕹️ Layer 2: ジョイスティックUI */} - {isMoving && ( -
-
-
- )} + {/* ✅ 複雑だった div タグの塊がたったの1行に! */} + +
); -} +} \ No newline at end of file diff --git a/apps/client/src/entities/GameMap.tsx b/apps/client/src/entities/GameMap.tsx new file mode 100644 index 0000000..1b9adf0 --- /dev/null +++ b/apps/client/src/entities/GameMap.tsx @@ -0,0 +1,27 @@ +import { useCallback } from "react"; +import { Graphics } from "@pixi/react"; + +// マップの広さ(他のファイルでも使えるように export する) +export const MAP_SIZE = 2000; + +export const GameMap = () => { + const draw = useCallback((g: any) => { + g.clear(); + g.beginFill(0x111111); + g.drawRect(0, 0, MAP_SIZE, MAP_SIZE); + 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); + } + + // 外枠 + g.lineStyle(5, 0xff4444); + g.drawRect(0, 0, MAP_SIZE, MAP_SIZE); + }, []); + + return ; +}; \ No newline at end of file diff --git a/apps/client/src/entities/PlayerSprite.tsx b/apps/client/src/entities/PlayerSprite.tsx new file mode 100644 index 0000000..956c630 --- /dev/null +++ b/apps/client/src/entities/PlayerSprite.tsx @@ -0,0 +1,32 @@ +import { useCallback } from "react"; +import { Graphics } from "@pixi/react"; + +// 受け取るデータの型を定義 +type PlayerProps = { + x: number; + y: number; + color: string; + isMe: boolean; +}; + +export const PlayerSprite = ({ x, y, color, isMe }: PlayerProps) => { + const draw = useCallback( + (g: any) => { + g.clear(); + const hexColor = parseInt(color.replace("#", "0x"), 16) || 0xff0000; + + g.beginFill(hexColor); + g.drawCircle(0, 0, 10); + g.endFill(); + + // 自分の場合は黄色い枠線をつける + if (isMe) { + g.lineStyle(3, 0xffff00); + g.drawCircle(0, 0, 10); + } + }, + [color, isMe] + ); + + return ; +}; \ No newline at end of file diff --git a/apps/client/src/input/VirtualJoystick.tsx b/apps/client/src/input/VirtualJoystick.tsx new file mode 100644 index 0000000..a90eda7 --- /dev/null +++ b/apps/client/src/input/VirtualJoystick.tsx @@ -0,0 +1,97 @@ +import { useState } from "react"; + +export const MAX_DIST = 60; // ジョイスティックの可動範囲 + +type Props = { + // スティックが動いたときのコールバック関数 + onMove: (moveX: number, moveY: number) => void; +}; + +export const VirtualJoystick = ({ onMove }: Props) => { + const [isMoving, setIsMoving] = useState(false); + const [basePos, setBasePos] = useState({ x: 0, y: 0 }); + const [stickPos, setStickPos] = useState({ x: 0, y: 0 }); + + const handleStart = (e: React.TouchEvent | React.MouseEvent) => { + const clientX = "touches" in e ? e.touches[0].clientX : (e as React.MouseEvent).clientX; + const clientY = "touches" in e ? e.touches[0].clientY : (e as React.MouseEvent).clientY; + setBasePos({ x: clientX, y: clientY }); + setStickPos({ x: 0, y: 0 }); + setIsMoving(true); + }; + + const handleMove = (e: React.TouchEvent | React.MouseEvent) => { + if (!isMoving) return; + const clientX = "touches" in e ? e.touches[0].clientX : (e as React.MouseEvent).clientX; + const clientY = "touches" in e ? e.touches[0].clientY : (e as React.MouseEvent).clientY; + + const dx = clientX - basePos.x; + const dy = clientY - basePos.y; + const dist = Math.sqrt(dx * dx + dy * dy); + const angle = Math.atan2(dy, dx); + + const limitedDist = Math.min(dist, MAX_DIST); + const moveX = Math.cos(angle) * limitedDist; + const moveY = Math.sin(angle) * limitedDist; + + setStickPos({ x: moveX, y: moveY }); + // app.tsx に計算結果だけを渡す + onMove(moveX, moveY); + }; + + const handleEnd = () => { + setIsMoving(false); + setStickPos({ x: 0, y: 0 }); + }; + + return ( +
+ {isMoving && ( +
+
+
+ )} +
+ ); +}; \ No newline at end of file diff --git a/apps/client/src/network/SocketClient.ts b/apps/client/src/network/SocketClient.ts new file mode 100644 index 0000000..21493e3 --- /dev/null +++ b/apps/client/src/network/SocketClient.ts @@ -0,0 +1,58 @@ +import { io, Socket } from "socket.io-client"; + +// app.tsx で定義されている型と同じものをこちらでも定義しておきます +export type PlayerData = { + id: string; + x: number; + y: number; + color: string; +}; + +export class SocketClient { + public socket: Socket; + + constructor() { + // app.tsx に合わせて引数なしの io() で接続します + this.socket = io(); + } + + // 1. 接続完了 + onConnect(callback: (id: string) => void) { + this.socket.on("connect", () => { + callback(this.socket.id || ""); + }); + } + + // 2. 初期プレイヤー一覧の受信 + onCurrentPlayers(callback: (players: any) => void) { + this.socket.on("current_players", callback); + } + + // 3. 新規プレイヤーの参加 + onNewPlayer(callback: (player: PlayerData) => void) { + this.socket.on("new_player", callback); + } + + // 4. 他プレイヤーの移動・更新 + onUpdatePlayer(callback: (data: any) => void) { + this.socket.on("update_player", callback); + } + + // 5. プレイヤーの退出 + onRemovePlayer(callback: (id: string) => void) { + this.socket.on("remove_player", callback); + } + + // 6. 自分の移動を送信 + sendMove(x: number, y: number) { + this.socket.emit("move", { x, y }); + } + + // 切断処理 + disconnect() { + this.socket.disconnect(); + } +} + +// どこからでも同じ接続を使えるようにインスタンス化してエクスポート +export const socketClient = new SocketClient(); \ No newline at end of file