@browndragon/sg

More formal support for phaser3 groups and subclassed game objects.

Usage no npm install needed!

<script type="module">
  import browndragonSg from 'https://cdn.skypack.dev/@browndragon/sg';
</script>

README

@browndragon/sg: Phaser3 SingletonGroups

More formal support for phaser3 groups and subclassed game objects.

Motivation

Sometimes, you have a phaser3 game object whose subclass behavior exists entirely in terms of its interactions with other objects.

For instance, suppose that you have a few Phaser.GameObject.Sprite subclasses like Player, Enemy, Coin, and Bullet. Their interactions are straightforward: Players collect Coins (incrementing the score); Bullets that collide with Player or Enemys hurt that entity (and can be generated by either type), and Players and Enemys that intersect should separate (without hurting either).

In normal phaser code, this would be a lot of code in your Scene.create() to associate all of the collision information -- which is annoyingly distant from the code you want to write, because it's not in your Player.js file along with other information associated with playeritude (or whatever). But if you use this library, see Usage below!

Usage

npm i @browndragon/sg alongside your phaser ^3.5x install, and then in your code:

import Phaser from 'phaser';
import SG from '@browndragon/sg';

// Base class & SingletonGroup for player & enemy sprites.
class Mob extends SG.Member(
    Phaser.GameObjects.Sprite,
    class extends SG.PGroup {
        static get collides() { return [this] }  // Self collision: causes Mobs to bounce off of each other. Since Player and Enemy subclass Mob and include parent's singletongroups in their own, Enemies are in the Mob SG, and so bounce off of each other (and players against players, and players and enemies off of each other).
        // There is no need to define `collide(self, other)` because all we want to do is physics exclusion. If we wanted to have colliding with someone on the opposite faction hurt you, we could easily do that with `collide(self, other) { if (self.prototype != other.prototype) { self.getHurt(); other.getHurt(); } }` or similar.
        // Note that Mob.LastGroup (`==this`!) is referenced in Bullet, which collides with it.
    },
) {
    getHurt() { /* ... */ }  // Play a hurt animation and deduct health, destroying if dead.
    shootBullet() { /* ... */ }  // Spawn a bullet along the current facing, like `this.scene.add.existing(new Bullet(this))
}

// Collectable coin pickups.
class Coin extends SG.Member(
    Phaser.GameObjects.Image,
    class extends SG.PGroup {
        static get overlaps() { return [Coin.Collectors] }  // Run an overlap test with the `CoinCollectors.group(...)`.
        overlap(coin, collector) {  // And when they overlap, give the collector points and destroy the coin.
            collector.getScore(coin.value);
            coin.getCollected();
        }
    },
) { 
    constructor(scene, x, y, value) { /* ... */ }  // Hardcode image; value determines penny/nickel/dime/quarter.
    getCollected() { /* ... */ }  // Play a chime, animate away, then `this.destroy()`.
}
Coin.Collectors = class extends SG.PGroup {}  // Nothing in here; the Coin's anonymous singleton group collides with it, and Player is a member -- this just avoids any circularity of reference, and would let other things collect groups too if you wanted.

// Human-controlled sprite.
class Player extends Member(Mob, Coin.Collectors) {
    constructor(scene, x, y) { /* ... */ }  // Create a keyboard listener, hardcode image.
    getScore(value) { /* ... */ }  // Increase score; required by Coin.Collectors.
    preUpdate(time, delta) { /* ... */ }  // Listen to the keyboard.
}

// AI-controlled sprite.
class Enemy extends Mob {
    constructor(scene, x, y) { /* ... */ }  // Hardcode image.
    preUpdate(time, delta) { /* ... */ }  // Run AI; sometimes call `this.shootBullet` or `this.spawnCoin`.
    spawnCoin() { /* ... */ }  // Used in preUpdate.
}

// Projectile.
class Bullet extends SG.Member(
    Phaser.GameObjects.Image,
    class extends SG.PGroup {
        static get overlaps() { return [Mob.LastGroup] }  // Collide with the anonymous singletongroup created at Mob.
        overlap(bullet, victim) {
            victim.getHurt();
            bullet.destroy();
        }
    },
) {
    constructor(parent) { /* ... */ }  // Create with `parent.scene`; create bullet with physics body, set initial velocity from parent; Hardcode image.
}


new Phaser.Game({
    scene: class extends Scene {
        constructor() {
            super({key:'GameScene'});
        }
        create() {
            this.add.existing(new Player(this, 250, 250));
            this.add.existing(new Enemy(this, 32, 32));
            this.add.existing(new Enemy(this, 32, 480));
        }        
    },  /* other phaser config */ 
});

Another example has sprites which need to manage multiple child sprites; for instance, you might have a Rectangle sprite that spawns a 1-pixel probe at each corner in order to collide probes against the rest of the universe:

class Box extends Phaser.GameObjects.Rectangle {
    constructor(scene, x, y) {
        super(scene, x, y, 32, 32);
    }
    addedToScene() {
        super.addedToScene();
        this.scene.add.existing(this.a = new Probe(this, 0, 0));
        this.scene.add.existing(this.b = new Probe(this, 32, 0));
        this.scene.add.existing(this.c = new Probe(this, 0, 32));
        this.scene.add.existing(this.d = new Probe(this, 32, 32));
    }
    preUpdate(time, delta) {
        for (let p of [this.a, this.b, this.c, this.d]) {
            p.x = this.x + p.xoff;
            p.y = this.y + p.yoff;
        }
    }
}
class Probe extends SG.Member(
    Phaser.GameObjects.Image, 
    class extends SG.PGroup {
        overlaps() { return [this] }  // All probes can intersect. Could add other SGs based on what we're probing for; maybe they probe for Ground or something?
        overlap(aProbe, bProbe) { /* ... */ }  // Do something to the probes or the boxes they're attached to.
    },
) {
    constructor(parent, xoff, yoff) {
        super(parent.scene, parent.x + xoff, parent.y + yoff, 'blank1px.png');
        this.parent = parent;
        this.xoff = xoff;
        this.yoff = yoff;
    }
}

Given this setup, we can ensure the probes are always created for any Box just by creating the box; the rest of the wiring takes care of itself.