Skip to content

Instantly share code, notes, and snippets.

@rmartone
Last active August 6, 2018 20:14
Show Gist options
  • Save rmartone/01e63fd36aa825bb08f85872b5493a56 to your computer and use it in GitHub Desktop.
Save rmartone/01e63fd36aa825bb08f85872b5493a56 to your computer and use it in GitHub Desktop.
Excerpt from "Building Multiplayer Board Games with Phaser"
import { guid } from '../shared/stringutils';
import { READONLY_ERR_MSG } from './constants';
/**
* Components add specific behaviors to their GameObjects through composition.
* Components typically span diverse domains from AI and rendering to physics.
* This provides a means for them to coexist within a single entity with
* minimal coupling.
*
* @see GameObject
* @link http://gameprogrammingpatterns.com/component.html
*/
export class Component {
/**
* @param json Component settings as JSON
*/
constructor(json) {
this._container = null;
this._name = json.name || guid();
}
/**
* @returns {Container} typically its GameObject
*/
get container() {
return this._container;
}
/**
* throw Error on attempt to set read-only prop
* @throws Error
*/
set container(value) {
throw Error(READONLY_ERR_MSG);
}
/**
* @returns {string} name for component
*/
get name() {
return this._name;
}
/**
* throw Error on attempt to set read-only prop
* @throws Error
*/
set name(value) {
throw Error(READONLY_ERR_MSG);
}
/**
* Registers a component with its container
* @param container typically a GameObject or another component
* @returns {this}
*/
register(container) {
this._container = container;
this.onAdd();
return this;
}
/**
* Unregister the component from its Container
* @returns {this}
*/
unregister() {
this.onRemove();
// avoid circular reference
this._container = null;
return this;
}
/**
* Called when component is added to a Container. Override to add
* component specific initialization like event registration.
*/
onAdd() {}
/**
* Called when the component is removed from a Container. Override to
* perform component specific cleanup like removing event listeners.
*/
onRemove() {}
}
import { Category, logError, logInfo } from '../shared/log';
import { Client } from './client';
/**
* Adds a batch of assets to the loader's queue
* @param assets is a JSON object with one or more keyed assets
* in the form {type: string, url?: string ...}
*/
export function loadAssets(assets) {
for (let key in assets) {
queueAsset(key, assets[key]);
}
}
/**
* adds an Asset to the loader's queue
* @param key cache key
* @param asset JSON
*/
function queueAsset(key, asset) {
const loader = Client.phaser.load;
loader.crossOrigin = 'anonymous';
// register loader listeners, if not present
if (!loader.onFileComplete.has(fileComplete)) {
loader.onFileComplete.add(fileComplete);
}
if (!loader.onFileError.has(fileError)) {
loader.onFileError.add(fileError);
}
switch (asset.type) {
case assetType.audio:
loader.audio(key, resolveAll(asset.urls), true);
break;
case assetType.audioSprite:
loader.audiosprite(key, resolveAll(asset.urls), undefined, { spritemap: asset.spritemap }, true);
break;
case assetType.image:
loader.image(key, asset.url);
break;
case assetType.textureAtlas:
loader.atlasJSONHash(key, asset.url, resolve(asset.atlas));
break;
case assetType.textureAtlas:
loader.json(key, asset.url);
break;
default:
throw Error(`undefined asset type ${asset.type} for ${key}`);
}
}
/**
* Fully qualifies an array of URLs
* @param urls array of potentially relative urls
* @return urls new array of urls that are fully qualified
*/
function resolveAll(urls) {
// careful not to mutate arg
const result = urls.slice();
for (let i = 0; i < result.length; ++i) {
result[i] = resolve(result[i]);
}
return result;
}
/**
* Qualifies an asset's URL if it's relative
* @param url
* @returns fully qualified url to an asset
*/
function resolve(url) {
// if relative url prepend root
if (url !== '/' && !/^https?:\/\//.test(url)) {
// combine s3 bucket name w/ asset's relative url
return `${Client.bucket}${url}`;
}
return url;
}
/**
* Event is dispatched when a file completes loading successfully
* @param progress percent complete
* @param key asset's cache key
*/
function fileComplete(progress, key) {
logInfo(Category.Loader, `loaded asset for key: "${key}"`);
}
/**
* Event dispatched when a file errors as a result of the load request
* @param key asset's cache key
*/
function fileError(key) {
logError(Category.Loader, `file error loading asset for key: "${key}"`);
}
const assetType = {
audio: 'Audio',
audioSprite: 'AudioSprite',
image: 'Image',
textureAtlas: 'TextureAtlas',
json: 'JSON',
};
import { classNameForInstance, classNameFromType } from '../shared/classutils';
import { Category, logInfo } from '../shared/log';
import { READONLY_ERR_MSG } from './constants';
import { Client } from './client';
import { GameObject } from './gameobject';
import { loadAssets } from './loader';
/**
* Screen class extends Phaser.State and provides screen
* wrangling by factoring common tasks.
*/
export class Screen extends Phaser.State {
constructor() {
super();
// declare props upfront
this._gameObject = null;
// subclass name is used to lookup a screen's assets
this._name = classNameForInstance(this);
}
/**
* a screen's root GameObject
* @returns {GameObject}
*/
get gameObject() {
return this._gameObject;
}
/**
* throw Error on attempt to set read-only prop
* @throws Error
*/
set gameObject(value) {
throw Error(READONLY_ERR_MSG);
}
/**
* ensures the state is registered using its class name
* e.g. MyState.register();
*/
static register() {
Client.phaser.state.add(classNameFromType(this), this);
}
/**
* starts the state for this class i.e. preload, create sequence
*/
static start() {
// starts the state using name of subclass registered above
Client.phaser.state.start(classNameFromType(this));
}
/**
* called once preload has completed
* typically where you'd setup a screen
*/
create() {
// get the screen's children from its JSON
const json = Client.screen[this._name];
// create the root gameObject
this._gameObject = new GameObject(json);
// attach the gameObject
Client.phaser.world.add(this._gameObject);
logInfo(Category.System, `cache size ${GameObject.cacheSize}`);
return this;
}
/**
* Loads assets required by the preloader
*/
preload() {
// preload assets keyed to this state
loadAssets(Client.assets[this._name]);
}
/**
* called when the Screen is shutdown (i.e. you switched to another screen).
*/
shutdown() {
if (this._gameObject) {
this._gameObject.destroy();
// explicitly clear since Phaser keeps States around
this._gameObject = null;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment