From 1210201802facc1ccdfcc6f1fb652fc5bb861ea6 Mon Sep 17 00:00:00 2001 From: "azykov@mail.ru" Date: Thu, 4 Jun 2026 14:55:19 +0300 Subject: [PATCH] fixed touchscreen controls stuck bug --- src/components/JoystickView.tsx | 99 ++++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 33 deletions(-) diff --git a/src/components/JoystickView.tsx b/src/components/JoystickView.tsx index 79f6af1..c0043e1 100644 --- a/src/components/JoystickView.tsx +++ b/src/components/JoystickView.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef } from "react"; import { create } from "nipplejs"; -import { DEFAULT_JOYSTICK_VALUES, joystickValues } from "../joystickInput"; +import { joystickValues } from "../joystickInput"; // const isTouch = navigator.maxTouchPoints > 0; const isTouch = true; // debug @@ -11,7 +11,7 @@ export function JoystickView() { const lookZoneRef = useRef(null); useEffect(() => { - + if (!isTouch) return; @@ -22,67 +22,100 @@ export function JoystickView() { if (!moveZone || !jumpBtn || !lookZone) return; - const moveJoystick = create({ - zone: moveZone, - mode: 'static', - position: { left: '80px', bottom: '80px' }, - color: 'white', - shape: 'square', - size: 100, - }); + let moveJoystick: ReturnType; + let lookJoystick: ReturnType; - const lookJoystick = create({ - zone: lookZone, - mode: 'dynamic', - dataOnly: true, - }); + const setup = () => { + moveJoystick = create({ + zone: moveZone, + mode: 'static', + position: { left: '80px', bottom: '80px' }, + color: 'white', + shape: 'circle', + size: 100, + }); - moveJoystick.on('move', (evt) => { - joystickValues.move.x = evt.data.vector.x; - joystickValues.move.y = evt.data.vector.y; - }); - moveJoystick.on('end', () => { + lookJoystick = create({ + zone: lookZone, + mode: 'dynamic', + dataOnly: true, + }); + + moveJoystick.on('move', (evt) => { + joystickValues.move.x = evt.data.vector.x; + joystickValues.move.y = evt.data.vector.y; + }); + moveJoystick.on('end', () => { + joystickValues.move = { x: 0, y: 0 }; + }); + + lookJoystick.on('move', (evt) => { + joystickValues.look = evt.data.vector; + }); + lookJoystick.on('end', () => { + joystickValues.look = { x: 0, y: 0 }; + }); + }; + + const teardown = () => { + moveJoystick?.destroy(); + lookJoystick?.destroy(); joystickValues.move = { x: 0, y: 0 }; - }); - - lookJoystick.on('move', (evt) => { - joystickValues.look = evt.data.vector; - }); - lookJoystick.on('end', () => { joystickValues.look = { x: 0, y: 0 }; - }); + joystickValues.jump = false; + }; + + setup(); const onJumpDown = () => { joystickValues.jump = true; }; const onJumpUp = () => { joystickValues.jump = false; }; jumpBtn.addEventListener('pointerdown', onJumpDown); jumpBtn.addEventListener('pointerup', onJumpUp); + jumpBtn.addEventListener('pointercancel', onJumpUp); jumpBtn.addEventListener('pointerleave', onJumpUp); + // When the OS interrupts a touch (app switch, notification, etc.) the browser + // fires pointercancel instead of pointerup, leaving nipplejs with a stuck active + // identifier. In dynamic mode this silently drops all subsequent touches on the + // look zone. Destroying and recreating resets nipplejs's internal touch tracking. + const onVisibilityChange = () => { + if (document.hidden) { + teardown(); + } else { + setup(); + } + }; + document.addEventListener('visibilitychange', onVisibilityChange); + return () => { - moveJoystick.destroy(); - lookJoystick.destroy(); + teardown(); jumpBtn.removeEventListener('pointerdown', onJumpDown); jumpBtn.removeEventListener('pointerup', onJumpUp); + jumpBtn.removeEventListener('pointercancel', onJumpUp); jumpBtn.removeEventListener('pointerleave', onJumpUp); - Object.assign(joystickValues, DEFAULT_JOYSTICK_VALUES); + document.removeEventListener('visibilitychange', onVisibilityChange); }; }, []); - if (!isTouch) return null; + if (!isTouch) + return null; return ( <>