diff --git a/examples/minecraft/index.tsx b/examples/minecraft/index.tsx index 2d8ffd8b..99a3db79 100644 --- a/examples/minecraft/index.tsx +++ b/examples/minecraft/index.tsx @@ -1,6 +1,6 @@ import { createRoot } from 'react-dom/client' //@ts-ignore -import { App } from './src/App.jsx' +import { App } from './src/App.js' import { StrictMode } from 'react' createRoot(document.getElementById('root')!).render( diff --git a/examples/minecraft/package.json b/examples/minecraft/package.json index e18bb311..ef7048cd 100644 --- a/examples/minecraft/package.json +++ b/examples/minecraft/package.json @@ -1,10 +1,9 @@ { "dependencies": { - "@dimforge/rapier3d-compat": "^0.13.1", - "@react-three/drei": "^9.108.3", + "@react-three/drei": "^9.108.4", "@react-three/rapier": "^1.4.0", "@react-three/xr": "workspace:^", - "zustand": "^4.5.2" + "zustand": "^4.5.4" }, "type": "module", "scripts": { diff --git a/examples/minecraft/src/App.jsx b/examples/minecraft/src/App.tsx similarity index 89% rename from examples/minecraft/src/App.jsx rename to examples/minecraft/src/App.tsx index 7a61a4cc..99475554 100644 --- a/examples/minecraft/src/App.jsx +++ b/examples/minecraft/src/App.tsx @@ -1,6 +1,6 @@ import { Canvas } from '@react-three/fiber' import { Sky, PointerLockControls, KeyboardControls } from '@react-three/drei' -import { Physics } from '@react-three/rapier' +import { interactionGroups, Physics } from '@react-three/rapier' import { Ground } from './Ground.jsx' import { Player } from './Player.jsx' import { Cube, Cubes } from './Cube.jsx' @@ -51,9 +51,9 @@ export function App() { - + - + diff --git a/examples/minecraft/src/Axe.jsx b/examples/minecraft/src/Axe.tsx similarity index 63% rename from examples/minecraft/src/Axe.jsx rename to examples/minecraft/src/Axe.tsx index e457c288..72f3b881 100644 --- a/examples/minecraft/src/Axe.jsx +++ b/examples/minecraft/src/Axe.tsx @@ -7,14 +7,16 @@ title: Minecraft Diamond Axe */ import { useGLTF } from '@react-three/drei' +import { GroupProps } from '@react-three/fiber' +import { Mesh } from 'three' -export function Axe(props) { +export function Axe(props: GroupProps) { const { nodes, materials } = useGLTF('axe.glb') return ( - - + + ) diff --git a/examples/minecraft/src/Cube.jsx b/examples/minecraft/src/Cube.tsx similarity index 58% rename from examples/minecraft/src/Cube.jsx rename to examples/minecraft/src/Cube.tsx index c37da0df..47ca3bbc 100644 --- a/examples/minecraft/src/Cube.jsx +++ b/examples/minecraft/src/Cube.tsx @@ -1,33 +1,43 @@ import { useCallback, useRef, useState } from 'react' import { useTexture } from '@react-three/drei' -import { RigidBody } from '@react-three/rapier' -import create from 'zustand' +import { interactionGroups, RapierRigidBody, RigidBody, RigidBodyProps } from '@react-three/rapier' +import { create } from 'zustand' +import { Vector3Tuple } from 'three' +import { ThreeEvent } from '@react-three/fiber' // This is a naive implementation and wouldn't allow for more than a few thousand boxes. // In order to make this scale this has to be one instanced mesh, then it could easily be // hundreds of thousands. -const useCubeStore = create((set) => ({ - cubes: [], - addCube: (x, y, z) => set((state) => ({ cubes: [...state.cubes, [x, y, z]] })), -})) +const useCubeStore = create<{ cubes: Array; addCube: (x: number, y: number, z: number) => void }>( + (set) => ({ + cubes: [], + addCube: (x: number, y: number, z: number) => set((state) => ({ cubes: [...state.cubes, [x, y, z]] })), + }), +) export const Cubes = () => { const cubes = useCubeStore((state) => state.cubes) return cubes.map((coords, index) => ) } -export function Cube(props) { - const ref = useRef() - const [hover, set] = useState(null) +export function Cube(props: RigidBodyProps) { + const ref = useRef(null) + const [hover, set] = useState(null) const addCube = useCubeStore((state) => state.addCube) const texture = useTexture('dirt.jpg') - const onMove = useCallback((e) => { + const onMove = useCallback((e: ThreeEvent) => { + if (e.faceIndex == null) { + return + } e.stopPropagation() set(Math.floor(e.faceIndex / 2)) }, []) const onOut = useCallback(() => set(null), []) - const onClick = useCallback((e) => { + const onClick = useCallback((e: ThreeEvent) => { + if (ref.current == null || e.faceIndex == null) { + return + } e.stopPropagation() const { x, y, z } = ref.current.translation() const dir = [ @@ -37,11 +47,11 @@ export function Cube(props) { [x, y - 1, z], [x, y, z + 1], [x, y, z - 1], - ] + ] as const addCube(...dir[Math.floor(e.faceIndex / 2)]) }, []) return ( - + {[...Array(6)].map((_, index) => ( { - const { forward, backward, left, right, jump } = get() - const velocity = ref.current.linvel() - vectorHelper.set(velocity.x, velocity.y, velocity.z) - // update camera - const { x, y, z } = ref.current.translation() - state.camera.position.set(x, y, z) - // update axe - if (axe.current != null) { - axe.current.children[0].rotation.x = lerp( - axe.current.children[0].rotation.x, - Math.sin((vectorHelper.length() > 1) * state.clock.elapsedTime * 10) / 6, - 0.1, - ) - axe.current.rotation.copy(state.camera.rotation) - axe.current.position.copy(state.camera.position).add(state.camera.getWorldDirection(rotation).multiplyScalar(1)) - } // movement - frontVector.set(0, 0, backward - forward) - sideVector.set(left - right, 0, 0) - direction.subVectors(frontVector, sideVector).normalize().multiplyScalar(SPEED).applyEuler(state.camera.rotation) - ref.current.setLinvel({ x: direction.x, y: velocity.y, z: direction.z }) - // jumping - const world = rapier.world - // eslint-disable-next-line @react-three/no-new-in-loop - const ray = world.castRay(new RAPIER.Ray(ref.current.translation(), { x: 0, y: -1, z: 0 })) - const grounded = ray && ray.collider && Math.abs(ray.toi) <= 1.75 - if (jump && grounded) ref.current.setLinvel({ x: 0, y: 7.5, z: 0 }) - }) - return ( - <> - - - - - (axe.current.children[0].rotation.x = -0.5)}> - - - - - ) -} diff --git a/examples/minecraft/src/Player.tsx b/examples/minecraft/src/Player.tsx new file mode 100644 index 00000000..f833c814 --- /dev/null +++ b/examples/minecraft/src/Player.tsx @@ -0,0 +1,143 @@ +import * as THREE from 'three' +import { useRef } from 'react' +import { useFrame } from '@react-three/fiber' +import { useKeyboardControls } from '@react-three/drei' +import { CapsuleCollider, interactionGroups, RapierRigidBody, RigidBody, useRapier } from '@react-three/rapier' +import { IfInSessionMode } from '@react-three/xr' + +import { Axe } from './Axe.jsx' +import { VRPlayerControl } from './VRPlayerControl.jsx' + +const SPEED = 5 +const direction = new THREE.Vector3() +const frontVector = new THREE.Vector3() +const sideVector = new THREE.Vector3() +const rotation = new THREE.Vector3() + +const vectorHelper = new THREE.Vector3() + +export function Player({ lerp = THREE.MathUtils.lerp }) { + const axe = useRef(null) + const ref = useRef(null) + const { rapier, world } = useRapier() + const [, getKeys] = useKeyboardControls() + + const playerMove = ({ + forward, + backward, + left, + right, + rotation, + velocity, + }: { + forward: boolean + backward: boolean + left: boolean + right: boolean + rotation: THREE.Euler + velocity?: any + }) => { + if (!velocity) { + velocity = ref.current?.linvel() + } + + frontVector.set(0, 0, (backward ? 1 : 0) - (forward ? 1 : 0)) + sideVector.set((left ? 1 : 0) - (right ? 1 : 0), 0, 0) + direction.subVectors(frontVector, sideVector).normalize().multiplyScalar(SPEED).applyEuler(rotation) + ref.current?.setLinvel({ x: direction.x, y: velocity.y, z: direction.z }, true) + } + + const playerJump = () => { + if (ref.current == null) { + return + } + const ray = world.castRay( + new rapier.Ray(ref.current.translation(), { x: 0, y: -1, z: 0 }), + Infinity, + false, + undefined, + interactionGroups([1, 0], [1]), + ) + const grounded = ray != null && Math.abs(ray.timeOfImpact) <= 1.25 + + if (grounded) { + ref.current.setLinvel({ x: 0, y: 7.5, z: 0 }, true) + } + } + + useFrame((state) => { + if (ref.current == null) { + return + } + const { forward, backward, left, right, jump } = getKeys() + const velocity = ref.current.linvel() + + vectorHelper.set(velocity.x, velocity.y, velocity.z) + + // update camera + const { x, y, z } = ref.current.translation() + state.camera.position.set(x, y, z) + + // update axe + if (axe.current != null) { + axe.current.children[0].rotation.x = lerp( + axe.current.children[0].rotation.x, + Math.sin((vectorHelper.length() > 1 ? 1 : 0) * state.clock.elapsedTime * 10) / 6, + 0.1, + ) + axe.current.rotation.copy(state.camera.rotation) + axe.current.position.copy(state.camera.position).add(state.camera.getWorldDirection(rotation).multiplyScalar(1)) + } + + // movement + if (ref.current) { + playerMove({ + forward, + backward, + left, + right, + rotation: state.camera.rotation, + velocity, + }) + + if (jump) { + playerJump() + } + } + }) + + return ( + <> + + + + + + + + + + { + if (axe.current == null) { + return + } + axe.current.children[0].rotation.x = -0.5 + }} + > + + + + + ) +} diff --git a/examples/minecraft/src/VRPlayerControl.tsx b/examples/minecraft/src/VRPlayerControl.tsx new file mode 100644 index 00000000..6c092d2b --- /dev/null +++ b/examples/minecraft/src/VRPlayerControl.tsx @@ -0,0 +1,61 @@ +import * as THREE from 'three' +import { useRef } from 'react' +import { useFrame } from '@react-three/fiber' +import { useXRControllerState, XROrigin } from '@react-three/xr' + +const TURN_SPEED = 1.5, + THUMBSTICK_X_WIGGLE = 0.5 + +const helpers = { + euler: new THREE.Euler(), + quaternion: new THREE.Quaternion(), +} + +export function VRPlayerControl({ + playerJump, + playerMove, +}: { + playerJump?: () => void + playerMove: (params: { + forward: boolean + backward: boolean + left: boolean + right: boolean + rotation: THREE.Euler + }) => void +}) { + const originRef = useRef(null) + + const controllerLeft = useXRControllerState('right') + const controllerRight = useXRControllerState('left') + + useFrame((state, delta) => { + if (controllerRight != null) { + const thumbstick = controllerRight.gamepad?.['xr-standard-thumbstick'] + if (originRef.current != null && thumbstick?.xAxis != null && thumbstick.xAxis != 0) { + originRef.current.rotateY((thumbstick.xAxis < 0 ? 1 : -1) * TURN_SPEED * delta) + } + } + + if (controllerLeft?.gamepad?.['a-button']?.state === 'pressed') { + playerJump?.() + } + + const thumbstick = controllerLeft?.gamepad['xr-standard-thumbstick'] + if (thumbstick?.xAxis != null && thumbstick.yAxis != null) { + state.camera.getWorldQuaternion(helpers.quaternion) + + playerMove?.({ + forward: thumbstick.yAxis < 0, + backward: thumbstick.yAxis > 0, + left: thumbstick.xAxis < -THUMBSTICK_X_WIGGLE, + right: thumbstick.xAxis > THUMBSTICK_X_WIGGLE, + + // rotation: state.camera.rotation + rotation: helpers.euler.setFromQuaternion(helpers.quaternion), + }) + } + }) + + return +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8b2cd62..f19e2e8d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,12 +101,9 @@ importers: examples/minecraft: dependencies: - '@dimforge/rapier3d-compat': - specifier: ^0.13.1 - version: 0.13.1 '@react-three/drei': - specifier: ^9.108.3 - version: 9.108.3(@react-three/fiber@8.16.8)(@types/react@18.3.3)(@types/three@0.164.1)(react-dom@18.3.1)(react@18.3.1)(three@0.167.1) + specifier: ^9.108.4 + version: 9.109.2(@react-three/fiber@8.16.8)(@types/react@18.3.3)(@types/three@0.164.1)(react-dom@18.3.1)(react@18.3.1)(three@0.167.1) '@react-three/rapier': specifier: ^1.4.0 version: 1.4.0(@react-three/fiber@8.16.8)(react@18.3.1)(three@0.167.1) @@ -114,8 +111,8 @@ importers: specifier: workspace:^ version: link:../../packages/react/xr zustand: - specifier: ^4.5.2 - version: 4.5.2(@types/react@18.3.3)(react@18.3.1) + specifier: ^4.5.4 + version: 4.5.4(@types/react@18.3.3)(react@18.3.1) examples/miniature: dependencies: @@ -1407,7 +1404,7 @@ packages: prompts: 2.4.2 react: 18.3.1 zod: 3.23.8 - zustand: 4.5.2(@types/react@18.3.3)(react@18.3.1) + zustand: 4.5.4(@types/react@18.3.3)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer @@ -1432,7 +1429,7 @@ packages: prompts: 2.4.2 react: 18.3.1 zod: 3.23.8 - zustand: 4.5.2(@types/react@18.3.3)(react@18.3.1) + zustand: 4.5.4(@types/react@18.3.3)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer @@ -4974,7 +4971,7 @@ packages: /tunnel-rat@0.1.2(@types/react@18.3.3)(react@18.3.1): resolution: {integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==} dependencies: - zustand: 4.5.2(@types/react@18.3.3)(react@18.3.1) + zustand: 4.5.4(@types/react@18.3.3)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer @@ -5408,3 +5405,23 @@ packages: react: 18.3.1 use-sync-external-store: 1.2.0(react@18.3.1) dev: false + + /zustand@4.5.4(@types/react@18.3.3)(react@18.3.1): + resolution: {integrity: sha512-/BPMyLKJPtFEvVL0E9E9BTUM63MNyhPGlvxk1XjrfWTUlV+BR8jufjsovHzrtR6YNcBEcL7cMHovL1n9xHawEg==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + dependencies: + '@types/react': 18.3.3 + react: 18.3.1 + use-sync-external-store: 1.2.0(react@18.3.1) + dev: false