diff --git a/apps/client/src/scenes/game/input/joystick/JoystickInputPresenter.tsx b/apps/client/src/scenes/game/input/joystick/JoystickInputPresenter.tsx index e1db3b9..ad6d91f 100644 --- a/apps/client/src/scenes/game/input/joystick/JoystickInputPresenter.tsx +++ b/apps/client/src/scenes/game/input/joystick/JoystickInputPresenter.tsx @@ -8,24 +8,31 @@ import type { UseJoystickInputPresenterProps } from "./common"; /** 入力と表示状態の橋渡しを行う */ -export const JoystickInputPresenter = ({ onInput, maxDist }: UseJoystickInputPresenterProps) => { - const { isMoving, center, knobOffset, radius, handleStart, handleMove, handleEnd } = - useJoystickController({ onInput, maxDist }); +export const JoystickInputPresenter = ({ + onInput, + maxDist, +}: UseJoystickInputPresenterProps) => { + const { + isMoving, + center, + knobOffset, + radius, + handleStart, + handleMove, + handleEnd, + } = useJoystickController({ onInput, maxDist }); return (
{/* 入力イベントをコントローラーへ渡し,描画用状態をViewへ渡す */} - +
); }; diff --git a/apps/client/src/scenes/game/input/joystick/common/joystick.types.ts b/apps/client/src/scenes/game/input/joystick/common/joystick.types.ts index be5b9b9..b1c89db 100644 --- a/apps/client/src/scenes/game/input/joystick/common/joystick.types.ts +++ b/apps/client/src/scenes/game/input/joystick/common/joystick.types.ts @@ -4,7 +4,7 @@ * 座標や入力の共通表現を定義する */ -import type React from 'react'; +import type React from "react"; /** 2D座標の簡易型 */ export type Point = { x: number; y: number }; @@ -13,51 +13,51 @@ export type NormalizedInput = { x: number; y: number }; /** ジョイスティックで扱うポインター入力イベント型 */ -export type JoystickPointerEvent = React.TouchEvent | React.MouseEvent; +export type JoystickPointerEvent = React.PointerEvent; /** useJoystickState に渡す設定型 */ export type UseJoystickStateProps = { - maxDist?: number; + maxDist?: number; }; /** useJoystickState が返すUI向けの状態とハンドラ型 */ export type UseJoystickStateReturn = { - isMoving: boolean; - center: Point; - knobOffset: Point; - radius: number; - handleStart: (e: JoystickPointerEvent) => void; - handleMove: (e: JoystickPointerEvent) => NormalizedInput | null; - handleEnd: () => void; + isMoving: boolean; + center: Point; + knobOffset: Point; + radius: number; + handleStart: (e: JoystickPointerEvent) => void; + handleMove: (e: JoystickPointerEvent) => NormalizedInput | null; + handleEnd: (e: JoystickPointerEvent) => void; }; /** useJoystickController に渡す入力設定型 */ export type UseJoystickControllerProps = { - onInput: (moveX: number, moveY: number) => void; - maxDist?: number; + onInput: (moveX: number, moveY: number) => void; + maxDist?: number; }; /** useJoystickController が返す描画状態と入力ハンドラ型 */ export type UseJoystickControllerReturn = { - isMoving: boolean; - center: Point; - knobOffset: Point; - radius: number; - handleStart: (e: JoystickPointerEvent) => void; - handleMove: (e: JoystickPointerEvent) => void; - handleEnd: () => void; + isMoving: boolean; + center: Point; + knobOffset: Point; + radius: number; + handleStart: (e: JoystickPointerEvent) => void; + handleMove: (e: JoystickPointerEvent) => void; + handleEnd: (e: JoystickPointerEvent) => void; }; /** JoystickInputPresenter に渡す入力設定型 */ export type UseJoystickInputPresenterProps = { - onInput: (moveX: number, moveY: number) => void; - maxDist?: number; + onInput: (moveX: number, moveY: number) => void; + maxDist?: number; }; /** JoystickView に渡す描画状態型 */ export type UseJoystickViewProps = { - isActive: boolean; - center: Point; - knobOffset: Point; - radius: number; + isActive: boolean; + center: Point; + knobOffset: Point; + radius: number; }; diff --git a/apps/client/src/scenes/game/input/joystick/useJoystickController.ts b/apps/client/src/scenes/game/input/joystick/useJoystickController.ts index be2f033..e85fb02 100644 --- a/apps/client/src/scenes/game/input/joystick/useJoystickController.ts +++ b/apps/client/src/scenes/game/input/joystick/useJoystickController.ts @@ -3,16 +3,19 @@ * 入力イベントとジョイスティック計算結果の仲介を担うフック * useJoystickState の出力を受けて onInput 通知と終了時リセット通知を統一する */ -import { useCallback } from 'react'; -import { useRef } from 'react'; +import { useCallback } from "react"; +import { useRef } from "react"; import type { JoystickPointerEvent, NormalizedInput, UseJoystickControllerProps, UseJoystickControllerReturn, -} from './common'; -import { JOYSTICK_MIN_MOVEMENT_DELTA, JOYSTICK_SEND_ZERO_ON_END } from './common'; -import { useJoystickState } from './useJoystickState'; +} from "./common"; +import { + JOYSTICK_MIN_MOVEMENT_DELTA, + JOYSTICK_SEND_ZERO_ON_END, +} from "./common"; +import { useJoystickState } from "./useJoystickState"; /** 入力イベントと通知処理を仲介するフック */ export const useJoystickController = ({ @@ -35,7 +38,7 @@ (normalized: NormalizedInput) => { onInput(normalized.x, normalized.y); }, - [onInput] + [onInput], ); const handleMove = useCallback( @@ -54,20 +57,23 @@ emitInput(normalized); lastEmittedRef.current = normalized; }, - [baseHandleMove, emitInput] + [baseHandleMove, emitInput], ); - const handleEnd = useCallback(() => { - baseHandleEnd(); + const handleEnd = useCallback( + (e: JoystickPointerEvent) => { + baseHandleEnd(e); - if (JOYSTICK_SEND_ZERO_ON_END) { - emitInput({ x: 0, y: 0 }); - lastEmittedRef.current = { x: 0, y: 0 }; - return; - } + if (JOYSTICK_SEND_ZERO_ON_END) { + emitInput({ x: 0, y: 0 }); + lastEmittedRef.current = { x: 0, y: 0 }; + return; + } - lastEmittedRef.current = null; - }, [baseHandleEnd, emitInput]); + lastEmittedRef.current = null; + }, + [baseHandleEnd, emitInput], + ); return { isMoving, diff --git a/apps/client/src/scenes/game/input/joystick/useJoystickState.ts b/apps/client/src/scenes/game/input/joystick/useJoystickState.ts index d675876..b5199d9 100644 --- a/apps/client/src/scenes/game/input/joystick/useJoystickState.ts +++ b/apps/client/src/scenes/game/input/joystick/useJoystickState.ts @@ -3,40 +3,41 @@ * ジョイスティック入力状態の管理と入力ハンドラの提供を担うフック * UI描画に必要な中心点,ノブ位置,半径を保持する */ -import { useCallback, useState } from 'react'; -import { JOYSTICK_DEADZONE, MAX_DIST } from './common'; -import { computeJoystick } from './JoystickModel'; +import { useCallback, useRef, useState } from "react"; +import { JOYSTICK_DEADZONE, MAX_DIST } from "./common"; +import { computeJoystick } from "./JoystickModel"; import type { JoystickPointerEvent, Point, UseJoystickStateProps, UseJoystickStateReturn, -} from './common'; +} from "./common"; /** タッチとマウスからクライアント座標を共通化して取得する */ const getClientPoint = (e: JoystickPointerEvent): 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 useJoystickState = ({ maxDist }: UseJoystickStateProps): UseJoystickStateReturn => { +export const useJoystickState = ({ + maxDist, +}: UseJoystickStateProps): UseJoystickStateReturn => { const [isMoving, setIsMoving] = useState(false); const [center, setCenter] = useState({ x: 0, y: 0 }); const [knobOffset, setKnobOffset] = useState({ x: 0, y: 0 }); + const activePointerIdRef = useRef(null); const radius = maxDist ?? MAX_DIST; // 入力開始時の基準座標をセットする const handleStart = useCallback((e: JoystickPointerEvent) => { + if (activePointerIdRef.current !== null) return; + const point = getClientPoint(e); if (!point) return; if (point.x > window.innerWidth / 2) return; + activePointerIdRef.current = e.pointerId; + e.currentTarget.setPointerCapture(e.pointerId); setCenter(point); setKnobOffset({ x: 0, y: 0 }); setIsMoving(true); @@ -45,13 +46,16 @@ // 入力座標からベクトルを計算し,半径でクランプして正規化する const handleMove = useCallback( (e: JoystickPointerEvent) => { - if (!isMoving) return null; + if (!isMoving || activePointerIdRef.current !== e.pointerId) return null; const point = getClientPoint(e); if (!point) return null; const computed = computeJoystick(center, point, radius); - const magnitude = Math.hypot(computed.normalized.x, computed.normalized.y); + const magnitude = Math.hypot( + computed.normalized.x, + computed.normalized.y, + ); if (magnitude < JOYSTICK_DEADZONE) { setKnobOffset({ x: 0, y: 0 }); return { x: 0, y: 0 }; @@ -60,14 +64,29 @@ setKnobOffset(computed.knobOffset); return computed.normalized; }, - [isMoving, center.x, center.y, radius] + [isMoving, center.x, center.y, radius], ); // 入力終了時に状態をリセットする - const handleEnd = useCallback(() => { + const handleEnd = useCallback((e: JoystickPointerEvent) => { + if (activePointerIdRef.current !== e.pointerId) return; + + if (e.currentTarget.hasPointerCapture(e.pointerId)) { + e.currentTarget.releasePointerCapture(e.pointerId); + } + + activePointerIdRef.current = null; setIsMoving(false); setKnobOffset({ x: 0, y: 0 }); }, []); - return { isMoving, center, knobOffset, radius, handleStart, handleMove, handleEnd }; + return { + isMoving, + center, + knobOffset, + radius, + handleStart, + handleMove, + handleEnd, + }; };