English | Chinese
Eva.js is a front-end game engine specifically for creating interactive game projects.
Easy to Use: Eva.js provides out-of-box game components for developers to use right away. Yes, it's simple and elegant!
High-performance: Eva.js is powered by efficient runtime and rendering pipeline (Pixi.JS) which makes it possible to unleash the full potential of your device.
Scalability: Thanks to the ECS(Entity-Component-System) structure, you can expand your needs by highly customizable APIs. The only limitation is your imagination!
You can find the Eva.js Documentation on eva.js.org, we appreciate your devotion by sending pull requests to this repository.
Checking out the Live example.
| Package | Description |
|---|---|
@eva/eva.js |
Core engine: Game, GameObject, Component, System, Resource |
@eva/plugin-renderer |
Core renderer (PixiJS) |
@eva/plugin-renderer-img |
Image rendering |
@eva/plugin-renderer-text |
Text rendering (Text, HTMLText, BitmapText) |
@eva/plugin-renderer-sprite |
Sprite sheet rendering |
@eva/plugin-renderer-sprite-animation |
Frame animation |
@eva/plugin-renderer-spine |
Spine skeleton animation |
@eva/plugin-renderer-dragonbone |
DragonBones skeleton animation |
@eva/plugin-renderer-lottie |
Lottie animation |
@eva/plugin-renderer-graphics |
Vector graphics drawing |
@eva/plugin-renderer-nine-patch |
Nine-slice scaling |
@eva/plugin-renderer-tiling-sprite |
Tiling sprite |
@eva/plugin-renderer-mask |
Mask / clipping |
@eva/plugin-renderer-mesh |
Perspective mesh deformation |
@eva/plugin-renderer-render |
Render properties (alpha, zIndex, visible) |
@eva/plugin-renderer-event |
Touch / pointer events |
@eva/plugin-sound |
Audio playback |
@eva/plugin-transition |
Tween animation |
@eva/plugin-a11y |
Accessibility |
@eva/plugin-evax |
Global state management |
@eva/plugin-matterjs |
Physics engine (Matter.js) |
@eva/plugin-layout |
Flexbox layout |
@eva/plugin-stats |
Performance monitor |
npm i @eva/eva.js @eva/plugin-renderer @eva/plugin-renderer-img --save<canvas id="canvas"></canvas>import { Game, GameObject, resource, RESOURCE_TYPE } from '@eva/eva.js';
import { RendererSystem } from '@eva/plugin-renderer';
import { Img, ImgSystem } from '@eva/plugin-renderer-img';
resource.addResource([
{
name: 'imageName',
type: RESOURCE_TYPE.IMAGE,
src: {
image: {
type: 'png',
url: 'https://gw.alicdn.com/tfs/TB1DNzoOvb2gK0jSZK9XXaEgFXa-658-1152.webp',
},
},
preload: true,
},
]);
const game = new Game();
await game.init({
systems: [
new RendererSystem({
canvas: document.querySelector('#canvas'),
width: 750,
height: 1000,
}),
new ImgSystem(),
],
});
const image = new GameObject('image', {
size: { width: 750, height: 1319 },
origin: { x: 0, y: 0 },
position: { x: 0, y: -319 },
anchor: { x: 0, y: 0 },
});
image.addComponent(
new Img({
resource: 'imageName',
}),
);
game.scene.addChild(image);Game engine entry. Manages systems, scenes, and the game loop.
import { Game } from '@eva/eva.js';
const game = new Game();
await game.init({
autoStart: true, // auto start the game loop (default: true)
frameRate: 60, // target frame rate (default: 60)
systems: [], // systems to register
needScene: true, // auto create default scene (default: true)
});| Method | Description |
|---|---|
addSystem(system) |
Register a system |
removeSystem(system) |
Remove a system |
getSystem(SystemClass) |
Get registered system instance |
start() |
Start the game loop |
pause() |
Pause the game loop |
resume() |
Resume the game loop |
destroy() |
Destroy the game |
loadScene({ scene, mode?, params? }) |
Load a scene |
findByName(name) |
Find first GameObject by name |
findAllByName(name) |
Find all GameObjects by name |
| Property | Description |
|---|---|
scene |
Current main scene |
playing |
Whether the game is running |
ticker |
Ticker instance |
systems |
Registered systems array |
Entity in the ECS architecture. Holds components and supports parent-child hierarchy.
import { GameObject } from '@eva/eva.js';
const go = new GameObject('name', {
position: { x: 0, y: 0 },
size: { width: 100, height: 100 },
origin: { x: 0, y: 0 }, // transform origin
anchor: { x: 0.5, y: 0.5 }, // anchor point
scale: { x: 1, y: 1 },
rotation: 0, // radians
skew: { x: 0, y: 0 },
});| Method | Description |
|---|---|
addComponent(component) |
Add a component instance |
addComponent(ComponentClass, params) |
Add component by class + params |
removeComponent(component) |
Remove a component |
getComponent(ComponentClass) |
Get component by class |
addChild(gameObject) |
Add child GameObject |
removeChild(gameObject) |
Remove child GameObject |
remove() |
Remove self from parent |
destroy() |
Destroy self and all children |
| Property | Description |
|---|---|
transform |
Transform component |
parent |
Parent GameObject |
children |
Child GameObjects |
scene |
Scene this object belongs to |
Base class for all components.
import { Component } from '@eva/eva.js';
class MyComponent extends Component {
static componentName = 'MyComponent';
init(params) {} // called when added to GameObject
awake() {} // called after init
start() {} // called before first update
update({ deltaTime, time, fps }) {}
lateUpdate({ deltaTime }) {}
onPause() {}
onResume() {}
onDestroy() {}
}Base class for all systems. Processes components each frame.
import { System } from '@eva/eva.js';
class MySystem extends System {
static systemName = 'MySystem';
init(params) {}
awake() {}
start() {}
update({ deltaTime, time, fps }) {}
lateUpdate({ deltaTime }) {}
onPause() {}
onResume() {}
onDestroy() {}
}Global resource manager singleton.
import { resource, RESOURCE_TYPE, LOAD_EVENT } from '@eva/eva.js';
// Add resources
resource.addResource([
{
name: 'img',
type: RESOURCE_TYPE.IMAGE,
src: { image: { type: 'png', url: 'path/to/image.png' } },
preload: true,
},
]);
// Preload all preload:true resources
resource.preload();
// Listen to loading progress
resource.on(LOAD_EVENT.PROGRESS, (progress) => {}); // 0-1
resource.on(LOAD_EVENT.COMPLETE, () => {});
resource.on(LOAD_EVENT.ERROR, (err) => {});
// Get resource (async)
const res = await resource.getResource('img');
// Destroy resource
resource.destroy('img');RESOURCE_TYPE: IMAGE, SPRITE, SPRITE_ANIMATION, AUDIO, VIDEO, FONT
Core rendering system powered by PixiJS. Required by all renderer plugins.
import { RendererSystem } from '@eva/plugin-renderer';
new RendererSystem({
canvas: document.querySelector('#canvas'),
width: 750,
height: 1000,
preference: 'webgl', // 'webgl' | 'webgpu' | 'canvas'
backgroundAlpha: 1, // 0=fully transparent, 1=opaque
antialias: false,
resolution: window.devicePixelRatio,
backgroundColor: 0x000000,
enableScroll: false,
debugMode: false,
});| Method | Description |
|---|---|
resize(width, height) |
Resize the canvas |
Render a single image.
import { Img, ImgSystem } from '@eva/plugin-renderer-img';
// Register system
game.addSystem(new ImgSystem());
// Add component
go.addComponent(new Img({ resource: 'imageName' }));| Param | Type | Description |
|---|---|---|
resource |
string |
Resource name (IMAGE type) |
Render a sub-image from a sprite sheet.
import { Sprite, SpriteSystem } from '@eva/plugin-renderer-sprite';
game.addSystem(new SpriteSystem());
go.addComponent(new Sprite({
resource: 'spriteName',
spriteName: 'frame01.png',
}));| Param | Type | Description |
|---|---|---|
resource |
string |
Resource name (SPRITE type) |
spriteName |
string |
Sub-image name in the sprite sheet |
Play frame-by-frame animation from a sprite sheet.
import { SpriteAnimation, SpriteAnimationSystem } from '@eva/plugin-renderer-sprite-animation';
game.addSystem(new SpriteAnimationSystem());
const anim = go.addComponent(new SpriteAnimation({
resource: 'animResource',
autoPlay: true,
speed: 100, // ms per frame
forwards: false, // stop at last frame when done
}));
anim.play(3); // play 3 times
anim.gotoAndPlay(5); // jump to frame 5 and play
anim.gotoAndStop(0); // jump to frame 0 and stop
anim.stop();| Param | Type | Default | Description |
|---|---|---|---|
resource |
string |
Resource name (SPRITE_ANIMATION type) | |
autoPlay |
boolean |
true |
Auto play on load |
speed |
number |
100 |
Milliseconds per frame |
forwards |
boolean |
false |
Freeze on last frame when complete |
| Property | Description |
|---|---|
currentFrame |
Current frame number |
totalFrames |
Total frame count |
| Event | Description |
|---|---|
complete |
All play iterations finished |
loop |
Each loop iteration |
frameChange |
Frame changed |
Render text content with three rendering modes.
import { Text, HTMLText, BitmapText, TextSystem } from '@eva/plugin-renderer-text';
game.addSystem(new TextSystem());
// Canvas Text
go.addComponent(new Text({
text: 'Hello World',
style: {
fontFamily: 'Arial',
fontSize: 36,
fill: 0xff1010,
stroke: { color: 0xffffff, width: 5 },
fontWeight: 'bold',
wordWrap: true,
wordWrapWidth: 200,
align: 'center',
dropShadow: {
alpha: 1, angle: Math.PI / 6,
blur: 5, color: 0x000000, distance: 5,
},
},
}));
// HTML Rich Text (supports <b>, <i>, <span>, <br> tags)
go.addComponent(new HTMLText({
text: '<b>Bold</b> and <i>italic</i>',
style: {
fontFamily: 'Arial',
fontSize: 24,
fill: 0x000000,
wordWrap: true,
wordWrapWidth: 300,
},
}));
// Bitmap Text (using bitmap font resource)
go.addComponent(new BitmapText({
text: 'Score: 100',
style: {
fontFamily: 'myBitmapFont',
fontSize: 32,
},
}));Draw vector shapes using PixiJS Graphics API.
import { Graphics, GraphicsSystem } from '@eva/plugin-renderer-graphics';
game.addSystem(new GraphicsSystem());
const comp = go.addComponent(new Graphics());
// Use PixiJS Graphics API directly
comp.graphics.rect(0, 0, 100, 100);
comp.graphics.fill(0xff0000);
comp.graphics.circle(50, 50, 30);
comp.graphics.fill(0x00ff00);Nine-slice scaling. Corners stay fixed while edges and center stretch.
import { NinePatch, NinePatchSystem } from '@eva/plugin-renderer-nine-patch';
game.addSystem(new NinePatchSystem());
go.addComponent(new NinePatch({
resource: 'panelImg',
leftWidth: 20,
topHeight: 20,
rightWidth: 20,
bottomHeight: 20,
}));| Param | Type | Description |
|---|---|---|
resource |
string |
Image or sprite resource name |
spriteName |
string |
Sub-image name (when using SPRITE resource) |
leftWidth |
number |
Left non-stretch width |
topHeight |
number |
Top non-stretch height |
rightWidth |
number |
Right non-stretch width |
bottomHeight |
number |
Bottom non-stretch height |
Repeating tiled texture within a region.
import { TilingSprite, TilingSpriteSystem } from '@eva/plugin-renderer-tiling-sprite';
game.addSystem(new TilingSpriteSystem());
go.addComponent(new TilingSprite({
resource: 'bgTexture',
tileScale: { x: 1, y: 1 },
tilePosition: { x: 0, y: 0 },
}));| Param | Type | Default | Description |
|---|---|---|---|
resource |
string |
Image resource name | |
tileScale |
{x, y} |
{x:1, y:1} |
Tile scale |
tilePosition |
{x, y} |
{x:0, y:0} |
Tile offset |
Clip the display area of a GameObject.
import { Mask, MaskSystem, MASK_TYPE } from '@eva/plugin-renderer-mask';
game.addSystem(new MaskSystem());
// Circle mask
go.addComponent(new Mask({
type: MASK_TYPE.Circle,
style: { x: 50, y: 50, radius: 50 },
}));
// Rect mask
go.addComponent(new Mask({
type: MASK_TYPE.Rect,
style: { x: 0, y: 0, width: 200, height: 100 },
}));
// Image mask (alpha-based)
go.addComponent(new Mask({
type: MASK_TYPE.Img,
resource: 'maskImg',
style: { x: 0, y: 0, width: 200, height: 200 },
}));MASK_TYPE: Circle, Ellipse, Rect, RoundedRect, Polygon, Img, Sprite
Perspective mesh deformation by adjusting four corner points.
import { PerspectiveMesh, MeshSystem } from '@eva/plugin-renderer-mesh';
game.addSystem(new MeshSystem());
const mesh = go.addComponent(new PerspectiveMesh({
resource: 'cardImg',
verticesX: 10, // horizontal vertex count
verticesY: 10, // vertical vertex count
}));
// Set four corners (x0,y0 x1,y1 x2,y2 x3,y3)
// top-left, top-right, bottom-right, bottom-left
mesh.setCorners(0, 0, 200, 20, 180, 300, 20, 280);Control render properties: visibility, transparency, z-order.
import { Render, RenderSystem } from '@eva/plugin-renderer-render';
game.addSystem(new RenderSystem());
go.addComponent(new Render({
alpha: 1, // opacity 0-1
visible: true,
zIndex: 0,
sortableChildren: false,
resolution: 1,
}));Add touch/pointer interaction to GameObjects.
import { Event, EventSystem, HIT_AREA_TYPE } from '@eva/plugin-renderer-event';
game.addSystem(new EventSystem());
const evt = go.addComponent(new Event({
hitArea: {
type: HIT_AREA_TYPE.Rect,
style: { x: 0, y: 0, width: 100, height: 100 },
},
}));
evt.on('tap', (e) => {
console.log('tapped!', e.data.position);
});
evt.on('touchstart', (e) => {
e.stopPropagation(); // stop bubbling
});Events: tap, touchstart, touchmove, touchend, touchendoutside, touchcancel
Event data:
data.position- global coordinates{x, y}data.localPosition- local coordinates{x, y}data.pointerId- pointer IDgameObject- target GameObjectstopPropagation()- stop event bubbling
HIT_AREA_TYPE: Circle, Ellipse, Polygon, Rect, RoundedRect
Play Spine skeleton animations. Use @eva/plugin-renderer-spine36 for Spine 3.6 format.
import { Spine, SpineSystem } from '@eva/plugin-renderer-spine';
game.addSystem(new SpineSystem());
const spine = go.addComponent(new Spine({
resource: 'spineRes',
animationName: 'idle',
autoPlay: true,
scale: 1,
}));
spine.play('walk', true); // play looping
spine.stop();
spine.addAnimation('attack', 0, false); // queue animation
spine.setMix('idle', 'walk', 0.2); // transition blend
spine.setAttachment('weapon', 'sword'); // slot attachment (skin swap)
spine.getBone('head'); // get bone
// Mount a GameObject to a spine slot
spine.addSlotObject('hand', weaponGameObject);
spine.removeSlotObject(weaponGameObject);| Event | Description |
|---|---|
complete |
Animation complete |
start |
Animation started |
end |
Animation ended |
event |
Spine event triggered |
interrupt |
Animation interrupted |
Play Lottie (After Effects) animations.
import { Lottie, LottieSystem } from '@eva/plugin-renderer-lottie';
game.addSystem(new LottieSystem());
const lottie = go.addComponent(new Lottie({
resource: 'lottieRes',
autoStart: false,
}));
// Play frame range [0, 60], loop infinitely
lottie.play([0, 60], { repeats: -1 });
// Play with slot replacement
lottie.play([0, 100], {
slot: [
{ type: 'IMAGE', name: 'layerName', url: 'newImg.png' },
{ type: 'TEXT', name: 'textLayer', value: 'New Text', style: { fontSize: 24 } },
],
});
// Tap interaction on named layer
lottie.onTap('buttonLayer', () => console.log('clicked'));| Event | Description |
|---|---|
complete |
Play complete |
loopComplete |
Loop iteration |
enterFrame |
Each frame |
Audio playback based on Web Audio API.
import { Sound, SoundSystem } from '@eva/plugin-sound';
game.addSystem(new SoundSystem({
autoPauseAndStart: true, // sync with game pause/resume
}));
const sound = go.addComponent(new Sound({
resource: 'bgm',
autoplay: false,
loop: true,
volume: 0.8,
speed: 1,
muted: false,
onEnd: () => console.log('done'),
}));
sound.play();
sound.pause();
sound.resume();
sound.stop();
sound.volume = 0.5;
sound.muted = true;| Property | Description |
|---|---|
playing |
Whether sound is playing |
volume |
Volume (0-1) |
muted |
Mute state |
state |
'unloaded' / 'loading' / 'loaded' |
Tween animation with keyframes and easing.
import { Transition, TransitionSystem } from '@eva/plugin-transition';
game.addSystem(new TransitionSystem());
const render = go.addComponent(new Render({ alpha: 1 }));
const transition = go.addComponent(new Transition({
group: {
fadeIn: [
{
name: 'alpha',
component: render,
values: [
{ time: 0, value: 0, tween: 'ease-in' },
{ time: 1000, value: 1 },
],
},
],
moveRight: [
{
name: 'position.x',
component: go.transform,
values: [
{ time: 0, value: 0, tween: 'ease-out' },
{ time: 500, value: 300 },
],
},
],
},
}));
transition.play('fadeIn');
transition.play('moveRight', Infinity); // loop forever
transition.stop('fadeIn');
transition.on('finish', (name) => console.log(`${name} finished`));Easing functions: linear, ease-in, ease-out, ease-in-out, bounce-in, bounce-out, bounce-in-out
Accessibility support. Creates transparent DOM overlay with ARIA attributes over canvas.
import { A11y, A11ySystem, A11yActivate } from '@eva/plugin-a11y';
game.addSystem(new A11ySystem({
debug: false,
activate: A11yActivate.CHECK, // CHECK | ENABLE | DISABLE
delay: 100,
}));
go.addComponent(new A11y({
hint: 'Play button',
role: 'button',
'aria-label': 'Start the game',
}));2D physics powered by Matter.js.
import { Physics, PhysicsSystem, PhysicsType } from '@eva/plugin-matterjs';
game.addSystem(new PhysicsSystem({
resolution: 1,
isTest: false, // debug render
world: {
gravity: { x: 0, y: 1 },
},
}));
// Rectangle body
go.addComponent(new Physics({
type: PhysicsType.RECTANGLE,
bodyOptions: {
isStatic: false,
restitution: 0.8,
density: 0.01,
},
}));
// Circle body
go.addComponent(new Physics({
type: PhysicsType.CIRCLE,
radius: 25,
bodyOptions: { restitution: 1 },
}));
// Polygon body
go.addComponent(new Physics({
type: PhysicsType.POLYGON,
sides: 6,
radius: 30,
}));PhysicsType: RECTANGLE, CIRCLE, POLYGON
Flexbox-like layout system.
import { Layout, LayoutChild, LayoutSystem } from '@eva/plugin-layout';
game.addSystem(new LayoutSystem());
// Container
container.addComponent(new Layout({
direction: 'row', // 'row' | 'column'
justifyContent: 'center', // 'start' | 'center' | 'end' | 'space-between' | 'space-around'
alignItems: 'center', // 'start' | 'center' | 'end' | 'stretch'
gap: 10,
padding: [10, 20], // number | [v,h] | [top,right,bottom,left]
autoSize: true, // auto-fit container size
}));
// Child item
child.addComponent(new LayoutChild({
flexGrow: 1,
flexShrink: 0,
alignSelf: 'center',
margin: 5,
fixedSize: { width: 100 },
}));Performance monitoring panel displaying FPS and other metrics.
import { Stats, StatsSystem } from '@eva/plugin-stats';
game.addSystem(new StatsSystem({
show: true,
style: { x: 0, y: 0, width: 200, height: 100 },
}));
go.addComponent(new Stats());For questions and support please use Gitter or WeChat (微信) to scan this QR Code.
Please make sure to read the Issue Reporting Checklist before opening an issue. Issues not conforming to the guidelines may be closed immediately.
release notes in documentation.
The Eva.js is released under the MIT license. See LICENSE file.
