@energy8platform/game-engine
v0.5.0
Published
Universal casino game engine built on PixiJS v8 and @energy8platform/game-sdk
Maintainers
Readme
@energy8platform/game-engine
A universal casino game engine built on PixiJS v8 and @energy8platform/game-sdk. Provides a batteries-included framework for developing slot machines, card games, and other iGaming titles with responsive scaling, animated loading screens, scene management, audio, state machines, tweens, and a rich UI component library.
Table of Contents
- Quick Start
- Installation
- Architecture
- Configuration
- Scenes
- Lifecycle
- Assets
- Audio
- Viewport & Scaling
- State Machine
- Animation
- UI Components
- Input
- Vite Configuration
- DevBridge
- Debug
- API Reference
- License
Quick Start
# Create a new project
mkdir my-game && cd my-game
npm init -y
# Install dependencies
npm install pixi.js @energy8platform/game-sdk @energy8platform/game-engine
# Install UI layout dependencies (optional — needed for Layout, Panel, ScrollContainer)
npm install @pixi/ui @pixi/layout yoga-layout
# (Optional) install spine and audio support
npm install @pixi/sound @esotericsoftware/spine-pixi-v8Create the entry point:
// src/main.ts
import { GameApplication, ScaleMode } from '@energy8platform/game-engine';
import { GameScene } from './scenes/GameScene';
async function bootstrap() {
const game = new GameApplication({
container: '#game',
designWidth: 1920,
designHeight: 1080,
scaleMode: ScaleMode.FIT,
loading: {
backgroundColor: 0x0a0a1a,
backgroundGradient:
'linear-gradient(135deg, #0a0a1a 0%, #1a1a3e 50%, #0a0a1a 100%)',
showPercentage: true,
tapToStart: true,
tapToStartText: 'TAP TO PLAY',
minDisplayTime: 2000,
},
manifest: {
bundles: [
{ name: 'preload', assets: [] },
{
name: 'game',
assets: [
{ alias: 'background', src: 'background.png' },
{ alias: 'symbols', src: 'symbols.json' },
],
},
],
},
audio: {
music: 0.5,
sfx: 1.0,
persist: true,
},
debug: true,
});
game.scenes.register('game', GameScene);
game.on('initialized', () => console.log('Engine initialized'));
game.on('loaded', () => console.log('Assets loaded'));
game.on('started', () => console.log('Game started'));
game.on('error', (err) => console.error('Error:', err));
await game.start('game');
}
bootstrap();Installation
Peer Dependencies
| Package | Version | Required |
| --- | --- | --- |
| pixi.js | ^8.16.0 | Yes |
| @energy8platform/game-sdk | ^2.6.0 | Yes |
| @pixi/ui | ^2.3.0 | Optional — for Button, ScrollContainer, ProgressBar |
| @pixi/layout | ^3.2.0 | Optional — for Layout, Panel (Yoga flexbox) |
| yoga-layout | ^3.0.0 | Optional — peer dep of @pixi/layout |
| @pixi/sound | ^6.0.0 | Optional — for audio |
| @esotericsoftware/spine-pixi-v8 | ~4.2.0 | Optional — for Spine animations |
Sub-path Exports
The package exposes granular entry points for tree-shaking:
import { GameApplication } from '@energy8platform/game-engine'; // full bundle
import { Scene, SceneManager } from '@energy8platform/game-engine/core';
import { AssetManager } from '@energy8platform/game-engine/assets';
import { AudioManager } from '@energy8platform/game-engine/audio';
import { Button, Label, Modal, Layout, ScrollContainer, Panel, Toast } from '@energy8platform/game-engine/ui';
import { Tween, Timeline, Easing, SpriteAnimation } from '@energy8platform/game-engine/animation';
import { DevBridge, FPSOverlay } from '@energy8platform/game-engine/debug';
import { defineGameConfig } from '@energy8platform/game-engine/vite';Architecture
┌──────────────────────────────────────────────────────┐
│ GameApplication │
│ (orchestrates lifecycle, holds all sub-systems) │
├──────────┬───────────┬───────────┬───────────────────┤
│ Viewport │ Scenes │ Assets │ Audio │ Input │
│ Manager │ Manager │ Manager │ Manager │ Manager │
├──────────┴───────────┴───────────┴─────────┴─────────┤
│ PixiJS v8 Application │
├──────────────────────────────────────────────────────┤
│ @energy8platform/game-sdk │
└──────────────────────────────────────────────────────┘Boot Sequence
- CSS Preloader — an instant HTML/CSS overlay shown while PixiJS initializes (inline SVG logo with a shimmer animation and "Loading..." text).
- PixiJS initialization — creates
Application, initializesResizeObserver. - SDK handshake — connects to the casino host (or DevBridge in dev mode via shared
MemoryChannel). - Canvas Loading Screen —
LoadingScenedisplays the SVG logo with an animated progress bar,preloadbundle is loaded first. - Asset loading — remaining bundles are loaded; the progress bar fills in real time.
- Tap-to-start — optional screen shown after loading (required on mobile for audio unlock).
- First scene — transitions to the registered first scene.
Configuration
GameApplicationConfig
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| container | HTMLElement \| string | document.body | Container element or CSS selector |
| designWidth | number | 1920 | Reference design width |
| designHeight | number | 1080 | Reference design height |
| scaleMode | ScaleMode | FIT | Scaling strategy |
| orientation | Orientation | ANY | Preferred orientation |
| loading | LoadingScreenConfig | — | Loading screen options |
| manifest | AssetManifest | — | Asset manifest |
| audio | AudioConfig | — | Audio configuration |
| sdk | object \| false | — | SDK options; false to disable |
| sdk.devMode | boolean | false | Use in-memory channel instead of postMessage (no iframe needed) |
| sdk.parentOrigin | string | — | Expected parent origin for postMessage validation |
| sdk.timeout | number | — | SDK handshake timeout in ms |
| sdk.debug | boolean | — | Enable SDK debug logging |
| pixi | Partial<ApplicationOptions> | — | PixiJS pass-through options |
| debug | boolean | false | Enable FPS overlay |
LoadingScreenConfig
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| backgroundColor | number \| string | 0x0a0a1a | Background color |
| backgroundGradient | string | — | CSS gradient for the preloader background |
| logoAsset | string | — | Logo texture alias from preload bundle |
| logoScale | number | 1 | Logo scale factor |
| showPercentage | boolean | true | Show loading percentage text |
| progressTextFormatter | (progress: number) => string | — | Custom progress text formatter |
| tapToStart | boolean | true | Show "Tap to start" overlay |
| tapToStartText | string | 'TAP TO START' | Tap-to-start label |
| minDisplayTime | number | 1500 | Minimum display time (ms) |
| cssPreloaderHTML | string | — | Custom HTML for the CSS preloader |
AudioConfig
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| music | number | 0.7 | Default music volume (0–1) |
| sfx | number | 1 | Default SFX volume |
| ui | number | 0.8 | Default UI sounds volume |
| ambient | number | 0.5 | Default ambient volume |
| persist | boolean | true | Save mute state to localStorage |
| storageKey | string | 'ge_audio' | localStorage key prefix |
Enums
enum ScaleMode {
FIT = 'FIT', // Letterbox/pillarbox — preserves aspect ratio
FILL = 'FILL', // Fill container, crop edges
STRETCH = 'STRETCH' // Stretch to fill (distorts)
}
enum Orientation {
LANDSCAPE = 'landscape',
PORTRAIT = 'portrait',
ANY = 'any'
}
enum TransitionType {
NONE = 'none',
FADE = 'fade',
SLIDE_LEFT = 'slide-left',
SLIDE_RIGHT = 'slide-right'
}Scenes
All game screens are scenes. Extend the base Scene class and override lifecycle hooks:
import { Scene } from '@energy8platform/game-engine';
import { Sprite, Assets } from 'pixi.js';
export class GameScene extends Scene {
async onEnter(data?: unknown) {
const bg = new Sprite(Assets.get('background'));
this.container.addChild(bg);
}
onUpdate(dt: number) {
// Called every frame (dt = delta time from PixiJS ticker)
}
onResize(width: number, height: number) {
// Called on viewport resize — use for responsive layout
}
async onExit() {
// Cleanup before leaving the scene
}
onDestroy() {
// Final cleanup when the scene is removed from the stack
}
}Scene Navigation
const scenes = game.scenes;
// Register scenes
scenes.register('menu', MenuScene);
scenes.register('game', GameScene);
scenes.register('bonus', BonusScene);
// Navigate (replaces entire stack)
await scenes.goto('game');
// Push overlay/modal (previous scene stays rendered)
await scenes.push('bonus', { multiplier: 3 });
// Pop back
await scenes.pop();
// Replace top scene
await scenes.replace('game');Transitions
import { TransitionType } from '@energy8platform/game-engine';
await scenes.goto('game', undefined, {
type: TransitionType.FADE,
duration: 500, // ms
});
await scenes.push('bonus', { data: 42 }, {
type: TransitionType.SLIDE_LEFT,
duration: 300,
easing: Easing.easeOutCubic,
});Lifecycle
GameApplication emits the following events:
| Event | Payload | When |
| --- | --- | --- |
| initialized | void | Engine initialized, PixiJS and SDK are ready |
| loaded | void | All asset bundles loaded |
| started | void | First scene entered, game loop running |
| resize | { width, height } | Viewport resized |
| orientationChange | Orientation | Device orientation changed |
| sceneChange | { from, to } | Scene transition completed |
| error | Error | An error occurred |
| destroyed | void | Engine destroyed |
game.on('resize', ({ width, height }) => {
console.log(`New size: ${width}x${height}`);
});
game.once('started', () => {
// Runs once after the first scene starts
});Assets
Asset Manifest
Assets are organized in named bundles:
const manifest: AssetManifest = {
bundles: [
{
name: 'preload',
assets: [
{ alias: 'logo', src: 'logo.png' },
],
},
{
name: 'game',
assets: [
{ alias: 'background', src: 'bg.webp' },
{ alias: 'symbols', src: 'symbols.json' },
{ alias: 'win-sound', src: 'sounds/win.mp3' },
],
},
{
name: 'bonus',
assets: [
{ alias: 'bonus-bg', src: 'bonus/bg.webp' },
],
},
],
};The preload bundle is loaded first (before the progress bar fills). All other bundles load together with a combined progress indicator.
AssetManager API
const assets = game.assets;
// Load on demand
await assets.loadBundle('bonus', (progress) => {
console.log(`${Math.round(progress * 100)}%`);
});
// Synchronous cache access
const texture = assets.get<Texture>('background');
// Background preloading (low priority)
await assets.backgroundLoad('bonus');
// Unload to free memory
await assets.unloadBundle('bonus');
// Check state
assets.isBundleLoaded('game'); // true
assets.getBundleNames(); // ['preload', 'game', 'bonus']Audio
AudioManager wraps @pixi/sound with category-based volume management. If @pixi/sound is not installed, all methods work silently as no-ops.
Audio Categories
Four categories with independent volume and mute controls: music, sfx, ui, ambient.
const audio = game.audio;
// Play a sound effect
audio.play('click', 'ui');
audio.play('coin-drop', 'sfx', { volume: 0.8 });
// Music with crossfade (smooth volume transition between tracks)
audio.playMusic('main-theme', 1000); // 1s crossfade
audio.stopMusic();
// Volume control
audio.setVolume('music', 0.3);
audio.muteCategory('sfx');
audio.unmuteCategory('sfx');
audio.toggleCategory('sfx'); // returns new state
// Global mute
audio.muteAll();
audio.unmuteAll();
audio.toggleMute(); // returns new state
// Music ducking (e.g., during a big win animation)
audio.duckMusic(0.2); // reduce to 20%
audio.unduckMusic(); // restoreMobile Audio Unlock
On iOS and many mobile browsers, audio cannot play until the first user interaction. The engine handles this automatically when tapToStart: true is set — the tap event serves as the audio context unlock.
Viewport & Scaling
ViewportManager handles responsive scaling using ResizeObserver with debouncing.
Scale Modes
| Mode | Behavior |
| --- | --- |
| FIT | Fits the entire design area inside the container. Adds letterbox (horizontal bars) or pillarbox (vertical bars) as needed. Industry standard for iGaming. |
| FILL | Fills the entire container, cropping edges. No bars, but some content may be hidden. |
| STRETCH | Stretches to fill the container. Distorts aspect ratio. Not recommended. |
const vp = game.viewport;
// Current dimensions
console.log(vp.width, vp.height, vp.scale);
console.log(vp.orientation); // 'landscape' | 'portrait'
// Reference design size
console.log(vp.designWidth, vp.designHeight);
// Force re-calculation
vp.refresh();
// Listen for changes
game.on('resize', ({ width, height }) => {
// Respond to viewport changes
});State Machine
StateMachine is a generic finite state machine with typed context, async hooks, guards, and per-frame updates.
import { StateMachine } from '@energy8platform/game-engine';
interface GameContext {
balance: number;
bet: number;
lastWin: number;
}
const fsm = new StateMachine<GameContext>({
balance: 10000,
bet: 100,
lastWin: 0,
});
fsm.addState('idle', {
enter: (ctx) => console.log('Waiting for player...'),
update: (ctx, dt) => { /* per-frame logic */ },
});
fsm.addState('spinning', {
enter: async (ctx) => {
// Play spin animation
await spinReels();
// Auto-transition to result
await fsm.transition('result');
},
});
fsm.addState('result', {
enter: async (ctx) => {
if (ctx.lastWin > 0) {
await showWinAnimation(ctx.lastWin);
}
await fsm.transition('idle');
},
});
// Guards
fsm.addGuard('idle', 'spinning', (ctx) => ctx.balance >= ctx.bet);
// Events
fsm.on('transition', ({ from, to }) => {
console.log(`${from} → ${to}`);
});
// Start
await fsm.start('idle');
// Trigger transitions
const success = await fsm.transition('spinning');
if (!success) {
console.log('Transition blocked by guard');
}
// Per-frame update (usually called from Scene.onUpdate)
fsm.update(dt);Animation
Tween
Tween provides a Promise-based animation system integrated with the PixiJS Ticker:
import { Tween, Easing } from '@energy8platform/game-engine';
// Animate to target values
await Tween.to(sprite, { alpha: 0, y: 100 }, 500, Easing.easeOutCubic);
// Animate from starting values to current
await Tween.from(sprite, { scale: 0 }, 300, Easing.easeOutBack);
// Animate between two sets of values
await Tween.fromTo(sprite, { x: -100 }, { x: 500 }, 1000, Easing.easeInOutQuad);
// Wait (uses PixiJS Ticker for consistent timing)
await Tween.delay(1000);
// Cancel tweens
Tween.killTweensOf(sprite);
Tween.killAll();
// Full reset — kill all tweens and remove ticker listener
// Useful for cleanup between game instances, tests, or hot-reload
Tween.reset();
// Supports nested properties
await Tween.to(sprite, { 'scale.x': 2, 'position.y': 300 }, 500);Timeline
Timeline chains sequential and parallel animation steps:
import { Timeline, Tween, Easing } from '@energy8platform/game-engine';
const tl = new Timeline();
tl.to(title, { alpha: 1, y: 0 }, 500, Easing.easeOutCubic)
.delay(200)
.parallel(
() => Tween.to(btn1, { alpha: 1 }, 300),
() => Tween.to(btn2, { alpha: 1 }, 300),
() => Tween.to(btn3, { alpha: 1 }, 300),
)
.call(() => console.log('Intro complete!'));
await tl.play();Available Easings
All 24 easing functions:
| Linear | Quad | Cubic | Quart |
| --- | --- | --- | --- |
| linear | easeInQuad | easeInCubic | easeInQuart |
| | easeOutQuad | easeOutCubic | easeOutQuart |
| | easeInOutQuad | easeInOutCubic | easeInOutQuart |
| Sine | Expo | Back | Bounce / Elastic |
| --- | --- | --- | --- |
| easeInSine | easeInExpo | easeInBack | easeOutBounce |
| easeOutSine | easeOutExpo | easeOutBack | easeInBounce |
| easeInOutSine | easeInOutExpo | easeInOutBack | easeInOutBounce |
| | | | easeOutElastic |
| | | | easeInElastic |
SpriteAnimation
Frame-based animation helper wrapping PixiJS AnimatedSprite. Cheaper than Spine for simple frame sequences — perfect for coin showers, symbol animations, sparkle trails, and win celebrations.
import { SpriteAnimation } from '@energy8platform/game-engine';
import { Assets } from 'pixi.js';
// From an array of textures
const coinAnim = SpriteAnimation.create(coinTextures, {
fps: 30,
loop: true,
});
scene.container.addChild(coinAnim);
// From a spritesheet using a name prefix
const sheet = Assets.get('effects');
const sparkle = SpriteAnimation.fromSpritesheet(sheet, 'sparkle_', {
fps: 24,
loop: true,
});
// From a numbered range (e.g., 'explosion_00' to 'explosion_24')
const explosion = SpriteAnimation.fromRange(sheet, 'explosion_{i}', 0, 24, {
fps: 60,
loop: false,
onComplete: () => explosion.destroy(),
});
// From pre-loaded texture aliases
const anim = SpriteAnimation.fromAliases(
['frame_0', 'frame_1', 'frame_2', 'frame_3'],
{ fps: 12 },
);
// Fire-and-forget: play once and auto-destroy
const { sprite, finished } = SpriteAnimation.playOnce(coinTextures, {
fps: 30,
});
scene.container.addChild(sprite);
await finished; // resolves when animation completesSpriteAnimationConfig
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| fps | number | 24 | Frames per second |
| loop | boolean | true | Whether to loop |
| autoPlay | boolean | true | Start playing immediately |
| onComplete | () => void | — | Callback when animation completes (non-looping) |
| anchor | number \| { x, y } | 0.5 | Anchor point |
Spine Animations
If @esotericsoftware/spine-pixi-v8 is installed:
import { SpineHelper } from '@energy8platform/game-engine';
// Create a Spine instance
const spine = await SpineHelper.create('character-skel', 'character-atlas', {
scale: 0.5,
});
container.addChild(spine);
// Play animation (returns Promise that resolves on completion)
await SpineHelper.playAnimation(spine, 'idle', true); // loop
// Queue animation
SpineHelper.addAnimation(spine, 'walk', 0.2, true);
// Skins
SpineHelper.setSkin(spine, 'warrior');
console.log(SpineHelper.getSkinNames(spine));
console.log(SpineHelper.getAnimationNames(spine));UI Components
UI components are built on top of
@pixi/uiand@pixi/layout(Yoga flexbox). Install them as peer dependencies:npm install @pixi/ui @pixi/layout yoga-layoutImportant: These packages are optional peer dependencies. Import UI components from the
@energy8platform/game-engine/uisub-path to ensure tree-shaking works correctly. The root@energy8platform/game-enginebarrel re-exports UI components but does not activate the@pixi/layoutmixin — that only happens when importing from/ui.Direct access: The engine wraps only the most common UI components. For anything else from
@pixi/ui(e.g.Slider,CheckBox,Input,Select,RadioGroup,List,DoubleSlider,Switcher) or@pixi/layout(e.g.LayoutContainer,LayoutView,Trackpad, layout-aware sprites), import directly from the source package:// Engine-wrapped components import { Button, Panel, Layout, ScrollContainer } from '@energy8platform/game-engine/ui'; // Raw @pixi/ui components (not wrapped by the engine) import { Slider, CheckBox, Input, Select, RadioGroup } from '@pixi/ui'; // Raw @pixi/layout components import { LayoutContainer, LayoutView } from '@pixi/layout/components'; import type { LayoutStyles } from '@pixi/layout';
Layout
Responsive layout container powered by @pixi/layout (Yoga flexbox engine). Supports horizontal, vertical, grid, and wrap modes with alignment, padding, gap, anchor positioning, and viewport breakpoints.
import { Layout } from '@energy8platform/game-engine';
// Horizontal toolbar anchored to bottom-center
const toolbar = new Layout({
direction: 'horizontal',
gap: 20,
alignment: 'center',
anchor: 'bottom-center',
padding: 16,
breakpoints: {
768: { direction: 'vertical', gap: 10 },
},
});
toolbar.addItem(spinButton);
toolbar.addItem(betLabel);
toolbar.addItem(balanceDisplay);
scene.container.addChild(toolbar);
// Update position on resize
toolbar.updateViewport(width, height);// Grid layout for a symbol paytable
const grid = new Layout({
direction: 'grid',
columns: 3,
gap: 16,
alignment: 'center',
anchor: 'center',
padding: [20, 40, 20, 40],
});
symbols.forEach((sym) => grid.addItem(sym));
grid.updateViewport(viewWidth, viewHeight);// Wrap layout — items flow and wrap to next line
const tags = new Layout({
direction: 'wrap',
gap: 8,
maxWidth: 600,
});LayoutConfig
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| direction | 'horizontal' \| 'vertical' \| 'grid' \| 'wrap' | 'vertical' | Layout direction |
| gap | number | 0 | Gap between children (px) |
| padding | number \| [top, right, bottom, left] | 0 | Inner padding |
| alignment | 'start' \| 'center' \| 'end' \| 'stretch' | 'start' | Cross-axis alignment |
| anchor | LayoutAnchor | 'top-left' | Position relative to viewport |
| columns | number | 2 | Column count (grid mode only) |
| maxWidth | number | Infinity | Max width before wrapping (wrap mode) |
| autoLayout | boolean | true | Auto-recalculate on add/remove |
| breakpoints | Record<number, Partial<LayoutConfig>> | — | Override config per viewport width |
Anchor values: top-left, top-center, top-right, center-left, center, center-right, bottom-left, bottom-center, bottom-right
Under the hood: Layout maps
direction→ YogaflexDirection/flexWrap,alignment→alignItems, and uses@pixi/layout'scontainer.layout = { ... }mixin. Grid mode usesflexGrow/flexBasiswhengap > 0to correctly distribute space, and percentage widths whengap === 0.
ScrollContainer
Scrollable container powered by @pixi/ui ScrollBox. Provides touch/drag scrolling, mouse wheel support, inertia, and dynamic rendering optimization for off-screen items.
import { ScrollContainer } from '@energy8platform/game-engine';
const scroll = new ScrollContainer({
width: 600,
height: 400,
direction: 'vertical',
elementsMargin: 8,
borderRadius: 12,
backgroundColor: 0x1a1a2e,
});
// Add items directly
for (let i = 0; i < 50; i++) {
scroll.addItem(createRow(i));
}
scene.container.addChild(scroll);// Or set content from a Container
const list = new Container();
items.forEach((item) => list.addChild(item));
scroll.setContent(list);
// Scroll to a specific item index
scroll.scrollToItem(5);
// Current position
const { x, y } = scroll.scrollPosition;ScrollContainerConfig
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| width | number | — | Visible viewport width |
| height | number | — | Visible viewport height |
| direction | 'vertical' \| 'horizontal' \| 'both' | 'vertical' | Scroll direction |
| backgroundColor | ColorSource | — | Background color (transparent if omitted) |
| borderRadius | number | 0 | Mask border radius |
| elementsMargin | number | 0 | Gap between items |
| padding | number | 0 | Content padding |
| disableDynamicRendering | boolean | false | Render all items even when off-screen |
| disableEasing | boolean | false | Disable scroll inertia/easing |
| globalScroll | boolean | true | Scroll even when mouse is not over the component |
Note: ScrollContainer extends
@pixi/uiScrollBox. AllScrollBoxmethods and options are available.
Button
Powered by @pixi/ui FancyButton. Supports both texture-based and Graphics-based rendering with per-state views, press animation, and built-in text.
import { Button } from '@energy8platform/game-engine';
const spinBtn = new Button({
width: 200,
height: 60,
borderRadius: 12,
colors: {
default: 0xffd700,
hover: 0xffe44d,
pressed: 0xccac00,
disabled: 0x666666,
},
pressScale: 0.95,
animationDuration: 100,
text: 'SPIN',
});
// Connect press event (Signal-based)
spinBtn.onPress.connect(() => {
console.log('Spin!');
});
// Or use texture-based states
const btn = new Button({
textures: {
default: 'btn-default',
hover: 'btn-hover',
pressed: 'btn-pressed',
disabled: 'btn-disabled',
},
});
btn.enable();
btn.disable();Breaking change: Button states renamed from
normaltodefault. TheonTapcallback is replaced byonPress.connect()(typed-signals Signal).
Label
import { Label } from '@energy8platform/game-engine';
const label = new Label({
text: 'TOTAL WIN',
style: { fontSize: 36, fill: 0xffffff },
});BalanceDisplay
Displays player balance with formatting:
import { BalanceDisplay } from '@energy8platform/game-engine';
const balance = new BalanceDisplay({
currency: 'USD',
animated: true,
// ... text style options
});
balance.setValue(9500); // Animates the balance changeWinDisplay
Animated win amount display with countup:
import { WinDisplay } from '@energy8platform/game-engine';
const winDisplay = new WinDisplay({
countupDuration: 2000,
// ... text style options
});
await winDisplay.showWin(5000); // Show $50.00, countup over 2 seconds
winDisplay.hide();ProgressBar
Horizontal progress bar powered by @pixi/ui ProgressBar. Supports animated smooth fill.
import { ProgressBar } from '@energy8platform/game-engine';
const bar = new ProgressBar({
width: 400,
height: 20,
fillColor: 0x00ff00,
trackColor: 0x333333,
borderRadius: 10,
animated: true,
});
bar.progress = 0.75; // 75%
// Call each frame for smooth animation
bar.update(dt);Panel
Background panel powered by @pixi/layout LayoutContainer. Supports both Graphics-based (color + border) and 9-slice sprite backgrounds. Children added to content participate in flexbox layout.
import { Panel } from '@energy8platform/game-engine';
// Simple colored panel
const panel = new Panel({
width: 600,
height: 400,
backgroundColor: 0x1a1a2e,
borderRadius: 16,
backgroundAlpha: 0.9,
padding: 16,
});
panel.content.addChild(myText);
// 9-slice panel (texture-based)
const nineSlicePanel = new Panel({
nineSliceTexture: 'panel-bg',
nineSliceBorders: [20, 20, 20, 20],
width: 400,
height: 300,
});Modal
Full-screen overlay dialog:
import { Modal } from '@energy8platform/game-engine';
const modal = new Modal({
overlayAlpha: 0.7,
overlayColor: 0x000000,
closeOnOverlay: true,
animationDuration: 300,
});
await modal.show(viewWidth, viewHeight);
await modal.hide();Toast
Brief notification messages with animated appearance/dismissal.
import { Toast } from '@energy8platform/game-engine';
const toast = new Toast({
duration: 2000,
bottomOffset: 60,
});
// Show with type and viewport size
await toast.show('Free spins activated!', 'success', viewWidth, viewHeight);
// Dismiss manually
await toast.dismiss();Toast types: info, success, warning, error — each with a distinct color.
Using Raw @pixi/ui and @pixi/layout Components
The engine wraps the most common components, but both libraries offer much more. Here are examples of using raw components alongside the engine:
import { Slider, CheckBox, Input, Select } from '@pixi/ui';
import { LayoutContainer } from '@pixi/layout/components';
import type { LayoutStyles } from '@pixi/layout';
// Slider (not wrapped by the engine)
const volumeSlider = new Slider({
bg: bgSprite,
fill: fillSprite,
slider: handleSprite,
min: 0,
max: 100,
value: 50,
});
// CheckBox
const muteCheck = new CheckBox({
style: {
unchecked: uncheckedView,
checked: checkedView,
},
});
// LayoutContainer with flexbox styles
const row = new LayoutContainer();
row.layout = {
flexDirection: 'row',
gap: 12,
alignItems: 'center',
padding: 16,
} satisfies LayoutStyles;
row.addChild(volumeSlider, muteCheck);Tip: Any container created after importing
@energy8platform/game-engine/uican use thecontainer.layout = { ... }mixin — the@pixi/layoutside-effect is automatically activated.
Input
InputManager provides unified touch/mouse/keyboard handling with gesture detection:
const input = game.input;
// Tap/click
input.on('tap', ({ x, y }) => {
console.log(`Tap at ${x}, ${y}`);
});
// Swipe gesture
input.on('swipe', ({ direction, velocity }) => {
console.log(`Swipe ${direction} at ${velocity}px/s`);
// direction: 'up' | 'down' | 'left' | 'right'
});
// Keyboard
input.on('keydown', ({ key, code }) => {
if (code === 'Space') startSpin();
});
// Check current key state
if (input.isKeyDown('ArrowLeft')) {
// Move left
}
// Lock input during animations
input.lock();
// ... animation plays ...
input.unlock();
// Convert DOM canvas position to game-world coordinates
// (accounts for viewport scaling and offset)
const worldPos = input.getWorldPosition(canvasX, canvasY);
console.log(worldPos.x, worldPos.y);Events: tap, press, release, move, swipe, keydown, keyup
Note: The viewport transform for coordinate mapping is wired up automatically by
GameApplication. CallgetWorldPosition()when you need to convert raw DOM coordinates to game-world space.
Vite Configuration
The engine provides a pre-configured Vite setup via defineGameConfig:
// vite.config.ts
import { defineGameConfig } from '@energy8platform/game-engine/vite';
export default defineGameConfig({
base: '/games/my-slot/',
devBridge: true, // Auto-inject DevBridge in dev mode
devBridgeConfig: './dev.config', // Custom config path (optional)
vite: {
// Additional Vite config overrides
},
});GameConfig
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| base | string | '/' | Vite base path for deployment |
| devBridge | boolean | false | Auto-inject DevBridge in dev mode |
| devBridgeConfig | string | './dev.config' | Path to DevBridge config file |
| vite | UserConfig | — | Additional Vite config to merge |
What defineGameConfig Provides
- Build target: ESNext — required for
yoga-layoutWASM (uses top-levelawait) - Asset inlining — files under 8KB are auto-inlined
- PixiJS chunk splitting —
pixi.jsis extracted into a separate chunk for caching - DevBridge injection — automatically available in dev mode via virtual module
- Dev server — port 3000, auto-open browser
- Dependency deduplication —
resolve.dedupeensures a single copy ofpixi.js,@pixi/layout,@pixi/ui, andyoga-layoutacross all packages (prevents registration issues when used as a linked dependency) - Dependency optimization —
pixi.js,@pixi/layout,@pixi/layout/components,@pixi/ui, andyoga-layout/loadare pre-bundled for faster dev starts;yoga-layoutmain entry is excluded from pre-bundling because it contains a top-levelawaitincompatible with esbuild's default target
Custom DevBridge Configuration
Create dev.config.ts at the project root:
// dev.config.ts
import type { DevBridgeConfig } from '@energy8platform/game-engine/debug';
export default {
balance: 50000,
currency: 'EUR',
networkDelay: 100,
onPlay: ({ action, bet }) => {
// Custom play result logic
const win = Math.random() < 0.3 ? bet * 10 : 0;
return { win, balance: 50000 - bet + win };
},
} satisfies DevBridgeConfig;This file is auto-imported by the Vite plugin when devBridge: true. The plugin injects a virtual module (/@dev-bridge-entry.js) that starts DevBridge before importing your app entry point, ensuring the MemoryChannel is ready when the SDK calls ready().
DevBridge
DevBridge simulates a casino host for local development. It uses the SDK's Bridge class in devMode, communicating with CasinoGameSDK through a shared in-memory MemoryChannel — no postMessage or iframe required.
Requires
@energy8platform/game-sdk>= 2.6.0
import { DevBridge } from '@energy8platform/game-engine/debug';
const bridge = new DevBridge({
balance: 10000,
currency: 'USD',
assetsUrl: '/assets/',
networkDelay: 200,
debug: true,
gameConfig: {
id: 'my-slot',
type: 'slot',
viewport: { width: 1920, height: 1080 },
betLevels: [0.1, 0.2, 0.5, 1, 2, 5, 10],
},
onPlay: ({ action, bet, roundId }) => {
// Return custom play result
const win = Math.random() < 0.4 ? bet * 5 : 0;
return { win };
},
});
bridge.start(); // Creates SDK Bridge({ devMode: true }) + registers handlers
// Update balance programmatically
bridge.setBalance(5000);
// Cleanup
bridge.destroy();When using the Vite plugin with devBridge: true, the SDK is automatically configured with devMode: true so both sides use the same MemoryChannel.
Handled Messages
| Message | Description |
| --- | --- |
| GAME_READY | SDK initialization handshake |
| PLAY_REQUEST | Player action (spin, deal, etc.) |
| PLAY_RESULT_ACK | Acknowledge play result |
| GET_BALANCE | Balance query |
| GET_STATE | Game state query |
| OPEN_DEPOSIT | Deposit dialog request |
Debug
FPS Overlay
import { FPSOverlay } from '@energy8platform/game-engine/debug';
const fps = new FPSOverlay(game.app);
fps.show();
fps.toggle();
fps.hide();When debug: true is set in GameApplicationConfig, the FPS overlay is created and shown automatically — no manual setup needed.
The overlay displays:
- Average FPS
- Minimum FPS
- Frame time (ms)
Updated every ~500ms, sampled over 60 frames.
API Reference
GameApplication
class GameApplication extends EventEmitter<GameEngineEvents> {
// Fields
app: Application;
scenes: SceneManager;
assets: AssetManager;
audio: AudioManager;
input: InputManager;
viewport: ViewportManager;
sdk: CasinoGameSDK | null;
initData: InitData | null;
readonly config: GameApplicationConfig;
// Getters
get gameConfig(): GameConfigData | null;
get session(): SessionData | null;
get balance(): number;
get currency(): string;
get isRunning(): boolean;
// Methods
constructor(config?: GameApplicationConfig);
async start(firstScene: string, sceneData?: unknown): Promise<void>;
destroy(): void;
}SceneManager
class SceneManager extends EventEmitter<{ change: { from: string | null; to: string } }> {
get current(): SceneEntry | null;
get currentKey(): string | null;
get isTransitioning(): boolean;
setRoot(root: Container): void;
register(key: string, ctor: SceneConstructor): this;
async goto(key: string, data?: unknown, transition?: TransitionConfig): Promise<void>;
async push(key: string, data?: unknown, transition?: TransitionConfig): Promise<void>;
async pop(transition?: TransitionConfig): Promise<void>;
async replace(key: string, data?: unknown, transition?: TransitionConfig): Promise<void>;
update(dt: number): void;
resize(width: number, height: number): void;
destroy(): void;
}AssetManager
class AssetManager {
get initialized(): boolean;
get basePath(): string;
get loadedBundles(): ReadonlySet<string>;
constructor(basePath?: string, manifest?: AssetManifest);
async init(): Promise<void>;
async loadBundle(name: string, onProgress?: (p: number) => void): Promise<Record<string, unknown>>;
async loadBundles(names: string[], onProgress?: (p: number) => void): Promise<Record<string, unknown>>;
async load<T>(urls: string | string[], onProgress?: (p: number) => void): Promise<T>;
get<T>(alias: string): T;
async unloadBundle(name: string): Promise<void>;
async backgroundLoad(name: string): Promise<void>;
getBundleNames(): string[];
isBundleLoaded(name: string): boolean;
}AudioManager
class AudioManager {
get initialized(): boolean;
get muted(): boolean;
constructor(config?: AudioConfig);
async init(): Promise<void>;
play(alias: string, category?: AudioCategoryName, options?: { volume?: number; loop?: boolean; speed?: number }): void;
playMusic(alias: string, fadeDuration?: number): void;
stopMusic(): void;
stopAll(): void;
setVolume(category: AudioCategoryName, volume: number): void;
getVolume(category: AudioCategoryName): number;
muteCategory(category: AudioCategoryName): void;
unmuteCategory(category: AudioCategoryName): void;
toggleCategory(category: AudioCategoryName): boolean;
muteAll(): void;
unmuteAll(): void;
toggleMute(): boolean;
duckMusic(factor: number): void;
unduckMusic(): void;
destroy(): void;
}ViewportManager
class ViewportManager extends EventEmitter<ViewportEvents> {
get width(): number;
get height(): number;
get scale(): number;
get orientation(): Orientation;
get designWidth(): number;
get designHeight(): number;
constructor(app: Application, container: HTMLElement, config: ViewportConfig);
refresh(): void;
destroy(): void;
}StateMachine
class StateMachine<TContext> extends EventEmitter<StateMachineEvents> {
get current(): string | null;
get isTransitioning(): boolean;
get context(): TContext;
constructor(context: TContext);
addState(name: string, config: { enter?, exit?, update? }): this;
addGuard(from: string, to: string, guard: (ctx: TContext) => boolean): this;
async start(initialState: string, data?: unknown): Promise<void>;
async transition(to: string, data?: unknown): Promise<boolean>;
update(dt: number): void;
hasState(name: string): boolean;
canTransition(to: string): boolean;
async reset(): Promise<void>;
async destroy(): Promise<void>;
}Tween
class Tween {
static get activeTweens(): number;
static to(target: any, props: Record<string, number>, duration: number, easing?: EasingFunction, onUpdate?: (p: number) => void): Promise<void>;
static from(target: any, props: Record<string, number>, duration: number, easing?, onUpdate?): Promise<void>;
static fromTo(target: any, fromProps: Record<string, number>, toProps: Record<string, number>, duration: number, easing?, onUpdate?): Promise<void>;
static delay(ms: number): Promise<void>; // Uses PixiJS Ticker
static killTweensOf(target: any): void;
static killAll(): void;
static reset(): void; // Kill all + remove ticker listener
}Timeline
class Timeline {
get isPlaying(): boolean;
to(target: any, props: Record<string, number>, duration: number, easing?: EasingFunction): this;
from(target: any, props: Record<string, number>, duration: number, easing?: EasingFunction): this;
delay(ms: number): this;
call(fn: () => void | Promise<void>): this;
parallel(...fns: Array<() => Promise<void>>): this;
async play(): Promise<void>;
cancel(): void;
clear(): this;
}InputManager
class InputManager extends EventEmitter<InputEvents> {
get locked(): boolean;
constructor(canvas: HTMLCanvasElement);
lock(): void;
unlock(): void;
isKeyDown(key: string): boolean;
setViewportTransform(scale: number, offsetX: number, offsetY: number): void;
getWorldPosition(canvasX: number, canvasY: number): { x: number; y: number };
destroy(): void;
}Layout
Powered by
@pixi/layout(Yoga flexbox). Uses the@pixi/layoutmixin on a standardContainer.
class Layout extends Container {
get items(): readonly Container[];
constructor(config?: LayoutConfig);
addItem(child: Container): this;
removeItem(child: Container): this;
clearItems(): this;
updateViewport(width: number, height: number): void;
}ScrollContainer
Extends
@pixi/uiScrollBox — inherits touch/drag scrolling, mouse wheel, inertia, and dynamic rendering.
class ScrollContainer extends ScrollBox {
get scrollPosition(): { x: number; y: number };
constructor(config: ScrollContainerConfig);
setContent(content: Container): void;
addItem(...items: Container[]): Container;
scrollToItem(index: number): void;
}Button
Extends
@pixi/uiFancyButton — per-state views, press animation, and text.
class Button extends FancyButton {
get disabled(): boolean;
constructor(config?: ButtonConfig);
enable(): void;
disable(): void;
// Inherited from FancyButton
onPress: Signal; // btn.onPress.connect(() => { … })
enabled: boolean;
}Panel
Extends
@pixi/layout/componentsLayoutContainer — flex-layout background panel with optional 9-slice texture.
class Panel extends LayoutContainer {
get content(): Container; // children added here participate in flexbox layout
constructor(config?: PanelConfig);
setSize(width: number, height: number): void;
}ProgressBar
Wraps
@pixi/uiProgressBar — optional smooth animated fill via per-frameupdate().
class ProgressBar extends Container {
get progress(): number;
set progress(value: number); // 0..1
constructor(config?: ProgressBarConfig);
update(dt: number): void; // call each frame when animated: true
}Toast
Lightweight toast using
Graphicsfor the background. Supports info / success / warning / error types.
class Toast extends Container {
constructor(config?: ToastConfig);
async show(message: string, type?: ToastType, viewWidth?: number, viewHeight?: number): Promise<void>;
async dismiss(): Promise<void>;
}SpriteAnimation
class SpriteAnimation {
static create(textures: Texture[], config?: SpriteAnimationConfig): AnimatedSprite;
static fromSpritesheet(sheet: Spritesheet, prefix: string, config?: SpriteAnimationConfig): AnimatedSprite;
static fromRange(sheet: Spritesheet, pattern: string, start: number, end: number, config?: SpriteAnimationConfig): AnimatedSprite;
static fromAliases(aliases: string[], config?: SpriteAnimationConfig): AnimatedSprite;
static playOnce(textures: Texture[], config?: SpriteAnimationConfig): { sprite: AnimatedSprite; finished: Promise<void> };
static getTexturesByPrefix(sheet: Spritesheet, prefix: string): Texture[];
}EventEmitter
class EventEmitter<TEvents extends {}> {
on<K>(event: K, handler: (data: TEvents[K]) => void): this;
once<K>(event: K, handler: (data: TEvents[K]) => void): this;
off<K>(event: K, handler: (data: TEvents[K]) => void): this;
// Void events can be emitted without a data argument
emit<K>(event: K, ...args): void;
removeAllListeners(event?: keyof TEvents): this;
}License
MIT
