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.
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.
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.