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 { Character } from './Character'
|
||||||
import { Car } from './Car'
|
import { Car } from './Car'
|
||||||
import { WorldBorder } from './WorldBorder'
|
import { WorldBorder } from './WorldBorder'
|
||||||
import { Mesh } from 'three'
|
import { Mesh, Group } from 'three'
|
||||||
|
|
||||||
import { CheatMenu } from './CheatMenu'
|
import { CheatMenu } from './CheatMenu'
|
||||||
import { Ramp } from './Ramp'
|
import { Ramp } from './Ramp'
|
||||||
@@ -15,6 +15,7 @@ export default function App() {
|
|||||||
const [showHitboxes, setShowHitboxes] = useState(false)
|
const [showHitboxes, setShowHitboxes] = useState(false)
|
||||||
const [ramps, setRamps] = useState<{ id: number; position: [number, number, number]; rotation: [number, number, number] }[]>([])
|
const [ramps, setRamps] = useState<{ id: number; position: [number, number, number]; rotation: [number, number, number] }[]>([])
|
||||||
const carRef = useRef<Mesh>(null)
|
const carRef = useRef<Mesh>(null)
|
||||||
|
const terrainRef = useRef<Group>(null)
|
||||||
|
|
||||||
const spawnRamp = () => {
|
const spawnRamp = () => {
|
||||||
if (carRef.current) {
|
if (carRef.current) {
|
||||||
@@ -33,7 +34,7 @@ export default function App() {
|
|||||||
|
|
||||||
setRamps(prev => [...prev, {
|
setRamps(prev => [...prev, {
|
||||||
id: Date.now(),
|
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
|
rotation: spawnRot
|
||||||
}])
|
}])
|
||||||
}
|
}
|
||||||
@@ -46,11 +47,17 @@ export default function App() {
|
|||||||
<ambientLight intensity={0.5} />
|
<ambientLight intensity={0.5} />
|
||||||
<pointLight position={[10, 10, 10]} />
|
<pointLight position={[10, 10, 10]} />
|
||||||
|
|
||||||
{/* Ground */}
|
<group ref={terrainRef}>
|
||||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.5, 0]} userData={{ isGround: true }}>
|
{/* Ground */}
|
||||||
<planeGeometry args={[2500, 2500]} />
|
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.5, 0]} userData={{ isGround: true }}>
|
||||||
<meshStandardMaterial color="#55aaff" />
|
<planeGeometry args={[2500, 2500]} />
|
||||||
</mesh>
|
<meshStandardMaterial color="#55aaff" />
|
||||||
|
</mesh>
|
||||||
|
|
||||||
|
{ramps.map(ramp => (
|
||||||
|
<Ramp key={ramp.id} position={ramp.position} rotation={ramp.rotation} />
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
|
||||||
<Grid
|
<Grid
|
||||||
args={[2500, 2500]}
|
args={[2500, 2500]}
|
||||||
@@ -65,21 +72,19 @@ export default function App() {
|
|||||||
|
|
||||||
<WorldBorder />
|
<WorldBorder />
|
||||||
|
|
||||||
{ramps.map(ramp => (
|
|
||||||
<Ramp key={ramp.id} position={ramp.position} rotation={ramp.rotation} />
|
|
||||||
))}
|
|
||||||
|
|
||||||
<Character
|
<Character
|
||||||
isDriving={isDriving}
|
isDriving={isDriving}
|
||||||
onEnter={() => setIsDriving(true)}
|
onEnter={() => setIsDriving(true)}
|
||||||
carRef={carRef}
|
carRef={carRef}
|
||||||
showHitboxes={showHitboxes}
|
showHitboxes={showHitboxes}
|
||||||
|
terrainRef={terrainRef}
|
||||||
/>
|
/>
|
||||||
<Car
|
<Car
|
||||||
ref={carRef}
|
ref={carRef}
|
||||||
isDriving={isDriving}
|
isDriving={isDriving}
|
||||||
onExit={() => setIsDriving(false)}
|
onExit={() => setIsDriving(false)}
|
||||||
showHitboxes={showHitboxes}
|
showHitboxes={showHitboxes}
|
||||||
|
terrainRef={terrainRef}
|
||||||
/>
|
/>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
<CheatMenu
|
<CheatMenu
|
||||||
|
|||||||
53
src/Car.tsx
53
src/Car.tsx
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { useRef, useEffect, forwardRef, useImperativeHandle } from 'react'
|
import { useRef, useEffect, forwardRef, useImperativeHandle } from 'react'
|
||||||
import { useFrame, useThree } from '@react-three/fiber'
|
import { useFrame, useThree } from '@react-three/fiber'
|
||||||
import { Vector3, Mesh, Euler } from 'three'
|
import { Vector3, Mesh, Euler } from 'three'
|
||||||
@@ -10,9 +9,10 @@ interface CarProps {
|
|||||||
onExit: () => void
|
onExit: () => void
|
||||||
position?: [number, number, number]
|
position?: [number, number, number]
|
||||||
showHitboxes?: boolean
|
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)
|
const internalRef = useRef<Mesh>(null)
|
||||||
useImperativeHandle(ref, () => internalRef.current!)
|
useImperativeHandle(ref, () => internalRef.current!)
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@ export const Car = forwardRef<Mesh, CarProps>(({ isDriving, onExit, position = [
|
|||||||
const { camera } = useThree()
|
const { camera } = useThree()
|
||||||
const keys = useRef({ w: false, a: false, s: false, d: false, f: false })
|
const keys = useRef({ w: false, a: false, s: false, d: false, f: false })
|
||||||
const speed = useRef(0)
|
const speed = useRef(0)
|
||||||
|
const verticalVelocity = useRef(0)
|
||||||
|
|
||||||
// Camera state for car
|
// Camera state for car
|
||||||
const cameraState = useRef({
|
const cameraState = useRef({
|
||||||
@@ -129,22 +130,12 @@ export const Car = forwardRef<Mesh, CarProps>(({ isDriving, onExit, position = [
|
|||||||
const absSin = Math.abs(Math.sin(yaw))
|
const absSin = Math.abs(Math.sin(yaw))
|
||||||
const absCos = Math.abs(Math.cos(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
|
// Ground Collision & Pitch
|
||||||
// Cast rays from front and back to determine pitch and height
|
|
||||||
const carPos = internalRef.current.position
|
const carPos = internalRef.current.position
|
||||||
const carRot = internalRef.current.rotation
|
const carRot = internalRef.current.rotation
|
||||||
|
|
||||||
const forwardDir = new Vector3(0, 0, 1).applyEuler(carRot)
|
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 backOffset = forwardDir.clone().multiplyScalar(-2.5)
|
||||||
|
|
||||||
const rayOriginFront = carPos.clone().add(frontOffset).add(new Vector3(0, 5, 0))
|
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 raycasterFront = new THREE.Raycaster(rayOriginFront, rayDir, 0, 10)
|
||||||
const raycasterBack = new THREE.Raycaster(rayOriginBack, rayDir, 0, 10)
|
const raycasterBack = new THREE.Raycaster(rayOriginBack, rayDir, 0, 10)
|
||||||
|
|
||||||
const intersectsFront = raycasterFront.intersectObjects(_state.scene.children, true)
|
const objectsToTest = terrainRef?.current ? terrainRef.current.children : []
|
||||||
const intersectsBack = raycasterBack.intersectObjects(_state.scene.children, true)
|
|
||||||
|
const intersectsFront = raycasterFront.intersectObjects(objectsToTest, true)
|
||||||
|
const intersectsBack = raycasterBack.intersectObjects(objectsToTest, true)
|
||||||
|
|
||||||
const groundHitFront = intersectsFront.find(hit => hit.object.userData.isGround)
|
const groundHitFront = intersectsFront.find(hit => hit.object.userData.isGround)
|
||||||
const groundHitBack = intersectsBack.find(hit => hit.object.userData.isGround)
|
const groundHitBack = intersectsBack.find(hit => hit.object.userData.isGround)
|
||||||
|
|
||||||
let frontY = 0.5
|
let frontY = -100
|
||||||
let backY = 0.5
|
let backY = -100
|
||||||
|
|
||||||
if (groundHitFront) frontY = groundHitFront.point.y + 0.5 // Car half height
|
if (groundHitFront) frontY = groundHitFront.point.y - 1.0
|
||||||
if (groundHitBack) backY = groundHitBack.point.y + 0.5
|
if (groundHitBack) backY = groundHitBack.point.y - 1.0
|
||||||
|
|
||||||
// Average height
|
let targetY = -100
|
||||||
const targetY = (frontY + backY) / 2
|
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)
|
if (targetY > -90) {
|
||||||
// For ramps, we want to snap to it
|
if (internalRef.current.position.y <= targetY) {
|
||||||
internalRef.current.position.y = THREE.MathUtils.lerp(internalRef.current.position.y, targetY, 0.2)
|
internalRef.current.position.y = targetY
|
||||||
|
verticalVelocity.current = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate pitch
|
// Calculate pitch
|
||||||
// atan2(deltaY, length)
|
|
||||||
const deltaY = frontY - backY
|
const deltaY = frontY - backY
|
||||||
const dist = 5 // Distance between ray points
|
const dist = 5
|
||||||
const targetPitch = Math.atan2(deltaY, dist)
|
const targetPitch = Math.atan2(deltaY, dist)
|
||||||
|
|
||||||
// Smoothly interpolate pitch
|
// Smoothly interpolate pitch
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ interface CharacterProps {
|
|||||||
onEnter: () => void
|
onEnter: () => void
|
||||||
carRef: React.RefObject<Mesh | null>
|
carRef: React.RefObject<Mesh | null>
|
||||||
showHitboxes?: boolean
|
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 ref = useRef<Mesh>(null)
|
||||||
const gltf = useGLTF('/character.glb')
|
const gltf = useGLTF('/character.glb')
|
||||||
const { camera } = useThree()
|
const { camera } = useThree()
|
||||||
@@ -182,8 +183,10 @@ export function Character({ isDriving, onEnter, carRef, showHitboxes = false }:
|
|||||||
const rayDir = new Vector3(0, -1, 0)
|
const rayDir = new Vector3(0, -1, 0)
|
||||||
const raycaster = new THREE.Raycaster(rayOrigin, rayDir, 0, 10)
|
const raycaster = new THREE.Raycaster(rayOrigin, rayDir, 0, 10)
|
||||||
|
|
||||||
// Filter objects with userData.isGround
|
// Use terrainRef for intersection if available
|
||||||
const intersects = raycaster.intersectObjects(_state.scene.children, true)
|
const objectsToTest = terrainRef?.current ? terrainRef.current.children : []
|
||||||
|
|
||||||
|
const intersects = raycaster.intersectObjects(objectsToTest, true)
|
||||||
const groundHit = intersects.find(hit => hit.object.userData.isGround)
|
const groundHit = intersects.find(hit => hit.object.userData.isGround)
|
||||||
|
|
||||||
let groundY = 0.5 // Default ground
|
let groundY = 0.5 // Default ground
|
||||||
|
|||||||
Reference in New Issue
Block a user