Stop Particles from Sliding in Phaser
17 Dec 2016Summary
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
- gameState.js is common to all my Phaser demos
- demo.js is code specific to this demo
- isogrid.js is the iso grid drawer that you can just drop into your code if you need to draw an isometric grid
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
- For the awesome skeleton
- For the spriter player
- and of course the awesome HTML5 Phaser game framework by Richard Davey and his website.
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);
}
}
};