- {/* 🚀 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