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 }; };