Skip to content
On this page

My PIXI-based html5 2d game engine has 5 components so far:

engine

This is the facade that manages all of the rest.

It also is an instance of yog/pubsub, and that will be one of the two event buses for the game engine, the other being the active scene. (See 'show' and 'hide' in the next code sample for how to emit an event that has no attributes.)

If there is a list of images, init() returns a promise for the images_loaded event.

import {api1 as engine} from '@ashnazg/mischievous';
var header_nav_height = 98;
var configs = {
	render: {height: -header_nav_height},
	scene: {fps:30, worldWidth: 1000, worldHeight: 1000, minWidth: 100, maxWidth: 1000},
	images: ['ship1.png']
};
engine.init(gamediv_element, configs).then(() => console.log("images are all loaded"));
engine('hide'); // results in the gamediv_element becoming display:none and the event loop will pause.
engine('show'); // results in the gamediv_element becoming display:block and the event loop will resume.
engine.destroy();

Engine exposes factory methods on engine.new that you can use to create scenes and entities.

var scrolly_scenegraph = engine.new.viewport(...);
scrolly_scenegraph.on('tick', updateThings); // all scenes must have a 'tick' subscriber.
var fixed_scenegraph = engine.new.scene();
var ship_entity = engine.new.png('spaceship.png', clickHandler);

If you need to get handles to the sub libraries:

var extra_clock = engine.clock.createClock(hook, 33);
engine.sprites.foo();

Render config

  • height/width: set these to either:
    • the width and height you want to pin the display size to
    • zero or a negative integer to use the screen's dimensions (negative values will be subtracted from the screen's dimension; this allows you to exclude the size of headers or sidebars from the resulting screen-based dimensions.)
  • any other property that you can pass to PIXI.autoDetectRenderer()

Scene config

  • fps defaults to 30 (as we're syncopating between RAF and setTimeout as an energy saver, you might need to tweak this to be faster than your target framerate to get your average where you want it.)
  • if worldWidth is not set, the initial stage will be a fixed one (just a plain PIXI.Container); if it is set, a pixi-viewport will be the initial stage
    • in viewport mode, these scene config fields are used:
      • worldWidth and worldHeight control the logical game units
      • minWidth and maxWidth control viewport's zooming limits

idempotence

This engine is not built with multiple webGL contexts in mind. It is built to work inside a hot-loading vue dev stack without creating multiple webGLs, so engine is a singleton that has a singleton renderer.

engine.init() returns a promise; it'll resolve only if this is a proper new renderer.

engine.destroy() kills the renderer singleton, so at that point you can call init() again.

Important note: destroy is about removing any dependency on either a div managed by the parent app or an instance of PIXI renderer; it's not about reseting the game logic or actors. tick events stop flowing while there's no place to render, but the intended usage is that when your PWA goes back to a game-render page, everything just picks up where it left off and renders to the new div/context.

The downside is that since the listener lists aren't purged by a renderer destroy, you'd better remember to use engine.off(hook) for any listeners touching vue instances, or they'll leak and continue being called in the new renderer's stack as well.

core

This sets up the PIXI renderer and turns off OSX-browser pinching events outside the webGL display. It's configured by the {render} field on engine.init.

clock

This is the render/tick clock; it uses both setTimeout and RPAF/RAF to avoid running faster than requested or running at all when the browser tab is not visible, as we don't want to suck more phone battery than needed.

On each game tick, the scenegraph layer (see 'scenes' below) gets called with the time delta since the last tick.

deltas

  • On each tick, the engine, then the scene, gets a 'tick' event with {global_delta,scene_delta}

I thought about supporting scene push/pop during tick, but it feels like a bad idea to have some of your ticks think scene-a is active, and others in the same tick think it's scene-b. (Also, if the active scene has swapped, that newly active scene has just reset counting ms between ticks, so that would result in scene_delta being able to go negative.)

So don't. You should do your scene swapping outside the tick/render loop. (even a simple setTimeout(..., 0) works.)

scenes

This is where the creation, update, and rendering core loop comes together. scenes.js defines the scene stack manager; each scene is a PIXI.container-oid and a list of 'agents' that are called on every tick.

A scene, like the main engine itself, is a pubsub. Both the active scene and the engine get 'tick' events right before the rendering happens.

There's always exactly one active scene, and you can access it as engine.scene; pushing your new scene means that it is the only one interacting, updating, or rendering til you pop() it.

engine.scene.agents.forEach(agent => console.log("this agent does stuff on every tick:", agent));

engine.push(fixed_scenegraph);
engine.scenes.forEach(scene => {
	if (scene !== engine.scene) console.log("I'm paused while someone else is 'modal' over me", scene);
});
engine.pop();

engine's push/pop need to be used instead of the raw array's, becuase these also manage visibility events and tick timing data.

TODO

  • fixed scenes need pointertap events streaming into my event bus, with the same schema as viewport's adapter.
  • engine(w/h) should just use the parent div's size when those init configs aren't set.

toWorld

a scene has a toWorld({x,y}) => {x,y} that translates from canvas-click coordinates to in-game coordinates. (Which, if you're using a fixed scene and not a viewport, is a noop.)

sprites

Mostly this lib just adds factory methods like engine.new.png('ship1.png', clickHandler); but if you need to load more images after init({images:[]}) has already been called, then know that that init field is just a convenience wrapper around:

engine.sprites.loadImages(['ship1.png', 'ship2.png']).then(startGame);

TODO stronger error handling

Notes about possible non-PIXI projects

TODO: read these

JavaScript/Bash source released under the MIT License.