Skip to content

Instantly share code, notes, and snippets.

@drhayes
Last active April 16, 2016 02:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save drhayes/d9b0d86046459389d2bd201c1a597ebc to your computer and use it in GitHub Desktop.
Save drhayes/d9b0d86046459389d2bd201c1a597ebc to your computer and use it in GitHub Desktop.
Entity Factory in Phaser with Webpack

entityFactory Overview

I have a lot of classes extending Phaser.Sprite. Building each one by hand via some if statement when loading a tilemap is dumb. Thankfully, webpack is here to rescue me.

Explain

The full code of entityFactory.js is below.

Say you've got a directory full of sprites, each one a subclass of Phaser.Sprite:

.
├── sprites
    ├── beetle.js
    ├── rockColumn.js
    ├── sword.js
    ├── etc...

Now let's say that some of them have constructors that don't follow the Phaser.Sprite form: maybe they take an extra id param, maybe they need extra properties that come from the tilemap.

The first time you started writing the code to instantiate these sprites you wrote this:

let entities = map.objects.entities;
let game = this.game;
for (let i = 0; i < entities.length; i++) {
  const entity = entities[i];
  const { x, y, width, height, properties: { id, name }} = entity;
  currentEntity = null;
  if (entity.name === 'beetle') {
    groups.enemies.add(new Beetle(game, id, x + width / 2, y + height));
  }
  if (entity.name === 'sword') {
    groups.objects.add(new Sword(game, id, x, y));
  }
  if (entity.name === 'rockColumn') {
    groups.obstacles.add(new RockColumn(game, id, x, y, height, name));
  }
  if (entity.name === 'gargoyle') {
    let faceRight = entity.properties.faceRight === 'true';
    let fireTiming = entity.properties.fireTiming ? parseInt(entity.properties.fireTiming, 10) : false;
    let numFireballs = entity.properties.numFireballs ? parseInt(entity.properties.numFireballs, 10) : false;
    groups.enemies.add(
      new Gargoyle(game, x, y, faceRight, fireTiming, numFireballs));
  }
  // etc...

Ugh, this sucks. Each sprite has its own method of being constructed. It then gets added to its own group. Never mind the complicated ones like the gargoyles which have to parse their properties from the Tiled map data directly.

Enter this bit of magic via webpack:

const spritesContext = require.context('../sprites', false, /\.js$/)

That, my friends, tells webpack that I'm going to load every file in that directory that ends in .js. Now what?

spritesContext.keys()
  .filter(key => !key.startsWith('./base'))
  .forEach(key => {
    const name = basename(key, '.js');
    const module = spritesContext(key);

Okay. First I filter out any file that starts with "./base" (for any uninteresting base classes). Then I reduce the filename to its basename. In other words, ./player.js becomes player. That just happens to be what I call these things in the object layer in Tiled! What a coincidence! That means that the name of the thing in Tiled turns into the name of the file that contains it. w00t!

    let factory = DEFAULT_FACTORY(module.default);
    if (module.hasOwnProperty('FACTORY')) {
      factory = module.FACTORY;
    }
    let groupName = 'enemies';
    if (module.hasOwnProperty('GROUP_NAME')) {
      groupName = module.GROUP_NAME;
    }
    FACTORY_MAP.set(name, { factory, groupName });
  });

Well, wait, what's this? This is where I give every module a chance to tell this piece of code "I have a special way of being built" and "I belong in a group besides 'enemies'".

For example, here's what rockColumn.js exports:

export const FACTORY = (game, mapEntity) => {
  const { x, y, height, properties: { id } } = mapEntity;
  return new RockColumn(game, id, x, y, height);
}
export const GROUP_NAME = 'obstacles';

This code is saying, "I go in the obstacles group" and "I really care about my height, something almost nothing else does".

Voila.

Added Bonus

Now, when you add a new sprite to that directory, none of the other code has to change at all. If it happens to be an enemy that follows the normal construction pattern in my game of (game, id, x, y) then you don't even have to define a custom FACTORY for it.

import { basename } from 'path';
const DEFAULT_FACTORY = function (EntityType) {
return (game, mapEntity) => {
const { x, y, width, height, properties: { id, name }} = mapEntity;
const middleX = x + width / 2;
const bottomY = y + height;
return new EntityType(game, id, middleX, bottomY);
}
}
const FACTORY_MAP = new Map();
// Populate the FACTORY_MAP with all the ways we know of to make these sprites.
const spritesContext = require.context('../sprites', false, /\.js$/)
spritesContext.keys()
.filter(key => !key.startsWith('./base'))
.forEach(key => {
const name = basename(key, '.js');
const module = spritesContext(key);
let factory = DEFAULT_FACTORY(module.default);
if (module.hasOwnProperty('FACTORY')) {
factory = module.FACTORY;
}
let groupName = 'enemies';
if (module.hasOwnProperty('GROUP_NAME')) {
groupName = module.GROUP_NAME;
}
FACTORY_MAP.set(name, { factory, groupName });
});
export function entityFactory(game, entities) {
const namedEntities = new Map();
const { groups } = game;
entities.forEach(mapEntity => {
const { properties: { name } } = mapEntity;
let currentEntity = null;
if (FACTORY_MAP.has(mapEntity.name)) {
const { factory, groupName } = FACTORY_MAP.get(mapEntity.name);
currentEntity = factory(game, mapEntity);
currentEntity.name = name;
groups[groupName].add(currentEntity);
} else {
throw Error(`No FACTORY_MAP entry for ${mapEntity.name}`);
}
if (currentEntity && name) {
namedEntities.set(name, currentEntity);
}
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment