React-Three-Fiber中如何实现第三人称相机从后方跟随坦克?
稳定的第三人称相机跟随方案(结合useRaycastVehicle)
问题描述
正在开发一款3D坦克游戏,基于useRaycastVehicle实现了坦克控制,但第三人称相机的实现存在以下问题:
- 当前通过判断z轴坐标是否为负值翻转视角,逻辑生硬,体验不佳。
- 尝试参考教程代码时,出现相机卡入地面、无法正常跟随坦克的情况。
当前代码
import { useFrame, useThree } from "@react-three/fiber"; import { useRaycastVehicle } from "@react-three/cannon"; import { useEffect, useRef } from "react"; import { useKeyboardControls } from "@react-three/drei"; import Beetle from "./Beetle"; import Wheel from "./Wheel"; import { Vector3 } from "three"; export default function Tank({ radius = 0.7, width = 1.2, height = -0.04, front = 1.3, back = -1.15, steer = 0.75, force = 2000, maxBrake = 1e5, ...props }) { const { forward, backward, leftward, rightward, // shoot: jump, } = useKeyboardControls((state) => state); const chassis = useRef(); const wheel1 = useRef(); const wheel2 = useRef(); const wheel3 = useRef(); const wheel4 = useRef(); const wheelInfo = { radius, directionLocal: [0, -1, 0], suspensionStiffness: 30, suspensionRestLength: 0.3, maxSuspensionForce: 1e4, maxSuspensionTravel: 0.3, dampingRelaxation: 10, dampingCompression: 4.4, axleLocal: [-1, 0, 0], chassisConnectionPointLocal: [1, 0, 1], useCustomSlidingRotationalSpeed: true, customSlidingRotationalSpeed: -30, frictionSlip: 2, }; const wheelInfo1 = { ...wheelInfo, isFrontWheel: true, chassisConnectionPointLocal: [-width / 2, height, front], }; const wheelInfo2 = { ...wheelInfo, isFrontWheel: true, chassisConnectionPointLocal: [width / 2, height, front], }; const wheelInfo3 = { ...wheelInfo, isFrontWheel: false, chassisConnectionPointLocal: [-width / 2, height, back], }; const wheelInfo4 = { ...wheelInfo, isFrontWheel: false, chassisConnectionPointLocal: [width / 2, height, back], }; const [myTank, api] = useRaycastVehicle(() => ({ chassisBody: chassis, wheels: [wheel1, wheel2, wheel3, wheel4], wheelInfos: [wheelInfo1, wheelInfo2, wheelInfo3, wheelInfo4], indexForwardAxis: 2, indexRightAxis: 0, indexUpAxis: 1, })); const { camera, scene } = useThree(); const pos = useRef([0, 0, 0]); const rot = useRef([0, 0, 0]); useEffect(() => { if (chassis.current) { chassis.current.api.rotation.subscribe((r) => (rot.current = r)); chassis.current.api.position.subscribe((p) => (pos.current = p)); } }, [chassis.current]); useFrame(() => { for (let e = 2; e < 4; e++) api.applyEngineForce( forward || backward ? force * (forward && !backward ? -1 : 1) : 0, 2 ); for (let s = 0; s < 2; s++) api.setSteeringValue( leftward || rightward ? steer * (leftward && !rightward ? 1 : -1) : 0, s ); const cameraOffset = new Vector3(0, 4, -5); var zDimensionNegative = pos.current[2] > -5 ? 0 : 10; camera.position .copy( new Vector3( pos.current[0], pos.current[1], pos.current[2] + zDimensionNegative ) ) .add(cameraOffset); }); return ( <group ref={myTank} position={[0, -0.4, 0]}> <Beetle ref={chassis} rotation={props.rotation} position={props.position} angularVelocity={props.angularVelocity} /> <Wheel ref={wheel1} radius={radius} leftSide /> <Wheel ref={wheel2} radius={radius} /> <Wheel ref={wheel3} radius={radius} leftSide /> <Wheel ref={wheel4} radius={radius} /> </group> ); }
截图说明
- 理想效果:相机始终稳定跟随坦克后方,视角自然流畅,无卡顿或生硬翻转。
- 问题效果:相机卡入地面,无法随坦克正常移动。
- 当前方案效果:视角会基于z轴生硬翻转,体验不流畅。
解决方案
以下是优化后的相机实现逻辑,核心是基于坦克实时姿态计算相机位置,并添加平滑过渡和碰撞检测:
代码修改
- 新增Three.js相关导入:
import { Vector3, Euler, Quaternion, Raycaster } from "three";
- 替换
useFrame中的相机控制代码:
useFrame((state, delta) => { // 保留原有坦克控制逻辑 for (let e = 2; e < 4; e++) api.applyEngineForce( forward || backward ? force * (forward && !backward ? -1 : 1) : 0, 2 ); for (let s = 0; s < 2; s++) api.setSteeringValue( leftward || rightward ? steer * (leftward && !rightward ? 1 : -1) : 0, s ); // 相机跟随逻辑 const tankPos = new Vector3(...pos.current); const tankRot = new Euler(...rot.current); const tankQuat = new Quaternion().setFromEuler(tankRot); // 定义相机相对于坦克的偏移量(可按需调整高度和距离) const localOffset = new Vector3(0, 4, -5); // 将偏移量应用坦克旋转,确保相机始终在坦克后方 localOffset.applyQuaternion(tankQuat); // 计算相机目标位置 const targetCamPos = tankPos.clone().add(localOffset); // 平滑过渡相机位置(delta*5控制平滑速度,数值越大越灵敏) camera.position.lerp(targetCamPos, delta * 5); // 让相机始终看向坦克主体(微调y轴偏移优化视角) camera.lookAt(tankPos.clone().add(new Vector3(0, 1, 0))); // 可选:碰撞检测,避免相机穿过地面或障碍物 const raycaster = new Raycaster( tankPos, targetCamPos.clone().sub(tankPos).normalize() ); // 排除坦克自身模型避免误检测 const excludeObjects = [chassis.current, wheel1.current, wheel2.current, wheel3.current, wheel4.current]; const intersects = raycaster.intersectObjects(state.scene.children.filter(obj => !excludeObjects.includes(obj)), true); if (intersects.length > 0) { const closestHit = intersects[0]; // 若障碍物在相机与坦克之间,调整相机位置避免穿模 if (closestHit.distance < localOffset.length()) { camera.position.copy( tankPos.clone().add(localOffset.clone().setLength(closestHit.distance - 0.5)) ); } } });
关键优化点
- 基于坦克姿态计算偏移:不再依赖固定z轴判断,相机始终跟随坦克朝向移动。
- 平滑过渡:使用
lerp实现相机位置的平滑插值,消除生硬跳转。 - 定向注视:相机始终聚焦坦克主体,保证视角稳定。
- 碰撞避免:通过射线检测防止相机穿过地面或场景障碍物,提升真实感。
注意事项
- 可根据游戏需求调整
localOffset的数值,优化相机高度和距离。 - 平滑速度参数
delta*5可根据帧率调整,确保不同设备体验一致。 - 碰撞检测中需排除坦克自身模型,避免误触发位置调整。
内容的提问来源于stack exchange,提问作者imback0526
相关产品推荐
相关产品推荐

