Skip to main content

Hybrid Mode

Hybrid mode combines Three.js (3D world) with Pixi.js (2D UI overlay) for the best of both worlds.

🎮Hybrid 3D Game with 2D UI
Loading demo...

When to Use Hybrid Mode

  • 3D games with complex UI (RPGs, strategy games)
  • 3D environments with 2D HUD elements
  • Games needing both 3D gameplay and 2D menus

Setup

import { createGame } from '@gamebyte/framework';
import * as THREE from 'three';
import * as PIXI from 'pixi.js';

const game = createGame();
await game.initialize(canvas, 'hybrid');

// Access both renderers
const renderer3D = game.make('renderer.3d');
const renderer2D = game.make('renderer.2d');

// Get Three.js scene
const scene3D = renderer3D.getScene();
const camera = renderer3D.getCamera();

// Get Pixi.js stage (for UI)
const stage2D = renderer2D.getStage();

Architecture

┌─────────────────────────────────────┐
│ Final Output │
├─────────────────────────────────────┤
│ ┌───────────────────────────────┐ │
│ │ 2D Layer (Pixi.js) │ │ ← UI, HUD, Menus
│ │ [Health Bar] [Score] [Map] │ │
│ └───────────────────────────────┘ │
│ ┌───────────────────────────────┐ │
│ │ 3D Layer (Three.js) │ │ ← Game World
│ │ [Characters] [Environment] │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘

Example: 3D Game with 2D HUD

import { BaseScene, UIButton, TopBar, TopBarItemType } from '@gamebyte/framework';
import * as THREE from 'three';

class GameScene extends BaseScene {
private renderer3D: ThreeRenderer;
private renderer2D: PixiRenderer;
private scene3D: THREE.Scene;
private topBar: TopBar;

async initialize(): Promise<void> {
await super.initialize();

// Get renderers
this.renderer3D = this.app.make('renderer.3d');
this.renderer2D = this.app.make('renderer.2d');
this.scene3D = this.renderer3D.getScene();

// Setup 3D world
this.setup3DWorld();

// Setup 2D UI overlay
this.setup2DUI();
}

private setup3DWorld(): void {
// Add lights
const ambient = new THREE.AmbientLight(0x404040);
this.scene3D.add(ambient);

const directional = new THREE.DirectionalLight(0xffffff, 1);
directional.position.set(5, 10, 5);
this.scene3D.add(directional);

// Add ground
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(50, 50),
new THREE.MeshStandardMaterial({ color: 0x228B22 })
);
ground.rotation.x = -Math.PI / 2;
this.scene3D.add(ground);

// Add player character
const player = new THREE.Mesh(
new THREE.CapsuleGeometry(0.5, 1),
new THREE.MeshStandardMaterial({ color: 0x4169E1 })
);
player.position.y = 1;
this.scene3D.add(player);
}

private setup2DUI(): void {
// Top bar with health and score
this.topBar = new TopBar({
width: 800,
items: [
{
id: 'health',
type: TopBarItemType.PROGRESS,
value: 100,
maxValue: 100,
color: 0xff4444
},
{
id: 'score',
type: TopBarItemType.RESOURCE,
icon: 'coin',
value: 0,
animated: true
}
]
});

// Add to 2D container (on top of 3D)
this.container.addChild(this.topBar.getContainer());

// Pause button
const pauseBtn = new UIButton({
text: '⏸',
width: 50,
height: 50,
backgroundColor: 0x333333
});
pauseBtn.setPosition(750, 550);
pauseBtn.on('click', () => this.pauseGame());
this.container.addChild(pauseBtn.getContainer());
}

update(deltaTime: number): void {
super.update(deltaTime);

// Update 3D world
// (rotate camera, move objects, etc.)

// 2D UI updates automatically
}
}

Layer Management

Z-Order Control

// 3D is always behind, 2D is always in front
// Within 2D, use Pixi's z-index:

uiContainer.sortableChildren = true;
background.zIndex = 0;
gameUI.zIndex = 10;
modal.zIndex = 100;
uiContainer.sortChildren();

Transparency

The 2D layer has a transparent background by default:

// 2D background is transparent (shows 3D behind)
renderer2D.setBackgroundColor(0x000000, 0); // alpha = 0

// For solid 2D areas (menus), use panels (Pixi v8 API):
const menuBackground = new PIXI.Graphics();
menuBackground.rect(0, 0, 800, 600);
menuBackground.fill({ color: 0x1a1a2e, alpha: 0.9 });

Input Handling

Input works across both layers:

const input = game.make('input');

// 2D UI gets priority for clicks
// If UI doesn't handle it, 3D world receives it

// For 3D raycasting
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

input.on('click', (event) => {
// Check if UI handled it
if (event.handled) return;

// Convert to 3D ray
mouse.x = (event.x / width) * 2 - 1;
mouse.y = -(event.y / height) * 2 + 1;

raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene3D.children);

if (intersects.length > 0) {
console.log('Clicked 3D object:', intersects[0].object);
}
});

World-to-Screen Projection

Show 2D elements at 3D positions:

function worldToScreen(position3D: THREE.Vector3): { x: number, y: number } {
const projected = position3D.clone().project(camera);

return {
x: (projected.x + 1) * width / 2,
y: (-projected.y + 1) * height / 2
};
}

// Example: Health bar above 3D character
function updateHealthBar(character: THREE.Object3D, healthBar: UIProgressBar) {
const worldPos = character.position.clone();
worldPos.y += 2; // Above character

const screenPos = worldToScreen(worldPos);
healthBar.setPosition(screenPos.x, screenPos.y);
}

Performance Considerations

1. Minimize Overdraw

// Don't render 2D background if 3D fills screen
renderer2D.setBackgroundColor(0x000000, 0);

2. Batch UI Updates

// Bad: Update every frame
update(dt) {
this.scoreText.text = `Score: ${this.score}`;
}

// Good: Update only when changed
addScore(points: number) {
this.score += points;
this.scoreText.text = `Score: ${this.score}`;
}

3. Use Object Pools for Floating Text

class DamageTextPool {
private pool: PIXI.Text[] = [];

showDamage(position: THREE.Vector3, amount: number) {
const text = this.pool.pop() || this.createText();
const screen = worldToScreen(position);

text.text = `-${amount}`;
text.position.set(screen.x, screen.y);
text.visible = true;

// Animate and return to pool
gsap.to(text, {
y: screen.y - 50,
alpha: 0,
duration: 1,
onComplete: () => {
text.visible = false;
this.pool.push(text);
}
});
}
}