diff --git a/apps/client/src/scenes/game/GameManager.ts b/apps/client/src/scenes/game/GameManager.ts
index 5570080..30eaa9c 100644
--- a/apps/client/src/scenes/game/GameManager.ts
+++ b/apps/client/src/scenes/game/GameManager.ts
@@ -4,7 +4,7 @@
import type { playerTypes } from "@repo/shared";
import { BasePlayer, LocalPlayer, RemotePlayer } from "./entities/player/Player";
import { GameMap } from "./entities/map/GameMap";
-import { MAX_DIST } from "./input/joystick/VirtualJoystick";
+import { MAX_DIST } from "./input/joystick/Joystick";
export class GameManager {
private app: Application;
diff --git a/apps/client/src/scenes/game/GameScene.tsx b/apps/client/src/scenes/game/GameScene.tsx
index 410ad99..8bd7e4e 100644
--- a/apps/client/src/scenes/game/GameScene.tsx
+++ b/apps/client/src/scenes/game/GameScene.tsx
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from "react";
-import { VirtualJoystick } from "./input/joystick/VirtualJoystick";
+import { Joystick } from "./input/joystick/Joystick";
import { GameManager } from "./GameManager";
import { config } from "@repo/shared";
@@ -74,7 +74,7 @@
{/* UI 配置領域 */}
-
{
// ジョイスティックの入力を毎フレーム Manager に渡す
gameManagerRef.current?.setJoystickInput(x, y);
diff --git a/apps/client/src/scenes/game/input/joystick/Joystick.tsx b/apps/client/src/scenes/game/input/joystick/Joystick.tsx
new file mode 100644
index 0000000..b8d2ea3
--- /dev/null
+++ b/apps/client/src/scenes/game/input/joystick/Joystick.tsx
@@ -0,0 +1,64 @@
+import { MAX_DIST, useJoystick } from "./useJoystick";
+
+export { MAX_DIST } from "./useJoystick";
+
+type Props = {
+ onMove: (moveX: number, moveY: number) => void;
+};
+
+export const Joystick = ({ onMove }: Props) => {
+ const { isMoving, basePos, stickPos, handleStart, handleMove, handleEnd } =
+ useJoystick({ onMove });
+
+ return (
+
+ {isMoving && (
+
+ )}
+
+ );
+};
\ No newline at end of file
diff --git a/apps/client/src/scenes/game/input/joystick/VirtualJoystick.tsx b/apps/client/src/scenes/game/input/joystick/VirtualJoystick.tsx
deleted file mode 100644
index 90928a1..0000000
--- a/apps/client/src/scenes/game/input/joystick/VirtualJoystick.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-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 });
- // 距離制限後入力ベクトル通知
- onMove(moveX, moveY);
- };
-
- const handleEnd = () => {
- setIsMoving(false);
- setStickPos({ x: 0, y: 0 });
- // 入力終了時停止ベクトル通知
- onMove(0, 0);
- };
-
- return (
-
- {isMoving && (
-
- )}
-
- );
-};
\ No newline at end of file
diff --git a/apps/client/src/scenes/game/input/joystick/useJoystick.ts b/apps/client/src/scenes/game/input/joystick/useJoystick.ts
new file mode 100644
index 0000000..0ffd6bb
--- /dev/null
+++ b/apps/client/src/scenes/game/input/joystick/useJoystick.ts
@@ -0,0 +1,75 @@
+import { useCallback, useState } from "react";
+import type React from "react";
+
+export const MAX_DIST = 60;
+
+type Props = {
+ onMove: (moveX: number, moveY: number) => void;
+ maxDist?: number;
+};
+
+type Point = { x: number; y: number };
+
+type UseJoystickReturn = {
+ isMoving: boolean;
+ basePos: Point;
+ stickPos: Point;
+ handleStart: (e: React.TouchEvent | React.MouseEvent) => void;
+ handleMove: (e: React.TouchEvent | React.MouseEvent) => void;
+ handleEnd: () => void;
+};
+
+const getClientPoint = (e: React.TouchEvent | React.MouseEvent): Point | null => {
+ if ("touches" in e) {
+ const touch = e.touches[0];
+ if (!touch) return null;
+ return { x: touch.clientX, y: touch.clientY };
+ }
+
+ return { x: e.clientX, y: e.clientY };
+};
+
+export const useJoystick = ({ onMove, maxDist }: Props): UseJoystickReturn => {
+ const [isMoving, setIsMoving] = useState(false);
+ const [basePos, setBasePos] = useState({ x: 0, y: 0 });
+ const [stickPos, setStickPos] = useState({ x: 0, y: 0 });
+ const limit = maxDist ?? MAX_DIST;
+
+ const handleStart = useCallback((e: React.TouchEvent | React.MouseEvent) => {
+ const point = getClientPoint(e);
+ if (!point) return;
+
+ setBasePos(point);
+ setStickPos({ x: 0, y: 0 });
+ setIsMoving(true);
+ }, []);
+
+ const handleMove = useCallback(
+ (e: React.TouchEvent | React.MouseEvent) => {
+ if (!isMoving) return;
+ const point = getClientPoint(e);
+ if (!point) return;
+
+ const dx = point.x - basePos.x;
+ const dy = point.y - basePos.y;
+ const dist = Math.sqrt(dx * dx + dy * dy);
+ const angle = Math.atan2(dy, dx);
+
+ const limitedDist = Math.min(dist, limit);
+ const moveX = Math.cos(angle) * limitedDist;
+ const moveY = Math.sin(angle) * limitedDist;
+
+ setStickPos({ x: moveX, y: moveY });
+ onMove(moveX, moveY);
+ },
+ [isMoving, basePos.x, basePos.y, limit, onMove]
+ );
+
+ const handleEnd = useCallback(() => {
+ setIsMoving(false);
+ setStickPos({ x: 0, y: 0 });
+ onMove(0, 0);
+ }, [onMove]);
+
+ return { isMoving, basePos, stickPos, handleStart, handleMove, handleEnd };
+};