Stop Particles from Sliding in Phaser

Last modified: 06 Feb 2017

Summary

When you have your skeleton’s bones flying away into pieces, you want them to fall where the skeleton was and stop it’s bones from sliding along the floor.

Source

How

Create an imaginary floor sprite attached to your skeleton where the partcles can land into and enable physics.

The Code

In order to have a more engaging experience for the players, we want to put in a little more effort to the effect. First we create the floor and attach it to our skeleton:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// create a sprite without an image as a child of the skeleton
var floor = game.add.sprite(0, 0, null);

// enable physics so collision applies
game.physics.arcade.enable(floor);

// add a little offset so it's along where the skeleton is standing
floor.anchor.setTo(0, -1);

// create a floor long enough for the particle to land onto and not fall off
floor.body.setSize(400, 1, -200);

// we don't want the floor to move when the collision kicks in
floor.body.immovable = true;

// a reference to our floor
_spriterGroup.floor = floor;

// add it to our group so it's position is relative to the skeleton
_spriterGroup.addChild(floor);

Each skeleton has it’s own particle emitter.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// position the emitter where the skeleton is, this will give us a nice
// effect where we will see the parts fly away from the skeleton's body
let emitter = game.add.emitter(skeleton.x, skeleton.y, 10);

// random size for the parts
emitter.minParticleScale = skeleton.scale.x;
emitter.maxParticleScale = skeleton.scale.x + 0.15;

// just enough velocity so they don't fly away far
emitter.setXSpeed(-100, 100);
emitter.setYSpeed(-300, -150);

// pull them down fast so they don't spread too much
emitter.gravity = 500;

// pass in the body parts but only the small ones. the names are in the atlas.json
emitter.makeParticles('skeleton', ['armL', 'legL', 'handL', 'feetL', 'elbowL', 'lower_legL']);

// referece to the emitter
skeleton.emitter = emitter;

You might wonder why not a single particle emitter and just explode them all at once on a skeleton’s position each time. The reason is because I don’t particles to bump on another skeleton’s floor especially when the floor is above the particle.

When we pop a skeleton:

1
2
3
4
5
6
7
8
9
10
11
let skeleton = game.rnd.pick(skeletonGroup.children.filter(function(e) { return e.alive; }));
if(skeleton) {
  // note that our skeleton is not a Phaser.Sprite. It is a SpriteGroup, created from using the Spriter Library
  // so here we are just introducing this variable so we can keep track which ones we have popped
  skeleton.alive = false;
  skeleton.setAnimationSpeedPercent(10);
  skeleton.playAnimationByName('Death');

  // explode it's parts away with a random lifespan
  skeleton.emitter.start(true, game.rnd.between(1000, 2500), null, 10);
}

Then the very point of this demo - when the particles collide we stop them from sliding

1
2
3
4
5
6
game.physics.arcade.collide(skeleton.emitter, skeleton.floor, (a, particle) => {
  // by multiplying by a fractional number, we are quickly reducing each particle's velocity on every update
  particle.body.velocity.x *= 0.9;
  particle.body.velocity.y *= 0.9;
  particle.body.angularVelocity *= 0.9;
});

And finally - we fade the particles and when they are all dead, we fade the skeleton and then remove each from the group. Just like in the games right?

1
2
3
4
5
6
7
8
9
10
11
12
// fade the particles
skeleton.emitter.forEachAlive(function(particle) {
  particle.alpha = game.math.clamp(particle.lifespan / 1000, 0, 1);
});

// and then fade the skeleton after all the particles are dead
if(skeleton.emitter.countDead() == 10) {
  let tween = game.add.tween(skeleton).to({ alpha: -1 }, 200, Phaser.Easing.Linear.None, true);
  tween.onComplete.add(() => {
      skeletonGroup.remove(skeleton);
  });
}

I didn’t like fading them the same time, looks better when they fade after another.

Bonus - Isometric Grid

So that we can see a better perspective where the skeletons are standing on and the particles are falling into, I have created an isometric grid you can just drop into any of your Phaser project.

Credits

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
'use strict';

class GameState extends Phaser.State {
  preload() {
    this.load.onLoadStart.add(this.loadStart, this);
    this.load.onFileComplete.add(this.fileComplete, this);
    this.load.onLoadComplete.add(this.loadComplete, this);
  }

  loadStart() {
    this.loadingText = this.add.text(20, this.world.height - 32, 'Loading...', { font: '20px Arial', fill: '#ffffff' });
  }

  fileComplete(progress, cacheKey, success, totalLoaded, totalFiles) {
    this.loadingText.setText('File Complete: ' + progress + '% - ' + totalLoaded + ' out of ' + totalFiles);
  }

  loadComplete() {
    game.world.remove(this.loadingText);

    this.time.advancedTiming = true;
  }

  create() {
    this.showDebug = false;
    game.input.keyboard.addKey(Phaser.KeyCode.D).onDown.add(() => {
      this.showDebug = !this.showDebug;
    });

    game.camera.x = game.world.centerX - game.width / 2;
  }

