touchscreen controls
This commit is contained in:
parent
8ef089c833
commit
c80b76e06e
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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<typeof useRapier>['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={() => { }}
|
||||
>
|
||||
<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>
|
||||
<mesh rotation={[-Math.PI / 2, -Math.PI / 4, 0]}>
|
||||
<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) {
|
||||
|
||||
const isGame = state.game && !state.game.isPaused;
|
||||
|
||||
if (isGame)
|
||||
return null;
|
||||
|
||||
function handleCloneTest1Object(): void {
|
||||
state.worldEditor.addObjectCloneAtRandomPosition('test1');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = () => <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 />
|
||||
{isGame ? <GameView /> : <SceneEditorView />}
|
||||
</Canvas>
|
||||
{isGame && <JoystickView />}
|
||||
<div style={{ position: 'absolute', top: 8, right: 8, display: 'flex', gap: 4 }}>
|
||||
{
|
||||
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