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:
27
src/App.tsx
27
src/App.tsx
@@ -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
|
||||
|
||||
53
src/Car.tsx
53
src/Car.tsx
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user