feat:(game) added mobile controls

pull/8/head
Alex 2024-02-13 17:40:00 +01:00
parent 5cc900ab35
commit 6ed6c0ff27
9 changed files with 717 additions and 26 deletions

10
package-lock.json generated
View File

@ -18,6 +18,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-gamepad": "^1.0.3",
"react-joystick-component": "^6.2.1",
"three": "^0.160.1",
"three-mesh-bvh": "^0.7.0",
"zustand": "^4.5.0"
@ -2607,6 +2608,15 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-joystick-component": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/react-joystick-component/-/react-joystick-component-6.2.1.tgz",
"integrity": "sha512-0G5Y5aX4hNuXB3xJCwz6Q+nYQOtC6kprNGKmZxmfoPvhepNYUiid0DbLEGZxmr/UKip3S/LUbcQUobtRCuB8IQ==",
"peerDependencies": {
"react": ">=17.0.2",
"react-dom": ">=17.0.2"
}
},
"node_modules/react-merge-refs": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/react-merge-refs/-/react-merge-refs-1.1.0.tgz",

View File

@ -19,6 +19,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-gamepad": "^1.0.3",
"react-joystick-component": "^6.2.1",
"three": "^0.160.1",
"three-mesh-bvh": "^0.7.0",
"zustand": "^4.5.0"

View File

