touchscreen controls
This commit is contained in:
parent
8ef089c833
commit
c80b76e06e
|
|
@ -18,6 +18,7 @@
|
||||||
"mobx": "^6.15.4",
|
"mobx": "^6.15.4",
|
||||||
"mobx-react-lite": "^4.1.1",
|
"mobx-react-lite": "^4.1.1",
|
||||||
"mobx-utils": "^6.1.1",
|
"mobx-utils": "^6.1.1",
|
||||||
|
"nipplejs": "^1.0.4",
|
||||||
"npm": "^11.16.0",
|
"npm": "^11.16.0",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ importers:
|
||||||
mobx-utils:
|
mobx-utils:
|
||||||
specifier: ^6.1.1
|
specifier: ^6.1.1
|
||||||
version: 6.1.1(mobx@6.15.4)
|
version: 6.1.1(mobx@6.15.4)
|
||||||
|
nipplejs:
|
||||||
|
specifier: ^1.0.4
|
||||||
|
version: 1.0.4
|
||||||
npm:
|
npm:
|
||||||
specifier: ^11.16.0
|
specifier: ^11.16.0
|
||||||
version: 11.16.0
|
version: 11.16.0
|
||||||
|
|
@ -1059,6 +1062,9 @@ packages:
|
||||||
natural-compare@1.4.0:
|
natural-compare@1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
|
|
||||||
|
nipplejs@1.0.4:
|
||||||
|
resolution: {integrity: sha512-YtvRZDuyWQ+tOj0nUjfnZt4ZQmJVWfK9+yPmV5OXpQ4k4AkOGcSfKvSXbPNkV0ERqc4eakkBSZ7/Wyx1y6h/Sg==}
|
||||||
|
|
||||||
node-addon-api@7.1.1:
|
node-addon-api@7.1.1:
|
||||||
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||||
|
|
||||||
|
|
@ -2362,6 +2368,8 @@ snapshots:
|
||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
|
|
||||||
|
nipplejs@1.0.4: {}
|
||||||
|
|
||||||
node-addon-api@7.1.1:
|
node-addon-api@7.1.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,15 @@ import { useEffect, useRef } from "react";
|
||||||
import { Euler, Quaternion, Vector3 } from "three";
|
import { Euler, Quaternion, Vector3 } from "three";
|
||||||
import { useFrame, useThree } from "@react-three/fiber";
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
import { useKeyboardControls } from "@react-three/drei";
|
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 SPEED = 5;
|
||||||
const JUMP_SPEED = 8;
|
const JUMP_SPEED = 8;
|
||||||
const GRAVITY = 20;
|
const GRAVITY = 20;
|
||||||
const SENSITIVITY = 0.002;
|
const SENSITIVITY = 0.002;
|
||||||
const SHOULDER_OFFSET = new Vector3(0.1, 1.5, 2.5);
|
const SHOULDER_OFFSET = new Vector3(0.1, 1.5, 2.5);
|
||||||
|
const LOOK_RATE = 2000;
|
||||||
|
|
||||||
// recreate private types
|
// recreate private types
|
||||||
type RapierWorldCreateCharacterControllerFunction = ReturnType<typeof useRapier>['world']['createCharacterController'];
|
type RapierWorldCreateCharacterControllerFunction = ReturnType<typeof useRapier>['world']['createCharacterController'];
|
||||||
|
|
@ -86,20 +88,21 @@ export const CharacterView = observer(function ({ character, editMode }: Charact
|
||||||
const { forward, backward, left, right, jump } = get();
|
const { forward, backward, left, right, jump } = get();
|
||||||
const fwdX = -Math.sin(yaw);
|
const fwdX = -Math.sin(yaw);
|
||||||
const fwdZ = -Math.cos(yaw);
|
const fwdZ = -Math.cos(yaw);
|
||||||
const fwdScale = (forward ? 1 : 0) - (backward ? 1 : 0);
|
const fwdScale = Math.max(-1, Math.min(1, (forward ? 1 : 0) - (backward ? 1 : 0) + joystickValues.move.y));
|
||||||
const rightScale = (right ? 1 : 0) - (left ? 1 : 0);
|
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 vx = (fwdX * fwdScale - fwdZ * rightScale) * SPEED;
|
||||||
const vz = (fwdZ * fwdScale + fwdX * rightScale) * SPEED;
|
const vz = (fwdZ * fwdScale + fwdX * rightScale) * SPEED;
|
||||||
|
|
||||||
|
const jumpInput = jump || joystickValues.jump;
|
||||||
const isGrounded = controller.computedGrounded();
|
const isGrounded = controller.computedGrounded();
|
||||||
if (jump && !jumpPressedRef.current && isGrounded) {
|
if (jumpInput && !jumpPressedRef.current && isGrounded) {
|
||||||
vyRef.current = JUMP_SPEED;
|
vyRef.current = JUMP_SPEED;
|
||||||
} else if (!isGrounded) {
|
} else if (!isGrounded) {
|
||||||
vyRef.current -= GRAVITY * dt;
|
vyRef.current -= GRAVITY * dt;
|
||||||
} else {
|
} else {
|
||||||
vyRef.current = 0;
|
vyRef.current = 0;
|
||||||
}
|
}
|
||||||
jumpPressedRef.current = jump;
|
jumpPressedRef.current = jumpInput;
|
||||||
|
|
||||||
controller.computeColliderMovement(collider, { x: vx * dt, y: vyRef.current * dt, z: vz * dt });
|
controller.computeColliderMovement(collider, { x: vx * dt, y: vyRef.current * dt, z: vz * dt });
|
||||||
const corrected = controller.computedMovement();
|
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 });
|
rb.setNextKinematicRotation({ x: _q.x, y: _q.y, z: _q.z, w: _q.w });
|
||||||
});
|
});
|
||||||
|
|
||||||
useFrame(({ camera }) => {
|
useFrame(({ camera }, delta) => {
|
||||||
if (editMode) return;
|
if (editMode) return;
|
||||||
const rb = rbRef.current;
|
const rb = rbRef.current;
|
||||||
if (!rb) return;
|
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;
|
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.x = 0;
|
||||||
mouseRef.current.y = 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]}
|
position={[pos[0] + 0.5, pos[1] + 0.5, pos[2] + 0.5]}
|
||||||
onSync={() => { }}
|
onSync={() => { }}
|
||||||
>
|
>
|
||||||
<CuboidCollider ref={colliderRef} args={[0.55, 0.4, 0.55]} />
|
{/* <BallCollider ref={colliderRef} args={[0.55]} /> */}
|
||||||
|
{/* <CapsuleCollider ref={colliderRef} args={[0.4, 0.5]} /> */}
|
||||||
|
{/* <CuboidCollider ref={colliderRef} args={[0.55, 0.4, 0.55]} /> */}
|
||||||
|
<RoundCuboidCollider ref={colliderRef} args={[0.4, 0.4, 0.4, 0.1]} />
|
||||||
<group>
|
<group>
|
||||||
<mesh rotation={[-Math.PI / 2, -Math.PI / 4, 0]}>
|
<mesh rotation={[-Math.PI / 2, -Math.PI / 4, 0]}>
|
||||||
<coneGeometry args={[0.55, 0.8, 4]} />
|
<coneGeometry args={[0.55, 0.8, 4]} />
|
||||||
|
|
|
||||||
|
|
@ -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<HTMLDivElement>(null);
|
||||||
|
const jumpBtnRef = useRef<HTMLDivElement>(null);
|
||||||
|
const lookZoneRef = useRef<HTMLDivElement>(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 (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
ref={moveZoneRef}
|
||||||
|
style={{ position: 'absolute', left: 0, bottom: 0, width: '50%', height: '45%', touchAction: 'none' }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
ref={lookZoneRef}
|
||||||
|
style={{ position: 'absolute', right: 0, bottom: 0, width: '50%', height: '45%', touchAction: 'none' }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
ref={jumpBtnRef}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: 30,
|
||||||
|
bottom: 30,
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: 'rgba(255,255,255,0.25)',
|
||||||
|
border: '2px solid rgba(255,255,255,0.6)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: 'white',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
touchAction: 'none',
|
||||||
|
userSelect: 'none',
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
JUMP
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,11 @@ export type PanelProps = {
|
||||||
|
|
||||||
export const Panel = observer(function ({ side = 'left' }: PanelProps) {
|
export const Panel = observer(function ({ side = 'left' }: PanelProps) {
|
||||||
|
|
||||||
|
const isGame = state.game && !state.game.isPaused;
|
||||||
|
|
||||||
|
if (isGame)
|
||||||
|
return null;
|
||||||
|
|
||||||
function handleCloneTest1Object(): void {
|
function handleCloneTest1Object(): void {
|
||||||
state.worldEditor.addObjectCloneAtRandomPosition('test1');
|
state.worldEditor.addObjectCloneAtRandomPosition('test1');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import { observer } from 'mobx-react-lite';
|
||||||
import { state } from '../state';
|
import { state } from '../state';
|
||||||
import { GameView } from './GameView';
|
import { GameView } from './GameView';
|
||||||
import { SceneEditorView } from './SceneEditorView';
|
import { SceneEditorView } from './SceneEditorView';
|
||||||
|
import { JoystickView } from './JoystickView';
|
||||||
import type { RefObject } from 'react';
|
import type { RefObject } from 'react';
|
||||||
|
|
||||||
const IconStop = () => <svg viewBox="0 0 14 14"><rect x="2" y="2" width="10" height="10" fill="currentColor" /></svg>;
|
const IconStop = () => <svg viewBox="0 0 14 14"><rect x="2" y="2" width="10" height="10" fill="currentColor" /></svg>;
|
||||||
|
|
@ -49,6 +50,7 @@ export const ThreeView = observer(function () {
|
||||||
<RenderInfoUpdater />
|
<RenderInfoUpdater />
|
||||||
{isGame ? <GameView /> : <SceneEditorView />}
|
{isGame ? <GameView /> : <SceneEditorView />}
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
{isGame && <JoystickView />}
|
||||||
<div style={{ position: 'absolute', top: 8, right: 8, display: 'flex', gap: 4 }}>
|
<div style={{ position: 'absolute', top: 8, right: 8, display: 'flex', gap: 4 }}>
|
||||||
{
|
{
|
||||||
state.game
|
state.game
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
Loading…
Reference in New Issue