diff options
author | 2025-02-10 01:29:10 -0500 | |
---|---|---|
committer | 2025-02-10 01:29:10 -0500 | |
commit | 55a0a3de6550f0142b79ab40645c20b465ddded8 (patch) | |
tree | cc767e6442ad71d7be782b0890726528107a41f0 | |
parent | 848d2b02064e2454edb85cd016e7b6840ab77e36 (diff) |
first person controls
-rw-r--r-- | assets/terrain.glb | bin | 0 -> 39846888 bytes | |||
-rw-r--r-- | package.json | 3 | ||||
-rw-r--r-- | public/vite.svg | 1 | ||||
-rw-r--r-- | src/App.jsx | 64 | ||||
-rw-r--r-- | src/components/chatbubble.jsx | 42 | ||||
-rw-r--r-- | src/components/ground.jsx | 17 | ||||
-rw-r--r-- | src/components/notes.jsx | 14 | ||||
-rw-r--r-- | src/components/player.jsx | 78 | ||||
-rw-r--r-- | src/main.jsx | 1 | ||||
-rw-r--r-- | src/style.scss | 41 | ||||
-rw-r--r-- | yarn.lock | 51 |
11 files changed, 284 insertions, 28 deletions
diff --git a/assets/terrain.glb b/assets/terrain.glb Binary files differnew file mode 100644 index 0000000..4ffde3c --- /dev/null +++ b/assets/terrain.glb diff --git a/package.json b/package.json index 3c4945c..94e77cb 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,10 @@ "dependencies": { "@react-three/drei": "^9.121.4", "@react-three/fiber": "^9.0.0-rc.7", + "@react-three/rapier": "^1.5.0", "@types/three": "^0.173.0", + "color": "^4.2.3", + "lodash.debounce": "^4.0.8", "react": "^19.0.0", "react-dom": "^19.0.0", "sass": "^1.84.0", diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
\ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index ebd5d36..25fe834 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,15 +1,63 @@ import './style.scss' -import { Canvas } from '@react-three/fiber'; -import ChatBubble from './components/chatbubble'; +import { Canvas, useFrame } from '@react-three/fiber'; +import Notes from './components/notes'; +import Player from './components/player'; +import Ground from './components/ground'; +import React, { Suspense, useContext, useEffect, useState } from 'react'; +import { Physics } from '@react-three/rapier'; + +export const AppContext = React.createContext(null); + +function KeyPressedClearer() { + const { setKeysPressed } = useContext(AppContext); + useFrame((_s, _d) => setKeysPressed([])); + return <></> +} function App() { + const [keys, setKeys] = useState([]); + const [keysPressed, setKeysPressed] = useState([]); + const playerKeyDown = (event) => { + setKeys(keys => { + if(!keys.includes(event.code)) { + setKeysPressed(keysPressed => [...keysPressed, event.code]); + } + return [...keys, event.code] + }); + } + const playerKeyUp = (event) => { + setKeys(keys => keys.filter(k => k != event.code)); + } + useEffect(() => { + window.addEventListener('keydown', playerKeyDown); + window.addEventListener('keyup', playerKeyUp); + return () => { + window.removeEventListener('keydown', playerKeyDown); + window.removeEventListener('keyup', playerKeyUp); + }; + }, []); return ( - <Canvas> - <ambientLight intensity={Math.PI / 2} /> - <spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} decay={0} intensity={Math.PI} /> - <pointLight position={[-10, -10, -10]} decay={0} intensity={Math.PI} /> - <ChatBubble position={[0, 0, 0]} /> - </Canvas> + <AppContext.Provider value={{ keys: keys, keysPressed: keysPressed, setKeysPressed: setKeysPressed }}> + <Canvas shadows> + <KeyPressedClearer /> + <Suspense> + <Physics timeStep={1/60}> + <directionalLight + castShadow + position={[100, 100, 100]} + lookAt={[0, 0, 0]} + intensity={Math.PI / 2} + shadow-mapSize-height={2048} + shadow-mapSize-width={2048} + /> + <ambientLight intensity={Math.PI / 4} /> + <Notes /> + <Player /> + <Ground /> + </Physics> + </Suspense> + </Canvas> + </AppContext.Provider> ) } diff --git a/src/components/chatbubble.jsx b/src/components/chatbubble.jsx index b3f4ad5..a1ef114 100644 --- a/src/components/chatbubble.jsx +++ b/src/components/chatbubble.jsx @@ -1,25 +1,51 @@ -import { useRef, useState } from 'react' +import { useContext, useRef, useState } from 'react' import * as everforest from '../_everforest.module.scss' import { useGLTF } from '@react-three/drei'; -import { useFrame } from '@react-three/fiber'; +import { useFrame, useThree } from '@react-three/fiber'; +import { Html } from '@react-three/drei'; +import { AppContext } from '../App'; +import Color from 'color'; +import { Vector3 } from 'three'; -export default function ChatBubble(props) { +export default function ChatBubble({ position, text }) { const meshRef = useRef(); const [hovered, setHovered] = useState(false); + const [activatable, setActivatable] = useState(false); const [active, setActive] = useState(false); - useFrame((_, delta) => (meshRef.current.rotation.y += delta)); - const {nodes} = useGLTF('../assets/message-bubble.glb'); + const { keysPressed } = useContext(AppContext); + const { camera } = useThree(); + useFrame((_, delta) => { + if (active) { + meshRef.current.rotation.y += delta; + } + if(hovered) { + let cameraPos = new Vector3(); + camera.getWorldPosition(cameraPos); + setActivatable(cameraPos.distanceToSquared(meshRef.current.position) < 9); + } else { + setActivatable(false); + } + if (keysPressed.includes('KeyE') && activatable) { + setActive(!active); + } + }); + const { nodes } = useGLTF('../assets/message-bubble.glb'); + let color = Color(active ? everforest.blue : everforest.orange); + if (activatable) { + color = color.lighten(.1); + } return ( <mesh - {...props} + position={position} ref={meshRef} scale={active ? 3 : 2} - onClick={(_) => setActive(!active)} geometry={nodes.Curve.geometry} + castShadow onPointerOver={(_) => setHovered(true)} onPointerOut={(_) => setHovered(false)}> - <meshStandardMaterial color={active ? everforest.blue : everforest.orange} /> + <meshStandardMaterial color={color.toString()} /> + {active && <Html center position={[0, .5, 0]} className='unselectable textPopup'><span>{text}</span></Html>} </mesh> ) } diff --git a/src/components/ground.jsx b/src/components/ground.jsx new file mode 100644 index 0000000..4042538 --- /dev/null +++ b/src/components/ground.jsx @@ -0,0 +1,17 @@ +import { useRef } from 'react'; +import * as everforest from '../_everforest.module.scss' +import { RigidBody } from '@react-three/rapier'; +import { DoubleSide } from 'three'; +import { useGLTF } from '@react-three/drei'; + +export default function Ground() { + const meshRef = useRef(); + const { nodes } = useGLTF('../assets/terrain.glb'); + return ( + <RigidBody type='fixed' colliders="trimesh"> + <mesh ref={meshRef} position={[0, 0, 0]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow geometry={nodes.Plane.geometry}> + <meshStandardMaterial color={everforest.yellow} side={DoubleSide}/> + </mesh> + </RigidBody> + ); +} diff --git a/src/components/notes.jsx b/src/components/notes.jsx new file mode 100644 index 0000000..0dd267a --- /dev/null +++ b/src/components/notes.jsx @@ -0,0 +1,14 @@ +import ChatBubble from "./chatbubble"; + +const chatbubbles = [ + {position: [0,0,-3], text: "ugh. really struggling with double bleeds in my ankles. makes it hard to do very much of anything, let alone focus for my hobbies"}, + {position: [-1,0,-5], text: "reddit is everywhere on google and i am sick of it... why can't there be a good forum site?"}, +]; + +export default function Notes() { + return (<> + {chatbubbles.map((chatbubble, index) => + <ChatBubble key={index} position={chatbubble.position} text={chatbubble.text}/> + )} + </>); +} diff --git a/src/components/player.jsx b/src/components/player.jsx new file mode 100644 index 0000000..bd8e392 --- /dev/null +++ b/src/components/player.jsx @@ -0,0 +1,78 @@ +import { Capsule, PerspectiveCamera, PointerLockControls } from "@react-three/drei"; +import { useFrame } from "@react-three/fiber"; +import { useContext, useEffect, useRef } from "react"; +import { AppContext } from "../App"; +import { CapsuleCollider, RapierCollider, RapierRigidBody, RigidBody, useRapier, vec3 } from "@react-three/rapier"; +import { quat } from "@react-three/rapier"; +import { Euler, Object3D, Vector3 } from "three"; +import { useBeforePhysicsStep } from "@react-three/rapier"; + +const _movespeed = 3.0; + +export default function Player() { + const controlsRef = useRef(); + const { keys } = useContext(AppContext); + const rapier = useRapier(); + const controller = useRef(); + const collider = useRef(); + const rigidbody = useRef(); + const camera = useRef(); + + const refState = useRef({ + grounded: false, + jumping: false, + velocity: vec3(), + }); + + useEffect(() => { + const c = rapier.world.createCharacterController(0.1); + c.setApplyImpulsesToDynamicBodies(true); + c.setCharacterMass(0.2); + controller.current = c; + }, [rapier]); + + useBeforePhysicsStep((world) => { + if (controller.current && rigidbody.current && collider.current) { + const move_axis_x = +(keys.includes('KeyD')) - +(keys.includes('KeyA')); + const move_axis_z = +(keys.includes('KeyW')) - +(keys.includes('KeyS')); + + const { velocity } = refState.current; + const position = vec3(rigidbody.current.translation()); + const movement = vec3(); + + const forward = new Vector3(); + camera.current.getWorldDirection(forward); + const left = new Vector3().crossVectors(forward, camera.current.up); + + movement.x += move_axis_z * world.timestep * _movespeed * forward.x; + movement.z += move_axis_z * world.timestep * _movespeed * forward.z; + movement.x += move_axis_x * world.timestep * _movespeed * left.x; + movement.z += move_axis_x * world.timestep * _movespeed * left.z; + + if (refState.current.grounded) { + velocity.y = 0; + } else { + velocity.y -= 9.81 * world.timestep * world.timestep; + } + + movement.add(velocity); + + controller.current.computeColliderMovement(collider.current, movement); + refState.current.grounded = controller.current.computedGrounded(); + + let correctedMovement = controller.current.computedMovement(); + position.add(vec3(correctedMovement)); + + rigidbody.current.setNextKinematicTranslation(position); + } + }); + + return ( + <RigidBody type="kinematicPosition" colliders={false} ref={rigidbody} position={[0, 2, 0]}> + <PerspectiveCamera makeDefault position={[0, .9, 0]} fov={90} ref={camera} /> + <PointerLockControls ref={controlsRef} /> + <CapsuleCollider ref={collider} args={[1, 0.5]} /> + </RigidBody> + ); +} + diff --git a/src/main.jsx b/src/main.jsx index 3d9da8a..97bc548 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -5,5 +5,6 @@ import App from './App.jsx' createRoot(document.getElementById('root')).render( <StrictMode> <App /> + <div className='dot'/> </StrictMode>, ) diff --git a/src/style.scss b/src/style.scss index 1bc4d97..23c3679 100644 --- a/src/style.scss +++ b/src/style.scss @@ -23,6 +23,17 @@ /// }}} +.dot { + position: absolute; + top: 50%; + left: 50%; + width: 5px; + height: 5px; + border-radius: 50%; + transform: translate3d(-50%, -50%, 0); + border: 2px solid white; +} + html, body, #root { @@ -39,15 +50,6 @@ html { height: 100%; } -/* body { */ -/* color: everforest.$fg; */ -/* background-color: everforest.$bg1; */ -/* margin: 0 auto; */ -/* max-width: 800px; */ -/* padding: 10px; */ -/* min-height: 100%; */ -/* } */ - a { color: everforest.$blue; } @@ -93,3 +95,24 @@ pre table { color: everforest.$orange; } } + +.textPopup { + /* text-align: center; */ + pointer-events: none; + background-color: everforest.$bg1; + color: everforest.$fg; + border-radius: 5px; + font-size: 15px; + width: 200px; + max-width: 50vw; + padding: 5px; +} + +.unselectable { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} @@ -171,6 +171,11 @@ "@babel/helper-string-parser" "^7.25.9" "@babel/helper-validator-identifier" "^7.25.9" +"@dimforge/rapier3d-compat@0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@dimforge/rapier3d-compat/-/rapier3d-compat-0.14.0.tgz#c6148f743aa99de320231527c466c1a48ada4c9f" + integrity sha512-/uHrUzS+CRQ+NQrrJCEDUkhwHlNsAAexbNXgbN9sHY+GwR+SFFAFrxRr8Llf5/AJZzqiLANdQIfJ63Cw4gJVqw== + "@esbuild/aix-ppc64@0.24.2": version "0.24.2" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz#38848d3e25afe842a7943643cbcd387cc6e13461" @@ -615,6 +620,15 @@ suspend-react "^0.1.3" zustand "^4.1.2" +"@react-three/rapier@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@react-three/rapier/-/rapier-1.5.0.tgz#f6af5b1dd6895a73df0d09e15576eed1f0398379" + integrity sha512-gylk2KyCer9EoymFyTyc+g2IqyAq4mTbZgaHoSJi6gHoXlJsC2LVeN4jedvegvjUsXPExdE60wHjCPa+DS4iXw== + dependencies: + "@dimforge/rapier3d-compat" "0.14.0" + suspend-react "^0.1.3" + three-stdlib "^2.29.4" + "@rollup/rollup-android-arm-eabi@4.34.6": version "4.34.6" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.6.tgz#9b726b4dcafb9332991e9ca49d54bafc71d9d87f" @@ -1073,11 +1087,27 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@~1.1.4: +color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-string@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" + integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" + integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== + dependencies: + color-convert "^2.0.1" + color-string "^1.9.0" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -1763,6 +1793,11 @@ is-array-buffer@^3.0.4, is-array-buffer@^3.0.5: call-bound "^1.0.3" get-intrinsic "^1.2.6" +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + is-async-function@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.1.1.tgz#3e69018c8e04e73b738793d020bfe884b9fd3523" @@ -2041,6 +2076,11 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -2545,6 +2585,13 @@ side-channel@^1.1.0: side-channel-map "^1.0.1" side-channel-weakmap "^1.0.2" +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== + dependencies: + is-arrayish "^0.3.1" + "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" @@ -2649,7 +2696,7 @@ three-mesh-bvh@^0.7.8: resolved "https://registry.yarnpkg.com/three-mesh-bvh/-/three-mesh-bvh-0.7.8.tgz#83156e4d3945734db076de1c94809331481b3fdd" integrity sha512-BGEZTOIC14U0XIRw3tO4jY7IjP7n7v24nv9JXS1CyeVRWOCkcOMhRnmENUjuV39gktAw4Ofhr0OvIAiTspQrrw== -three-stdlib@^2.35.6: +three-stdlib@^2.29.4, three-stdlib@^2.35.6: version "2.35.13" resolved "https://registry.yarnpkg.com/three-stdlib/-/three-stdlib-2.35.13.tgz#477fc5ffdef8a8923395ae2d7f26b0aeb6d60689" integrity sha512-AbXVObkM0OFCKX0r4VmHguGTdebiUQA+Yl+4VNta1wC158gwY86tCkjp2LFfmABtjYJhdK6aP13wlLtxZyLMAA== |