@ -63,7 +63,6 @@ function App() {
mode="concurrent"
onCreated={({ gl, camera }) => {
gl.toneMapping = THREE.AgXToneMapping
// gl.setClearColor(new THREE.Color('#020209'))
}}>
<Suspense fallback={null}>
<Preload all />

View File

@ -1,10 +1,11 @@
import React, { useEffect, useRef, useState } from "react";
import { useStore } from "./components/store";
import { Joystick } from "react-joystick-component";
export const HUD = () => {
const wheel = useRef();
const [image, setImage] = useState("");
const { item, gameStarted } = useStore();
const { item, gameStarted, actions } = useStore();
useEffect(() => {
const handleMouseMove = (e) => {
@ -26,6 +27,14 @@ export const HUD = () => {
};
}, []);
const handleMove = (e) => {
actions.setJoystickX(e.x);
};
const handleStop = () => {
actions.setJoystickX(0);
};
useEffect(() => {
switch (item) {
case "banana":
@ -55,6 +64,38 @@ export const HUD = () => {
</div>
</div>
</div>
<div className="controls joystick">
<Joystick
size={100}
sticky={false}
baseColor="rgba(255, 255, 255, 0.5)"
stickColor="rgba(255, 255, 255, 0.5)"
move={handleMove}
stop={handleStop}
></Joystick>
</div>
<div
className="controls drift"
onMouseDown={(e) => {
actions.setDriftButton(true);
}}
onMouseUp={(e) => {
actions.setDriftButton(false);
}}
>
drift
</div>
<div
className="controls itemButton"
onMouseDown={(e) => {
actions.setItemButton(true);
}}
onMouseUp={(e) => {
actions.setItemButton(false);
}}
>
item
</div>
</>
)}
</div>

View File

@ -9,7 +9,7 @@ export const Landing = () => {
const startButton = useRef();
const homeRef = useRef();
const [setupStatus, setSetupStatus] = useState(0);
const [controlStyle, setControlStyle] = useState("");
const [controlStyle, setControlStyle] = useState("touch");
useEffect(() => {
const tl = gsap.timeline();

View File

@ -9,6 +9,7 @@ import { Ground } from "./Ground";
import { PlayerController } from "./PlayerController";
import { PlayerControllerGamepad } from "./PlayerControllerGamepad";
import { PlayerControllerKeyboard } from "./PlayerControllerKeyboard";
import { PlayerControllerTouch } from "./PlayerControllerTouch";
import { Paris } from "./models/tracks/Tour_paris_promenade";
import {
EffectComposer,
@ -109,6 +110,8 @@ export const Experience = () => {
? PlayerControllerKeyboard
: controls === "gamepad"
? PlayerControllerGamepad
: controls === "touch"
? PlayerControllerTouch
: PlayerController;
return (

View File

@ -0,0 +1,586 @@
import { Controls } from "../App";
import { BallCollider, RigidBody, useRapier, vec3 } from "@react-three/rapier";
import {
useKeyboardControls,
PerspectiveCamera,
PositionalAudio,
} from "@react-three/drei";
import { useFrame, useThree, extend } from "@react-three/fiber";
import { useRef, useState, useEffect, useCallback } from "react";
import * as THREE from "three";
import { Mario } from "./models/characters/Mario_kart";
import { DriftParticlesLeft } from "./Particles/drifts/DriftParticlesLeft";
import { DriftParticlesRight } from "./Particles/drifts/DriftParticlesRight";
import { PointParticle } from "./Particles/drifts/PointParticle";
import { FlameParticles } from "./Particles/flames/FlameParticles";
import { useStore } from "./store";
import { Cylinder } from "@react-three/drei";
import FakeGlowMaterial from "./ShaderMaterials/FakeGlow/FakeGlowMaterial";
import { HitParticles } from "./Particles/hits/HitParticles";
import { CoinParticles } from "./Particles/coins/CoinParticles";
import { ItemParticles } from "./Particles/items/ItemParticles";
import { geometry } from "maath";
import { useGamepad } from "./useGamepad";
extend(geometry);
export const PlayerControllerTouch = ({
player,
userPlayer,
setNetworkBananas,
setNetworkShells,
networkBananas,
networkShells,
}) => {
const [isOnGround, setIsOnGround] = useState(false);
const body = useRef();
const kart = useRef();
const cam = useRef();
const initialSpeed = 0;
const maxSpeed = 30;
const boostSpeed = 50;
const acceleration = 0.1;
const decceleration = 0.2;
const damping = -0.1;
const MaxSteeringSpeed = 0.01;
const [currentSteeringSpeed, setCurrentSteeringSpeed] = useState(0);
const [currentSpeed, setCurrentSpeed] = useState(initialSpeed);
const camMaxOffset = 1;
let steeringAngle = 0;
const isOnFloor = useRef(false);
const jumpForce = useRef(0);
const jumpIsHeld = useRef(false);
const driftDirection = useRef(0);
const driftLeft = useRef(false);
const driftRight = useRef(false);
const driftForce = useRef(0);
const mario = useRef();
const accumulatedDriftPower = useRef(0);
const blueTurboThreshold = 10;
const orangeTurboThreshold = 30;
const purpleTurboThreshold = 60;
const [turboColor, setTurboColor] = useState(0xffffff);
const boostDuration = useRef(0);
const [isBoosting, setIsBoosting] = useState(false);
let targetXPosition = 0;
let targetZPosition = 8;
const [steeringAngleWheels, setSteeringAngleWheels] = useState(0);
const engineSound = useRef();
const driftSound = useRef();
const driftTwoSound = useRef();
const driftOrangeSound = useRef();
const driftPurpleSound = useRef();
const driftBlueSound = useRef();
const jumpSound = useRef();
const landingSound = useRef();
const turboSound = useRef();
const [scale, setScale] = useState(0);
const raycaster = new THREE.Raycaster();
const downDirection = new THREE.Vector3(0, -1, 0);
const [shouldLaunch, setShouldLaunch] = useState(false);
const effectiveBoost = useRef(0);
const text = useRef();
const { actions, shouldSlowDown, item, bananas, coins, id, controls, joystickX, driftButton, itemButton } = useStore();
const slowDownDuration = useRef(1500);
useFrame(({ pointer, clock }, delta) => {
if (player.id !== id) return;
const time = clock.getElapsedTime();
if (!body.current && !mario.current) return;
engineSound.current.setVolume(currentSpeed / 300 + 0.2);
engineSound.current.setPlaybackRate(currentSpeed / 10 + 0.1);
jumpSound.current.setPlaybackRate(1.5);
jumpSound.current.setVolume(0.5);
driftSound.current.setVolume(0.2);
driftBlueSound.current.setVolume(0.5);
driftOrangeSound.current.setVolume(0.6);
driftPurpleSound.current.setVolume(0.7);
// HANDLING AND STEERING
const kartRotation =
kart.current.rotation.y - driftDirection.current * driftForce.current;
const forwardDirection = new THREE.Vector3(
-Math.sin(kartRotation),
0,
-Math.cos(kartRotation)
);
// mouse steering
if (!driftLeft.current && !driftRight.current) {
steeringAngle = currentSteeringSpeed * -joystickX;
targetXPosition = -camMaxOffset * -joystickX;
} else if (driftLeft.current && !driftRight.current) {
steeringAngle = currentSteeringSpeed * -(joystickX - 1);
targetXPosition = -camMaxOffset * -joystickX;
} else if (driftRight.current && !driftLeft.current) {
steeringAngle = currentSteeringSpeed * -(joystickX + 1);
targetXPosition = -camMaxOffset * -joystickX;
}
// ACCELERATING
const shouldSlow = actions.getShouldSlowDown();
if ( currentSpeed < maxSpeed) {
// Accelerate the kart within the maximum speed limit
setCurrentSpeed(
Math.min(currentSpeed + acceleration * delta * 144, maxSpeed)
);
} else if (
currentSpeed > maxSpeed &&
effectiveBoost.current > 0
) {
setCurrentSpeed(
Math.max(currentSpeed - decceleration * delta * 144, maxSpeed)
);
}
if (currentSteeringSpeed < MaxSteeringSpeed) {
setCurrentSteeringSpeed(
Math.min(
currentSteeringSpeed + 0.0001 * delta * 144,
MaxSteeringSpeed
)
);
}
if (shouldSlow) {
setCurrentSpeed(
Math.max(currentSpeed - decceleration * 2 * delta * 144, 0)
);
setCurrentSteeringSpeed(0);
slowDownDuration.current -= 1500 * delta;
setShouldLaunch(true);
if (slowDownDuration.current <= 1) {
actions.setShouldSlowDown(false);
slowDownDuration.current = 1500;
setShouldLaunch(false);
}
}
// Update the kart's rotation based on the steering angle
kart.current.rotation.y += steeringAngle * delta * 144;
// Apply damping to simulate slowdown when no keys are pressed
body.current.applyImpulse(
{
x: -body.current.linvel().x * (1 - damping) * delta * 144,
y: 0,
z: -body.current.linvel().z * (1 - damping) * delta * 144,
},
true
);
const bodyPosition = body.current.translation();
kart.current.position.set(
bodyPosition.x,
bodyPosition.y - 0.5,
bodyPosition.z
);
// JUMPING
if (driftButton && isOnGround && !jumpIsHeld.current) {
jumpForce.current += 10;
isOnFloor.current = false;
jumpIsHeld.current = true;
jumpSound.current.play();
setIsOnGround(false);
if (jumpSound.current.isPlaying) {
jumpSound.current.stop();
jumpSound.current.play();
}
}
if (isOnFloor.current && jumpForce.current > 0) {
landingSound.current.play();
}
if (!isOnGround && jumpForce.current > 0) {
jumpForce.current -= 1 * delta * 144;
}
if (!driftButton) {
jumpIsHeld.current = false;
driftDirection.current = 0;
driftForce.current = 0;
driftLeft.current = false;
driftRight.current = false;
}
// DRIFTING
if (
jumpIsHeld.current &&
currentSteeringSpeed > 0 &&
joystickX < -0.1 &&
!driftRight.current
) {
driftLeft.current = true;
}
if (
jumpIsHeld.current &&
currentSteeringSpeed > 0 &&
joystickX > 0.1 &&
!driftLeft.current
) {
driftRight.current = true;
}
if (!jumpIsHeld.current && !driftLeft.current && !driftRight.current) {
mario.current.rotation.y = THREE.MathUtils.lerp(
mario.current.rotation.y,
0,
0.0001 * delta * 144
);
setTurboColor(0xffffff);
accumulatedDriftPower.current = 0;
driftSound.current.stop();
driftTwoSound.current.stop();
driftOrangeSound.current.stop();
driftPurpleSound.current.stop();
}
if (driftLeft.current) {
driftDirection.current = 1;
driftForce.current = 0.4;
mario.current.rotation.y = THREE.MathUtils.lerp(
mario.current.rotation.y,
steeringAngle * 25 + 0.4,
0.05 * delta * 144
);
accumulatedDriftPower.current += 0.1 * (steeringAngle + 1) * delta * 144;
}
if (driftRight.current) {
driftDirection.current = -1;
driftForce.current = 0.4;
mario.current.rotation.y = THREE.MathUtils.lerp(
mario.current.rotation.y,
-(-steeringAngle * 25 + 0.4),
0.05 * delta * 144
);
accumulatedDriftPower.current += 0.1 * (-steeringAngle + 1) * delta * 144;
}
if (!driftLeft.current && !driftRight.current) {
mario.current.rotation.y = THREE.MathUtils.lerp(
mario.current.rotation.y,
steeringAngle * 30,
0.05 * delta * 144
);
setScale(0);
}
if (accumulatedDriftPower.current > blueTurboThreshold) {
setTurboColor(0x00ffff);
boostDuration.current = 50;
driftBlueSound.current.play();
}
if (accumulatedDriftPower.current > orangeTurboThreshold) {
setTurboColor(0xffcf00);
boostDuration.current = 100;
driftBlueSound.current.stop();
driftOrangeSound.current.play();
}
if (accumulatedDriftPower.current > purpleTurboThreshold) {
setTurboColor(0xff00ff);
boostDuration.current = 250;
driftOrangeSound.current.stop();
driftPurpleSound.current.play();
}
if (driftLeft.current || driftRight.current) {
const oscillation = Math.sin(time * 1000) * 0.1;
const vibration = oscillation + 0.9;
if (turboColor === 0xffffff) {
setScale(vibration * 0.8);
} else {
setScale(vibration);
}
if (isOnFloor.current && !driftSound.current.isPlaying) {
driftSound.current.play();
driftTwoSound.current.play();
landingSound.current.play();
}
}
// RELEASING DRIFT
if (boostDuration.current > 1 && !jumpIsHeld.current) {
setIsBoosting(true);
effectiveBoost.current = boostDuration.current;
boostDuration.current = 0;
} else if (effectiveBoost.current <= 1) {
targetZPosition = 8;
setIsBoosting(false);
}
if (isBoosting && effectiveBoost.current > 1) {
setCurrentSpeed(boostSpeed);
effectiveBoost.current -= 1 * delta * 144;
targetZPosition = 10;
if (!turboSound.current.isPlaying) turboSound.current.play();
driftTwoSound.current.play();
driftBlueSound.current.stop();
driftOrangeSound.current.stop();
driftPurpleSound.current.stop();
} else if (effectiveBoost.current <= 1) {
setIsBoosting(false);
targetZPosition = 8;
turboSound.current.stop();
}
// CAMERA WORK
cam.current.updateMatrixWorld();
cam.current.position.x = THREE.MathUtils.lerp(
cam.current.position.x,
targetXPosition,
0.01 * delta * 144
);
cam.current.position.z = THREE.MathUtils.lerp(
cam.current.position.z,
targetZPosition,
0.01 * delta * 144
);
body.current.applyImpulse(
{
x: forwardDirection.x * currentSpeed * delta * 144,
y: 0 + jumpForce.current * delta * 144,
z: forwardDirection.z * currentSpeed * delta * 144,
},
true
);
// Update the kart's rotation based on the steering angle
setSteeringAngleWheels(steeringAngle * 25);
// SOUND WORK
// ITEMS
if (itemButton && item === "banana") {
const distanceBehind = 2;
const scaledBackwardDirection =
forwardDirection.multiplyScalar(distanceBehind);
const kartPosition = new THREE.Vector3(
...vec3(body.current.translation())
);
const bananaPosition = kartPosition.sub(scaledBackwardDirection);
const newBanana = {
id: Math.random() + "-" + +new Date(),
position: bananaPosition,
player: true,
};
setNetworkBananas([...networkBananas, newBanana]);
actions.useItem();
}
if (itemButton && item === "shell") {
const distanceBehind = -2;
const scaledBackwardDirection =
forwardDirection.multiplyScalar(distanceBehind);
const kartPosition = new THREE.Vector3(
body.current.translation().x,
body.current.translation().y,
body.current.translation().z
);
const shellPosition = kartPosition.sub(scaledBackwardDirection);
const newShell = {
id: Math.random() + "-" + +new Date(),
position: shellPosition,
player: true,
rotation: kartRotation,
};
setNetworkShells([...networkShells, newShell]);
actions.useItem();
}
if (itemButton && item === "mushroom") {
setIsBoosting(true);
effectiveBoost.current = 300;
actions.useItem();
}
player.setState("position", body.current.translation());
player.setState("rotation", kartRotation + mario.current.rotation.y);
player.setState("isBoosting", isBoosting);
player.setState("shouldLaunch", shouldLaunch);
player.setState("turboColor", turboColor);
player.setState("scale", scale);
player.setState("bananas", bananas);
});
return player.id === id ? (
<group>
<RigidBody
ref={body}
colliders={false}
position={[8, 60, -119]}
centerOfMass={[0, -1, 0]}
mass={3}
ccd
name="player"
type={player.id === id ? "dynamic" : "kinematic"}
>
<BallCollider
args={[0.5]}
mass={3}
onCollisionEnter={({ other }) => {
isOnFloor.current = true;
setIsOnGround(true);
}}
onCollisionExit={({ other }) => {
isOnFloor.current = false;
setIsOnGround(false);
}}
/>
</RigidBody>
<group ref={kart} rotation={[0, Math.PI / 2, 0]}>
<group ref={mario}>
<Mario
currentSpeed={currentSpeed}
steeringAngleWheels={steeringAngleWheels}
isBoosting={isBoosting}
shouldLaunch={shouldLaunch}
/>
<CoinParticles coins={coins} />
<ItemParticles item={item} />
<mesh position={[0.6, 0.05, 0.5]} scale={scale}>
<sphereGeometry args={[0.05, 16, 16]} />
<meshStandardMaterial
emissive={turboColor}
toneMapped={false}
emissiveIntensity={100}
transparent
opacity={0.4}
/>
</mesh>
<mesh position={[0.6, 0.05, 0.5]} scale={scale * 10}>
<sphereGeometry args={[0.05, 16, 16]} />
<FakeGlowMaterial
falloff={3}
glowInternalRadius={1}
glowColor={turboColor}
glowSharpness={1}
/>
</mesh>
<mesh position={[-0.6, 0.05, 0.5]} scale={scale}>
<sphereGeometry args={[0.05, 16, 16]} />
<meshStandardMaterial
emissive={turboColor}
toneMapped={false}
emissiveIntensity={100}
transparent
opacity={0.4}
/>
</mesh>
<mesh position={[-0.6, 0.05, 0.5]} scale={scale * 10}>
<sphereGeometry args={[0.05, 16, 16]} />
<FakeGlowMaterial
falloff={3}
glowInternalRadius={1}
glowColor={turboColor}
glowSharpness={1}
/>
</mesh>
{/* <FlameParticles isBoosting={isBoosting} /> */}
<DriftParticlesLeft turboColor={turboColor} scale={scale} />
<DriftParticlesRight turboColor={turboColor} scale={scale} />
<PointParticle
position={[-0.6, 0.05, 0.5]}
png="./particles/circle.png"
turboColor={turboColor}
/>
<PointParticle
position={[0.6, 0.05, 0.5]}
png="./particles/circle.png"
turboColor={turboColor}
/>
<PointParticle
position={[-0.6, 0.05, 0.5]}
png="./particles/star.png"
turboColor={turboColor}
/>
<PointParticle
position={[0.6, 0.05, 0.5]}
png="./particles/star.png"
turboColor={turboColor}
/>
<HitParticles shouldLaunch={shouldLaunch} />
</group>
{/* <ContactShadows frames={1} /> */}
<PerspectiveCamera
makeDefault
position={[0, 2, 8]}
fov={50}
ref={cam}
far={5000}
/>
<PositionalAudio
ref={engineSound}
url="./sounds/engine.wav"
autoplay
loop
distance={1000}
/>
<PositionalAudio
ref={driftSound}
url="./sounds/drifting.mp3"
loop
distance={1000}
/>
<PositionalAudio
ref={driftTwoSound}
url="./sounds/driftingTwo.mp3"
loop
distance={1000}
/>
<PositionalAudio
ref={driftOrangeSound}
url="./sounds/driftOrange.wav"
loop={false}
distance={1000}
/>
<PositionalAudio
ref={driftBlueSound}
url="./sounds/driftBlue.wav"
loop={false}
distance={1000}
/>
<PositionalAudio
ref={driftPurpleSound}
url="./sounds/driftPurple.wav"
loop={false}
distance={1000}
/>
<PositionalAudio
ref={jumpSound}
url="./sounds/jump.mp3"
loop={false}
distance={1000}
/>
<PositionalAudio
ref={landingSound}
url="./sounds/landing.wav"
loop={false}
distance={1000}
/>
<PositionalAudio
ref={turboSound}
url="./sounds/turbo.wav"
loop={false}
distance={1000}
/>
</group>
</group>
) : null;
};

View File

@ -14,7 +14,7 @@ export const items = [
export const useStore = create((set, get) => ({
gameStarted: false,
controls: "",
controls: "touch",
particles1: [],
particles2: [],
bodyPosition: [0, 0, 0],
@ -29,6 +29,9 @@ export const useStore = create((set, get) => ({
coins : 0,
players : [],
id : "",
joystickX: 0,
driftButton: false,
itemButton: false,
addPastPosition: (position) => {
set((state) => ({
pastPositions: [position, ...state.pastPositions.slice(0, 499)],
@ -147,8 +150,16 @@ export const useStore = create((set, get) => ({
},
setControls: (controls) => {
set({ controls });
}
},
setJoystickX: (joystickX) => {
set({ joystickX });
},
setDriftButton: (driftButton) => {
set({ driftButton });
},
setItemButton: (itemButton) => {
set({ itemButton });
},
},
}));

View File

@ -1,4 +1,4 @@
@import url('https://fonts.googleapis.com/css2?family=Hanken+Grotesk:ital,wght@0,100..900;1,100..900&display=swap');
@import url("https://fonts.googleapis.com/css2?family=Hanken+Grotesk:ital,wght@0,100..900;1,100..900&display=swap");
#root {
width: 100vw;
@ -87,22 +87,47 @@ body::-webkit-scrollbar {
border-box;
border: 2px solid transparent;
border-radius: 50em;
.background{
background-image: url('./images/scanline.jpg');
background-position: center;
background-size: cover;
width: 100%;
height: 100%;
border-radius: 50em;
display: flex;
justify-content: center;
align-items: center;
.background {
background-image: url("./images/scanline.jpg");
background-position: center;
background-size: cover;
width: 100%;
height: 100%;
border-radius: 50em;
display: flex;
justify-content: center;
align-items: center;
}
}
}
}
}
.controls {
position: absolute;
bottom: 150px;
}
.joystick {
left: 150px;
}
.drift {
right: 150px;
font-family: "Hanken Grotesk";
border-radius: 100px;
background: rgba(255, 255, 255, 0.5);
height: 66.6667px;
width: 66.6667px;
border: none;
flex-shrink: 0;
touch-action: none;
color: white;
display: grid;
place-content: center;
cursor: pointer;
}
@keyframes bounce {
0%,
100% {
@ -113,16 +138,15 @@ body::-webkit-scrollbar {
}
}
.annotation{
display:flex;
.annotation {
display: flex;
justify-content: center;
align-items: center;
background:none;
background: none;
backdrop-filter: blur(10px);
pointer-events: none;
}
}
.home {
position: absolute;
@ -248,11 +272,10 @@ body::-webkit-scrollbar {
}
}
.disabled{
.disabled {
pointer-events: none;
cursor: not-allowed;
opacity: 0.4;
}
@keyframes blinking {
0% {
@ -265,3 +288,20 @@ body::-webkit-scrollbar {
opacity: 1;
}
}
.controls.itemButton{
right: 250px;
font-family: "Hanken Grotesk";
border-radius: 100px;
background: rgba(255, 255, 255, 0.5);
height: 66.6667px;
width: 66.6667px;
border: none;
flex-shrink: 0;
touch-action: none;
color: white;
display: grid;
place-content: center;
cursor: pointer;
}