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
- in viewport mode, these scene config fields are used:
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.
- TODO: evaluate afterframe as a polyfill for RPAF
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
- in case assets don't load successfully, I must build error handling.
- this is the underlying class: https://englercj.github.io/resource-loader/classes/loader.html#onerrorsignal according to https://pixijs.download/dev/docs/PIXI.Loader.html
- an example of errors under heavy asset loading: https://www.html5gamedevs.com/topic/44529-loader-reload-failed-resources/
Notes about possible non-PIXI projects
TODO: read these
- https://www.html5rocks.com/en/tutorials/webgl/webgl_fundamentals/
- https://www.html5rocks.com/en/tutorials/speed/html5/
- https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Getting_started_with_WebGL
- https://webglfundamentals.org/webgl/lessons/webgl-animation.html
- https://www.html5rocks.com/en/tutorials/speed/animations/