Fix camera jitter: 4-phase physics/rendering audit#4
Conversation
…d HUD Root cause: truck position/rotation/speed were stored as React state, causing ~60 re-renders/second. Camera consumed stale state props, amplifying jitter. Phase 1 fix: - Convert truckPosition/truckRotation/speed from useState to useRef - FollowCamera reads RefObject props directly in useFrame (zero re-renders) - Add throttled HUD sync (100ms interval) for minimap/speedometer display - Gentler camera smoothing (Math.pow base 0.08 → posSmooth, 0.04 → lookSmooth) - Reduce default shake intensity from 0.4 to 0.2 - Update StadiumGame3D for new FollowCamera ref interface - Add 29 tests validating all Phase 1 changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…d prediction Phase 2 of camera/physics audit — smooth camera behavior: - Velocity-based look-ahead: camera looks further ahead at higher speeds - Speed-dependent pull-back: camera pulls back up to 4 units at max speed - Camera velocity dampening: smooths acceleration to prevent overshoot on turns - Angular velocity clamping: limits camera rotation to 2.5 rad/s max (no whip-pan) - smoothness prop now actually drives interpolation (was ignored before) - speedRef prop added: FollowCamera receives live speed data from truck - Update RaceGame3D and StadiumGame3D to pass speedRef - 31 Phase 2 tests + updated Phase 1 test (60 total, all passing) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ravity Phase 3 of camera/physics audit — stabilize physics output: - Reduce ground height lerp from 12/s to 4/s (both jump and race mode) Prevents vertical jitter that amplifies through camera - Smooth boundary corrections: jump arena and radial boundary use gradual push-back instead of hard position clamping - Align gravity: MonsterTruck 30→25, Scene3D Physics -20→-25 Consistent gravity across manual and Rapier physics - AITruck smooth terrain: add smoothGroundHeight lerp (was snapping directly to getRaceTrackHeight causing visible AI truck jitter) - Frame-rate independent friction: spin decay, coasting, mud pit, and boundary penalties use Math.pow(factor, delta*60) - 25 Phase 3 tests (85 total across all phases, all passing) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 4 of camera/physics audit — rendering pipeline optimization: - Wrap RaceHUD3D in React.memo to skip re-renders when props unchanged - Wrap Minimap sub-component in memo() to avoid SVG re-rendering - Throttle AI positions: move aiPositions from inline .map() in JSX to hudAiPositions state synced every 100ms (eliminates per-render array/object allocation that broke memoization) - Add raceTimeRef: handlePositionUpdate and position ranking useEffect now read from ref instead of depending on raceTime state — eliminates callback recreation 10x/sec - Extract StadiumGame3D constants: STADIUM_CAMERA_OFFSET (was inline new THREE.Vector3 every render) and STADIUM_STAR_POSITIONS - 22 Phase 4 tests (107 total across all phases, all passing) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the game's visual and physical stability by addressing camera jitter, improving physics calculations, and optimizing the rendering pipeline. The changes focus on reducing React re-renders by leveraging refs for frequently updated game state, smoothing camera movements with advanced interpolation and clamping, and making physics interactions more consistent across different frame rates. These improvements collectively lead to a much smoother and more predictable gameplay experience. Highlights
Changelog
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This is an excellent and comprehensive pull request that systematically addresses camera jitter and performance through a well-structured four-phase audit. The changes are of high quality, including the move from React state to refs for dynamic data, the implementation of advanced camera smoothing and physics stabilization techniques, and rendering optimizations with React.memo. The inclusion of tests for each phase is also a great practice. I have two main points of feedback: one to improve clarity and maintainability in the new camera logic, and another to address a potential bug where passing a ref's value instead of the ref object could undermine the performance gains. Overall, this is a fantastic improvement.
| {gameMode === 'jump' && ( | ||
| <> | ||
| <JumpArena isActive={true} truckPosition={truckPosition} /> | ||
| <JumpArena isActive={true} truckPosition={truckPositionRef.current} /> |
There was a problem hiding this comment.
Here, you're passing the value truckPositionRef.current to JumpArena. This means JumpArena will only receive an updated position when RaceGame3D re-renders (currently every 100ms due to HUD updates). If JumpArena performs any per-frame logic based on the truck's position, this could lead to jittery or incorrect behavior, which is what this PR aims to fix.
To ensure JumpArena has access to the live, per-frame position, you should pass the ref object itself, similar to how you're doing it for FollowCamera. This will require updating JumpArena to accept a truckPositionRef prop and use .current inside its useFrame loop.
| <JumpArena isActive={true} truckPosition={truckPositionRef.current} /> | |
| <JumpArena isActive={true} truckPositionRef={truckPositionRef} /> |
| const intendedVelZ = (lerpedZ - prevCameraPos.current.z) / delta; | ||
|
|
||
| // Smooth camera velocity toward intended velocity (dampening factor) | ||
| const velDamp = 1 - Math.pow(0.001, delta); // ~6 frames to reach full velocity |
There was a problem hiding this comment.
The comment here is a bit misleading. At 60fps, it takes about 6 frames to cover half the distance to the target velocity, not to reach "full velocity". Reaching ~99% of the target velocity would take closer to 40 frames.
Additionally, the value 0.001 is a magic number. For better readability and easier tuning, consider extracting it into a constant at the top of the file, e.g., const CAMERA_VELOCITY_DAMP_FACTOR = 0.001;.
| const velDamp = 1 - Math.pow(0.001, delta); // ~6 frames to reach full velocity | |
| const velDamp = 1 - Math.pow(0.001, delta); // ~6 frames to reach 50% of target velocity |
Summary
Test plan
🤖 Generated with Claude Code