  createKeyboardMovement() {
    this.keyboardCursors = game.input.keyboard.createCursorKeys();
    this.moveSpeed = { x: 0, y: 0 }

    this.wasd = {
      up: game.input.keyboard.addKey(Phaser.Keyboard.W),
      down: game.input.keyboard.addKey(Phaser.Keyboard.S),
      left: game.input.keyboard.addKey(Phaser.Keyboard.A),
      right: game.input.keyboard.addKey(Phaser.Keyboard.D),
    };
  }

  createVirtualGamepad() {
    // create virtual gamepad
    let gamepad = game.plugins.add(Phaser.Plugin.VirtualGamepad)
    this.joystick = gamepad.addJoystick(60, game.height - 60, 0.5, 'gamepad');

    // plugin wants the creation of a button
    // but there is no usage for it here so i'm just going to hide it
    this.gamepadbutton = gamepad.addButton(game.width - 60, game.height - 60, 0.5, 'gamepad');
    this.gamepadbutton.visible = false;
  }

  goingLeft() {
    return this.keyboardCursors.left.isDown || this.wasd.left.isDown || (this.joystick && this.joystick.properties.left);
  }

  goingRight() {
    return this.keyboardCursors.right.isDown || this.wasd.right.isDown || (this.joystick && this.joystick.properties.right);
  }

  update() {
  }

  render() {
    game.debug.text(game.time.fps, 5, 14, '#00ff00')
  }

  loadSpriter(key) {
    if(!this.spriterLoader) this.spriterLoader = new Spriter.Loader();

    let spriterFile = new Spriter.SpriterXml(game.cache.getXML(key + 'Animations'));

    // process loaded xml/json and create internal Spriter objects - these data can be used repeatly for many instances of the same animation
    let spriter = this.spriterLoader.load(spriterFile);

    return new Spriter.SpriterGroup(game, spriter, key, key);
  }

