diff --git a/package.json b/package.json index c0ab5f5..d9ba6fb 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "mobx": "^6.15.4", "mobx-react-lite": "^4.1.1", "mobx-utils": "^6.1.1", + "nipplejs": "^1.0.4", "npm": "^11.16.0", "react": "^19.2.6", "react-dom": "^19.2.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 230f488..bc737de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: mobx-utils: specifier: ^6.1.1 version: 6.1.1(mobx@6.15.4) + nipplejs: + specifier: ^1.0.4 + version: 1.0.4 npm: specifier: ^11.16.0 version: 11.16.0 @@ -1059,6 +1062,9 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + nipplejs@1.0.4: + resolution: {integrity: sha512-YtvRZDuyWQ+tOj0nUjfnZt4ZQmJVWfK9+yPmV5OXpQ4k4AkOGcSfKvSXbPNkV0ERqc4eakkBSZ7/Wyx1y6h/Sg==} + node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} @@ -2362,6 +2368,8 @@ snapshots: natural-compare@1.4.0: {} + nipplejs@1.0.4: {} + node-addon-api@7.1.1: optional: true diff --git a/src/components/CharacterView.tsx b/src/components/CharacterView.tsx index 33c6753..02aace4 100644 --- a/src/components/CharacterView.tsx +++ b/src/components/CharacterView.tsx @@ -6,13 +6,15 @@ import { useEffect, useRef } from "react"; import { Euler, Quaternion, Vector3 } from "three"; import { useFrame, useThree } from "@react-three/fiber"; import { useKeyboardControls } from "@react-three/drei"; -import { CuboidCollider, useRapier, useBeforePhysicsStep, type RapierRigidBody, type RapierCollider, type RapierContext } from "@react-three/rapier"; +import { CuboidCollider, useRapier, useBeforePhysicsStep, type RapierRigidBody, type RapierCollider, BallCollider, CapsuleCollider, RoundCuboidCollider } from "@react-three/rapier"; +import { joystickValues } from "../joystickInput"; const SPEED = 5; const JUMP_SPEED = 8; const GRAVITY = 20; const SENSITIVITY = 0.002; const SHOULDER_OFFSET = new Vector3(0.1, 1.5, 2.5); +const LOOK_RATE = 2000; // recreate private types type RapierWorldCreateCharacterControllerFunction = ReturnType['world']['createCharacterController']; @@ -86,20 +88,21 @@ export const CharacterView = observer(function ({ character, editMode }: Charact const { forward, backward, left, right, jump } = get(); const fwdX = -Math.sin(yaw); const fwdZ = -Math.cos(yaw); - const fwdScale = (forward ? 1 : 0) - (backward ? 1 : 0); - const rightScale = (right ? 1 : 0) - (left ? 1 : 0); + const fwdScale = Math.max(-1, Math.min(1, (forward ? 1 : 0) - (backward ? 1 : 0) + joystickValues.move.y)); + const rightScale = Math.max(-1, Math.min(1, (right ? 1 : 0) - (left ? 1 : 0) + joystickValues.move.x)); const vx = (fwdX * fwdScale - fwdZ * rightScale) * SPEED; const vz = (fwdZ * fwdScale + fwdX * rightScale) * SPEED; + const jumpInput = jump || joystickValues.jump; const isGrounded = controller.computedGrounded(); - if (jump && !jumpPressedRef.current && isGrounded) { + if (jumpInput && !jumpPressedRef.current && isGrounded) { vyRef.current = JUMP_SPEED; } else if (!isGrounded) { vyRef.current -= GRAVITY * dt; } else { vyRef.current = 0; } - jumpPressedRef.current = jump; + jumpPressedRef.current = jumpInput; controller.computeColliderMovement(collider, { x: vx * dt, y: vyRef.current * dt, z: vz * dt }); const corrected = controller.computedMovement(); @@ -114,13 +117,16 @@ export const CharacterView = observer(function ({ character, editMode }: Charact rb.setNextKinematicRotation({ x: _q.x, y: _q.y, z: _q.z, w: _q.w }); }); - useFrame(({ camera }) => { + useFrame(({ camera }, delta) => { if (editMode) return; const rb = rbRef.current; if (!rb) return; + mouseRef.current.x += joystickValues.look.x * LOOK_RATE * delta; + mouseRef.current.y += -joystickValues.look.y * LOOK_RATE * delta; + yawRef.current -= mouseRef.current.x * SENSITIVITY; - pitchRef.current = Math.max(-0.4, Math.min(0.6, pitchRef.current - mouseRef.current.y * SENSITIVITY)); + pitchRef.current = Math.max(-0.85, Math.min(0.5, pitchRef.current - mouseRef.current.y * SENSITIVITY)); mouseRef.current.x = 0; mouseRef.current.y = 0; @@ -140,7 +146,10 @@ export const CharacterView = observer(function ({ character, editMode }: Charact position={[pos[0] + 0.5, pos[1] + 0.5, pos[2] + 0.5]} onSync={() => { }} > - + {/* */} + {/* */} + {/* */} + diff --git a/src/components/JoystickView.tsx b/src/components/JoystickView.tsx new file mode 100644 index 0000000..79f6af1 --- /dev/null +++ b/src/components/JoystickView.tsx @@ -0,0 +1,110 @@ +import { useEffect, useRef } from "react"; +import { create } from "nipplejs"; +import { DEFAULT_JOYSTICK_VALUES, joystickValues } from "../joystickInput"; + +// const isTouch = navigator.maxTouchPoints > 0; +const isTouch = true; // debug + +export function JoystickView() { + const moveZoneRef = useRef(null); + const jumpBtnRef = useRef(null); + const lookZoneRef = useRef(null); + + useEffect(() => { + + if (!isTouch) + return; + + const moveZone = moveZoneRef.current; + const jumpBtn = jumpBtnRef.current; + const lookZone = lookZoneRef.current; + + if (!moveZone || !jumpBtn || !lookZone) + return; + + const moveJoystick = create({ + zone: moveZone, + mode: 'static', + position: { left: '80px', bottom: '80px' }, + color: 'white', + shape: 'square', + size: 100, + }); + + const 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 onJumpDown = () => { joystickValues.jump = true; }; + const onJumpUp = () => { joystickValues.jump = false; }; + + jumpBtn.addEventListener('pointerdown', onJumpDown); + jumpBtn.addEventListener('pointerup', onJumpUp); + jumpBtn.addEventListener('pointerleave', onJumpUp); + + return () => { + moveJoystick.destroy(); + lookJoystick.destroy(); + jumpBtn.removeEventListener('pointerdown', onJumpDown); + jumpBtn.removeEventListener('pointerup', onJumpUp); + jumpBtn.removeEventListener('pointerleave', onJumpUp); + Object.assign(joystickValues, DEFAULT_JOYSTICK_VALUES); + }; + }, []); + + if (!isTouch) return null; + + return ( + <> +
+
+
+ JUMP +
+ + ); +} diff --git a/src/components/Panel.tsx b/src/components/Panel.tsx index 9e40b48..00823be 100644 --- a/src/components/Panel.tsx +++ b/src/components/Panel.tsx @@ -10,6 +10,11 @@ export type PanelProps = { export const Panel = observer(function ({ side = 'left' }: PanelProps) { + const isGame = state.game && !state.game.isPaused; + + if (isGame) + return null; + function handleCloneTest1Object(): void { state.worldEditor.addObjectCloneAtRandomPosition('test1'); } diff --git a/src/components/ThreeView.tsx b/src/components/ThreeView.tsx index 2664a63..25630aa 100644 --- a/src/components/ThreeView.tsx +++ b/src/components/ThreeView.tsx @@ -24,6 +24,7 @@ import { observer } from 'mobx-react-lite'; import { state } from '../state'; import { GameView } from './GameView'; import { SceneEditorView } from './SceneEditorView'; +import { JoystickView } from './JoystickView'; import type { RefObject } from 'react'; const IconStop = () => ; @@ -49,6 +50,7 @@ export const ThreeView = observer(function () { {isGame ? : } + {isGame && }
{ state.game diff --git a/src/joystickInput.ts b/src/joystickInput.ts new file mode 100644 index 0000000..9f4664f --- /dev/null +++ b/src/joystickInput.ts @@ -0,0 +1,7 @@ +export const DEFAULT_JOYSTICK_VALUES = { + move: { x: 0, y: 0 }, + jump: false, + look: { x: 0, y: 0 }, +}; + +export const joystickValues = DEFAULT_JOYSTICK_VALUES;