Refactor terrain collision to use terrainRef group

Updated Car and Character components to use a shared terrainRef group for ground and ramp collision detection, improving accuracy and maintainability. The ground and ramps are now grouped in App and passed as a ref to both Car and Character, ensuring consistent collision logic.
This commit is contained in:
KaseToatz1337
2025-12-19 16:45:33 +01:00
parent 12a4e777f7
commit aa6217167c
3 changed files with 49 additions and 40 deletions

View File

@@ -4,7 +4,7 @@ import { Sky, Grid } from '@react-three/drei'
import { Character } from './Character'
import { Car } from './Car'
import { WorldBorder } from './WorldBorder'
import { Mesh } from 'three'
import { Mesh, Group } from 'three'
import { CheatMenu } from './CheatMenu'
import { Ramp } from './Ramp'
@@ -15,6 +15,7 @@ export default function App() {
const [showHitboxes, setShowHitboxes] = useState(false)
const [ramps, setRamps] = useState<{ id: number; position: [number, number, number]; rotation: [number, number, number] }[]>([])
const carRef = useRef<Mesh>(null)
const terrainRef = useRef<Group>(null)
const spawnRamp = () => {
if (carRef.current) {
@@ -33,7 +34,7 @@ export default function App() {
setRamps(prev => [...prev, {
id: Date.now(),
position: [spawnPos.x, 0.5, spawnPos.z], // Ensure it's on ground
position: [spawnPos.x, 0, spawnPos.z], // Ensure it's on ground (y=0 for center of 1-unit high box on -0.5 ground? No, ground is -0.5. Box height 1. Center at 0 puts bottom at -0.5. Perfect.)
rotation: spawnRot
}])
}
@@ -46,11 +47,17 @@ export default function App() {
<ambientLight intensity={0.5} />
<pointLight position={[10, 10, 10]} />
{/* Ground */}
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.5, 0]} userData={{ isGround: true }}>
<planeGeometry args={[2500, 2500]} />
<meshStandardMaterial color="#55aaff" />
</mesh>
<group ref={terrainRef}>
{/* Ground */}
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.5, 0]} userData={{ isGround: true }}>
<planeGeometry args={[2500, 2500]} />
<meshStandardMaterial color="#55aaff" />
</mesh>
{ramps.map(ramp => (
<Ramp key={ramp.id} position={ramp.position} rotation={ramp.rotation} />
))}
</group>
<Grid
args={[2500, 2500]}
@@ -65,21 +72,19 @@ export default function App() {
<WorldBorder />
{ramps.map(ramp => (
<Ramp key={ramp.id} position={ramp.position} rotation={ramp.rotation} />
))}
<Character
isDriving={isDriving}
onEnter={() => setIsDriving(true)}
carRef={carRef}
showHitboxes={showHitboxes}
terrainRef={terrainRef}
/>
<Car
ref={carRef}
isDriving={isDriving}
onExit={() => setIsDriving(false)}
showHitboxes={showHitboxes}
terrainRef={terrainRef}
/>
</Canvas>
<CheatMenu

View File

@@ -1,4 +1,3 @@
import { useRef, useEffect, forwardRef, useImperativeHandle } from 'react'
import { useFrame, useThree } from '@react-three/fiber'
import { Vector3, Mesh, Euler } from 'three'
@@ -10,9 +9,10 @@ interface CarProps {
onExit: () => void
position?: [number, number, number]
showHitboxes?: boolean
terrainRef: React.RefObject<THREE.Group | null>
}
export const Car = forwardRef<Mesh, CarProps>(({ isDriving, onExit, position = [5, 0.5, 5], showHitboxes = false }, ref) => {
export const Car = forwardRef<Mesh, CarProps>(({ isDriving, onExit, position = [5, 0, 5], showHitboxes = false, terrainRef }, ref) => {
const internalRef = useRef<Mesh>(null)
useImperativeHandle(ref, () => internalRef.current!)
@@ -20,6 +20,7 @@ export const Car = forwardRef<Mesh, CarProps>(({ isDriving, onExit, position = [
const { camera } = useThree()
const keys = useRef({ w: false, a: false, s: false, d: false, f: false })
const speed = useRef(0)
const verticalVelocity = useRef(0)
// Camera state for car
const cameraState = useRef({
@@ -129,22 +130,12 @@ export const Car = forwardRef<Mesh, CarProps>(({ isDriving, onExit, position = [
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))
// Ground Collision & Pitch
// Cast rays from front and back to determine pitch and height
const carPos = internalRef.current.position
const carRot = internalRef.current.rotation
const forwardDir = new Vector3(0, 0, 1).applyEuler(carRot)
const frontOffset = forwardDir.clone().multiplyScalar(2.5) // Half length approx
const frontOffset = forwardDir.clone().multiplyScalar(2.5)
const backOffset = forwardDir.clone().multiplyScalar(-2.5)
const rayOriginFront = carPos.clone().add(frontOffset).add(new Vector3(0, 5, 0))
@@ -154,29 +145,39 @@ export const Car = forwardRef<Mesh, CarProps>(({ isDriving, onExit, position = [
const raycasterFront = new THREE.Raycaster(rayOriginFront, rayDir, 0, 10)
const raycasterBack = new THREE.Raycaster(rayOriginBack, rayDir, 0, 10)
const intersectsFront = raycasterFront.intersectObjects(_state.scene.children, true)
const intersectsBack = raycasterBack.intersectObjects(_state.scene.children, true)
const objectsToTest = terrainRef?.current ? terrainRef.current.children : []
const intersectsFront = raycasterFront.intersectObjects(objectsToTest, true)
const intersectsBack = raycasterBack.intersectObjects(objectsToTest, true)
const groundHitFront = intersectsFront.find(hit => hit.object.userData.isGround)
const groundHitBack = intersectsBack.find(hit => hit.object.userData.isGround)
let frontY = 0.5
let backY = 0.5
let frontY = -100
let backY = -100
if (groundHitFront) frontY = groundHitFront.point.y + 0.5 // Car half height
if (groundHitBack) backY = groundHitBack.point.y + 0.5
if (groundHitFront) frontY = groundHitFront.point.y - 1.0
if (groundHitBack) backY = groundHitBack.point.y - 1.0
// Average height
const targetY = (frontY + backY) / 2
let targetY = -100
if (groundHitFront && groundHitBack) {
targetY = (frontY + backY) / 2
} else if (groundHitFront) {
targetY = frontY
} else if (groundHitBack) {
targetY = backY
}
// Apply gravity if in air (simple approach: just lerp to targetY)
// For ramps, we want to snap to it
internalRef.current.position.y = THREE.MathUtils.lerp(internalRef.current.position.y, targetY, 0.2)
if (targetY > -90) {
if (internalRef.current.position.y <= targetY) {
internalRef.current.position.y = targetY
verticalVelocity.current = 0
}
}
// Calculate pitch
// atan2(deltaY, length)
const deltaY = frontY - backY
const dist = 5 // Distance between ray points
const dist = 5
const targetPitch = Math.atan2(deltaY, dist)
// Smoothly interpolate pitch

View File

@@ -9,9 +9,10 @@ interface CharacterProps {
onEnter: () => void
carRef: React.RefObject<Mesh | null>
showHitboxes?: boolean
terrainRef: React.RefObject<THREE.Group | null>
}
export function Character({ isDriving, onEnter, carRef, showHitboxes = false }: CharacterProps) {
export function Character({ isDriving, onEnter, carRef, showHitboxes = false, terrainRef }: CharacterProps) {
const ref = useRef<Mesh>(null)
const gltf = useGLTF('/character.glb')
const { camera } = useThree()
@@ -182,8 +183,10 @@ export function Character({ isDriving, onEnter, carRef, showHitboxes = false }:
const rayDir = new Vector3(0, -1, 0)
const raycaster = new THREE.Raycaster(rayOrigin, rayDir, 0, 10)
// Filter objects with userData.isGround
const intersects = raycaster.intersectObjects(_state.scene.children, true)
// Use terrainRef for intersection if available
const objectsToTest = terrainRef?.current ? terrainRef.current.children : []
const intersects = raycaster.intersectObjects(objectsToTest, true)
const groundHit = intersects.find(hit => hit.object.userData.isGround)
let groundY = 0.5 // Default ground