  drawIsoGrid() {
    let isoGrid = new IsoGrid(game);
    isoGrid.drawGrid();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
'use strict';

class IsoGrid {
  constructor(game, tileWidth, tileHeight, numTilesPerRow) {
    this.game = game;
    this.tileWidth = tileWidth || 64;
    this.tileHeight = tileHeight || 32;
    this.numTilesPerRow = numTilesPerRow || 30;
  }
  drawGrid() {
    // define worldsize
    let ws = new Phaser.Rectangle(0, 0, this.tileWidth * this.numTilesPerRow, this.tileHeight * this.numTilesPerRow );

    // draw from the center
    let mapCenter = new Phaser.Point(this.game.world.centerX - (ws.width / 2), this.game.world.centerY - (ws.height / 2));
    let g = this.game.add.graphics(mapCenter.x, mapCenter.y);

    let gridWidth = this.tileWidth;
    let gridHeight = this.tileHeight;

    // set line color
    g.lineStyle(1, 0xaaaaaa);

    // draw first half
    for(let y = 0; y < this.numTilesPerRow; y++) {
      let startX = ws.width / 2;
      let startY = (ws.height / 2 + (this.numTilesPerRow / 2) * gridHeight) - 16;

      startY = startY - y * gridHeight / 2;
      startX = startX - y * gridWidth / 2;

      for(var x = 0; x < y + 1; x++) {
        var nextX = startX + (x * gridWidth);

        g.moveTo(nextX - gridWidth / 2, startY);
        g.lineTo(nextX, startY + gridHeight / 2);
        g.lineTo(nextX + gridWidth / 2, startY);
        g.lineTo(nextX, startY - gridHeight / 2);
        g.lineTo(nextX - gridWidth / 2, startY);
      }
    }

    // draw second half
    for(let y = 0; y < this.numTilesPerRow - 1; y++) {
      let startX = ws.width / 2;
      let startY = ws.height / 2 - (((this.numTilesPerRow / 2 - 1) - y) * gridHeight);
      startY = startY - 16;

      startY = startY - y * gridHeight / 2
      startX = startX - y * gridWidth / 2

      for(let x = 0; x < y + 1; x++) {
        let nextX = startX + (x * gridWidth);

        g.moveTo(nextX - gridWidth / 2, startY);
        g.lineTo(nextX, startY + gridHeight / 2);
        g.lineTo(nextX + gridWidth / 2, startY);
        g.lineTo(nextX, startY - gridHeight / 2);
        g.lineTo(nextX - gridWidth / 2, startY);
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
class DemoState extends GameState {
  preload() {
    super.preload();

    this.load.atlas('skeleton', '/files/phaser/assets/skeleton/atlas.png', '/files/phaser/assets/skeleton/atlas.json');
    this.load.xml('skeletonAnimations', '/files/phaser/assets/skeleton/animations.scml');
  }

  create() {
    super.create();

    game.input.keyboard.addKey(Phaser.KeyCode.P).onDown.add(() => {
      this.popSkeleton();
    });

    game.input.keyboard.addKey(Phaser.KeyCode.E).onDown.add(()=>{
      this.reset();
    });

    game.input.addPointer();
    game.input.onDown.add((pointer) => {
      if(pointer == game.input.pointer2) this.reset();
      else this.popSkeleton();
    });

    this.reset();
  }

  update() {
    super.update();

    this.skeletons.forEach(function(skeleton) {
      game.physics.arcade.collide(skeleton.emitter, skeleton.floor, (a, particle) => {
        // by multiplying by a fractional number, we are quickly reducing each particle's velocity on every update
        particle.body.velocity.x *= 0.9;
        particle.body.velocity.y *= 0.9;
        particle.body.angularVelocity *= 0.9;
      });

      skeleton.updateAnimation();

      // fade the particles
      skeleton.emitter.forEachAlive(function(particle) {
        particle.alpha = game.math.clamp(particle.lifespan / 1000, 0, 1);
      });

      // and then fade the skeleton after all the particles are dead
      if(skeleton.emitter.countDead() == 10) {
        let tween = game.add.tween(skeleton).to({ alpha: -1 }, 200, Phaser.Easing.Linear.None, true);
        tween.onComplete.add(() => {
          game.world.remove(skeleton.emitter);
          game.world.remove(skeleton);
        });
      }
    });
  }

  render() {
    super.render();

    if (this.showDebug) {
        this.skeletons.forEach((skeleton) => {
            game.debug.body(skeleton.floor);
            game.debug.geom(skeleton.position, 'rgb(255,255,255)');
        });
    }

    game.debug.text('Press D for any rendering debug', 32, 32);
    game.debug.text('Press P or touch to pop a skeleton', 32, 48);
    game.debug.text('Press E or use 2 touches to reset', 32, 64);
  }

  reset() {
    game.world.removeAll();

    this.skeletons = [];

    let isoGrid = new IsoGrid(game);
    isoGrid.drawGrid();

    for(let y = 1; y < 5; y++) {
      for(let x = 1; x < 6; x++) {
        let posx = x * 100 + ((y % 2) ? 0 : 50);
        let posy = y * 100;

        let skeleton = this.createSkeleton(posx, posy);
        skeleton.alive = true;
        this.createEmitter(skeleton);

        this.skeletons.push(skeleton);
        game.world.add(skeleton);
      }
    }
  }

  createSkeleton(x, y) {
    // load the skeleton using the Spriter Library
    let skeleton = this.loadSpriter('skeleton');
    skeleton.position.setTo(x, y);
    skeleton.scale.set(0.2);
    skeleton.setAnimationSpeedPercent(game.rnd.between(10, 20));
    skeleton.playAnimationByName('Idle');

    // create a sprite without an image as a child of the skeleton
    let floor = game.add.sprite(0, 0, null);
    // enable physics so collision applies
    game.physics.arcade.enable(floor);
    // add a little offset so it's along where the skeleton is standing
    floor.anchor.setTo(0, -1);
    // create a floor long enough for the particle to land onto and not fall off
    floor.body.setSize(400, 1, -200);
    // we don't want the floor to move when the collision kicks in
    floor.body.immovable = true;
    // a reference to our floor
    skeleton.floor = floor;
    // add it to our group so it's position is relative to the skeleton
    skeleton.addChild(floor);

    return skeleton;
  }

  createEmitter(skeleton) {
    // position the emitter where the skeleton is, this will give us a nice
    // effect where we will see the parts fly away from the skeleton's body
    let emitter = game.add.emitter(skeleton.x, skeleton.y, 10);
    // random size for the parts
    emitter.minParticleScale = skeleton.scale.x;
    emitter.maxParticleScale = skeleton.scale.x + 0.15;
    // just enough velocity so they don't fly away far
    emitter.setXSpeed(-100, 100);
    emitter.setYSpeed(-300, -150);
    // pull them down fast so they don't spread too much
    emitter.gravity = 500;
    // pass in the body parts but only the small ones. the names are in the atlas.json
    emitter.makeParticles('skeleton', ['armL', 'legL', 'handL', 'feetL', 'elbowL', 'lower_legL']);
    // referece to the emitter
    skeleton.emitter = emitter;
  }

  popSkeleton() {
    // pick a random skeleton that is still alive
    let skeleton = game.rnd.pick(this.skeletons.filter(function(e) { return e.alive; }));
    if(skeleton) {
      // note that our skeleton is not a Phaser.Sprite. It is a SpriteGroup, created from using the Spriter Library
      // so here we are just introducing this variable so we can keep track which ones we have popped
      skeleton.alive = false;
      skeleton.setAnimationSpeedPercent(10);
      skeleton.playAnimationByName('Death');

      // explode it's parts away with a random lifespan
      skeleton.emitter.start(true, game.rnd.between(1000, 2500), null, 10);
    }
  }
};

Look! I have new stuff coming right up about... but really, I rarely send out emails! Promise.