diff --git a/apps/client/src/scenes/game/input/joystick/Joystick.tsx b/apps/client/src/scenes/game/input/joystick/Joystick.tsx
index 83a3692..691c5cc 100644
--- a/apps/client/src/scenes/game/input/joystick/Joystick.tsx
+++ b/apps/client/src/scenes/game/input/joystick/Joystick.tsx
@@ -1,13 +1,22 @@
+/**
+ * Joystick
+ * 画面上のジョイスティックUIの入口
+ * ポインターイベントを受け取り,useJoystick に処理を委譲し,描画は JoystickView に渡す
+ */
+import { JoystickView } from "./JoystickView";
import { MAX_DIST, useJoystick } from "./useJoystick";
+/** 入力半径の既定値を外部から参照できるように再公開 */
export { MAX_DIST } from "./useJoystick";
+/** Joystick コンポーネントの入力コールバック */
type Props = {
onInput: (moveX: number, moveY: number) => void;
};
+/** ポインター入力と描画を結びつけるジョイスティックUI */
export const Joystick = ({ onInput }: Props) => {
- const { isMoving, basePos, stickPos, handleStart, handleMove, handleEnd } =
+ const { isMoving, center, knobOffset, radius, handleStart, handleMove, handleEnd } =
useJoystick({ onInput });
return (
@@ -30,35 +39,13 @@
touchAction: "none",
}}
>
- {isMoving && (
-
- )}
+ {/* 見た目のみの描画(入力は扱わない) */}
+
);
};
\ No newline at end of file
diff --git a/apps/client/src/scenes/game/input/joystick/JoystickView.tsx b/apps/client/src/scenes/game/input/joystick/JoystickView.tsx
new file mode 100644
index 0000000..5ef2feb
--- /dev/null
+++ b/apps/client/src/scenes/game/input/joystick/JoystickView.tsx
@@ -0,0 +1,50 @@
+/**
+ * JoystickView
+ * ジョイスティックの見た目だけを描画するコンポーネント
+ * 入力処理は持たず,受け取った座標情報をもとにUIを描く
+ */
+type Point = { x: number; y: number };
+
+/** 表示に必要な座標と状態 */
+type Props = {
+ isActive: boolean;
+ center: Point;
+ knobOffset: Point;
+ radius: number;
+};
+
+/** UIの見た目だけを描画するビュー */
+export const JoystickView = ({ isActive, center, knobOffset, radius }: Props) => {
+ if (!isActive) return null;
+
+ // ベースリングとノブの描画
+ return (
+
+ );
+};
diff --git a/apps/client/src/scenes/game/input/joystick/useJoystick.ts b/apps/client/src/scenes/game/input/joystick/useJoystick.ts
index 3affe09..c8b0236 100644
--- a/apps/client/src/scenes/game/input/joystick/useJoystick.ts
+++ b/apps/client/src/scenes/game/input/joystick/useJoystick.ts
@@ -1,24 +1,35 @@
+/**
+ * useJoystick
+ * ジョイスティック入力を受け取り,座標計算と正規化ベクトルの出力を行うフック
+ * UI描画に必要な中心点・ノブ位置・半径も合わせて提供する
+ */
import { useCallback, useState } from "react";
import type React from "react";
+/** UI側と共有する最大半径の既定値 */
export const MAX_DIST = 60;
+/** フックに渡す入力コールバックと設定 */
type Props = {
onInput: (moveX: number, moveY: number) => void;
maxDist?: number;
};
+/** 2D座標の簡易型 */
type Point = { x: number; y: number };
+/** フックが返すUI向けの状態とハンドラ */
type UseJoystickReturn = {
isMoving: boolean;
- basePos: Point;
- stickPos: Point;
+ center: Point;
+ knobOffset: Point;
+ radius: number;
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];
@@ -29,49 +40,53 @@
return { x: e.clientX, y: e.clientY };
};
+/** 正規化ベクトルの出力とUI用の座標を提供するフック */
export const useJoystick = ({ onInput, 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 [center, setCenter] = useState({ x: 0, y: 0 });
+ const [knobOffset, setKnobOffset] = useState({ x: 0, y: 0 });
+ const radius = 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 });
+ setCenter(point);
+ setKnobOffset({ 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 dx = point.x - center.x;
+ const dy = point.y - center.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
- const limitedDist = Math.min(dist, limit);
+ const limitedDist = Math.min(dist, radius);
const offsetX = Math.cos(angle) * limitedDist;
const offsetY = Math.sin(angle) * limitedDist;
- const normalizedX = offsetX / limit;
- const normalizedY = offsetY / limit;
+ const normalizedX = offsetX / radius;
+ const normalizedY = offsetY / radius;
- setStickPos({ x: offsetX, y: offsetY });
+ setKnobOffset({ x: offsetX, y: offsetY });
onInput(normalizedX, normalizedY);
},
- [isMoving, basePos.x, basePos.y, limit, onInput]
+ [isMoving, center.x, center.y, radius, onInput]
);
+ // 入力終了時に状態をリセットして停止入力を通知する
const handleEnd = useCallback(() => {
setIsMoving(false);
- setStickPos({ x: 0, y: 0 });
+ setKnobOffset({ x: 0, y: 0 });
onInput(0, 0);
}, [onInput]);
- return { isMoving, basePos, stickPos, handleStart, handleMove, handleEnd };
+ return { isMoving, center, knobOffset, radius, handleStart, handleMove, handleEnd };
};