From 7d22a4eee98ba2ec6c25d669ec5b9b74b65c6ab0 Mon Sep 17 00:00:00 2001 From: Florian Sylvain Date: Fri, 15 Mar 2024 02:36:00 +0100 Subject: [PATCH] clean: complete optimization of player movements --- src/App.tsx | 64 +++------- src/components/Cubes.tsx | 5 +- src/components/Player.tsx | 250 +++++++++++++++++++++++--------------- src/models/Animations.ts | 4 + 4 files changed, 177 insertions(+), 146 deletions(-) create mode 100644 src/models/Animations.ts diff --git a/src/App.tsx b/src/App.tsx index 7a3607f..9f1f7b7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,21 +2,15 @@ import "./App.css"; import { Canvas } from "@react-three/fiber"; import { KeyboardControls } from "@react-three/drei"; -import { MutableRefObject, useEffect, useRef, useState } from "react"; -import { Vector2, Vector3 } from "three"; -import { Cubes } from "./components/Cubes"; +import { useEffect, useRef, useState } from "react"; +import { Vector3 } from "three"; import { Player } from "./components/Player"; -import { - Bloom, - ChromaticAberration, - EffectComposer, - Vignette, -} from "@react-three/postprocessing"; -import { BlendFunction } from "postprocessing"; +import { Bloom, EffectComposer, Vignette } from "@react-three/postprocessing"; +import { Cubes } from "./components/Cubes"; + +const CUBES_QT = 10; function App() { - const CUBES_QT = 10; - const container = useRef(null); useEffect(() => { @@ -37,7 +31,7 @@ function App() { className="container" onClick={() => container.current?.requestPointerLock()} > - + - - + + - - } - > + @@ -74,37 +67,8 @@ function App() { {/* */} {/* */} - + - {/* */} - - - {/* */} diff --git a/src/components/Cubes.tsx b/src/components/Cubes.tsx index 07b343a..badff14 100644 --- a/src/components/Cubes.tsx +++ b/src/components/Cubes.tsx @@ -38,7 +38,10 @@ export function Cubes(props: { cubesPosition: Vector3[] }) { scale={[1, 1, 1]} > - + ); }); diff --git a/src/components/Player.tsx b/src/components/Player.tsx index 0666e2b..d441fcd 100644 --- a/src/components/Player.tsx +++ b/src/components/Player.tsx @@ -1,12 +1,6 @@ import { useAnimations, useGLTF, useKeyboardControls } from "@react-three/drei"; import { useFrame, useThree } from "@react-three/fiber"; -import { - MutableRefObject, - useCallback, - useEffect, - useRef, - useState, -} from "react"; +import { useEffect, useRef } from "react"; import { DirectionalLight, Euler, @@ -15,6 +9,7 @@ import { Vector2, Vector3, } from "three"; +import { AnimationState } from "../models/Animations"; export interface KeyPressed { moveForward: boolean; @@ -27,110 +22,175 @@ export interface KeyPressed { const PLAYER_SPEED = 4.75; const SHADOW_DRAW_DISTANCE = 30; +const MOUSE_X_SENSITIVITY = 0.002; +const MOUSE_Y_SENSITIVITY = 0.001; +const ANIM_SPEED = 0.2; +const CAMERA_RADIUS = 2; +const CAMERA_HEIGHT = 1.5; -export function Player(props: { - container: MutableRefObject; -}): JSX.Element { +export function Player(): JSX.Element { const model = useGLTF("/Adventurer.glb"); - const animations = useAnimations(model.animations, model.scene); - - // const walk = animations.actions["CharacterArmature|Walk"]; - const run = animations.actions["CharacterArmature|Run"]; - const idle = animations.actions["CharacterArmature|Idle"]; - + const anims = useAnimations(model.animations, model.scene); const [, getKeys] = useKeyboardControls(); - const [isWalking, setIsWalking] = useState(false); - - const [pivotX, setPivotX] = useState(0); - const [pivotY, setPivotY] = useState(0); - const [mouseRef, setMouseRef] = useState(new Vector2()); - const lightRef = useRef(null); - const camera = useThree((state) => state.camera); - const radius = 2; - const yOffset = 1.5; + const pivot = new Vector2(); + const moveVector = new Vector3(); + const moveEuler = new Euler(); + const targetRotation = new Quaternion(); + const targetRotationEuler = new Euler(); + const cameraRotationEuler = new Euler(); + const rotationQuaternion = new Quaternion(); + const rotationEuler = new Euler(); + const animsState: AnimationState[] = [ + { name: "CharacterArmature|Idle", isPlaying: true }, + { name: "CharacterArmature|Run", isPlaying: false }, + { name: "CharacterArmature|Run_Back", isPlaying: false }, + ]; + const lastAnimState = {} as AnimationState; + + const setAnimState = (name: string, isPlaying: boolean): void => { + const anim = animsState.find((anim) => anim.name === name); + if (!anim) return; + anim.isPlaying = isPlaying; + }; + + const getAnimState = (name: string): AnimationState => { + const anim = animsState.find((anim) => anim.name === name); + if (!anim) return { name: "", isPlaying: false }; + return anim; + }; + + const onMouseMove = (mouseEvent: MouseEvent): void => { + pivot.x = pivot.x - mouseEvent.movementX * MOUSE_X_SENSITIVITY; + pivot.y = Math.min( + Math.max(pivot.y + mouseEvent.movementY * MOUSE_Y_SENSITIVITY, -0.4), + 1.2 + ); + }; + + const someKeyPressed = (): boolean => + Object.values(getKeys()).some((key) => key); + + function setMoveVector(delta: number): void { + moveVector.set(0, 0, 0); + + const keys = getKeys(); + if (keys.moveLeft) moveVector.x += 1; + if (keys.moveRight) moveVector.x -= 1; + if (keys.moveForward) moveVector.z += 1; + if (keys.moveBackward) moveVector.z -= 1; + + moveVector.applyEuler(moveEuler.set(0, pivot.x - Math.PI, 0)); + moveVector.multiplyScalar(PLAYER_SPEED * delta); + } + + function setAnimationsState(): void { + const keys = getKeys(); + const isMovingSideways = keys.moveLeft || keys.moveRight; + const isMovingForward = keys.moveForward; + const isMovingBackward = keys.moveBackward; + const isIdle = !isMovingSideways && !isMovingForward && !isMovingBackward; + if (isMovingForward || (isMovingSideways && !isMovingBackward)) { + setAnimState("CharacterArmature|Idle", false); + setAnimState("CharacterArmature|Run", true); + setAnimState("CharacterArmature|Run_Back", false); + } else if (isMovingBackward) { + setAnimState("CharacterArmature|Idle", false); + setAnimState("CharacterArmature|Run", false); + setAnimState("CharacterArmature|Run_Back", true); + } else if (isIdle) { + setAnimState("CharacterArmature|Idle", true); + setAnimState("CharacterArmature|Run", false); + setAnimState("CharacterArmature|Run_Back", false); + } + } + + function setPlayerHeadRotationX(): void { + model.nodes?.Head.rotateX(pivot.y); + } + + function calculateTargetRotation(): void { + targetRotationEuler.y = pivot.x + Math.atan2(moveVector.x, moveVector.z); + targetRotation.setFromEuler(targetRotationEuler); + } + + function calculateCameraRotation(): void { + cameraRotationEuler.setFromQuaternion(camera.quaternion, "YXZ"); + rotationEuler.set(0, -cameraRotationEuler.y, 0); + } + + function applyCameraRotationToTargetRotation(): void { + rotationQuaternion.setFromEuler(rotationEuler); + targetRotation.multiplyQuaternions(targetRotation, rotationQuaternion); + } + + function rotateBackwards(): void { + rotationEuler.set(0, Math.PI, 0); + rotationQuaternion.setFromEuler(rotationEuler); + targetRotation.multiplyQuaternions(targetRotation, rotationQuaternion); + } + + function interpolatePlayerQuaternionToTargetRotation(): void { + model.scene.quaternion.slerp(targetRotation, 0.2); + } + + function setPlayerPosition(): void { + model.scene.position.add(moveVector); + calculateTargetRotation(); + calculateCameraRotation(); + applyCameraRotationToTargetRotation(); + if (getAnimState("CharacterArmature|Run_Back").isPlaying) { + rotateBackwards(); + } + interpolatePlayerQuaternionToTargetRotation(); + } + + function setCameraPosition(): void { + const player = model.scene; + camera.position.set( + CAMERA_RADIUS * Math.sin(pivot.x) * Math.cos(pivot.y) + player.position.x, + CAMERA_RADIUS * Math.sin(pivot.y) + player.position.y + CAMERA_HEIGHT, + CAMERA_RADIUS * Math.cos(pivot.x) * Math.cos(pivot.y) + player.position.z + ); + camera.lookAt( + player.position.x, + player.position.y + CAMERA_HEIGHT, + player.position.z + ); + } + + function setPlayerAnimations(): void { + animsState.forEach((anim) => { + if (lastAnimState.name !== anim.name && anim.isPlaying) { + anims.actions[lastAnimState.name]?.fadeOut(ANIM_SPEED); + anims.actions[anim.name]?.reset().fadeIn(ANIM_SPEED).play(); + lastAnimState.name = anim.name; + } + }); + } + + useFrame((_, delta) => { + setMoveVector(delta); + setAnimationsState(); + setPlayerHeadRotationX(); + if (someKeyPressed()) setPlayerPosition(); + setCameraPosition(); + setPlayerAnimations(); + }); useEffect(() => { - idle?.play(); model.scene.traverse((child) => { if (child instanceof Mesh) { child.castShadow = true; child.receiveShadow = true; } }); + window.addEventListener("mousemove", onMouseMove); + return () => window.removeEventListener("mousemove", onMouseMove); }); - const moveVector = new Vector3(); - const moveEuler = new Euler(); - const targetRotation = new Quaternion(); - const rotationEuler = new Euler(); - - useFrame((_, delta) => { - const player = model.scene.position; - moveVector.set(0, 0, 0); - - if (getKeys().moveForward) moveVector.z += 1; - if (getKeys().moveBackward) moveVector.z -= 1; - if (getKeys().moveLeft) moveVector.x += 1; - if (getKeys().moveRight) moveVector.x -= 1; - - moveVector.applyEuler(moveEuler.set(0, pivotX - Math.PI, 0)); - moveVector.multiplyScalar(PLAYER_SPEED * delta); - - model.nodes?.Head.rotateX(pivotY - model.nodes?.Head.rotation.x); - - if (Object.values(getKeys()).some((key) => key)) { - player.add(moveVector); - - targetRotation.setFromEuler(rotationEuler.set(0, pivotX - Math.PI, 0)); - model.scene.quaternion.slerp(targetRotation, 0.2); - } - - camera.position.x = radius * Math.sin(pivotX) * Math.cos(pivotY) + player.x; - camera.position.y = radius * Math.sin(pivotY) + player.y + yOffset; - camera.position.z = radius * Math.cos(pivotX) * Math.cos(pivotY) + player.z; - camera.lookAt(player.x, player.y + yOffset, player.z); - }); - - const onMouseMove = useCallback((event: MouseEvent) => { - setMouseRef(new Vector2(event.movementX, event.movementY)); - }, []); - - useEffect(() => { - props.container.current?.addEventListener("mousemove", onMouseMove); - return () => { - props.container.current?.removeEventListener("mousemove", onMouseMove); - }; - }, [onMouseMove, props.container]); - - useEffect(() => { - const MOUSE_X_SENSITIVITY = 0.005; - const MOUSE_Y_SENSITIVITY = 0.003; - setPivotX((pivotX) => pivotX - mouseRef.x * MOUSE_X_SENSITIVITY); - setPivotY((pivotY) => - Math.min(Math.max(pivotY + mouseRef.y * MOUSE_Y_SENSITIVITY, -0.2), 1.2) - ); - }, [mouseRef]); - - useEffect(() => { - const ANIM_SPEED = 0.2; - - if (isWalking) { - idle?.fadeOut(ANIM_SPEED); - run?.reset().fadeIn(ANIM_SPEED).play(); - } else { - run?.fadeOut(ANIM_SPEED); - idle?.reset().fadeIn(ANIM_SPEED).play(); - } - - return () => { - run?.fadeOut(ANIM_SPEED); - idle?.fadeOut(ANIM_SPEED); - }; - }, [isWalking, idle, run]); - return ( <>