ith CSS, and pass state up and down through normal React patterns.
function GameScreen() {
const [score, setScore] = useState(0);
return (
<div style={{ position: 'relative', width: 800, height: 600 }}>
<Game width={800} height={600}>
<Arena onScore={() => setScore((s) => s + 1)} />
</Game>
{/* HUD β absolute div over the canvas */}
<div
style={{
position: 'absolute',
top: 16,
left: 16,
color: 'white',
fontFamily: 'monospace',
pointerEvents: 'none',
}}
>
Score: {score}
</div>
</div>
);
}
Enter fullscreen mode Exit fullscreen mode
The score display is a position: absolute div. React renders both the canvas and the HUD. There is no second rendering pass, no HUD texture atlas, no DOM-over-canvas bridge.
2. Wiring Game Events to HUD State
Game events happen inside hooks that run within <Game>'s React context. To expose them outside that boundary, lift shared state into an external store.
Zustand works cleanly here:
// store.ts
import { create } from 'zustand';
interface GameState {
score: number;
hp: number;
addScore: (n: number) => void;
takeDamage: (n: number) => void;
}
export const useGameStore = create<GameState>((set) => ({
score: 0,
hp: 100,
addScore: (n) => set((s) => ({ score: s.score + n })),
takeDamage: (n) => set((s) => ({ hp: Math.max(0, s.hp - n) })),
}));
Enter fullscreen mode Exit fullscreen mode
Inside the game, actor hooks write to the store on collision:
// Player.tsx
function Player() {
const playerRef = useRef(null);
const addScore = useGameStore((s) => s.addScore);
const takeDamage = useGameStore((s) => s.takeDamage);
const coinHit = useCollision(playerRef, ['coin']);
const enemyHit = useCollision(playerRef, ['enemy']);
useEffect(() => {
if (coinHit) addScore(1);
}, [coinHit]);
useEffect(() => () => {
if (enemyHit) takeDamage(10);
}, [enemyHit]);
return <Actor ref={playerRef} tags={['player']} />;
}
Enter fullscreen mode Exit fullscreen mode
The HUD subscribes from outside <Game>:
// HUD.tsx
function HUD() {
const score = useGameStore((s) => s.score);
const hp = useGameStore((s) => s.hp);
return (
<div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
<div style={{ position: 'absolute', top: 16, left: 16, color: 'white' }}>
<div>HP: {hp} / 100</div>
<div>Score: {score}</div>
</div>
</div>
);
}
Enter fullscreen mode Exit fullscreen mode
No prop drilling. No event emitters. Standard Zustand subscriptions.
3. Health Bar Implementation
Once hp is in the store, the health bar is a styled div:
function HealthBar({ hp, max }: { hp: number; max: number }) {
const pct = Math.round((hp / max) * 100);
return (
<div
style={{
width: 200,
height: 12,
background: '#333',
borderRadius: 4,
overflow: 'hidden',
}}
>
<div
style={{
width: `${pct}%`,
height: '100%',
background: hp < 30 ? '#e53e3e' : '#48bb78',
borderRadius: 4,
transition: 'width 120ms linear',
}}
/>
</div>
);
}
Enter fullscreen mode Exit fullscreen mode
The width transition is CSS. React's reconciler handles updates. No animation library required.
4. Debug Overlays During Development
Because the HUD is React, a debug overlay is a conditional render:
{process.env.NODE_ENV === 'development' && (
<div
style={{
position: 'absolute',
bottom: 8,
right: 8,
color: 'lime',
fontSize: 11,
fontFamily: 'monospace',
pointerEvents: 'none',
}}
>
HP: {hp} | Score: {score}
</div>
)}
Enter fullscreen mode Exit fullscreen mode
Same component, same store, stripped in the production build by your bundler. No debug-mode game loop variant.
5. Full GameScreen Composition
function GameScreen() {
return (
<div style={{ position: 'relative', width: 800, height: 600 }}>
<Game width={800} height={600}>
<Player />
<EnemySpawner />
<CoinField />
</Game>
<HUD />
</div>
);
}
Enter fullscreen mode Exit fullscreen mode
Three layers, one component tree:
- Canvas β CarverJS actors, game loop, AABB collision detection
- HUD β React divs with score, health bar, any overlay widget
- Overlays β pause screen, inventory, settings as conditional JSX
State flows through Zustand subscriptions. Components re-render when the store updates.
Pitfall Guide
- Blocking Input with HUD Overlays: Failing to set
pointerEvents: 'none' on absolute-positioned HUD divs will intercept click/touch events meant for the canvas, breaking game controls. Always explicitly disable pointer events on overlay layers.
- Prop Drilling Across Render Boundaries: Lifting game state through deep component trees causes unnecessary React re-renders and breaks the separation between the game loop and UI. Use an external store (Zustand, Jotai, or Redux) to decouple game logic from HUD rendering.
- High-Frequency React Re-renders: Game loops tick at 60/120Hz. Subscribing React components to every tick will starve the main thread. Throttle store updates or use selective selectors (
useGameStore((s) => s.hp)) to batch UI updates to ~30Hz.
- Debug Overlay Leakage: Conditional rendering based on
process.env.NODE_ENV relies on bundler dead-code elimination. If your Webpack/Vite config doesn't replace process.env, debug UI will ship to production. Verify tree-shaking and environment replacement in your build pipeline.
- CSS Transition Jank on Rapid Values: Applying
transition: width 120ms to rapidly changing metrics (like score or ammo count) causes visual stutter and layout thrashing. Reserve CSS transitions for discrete state changes (HP, mana, cooldowns) and use instant updates for high-frequency counters.
- Relative Container Collapse: Positioning HUD elements absolutely without a
position: relative parent wrapper causes layout drift on responsive viewports or when the canvas scales. Always wrap <Game> and <HUD> in a container with explicit dimensions and relative positioning.
- State Desync Between Canvas and DOM: Mixing imperative canvas state with React state without a single source of truth leads to race conditions during collisions or physics updates. Centralize all mutable game state in the external store and let both CarverJS hooks and React components read from it.
Deliverables
π CarverJS React HUD Integration Blueprint
A structured architectural guide detailing the unified component tree, state flow diagrams, and store subscription patterns. Includes responsive container templates, collision-to-store wiring schemas, and production bundling configurations for optimal tree-shaking of debug overlays.
β
Production-Ready HUD Deployment Checklist