bigger world + worldborder
TODO: fix teleport glitch when exiting car
This commit is contained in:
24
src/App.tsx
24
src/App.tsx
@@ -1,8 +1,9 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import { Canvas } from '@react-three/fiber'
|
||||
import { Sky } from '@react-three/drei'
|
||||
import { Sky, Grid } from '@react-three/drei'
|
||||
import { Character } from './Character'
|
||||
import { Car } from './Car'
|
||||
import { WorldBorder } from './WorldBorder'
|
||||
import { Mesh } from 'three'
|
||||
|
||||
export default function App() {
|
||||
@@ -10,17 +11,30 @@ export default function App() {
|
||||
const carRef = useRef<Mesh>(null)
|
||||
|
||||
return (
|
||||
<Canvas camera={{ position: [0, 5, 10] }}>
|
||||
<Sky sunPosition={[100, 20, 100]} />
|
||||
<Canvas camera={{ position: [0, 5, 10] }} gl={{ antialias: true }}>
|
||||
<Sky sunPosition={[100, 20, 100]} distance={5000} />
|
||||
<ambientLight intensity={0.5} />
|
||||
<pointLight position={[10, 10, 10]} />
|
||||
|
||||
{/* Ground */}
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]}>
|
||||
<planeGeometry args={[500, 500]} />
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.5, 0]}>
|
||||
<planeGeometry args={[2500, 2500]} />
|
||||
<meshStandardMaterial color="#55aaff" />
|
||||
</mesh>
|
||||
|
||||
<Grid
|
||||
args={[2500, 2500]}
|
||||
fadeDistance={2000}
|
||||
sectionSize={10}
|
||||
sectionThickness={1}
|
||||
sectionColor="#ffffff"
|
||||
cellSize={1}
|
||||
cellThickness={0.5}
|
||||
cellColor="#aaccff"
|
||||
/>
|
||||
|
||||
<WorldBorder />
|
||||
|
||||
<Character
|
||||
isDriving={isDriving}
|
||||
onEnter={() => setIsDriving(true)}
|
||||
|
||||
50
src/Car.tsx
50
src/Car.tsx
@@ -1,3 +1,4 @@
|
||||
|
||||
import { useRef, useEffect, forwardRef, useImperativeHandle } from 'react'
|
||||
import { useFrame, useThree } from '@react-three/fiber'
|
||||
import { Vector3, Mesh, Euler } from 'three'
|
||||
@@ -13,7 +14,7 @@ export const Car = forwardRef<Mesh, CarProps>(({ isDriving, onExit, position = [
|
||||
const internalRef = useRef<Mesh>(null)
|
||||
useImperativeHandle(ref, () => internalRef.current!)
|
||||
|
||||
const { scene } = useGLTF('/car.glb')
|
||||
const gltf = useGLTF('/car.glb')
|
||||
const { camera } = useThree()
|
||||
const keys = useRef({ w: false, a: false, s: false, d: false, f: false })
|
||||
const speed = useRef(0)
|
||||
@@ -36,8 +37,8 @@ export const Car = forwardRef<Mesh, CarProps>(({ isDriving, onExit, position = [
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const key = e.key.toLowerCase()
|
||||
if (keys.current.hasOwnProperty(key)) {
|
||||
(keys.current as any)[key] = true
|
||||
if (Object.prototype.hasOwnProperty.call(keys.current, key)) {
|
||||
(keys.current as Record<string, boolean>)[key] = true
|
||||
}
|
||||
if (e.key.toLowerCase() === 'f') {
|
||||
if (isDriving) {
|
||||
@@ -47,8 +48,8 @@ export const Car = forwardRef<Mesh, CarProps>(({ isDriving, onExit, position = [
|
||||
}
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
const key = e.key.toLowerCase()
|
||||
if (keys.current.hasOwnProperty(key)) {
|
||||
(keys.current as any)[key] = false
|
||||
if (Object.prototype.hasOwnProperty.call(keys.current, key)) {
|
||||
(keys.current as Record<string, boolean>)[key] = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,8 +106,29 @@ export const Car = forwardRef<Mesh, CarProps>(({ isDriving, onExit, position = [
|
||||
// Move
|
||||
internalRef.current.translateZ(speed.current * delta)
|
||||
|
||||
// Clamp position to map boundaries with rotation-aware hitbox
|
||||
const mapSize = 2500
|
||||
const halfMapSize = mapSize / 2
|
||||
const carWidth = 3
|
||||
const carLength = 7
|
||||
const halfWidth = carWidth / 2
|
||||
const halfLength = carLength / 2
|
||||
|
||||
const yaw = internalRef.current.rotation.y
|
||||
const absSin = Math.abs(Math.sin(yaw))
|
||||
const absCos = Math.abs(Math.cos(yaw))
|
||||
|
||||
const extentX = absSin * halfLength + absCos * halfWidth
|
||||
const extentZ = absCos * halfLength + absSin * halfWidth
|
||||
|
||||
const boundaryX = halfMapSize - extentX
|
||||
const boundaryZ = halfMapSize - extentZ
|
||||
|
||||
internalRef.current.position.x = Math.max(-boundaryX, Math.min(boundaryX, internalRef.current.position.x))
|
||||
internalRef.current.position.z = Math.max(-boundaryZ, Math.min(boundaryZ, internalRef.current.position.z))
|
||||
|
||||
// Camera Follow
|
||||
const { yaw, pitch, distance } = cameraState.current
|
||||
const { yaw: camYaw, pitch, distance } = cameraState.current
|
||||
|
||||
// Target position (above car)
|
||||
const targetPos = internalRef.current.position.clone().add(new Vector3(0, 2, 0))
|
||||
@@ -114,7 +136,7 @@ export const Car = forwardRef<Mesh, CarProps>(({ isDriving, onExit, position = [
|
||||
// Calculate max camera position (relative to target)
|
||||
const maxCamPos = new Vector3(0, 0, distance)
|
||||
// Apply rotation: pitch, then yaw + car rotation + 180deg (to look from behind)
|
||||
maxCamPos.applyEuler(new Euler(pitch, yaw + internalRef.current.rotation.y + Math.PI, 0, 'YXZ'))
|
||||
maxCamPos.applyEuler(new Euler(pitch, camYaw + internalRef.current.rotation.y + Math.PI, 0, 'YXZ'))
|
||||
|
||||
// Check for ground collision
|
||||
const minCameraHeight = 0.5
|
||||
@@ -132,7 +154,7 @@ export const Car = forwardRef<Mesh, CarProps>(({ isDriving, onExit, position = [
|
||||
}
|
||||
|
||||
const actualCamPos = new Vector3(0, 0, actualDistance)
|
||||
actualCamPos.applyEuler(new Euler(pitch, yaw + internalRef.current.rotation.y + Math.PI, 0, 'YXZ'))
|
||||
actualCamPos.applyEuler(new Euler(pitch, camYaw + internalRef.current.rotation.y + Math.PI, 0, 'YXZ'))
|
||||
|
||||
// Lerp to the collided position
|
||||
// Increased lerp speed from 0.1 to 0.5 to reduce lag
|
||||
@@ -142,8 +164,14 @@ export const Car = forwardRef<Mesh, CarProps>(({ isDriving, onExit, position = [
|
||||
})
|
||||
|
||||
return (
|
||||
<mesh ref={internalRef} position={new Vector3(...position)}>
|
||||
<primitive object={scene} scale={1.5} rotation={[0, 0, 0]} />
|
||||
</mesh>
|
||||
<group ref={internalRef} position={new Vector3(...position)}>
|
||||
<primitive object={gltf.scene} scale={1.5} rotation={[0, 0, 0]} />
|
||||
|
||||
{/* Debug Hitbox */}
|
||||
<mesh position={[0, 0.75, 0]}>
|
||||
<boxGeometry args={[3, 1.5, 7]} />
|
||||
<meshBasicMaterial wireframe color="yellow" />
|
||||
</mesh>
|
||||
</group>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -11,7 +11,7 @@ interface CharacterProps {
|
||||
|
||||
export function Character({ isDriving, onEnter, carRef }: CharacterProps) {
|
||||
const ref = useRef<Mesh>(null)
|
||||
const { scene } = useGLTF('/character.glb')
|
||||
const gltf = useGLTF('/character.glb')
|
||||
const { camera } = useThree()
|
||||
const keys = useRef({ w: false, a: false, s: false, d: false, shift: false, space: false })
|
||||
const velocity = useRef(new Vector3(0, 0, 0))
|
||||
@@ -26,8 +26,8 @@ export function Character({ isDriving, onEnter, carRef }: CharacterProps) {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const key = e.key.toLowerCase()
|
||||
if (keys.current.hasOwnProperty(key)) {
|
||||
(keys.current as any)[key] = true
|
||||
if (Object.prototype.hasOwnProperty.call(keys.current, key)) {
|
||||
(keys.current as Record<string, boolean>)[key] = true
|
||||
}
|
||||
if (e.key === 'Shift') keys.current.shift = true
|
||||
if (e.key === ' ') keys.current.space = true
|
||||
@@ -43,8 +43,8 @@ export function Character({ isDriving, onEnter, carRef }: CharacterProps) {
|
||||
}
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
const key = e.key.toLowerCase()
|
||||
if (keys.current.hasOwnProperty(key)) {
|
||||
(keys.current as any)[key] = false
|
||||
if (Object.prototype.hasOwnProperty.call(keys.current, key)) {
|
||||
(keys.current as Record<string, boolean>)[key] = false
|
||||
}
|
||||
if (e.key === 'Shift') keys.current.shift = false
|
||||
if (e.key === ' ') keys.current.space = false
|
||||
@@ -100,7 +100,7 @@ export function Character({ isDriving, onEnter, carRef }: CharacterProps) {
|
||||
ref.current.position.y = 1 // Ensure above ground
|
||||
velocity.current.set(0, 0, 0)
|
||||
}
|
||||
}, [isDriving])
|
||||
}, [isDriving, carRef])
|
||||
|
||||
useFrame((_state, delta) => {
|
||||
if (!ref.current || isDriving) return
|
||||
@@ -128,6 +128,15 @@ export function Character({ isDriving, onEnter, carRef }: CharacterProps) {
|
||||
ref.current.lookAt(ref.current.position.clone().add(direction))
|
||||
}
|
||||
|
||||
// Clamp position to map boundaries with radius
|
||||
const mapSize = 2500
|
||||
const halfMapSize = mapSize / 2
|
||||
const radius = 0.5
|
||||
const boundary = halfMapSize - radius
|
||||
|
||||
ref.current.position.x = Math.max(-boundary, Math.min(boundary, ref.current.position.x))
|
||||
ref.current.position.z = Math.max(-boundary, Math.min(boundary, ref.current.position.z))
|
||||
|
||||
// Physics (Gravity & Jumping)
|
||||
const gravity = 30
|
||||
const jumpForce = 12
|
||||
@@ -190,8 +199,14 @@ export function Character({ isDriving, onEnter, carRef }: CharacterProps) {
|
||||
})
|
||||
|
||||
return (
|
||||
<mesh ref={ref} position={[0, 0.5, 0]} visible={!isDriving}>
|
||||
<primitive object={scene} scale={1.5} rotation={[0, 0, 0]} />
|
||||
</mesh>
|
||||
<group ref={ref} position={[0, 0.5, 0]} visible={!isDriving}>
|
||||
<primitive object={gltf.scene} rotation={[0, 0, 0]} />
|
||||
|
||||
{/* Debug Hitbox */}
|
||||
<mesh position={[0, 0.9, 0]}>
|
||||
<cylinderGeometry args={[0.5, 0.5, 1.8, 16]} />
|
||||
<meshBasicMaterial wireframe color="cyan" />
|
||||
</mesh>
|
||||
</group>
|
||||
)
|
||||
}
|
||||
|
||||
103
src/WorldBorder.tsx
Normal file
103
src/WorldBorder.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { useRef, useMemo } from 'react'
|
||||
import { useFrame, useThree } from '@react-three/fiber'
|
||||
import { ShaderMaterial, Vector3, DoubleSide } from 'three'
|
||||
|
||||
export function WorldBorder() {
|
||||
const materialRef = useRef<ShaderMaterial>(null)
|
||||
const { camera } = useThree()
|
||||
|
||||
const uniforms = useMemo(
|
||||
() => ({
|
||||
uPlayerPos: { value: new Vector3() },
|
||||
uVisibleDistance: { value: 200.0 },
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
useFrame(() => {
|
||||
if (materialRef.current) {
|
||||
materialRef.current.uniforms.uPlayerPos.value.copy(camera.position)
|
||||
}
|
||||
})
|
||||
|
||||
const vertexShader = `
|
||||
varying vec3 vWorldPos;
|
||||
void main() {
|
||||
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
|
||||
vWorldPos = worldPosition.xyz;
|
||||
gl_Position = projectionMatrix * viewMatrix * worldPosition;
|
||||
}
|
||||
`
|
||||
|
||||
const fragmentShader = `
|
||||
uniform vec3 uPlayerPos;
|
||||
uniform float uVisibleDistance;
|
||||
varying vec3 vWorldPos;
|
||||
|
||||
void main() {
|
||||
// Calculate distance from player to this fragment
|
||||
float distToPlayer = distance(uPlayerPos, vWorldPos);
|
||||
|
||||
// Fade based on distance
|
||||
float alpha = 1.0 - smoothstep(0.0, uVisibleDistance, distToPlayer);
|
||||
|
||||
if (alpha <= 0.0) discard;
|
||||
|
||||
gl_FragColor = vec4(1.0, 0.0, 0.0, alpha * 0.8);
|
||||
}
|
||||
`
|
||||
|
||||
const mapSize = 2500
|
||||
const halfSize = mapSize / 2
|
||||
const wallHeight = 100
|
||||
|
||||
return (
|
||||
<group>
|
||||
{/* North Wall (-Z) */}
|
||||
<mesh position={[0, wallHeight / 2, -halfSize]}>
|
||||
<planeGeometry args={[mapSize, wallHeight]} />
|
||||
<shaderMaterial
|
||||
ref={materialRef}
|
||||
vertexShader={vertexShader}
|
||||
fragmentShader={fragmentShader}
|
||||
uniforms={uniforms}
|
||||
transparent
|
||||
side={DoubleSide}
|
||||
/>
|
||||
</mesh>
|
||||
{/* South Wall (+Z) */}
|
||||
<mesh position={[0, wallHeight / 2, halfSize]}>
|
||||
<planeGeometry args={[mapSize, wallHeight]} />
|
||||
<shaderMaterial
|
||||
vertexShader={vertexShader}
|
||||
fragmentShader={fragmentShader}
|
||||
uniforms={uniforms}
|
||||
transparent
|
||||
side={DoubleSide}
|
||||
/>
|
||||
</mesh>
|
||||
{/* East Wall (+X) */}
|
||||
<mesh position={[halfSize, wallHeight / 2, 0]} rotation={[0, -Math.PI / 2, 0]}>
|
||||
<planeGeometry args={[mapSize, wallHeight]} />
|
||||
<shaderMaterial
|
||||
vertexShader={vertexShader}
|
||||
fragmentShader={fragmentShader}
|
||||
uniforms={uniforms}
|
||||
transparent
|
||||
side={DoubleSide}
|
||||
/>
|
||||
</mesh>
|
||||
{/* West Wall (-X) */}
|
||||
<mesh position={[-halfSize, wallHeight / 2, 0]} rotation={[0, Math.PI / 2, 0]}>
|
||||
<planeGeometry args={[mapSize, wallHeight]} />
|
||||
<shaderMaterial
|
||||
vertexShader={vertexShader}
|
||||
fragmentShader={fragmentShader}
|
||||
uniforms={uniforms}
|
||||
transparent
|
||||
side={DoubleSide}
|
||||
/>
|
||||
</mesh>
|
||||
</group>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user