Bloody Drippy Scene using Phaser
19 Feb 2018Summary
With the help of a little physics, and different easing functions, we’ll see how we can get pools of blood on the floor which drips down to the ground.
Source
- gameState.js is common to all my Phaser demos.
- demo.js are where the collision logic are and where we create the the blood drops from the user click and the drips.
- create.js contains the code that creates the bloody pool and drips, creates the tilemap and the bitmap shapes.
The Idea
My idea is to have a circle shape, squeeze vertically to make a disc shape for the blood pool when it hits the ground. Then another one squeeze horizontally this time, for the drip. We will prepare a couple of functions to do this, starting with the blood drop.
The BitmapData
We will need to create 2 of these, one which looks like a blood drop, and another which is a circle shape that we will use later on to squeeze to form a disc shape.
When we start to click and drop blood, I want it to look like this initially:
Then we squeeze it in while it falls down. To build a similar image like that, we’re going to use a triangle shape for the top and circle for the bottom shape. Code looks like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
DemoState.prototype.createBloodyDrop = function() {
let bitmapSize = 100
let bmd = game.add.bitmapData(bitmapSize, bitmapSize)
// Draw a triangle on top of a circle
bmd.ctx.fillStyle = 'rgb(131,3,3)'
bmd.ctx.beginPath()
bmd.ctx.moveTo(50,0)
bmd.ctx.lineTo(30,35)
bmd.ctx.lineTo(70,35)
bmd.ctx.fill()
// Draw a circle below the triangle
bmd.circle(50, 50, 25, 'rgb(131,3,3)')
return bmd
}
The blood pools and drips will just use another source of Phaser.BitmapData
that creates a circle shape:
1
2
3
4
5
6
7
DemoState.prototype.createBloodyCircle = function() {
let circleSize = 30
let bmd = game.add.bitmapData(circleSize, circleSize)
bmd.circle(circleSize / 2, circleSize / 2,circleSize / 2, 'rgb(131,3,3)')
return bmd
}
Now let’s begin with the bloody scene…
Dropping bloods
Basically the steps:
- pull a bloody drop sprite from a
Phaser.Group
- reset a few stuff
- tween it so it expands down
- after the tween finishes, enable physics
- put a random life, we will use this to alter it’s alpha over the course of it’s fall during
update()
- apply some downward velocity, just to get the sprite moving fast
- let gravity do its thing
- see if it hits the ground by checking it’s collision with the tilemap index
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
dropBlood(mx, my) {
// let's grab an unused bloody from the group
let bloodySprite = this.bloodyDropGroup.getFirstDead()
// this probably won't trigger but just incase so we don't get a fatal error
if(!bloodySprite) return
// we're going to do some tweening first
bloodySprite.body.enable = false
// make it show where we click
bloodySprite.position.set(mx, my)
// give our bloody drop a random size every time
bloodySprite.scale.set(game.rnd.realInRange(0.2, 0.3))
// set to 0, acts as our flag during update
// that reminds us now to change it's alpha if physics hasn't been enabled
bloodySprite.lifespan = 0
// make sure we start solid red
bloodySprite.alpha = 1
// this is a boolean flag set during collision
// with the tilemap so we don't process it twice
bloodySprite.hasCollided = false
// time to set it free, or rather make it appear
bloodySprite.revive()
// let's stretch it so it does look more like a blood dripping from our cursor
let fallTween = game.add.tween(bloodySprite.scale)
fallTween.to({
x: bloodySprite.scale.x * game.rnd.realInRange(0.3, 0.5),
y: bloodySprite.scale.y * game.rnd.realInRange(1.5, 2.5)
}, 500, Phaser.Easing.Cubic.In, true)
fallTween.onComplete.add(() => {
// we're going to provide it a lifespan
// and affect it's alpha during the update
// this way we can have variety of solid or faded blood
// this also means that some of the blood may never land
bloodySprite.lifespan = game.rnd.between(500, 1000)
bloodySprite.totallife = bloodySprite.lifespan
// help gravity a little bit, so we don't have a sprite
// that slowly just starts it's movement
bloodySprite.body.velocity.y = game.rnd.between(100, 300)
// okay let physics system kick in
bloodySprite.body.enable = true
})
}
And we begin our collision check.
Collision with the tilemap
The way I handled collision is through checking the tile index. When the blood hits the ground, we don’t want it to further drip down - because there’s no more to go down. Only when it hits our second floors we want it to drip.
The sequence of steps to create the next effect after the drop falls to a floor will be as follows:
- create a sprite from the bitmap circle shape, squeeze vertically so it looks like a flat disc which would form our bloody pool sprite
- create another sprite from the bitmap circle shape, now squeeze horizontally, this will form our bloody drip sprite
- when a blood drop falls to our second floors, a percentage of it (using a function
weightedRandom
) would drip to the ground
This is partial code for the entire collision check, you should check out the source file for the complete code.
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
// handles the second floor collision tiles
// blood that falls here has a chance it generates a bloody drip
// that continues to fall down to the ground
this.tilemap.setTileIndexCallback([356, 204, 206, 208, 209, 270, 271, 272], (sprite, tile) => {
// console.log('collided with tile index', tile.index)
// same as above, don't process 2x
if(sprite.hasCollided) return
// when we eventually had a chance to create that bloody drip to the ground floor
// it will start at this same tile, so collision callback for it may trigger right away
// this boolean flag helps us prevent that from happening
if(sprite.dripper) return
sprite.hasCollided = true
sprite.kill()
// notice that our map has different style of floor
// there's the floor that is angled and shows a larger area of the floor
// and the other one is just a straight line.
// we leverage that here so that depending on the floor
// our bloody pool can occupy a larger space
let posy = game.rnd.realInRange(tile.worldY - 10, tile.worldY)
if([204, 206, 208, 209].includes(tile.index)) {
posy = game.rnd.realInRange(tile.worldY, tile.worldY + 2)
}
// create a permanent bloody pool from the blood drop
let bloodPoolSprite = this.createBloodPool(sprite.x, posy, sprite.alpha)
// and then from the pool we create a permanent bloody drip
let bloodDripSprite = this.createBloodDrip(
game.rnd.realInRange(bloodPoolSprite.x - 5, bloodPoolSprite.x + 5),
bloodPoolSprite.y,
sprite.alpha
)
// only a fraction of it will fall to the ground
if(this.weightedRandom({0:0.8, 1:0.2})) {
// I'm trying to create a subtle effect
// where it tries to animate how blood drips in real-life
// using an easing back-in achieves that, because it will
// try to pull back the bloody drip first before we let it continue to the ground
let fallTween = game.add.tween(bloodDripSprite.scale)
fallTween.to({
y: bloodDripSprite.scale.y * game.rnd.realInRange(1.2, 1.7) },
game.rnd.between(1000, 2000), Phaser.Easing.Back.In, true)
// so after the animation we just let it go, by creating a new sprite in place of it
// notice the position y. i don't want it to start from the tip
fallTween.onComplete.add(() => {
this.dripBlood(bloodDripSprite.x, bloodDripSprite.y + bloodDripSprite.height - 5,
bloodDripSprite.scale.x, bloodDripSprite.scale.y
)
}, this)
}
}, this)
There’s a couple of functions in there that helps us create the bloody effects.
createBloodPool
- this is the flattened disc shape spritecreateBloodDrip
- this is the drips that hangs within the width of the disc shape spritedripBlood
- this is the sprite that would continue to fall down to the ground if a blood drops to our second floors.
The bloody pool
- create a sprite that uses the bloody circle shape bitmap data we created
- flatten it so it would look like a bloody pool by scaling up it’s
x
and scaling down it’sy
- use the same alpha from the bloody drop so it shares the same opacity with the blood drop impacts the floor
- from a percentage of these we created, we will have them grow it’s size by scaling it’s width. This provides a bit of variety our players tend to see
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// creates the blood pool effect
DemoState.prototype.createBloodPool = function(x, y, alpha) {
// by using our bloody circle shape and flatten it vertically
let bloodPoolSprite = game.add.sprite(x, y, this.bloodyCircleBitmap)
bloodPoolSprite.anchor.set(0.5)
// for variety, let's randomize it's size
// but make sure it's width is stretch further than it's height
bloodPoolSprite.scale.set(game.rnd.realInRange(0.3, 1.2), game.rnd.realInRange(0.1, 0.3))
// use the same alpha as when the blood drop make its impact
bloodPoolSprite.alpha = game.math.max(0.5, alpha)
// for further variety, 30% of the time we want to grow the blood pool
if(this.weightedRandom({0:0.7, 1:0.3})) {
// we just multiplay it's x scale so it stretches itself sideways
let scale = bloodPoolSprite.scale
game.add.tween(scale).to({x: scale.x * 1.5 }, game.rnd.between(500, 1500), Phaser.Easing.Linear.None, true)
}
// just keeping track
this.bloodCount++
return bloodPoolSprite
}
The bloody drip
- create a sprite that uses the bloody circle shape bitmap data we created
- flatten it the other way around so it would look like it’s dripping down from the bloody pool.
- randomly put it’s
x
position within the width of the bloody pool
The code looks very similar to the createBloodPool
but there’s a little explanation for when I decided to set it’s height. It looks like this:
1
2
3
4
5
6
7
8
9
10
11
let randomScales = [
[game.rnd.realInRange(0.1, 0.15), game.rnd.realInRange(0.2, 0.3)],
[game.rnd.realInRange(0.1, 0.15), game.rnd.realInRange(0.3, 0.4)],
[game.rnd.realInRange(0.1, 0.15), game.rnd.realInRange(0.6, 0.9)]
]
let rndIndex = this.weightedRandom({0:0.7, 1:0.2, 2:0.1})
// pull the randomize scale out from the array
let randomSize = randomScales[rndIndex]
// now set the random size
bloodDripSprite.scale.set(randomSize[0], randomSize[1])
I like the long drip - to be only a fraction. And most of it are just short ones. Here’s what happens when i just put a single random factor for their height:
With a weighted random - I can show more of the short ones, and only a few of the long ones and it will look like this:
Compared to the first one, I think I’d rather go with the 2nd version.
Like I mentioned earlier, a fraction of these drips would continue their fall - and yes, it’s using the weightedRandom
too. But I wanted to achieve this effect before it continues to fall:
Using Phaser.Easing.Back.In
, the drip will increase it’s height, and back
in shortening it’s height, then finally when it finishes we will let go.
And now for the final effect - the one that got away.
Dripping to the ground
For those that continues to drip to the ground, we will check their collision with a different tile index, so we can apply a different logic to it. That is, those that impacts the ground will drip no further.
The only difference in creating these is that we apply a Phaser.Easing.Exponential.In
meaning, we want the tween to get a slow start, and stretches it’s size very quickly as it finishes.
Then physics just kicks in, and it’s almost similar to the bloody drop. We put it in a group, so we can check it’s collision with the tilemap layer. Change it’s alpha
during update()
and kill it when it hits the ground, calling createBloodPool
and createBloodDrip
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
// drips the blood from the blood pool to the ground
dripBlood(x, y, scalex, scaley) {
let bloodySprite = game.add.sprite(x, y, this.bloodyCircleBitmap)
game.physics.arcade.enable(bloodySprite)
// stretch from the bottom
bloodySprite.anchor.set(0.5, 0)
// start with a short one
bloodySprite.scale.set(scalex, game.rnd.realInRange(0.05, 0.1))
// we're going to use the easing exponential-in
// because we want it to start travelling small and boxy for a time
// and quickly expand towards the end of the tween
game.add.tween(bloodySprite.scale).to({
x: scalex * game.rnd.realInRange(0.1, 0.3),
y: game.rnd.realInRange(0.8, 1.2) }, 500, Phaser.Easing.Exponential.In, true)
// boolean flag we can use so we know not to check collision with this
bloodySprite.dripper = true
// give it it's own life
bloodySprite.lifespan = game.rnd.between(1000, 3000)
bloodySprite.totallife = bloodySprite.lifespan
// so we can test against collision with the tilemap
this.bloodyDripGroup.add(bloodySprite)
}
Suggestions
- I think you could use different interpolation for the tween and see which provides a better animation, but I didn’t had any success with it.
- You’ll notice that we use a different group for the drips - that means we can actually also start recycling sprites from that group after we created a few in the demo
Credits
- the awesome Gothicvania artwork from Luis Zuno. He is creating pixel art in Petreon. To be honest there are only a handful of game artists actually delivering in Petreon and his one of them! He also delivers a complete Phaser demo with his artwork. The
map.json
that I used here is from his Gothicvania Phaser demo. - the random weighted function from maerics from his Stack Overflow answer
- if you wish to see how the different easing functions affect the speed of the tweens, you can see them all in easings.net
- a very great explanation of why we need to use different easing functions from Suresh V. Selvaraj
- the awesome phaser.io 2D game framework by Richard Davey
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
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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
class DemoState extends GameState {
preload() {
super.preload()
// tileset
game.load.image('tileset', '/files/phaser/gothic/environment/tileset.png')
game.load.tilemap('map', '/files/phaser/gothic/maps/map.json', null, Phaser.Tilemap.TILED_JSON)
}
create() {
super.create()
game.stage.backgroundColor = "#000000"
game.physics.startSystem(Phaser.Physics.ARCADE)
// set global gravity
game.physics.arcade.gravity.y = 400
// create our shapes and tilemap
this.bloodyDropBitmap = this.createBloodyDrop()
this.bloodyCircleBitmap = this.createBloodyCircle()
this.tilemap = this.createTileMap('map', 'tileset')
this.layer = this.createLayer(this.tilemap, 'Tile Layer 1')
// just for stats, to know how many sprites are there in the world
this.bloodCount = 0
// we're going to create a group of blood drops now during our setup phase
// so we don't have to create them at runtime
// and basically we're going to recycle them
let bloodyDropGroup = game.add.group()
for(let i = 0; i < 100; i++ ){
let bloodySprite = game.add.sprite(0, 0, this.bloodyDropBitmap)
// enable physics now, note that when we are using sprites from this group
// we will disable the physica first as you can see later on
game.physics.arcade.enable(bloodySprite)
// make sure the anchor y is 0, so it stretches from the bottom
bloodySprite.anchor.set(0.5, 0)
// we will revive them when we start our bloody move
bloodySprite.kill()
bloodyDropGroup.add(bloodySprite)
}
this.bloodyDropGroup = bloodyDropGroup
// we need to create another group for the drips
// because it uses a different bitmap data, not the bloody drop but the circle shape
this.bloodyDripGroup = game.add.group()
// our collision logic
// where we create the blood effects on impact
this.initCollision()
}
initCollision() {
// handle the ground floor collision
this.tilemap.setTileIndexCallback([354], (sprite, tile) => {
// if our sprite hits in-between 2 tiles, we're going to receive 2 callbacks
// this flag make sure we only process once
if(sprite.hasCollided) return
sprite.hasCollided = true
// and hide
sprite.kill()
// create a permanent bloody pool when the bloody drop hits the ground
// randomizing it's y position so they don't form a straight line
// the -10 is the amount that would make the blood cover the entire floor eventually
let bloodPoolSprite = this.createBloodPool(sprite.x,
// note that we are using the tile's coordinates
// this puts the bloody pool on the right spot
game.rnd.realInRange(tile.worldY - 10, tile.worldY), sprite.alpha)
// from the bloody pool, we create a permanent bloody drip
this.createBloodDrip(
// randomly within the bloody pool's width
game.rnd.realInRange(bloodPoolSprite.x - 5, bloodPoolSprite.x + 5),
// we're pushing it up a bit so it doesn't show any disconnect from the bloody pool
game.math.max(tile.worldY - 3, sprite.y), sprite.alpha)
})
// handles the second floor collision tiles
// blood that falls here has a chance it generates a bloody drip
// that continues to fall down to the ground
this.tilemap.setTileIndexCallback([356, 204, 206, 208, 209, 270, 271, 272], (sprite, tile) => {
// console.log('collided with tile index', tile.index)
// same as above, don't process 2x
if(sprite.hasCollided) return
// when we eventually had a chance to create that bloody drip to the ground floor
// it will start at this same tile, so collision callback for it may trigger right away
// this boolean flag helps us prevent that from happening
if(sprite.dripper) return
sprite.hasCollided = true
sprite.kill()
// notice that our map has different style of floor
// there's the floor that is angled and shows a larger area of the floor
// and the other one is just a straight line.
// we leverage that here so that depending on the floor
// our bloody pool can occupy a larger space
let posy = game.rnd.realInRange(tile.worldY - 10, tile.worldY)
if([204, 206, 208, 209].includes(tile.index)) {
posy = game.rnd.realInRange(tile.worldY, tile.worldY + 2)
}
// create a permanent bloody pool from the blood drop
let bloodPoolSprite = this.createBloodPool(sprite.x, posy, sprite.alpha)
// and then from the pool we create a permanent bloody drip
let bloodDripSprite = this.createBloodDrip(
game.rnd.realInRange(bloodPoolSprite.x - 5, bloodPoolSprite.x + 5),
bloodPoolSprite.y,
sprite.alpha
)
// only a fraction of it will fall to the ground
if(this.weightedRandom({0:0.8, 1:0.2})) {
// I'm trying to create a subtle effect
// where it tries to animate how blood drips in real-life
// using an easing back-in achieves that, because it will
// try to pull back the bloody drip first before we let it continue to the ground
let fallTween = game.add.tween(bloodDripSprite.scale)
fallTween.to({
y: bloodDripSprite.scale.y * game.rnd.realInRange(1.2, 1.7) },
game.rnd.between(1000, 2000), Phaser.Easing.Back.In, true)
// so after the animation we just let it go, by creating a new sprite in place of it
// notice the position y. i don't want it to start from the tip
fallTween.onComplete.add(() => {
this.dripBlood(bloodDripSprite.x, bloodDripSprite.y + bloodDripSprite.height - 5,
bloodDripSprite.scale.x, bloodDripSprite.scale.y
)
}, this)
}
}, this)
}
// this is the method that gets called when you start clicking the canvas
dropBlood(mx, my) {
// let's grab an unused bloody from the group
let bloodySprite = this.bloodyDropGroup.getFirstDead()
// this probably won't trigger but just incase so we don't get a fatal error
if(!bloodySprite) return
// we're going to do some tweening first
bloodySprite.body.enable = false
// make it show where we click
bloodySprite.position.set(mx, my)
// give our bloody drop a random size every time
bloodySprite.scale.set(game.rnd.realInRange(0.2, 0.3))
// set to 0, acts as our flag during update
// that reminds us now to change it's alpha if physics hasn't been enabled
bloodySprite.lifespan = 0
// make sure we start solid red
bloodySprite.alpha = 1
// this is a boolean flag set during collision
// with the tilemap so we don't process it twice
bloodySprite.hasCollided = false
// time to set it free, or rather make it appear
bloodySprite.revive()
// let's stretch it so it does look more like a blood dripping from our cursor
let fallTween = game.add.tween(bloodySprite.scale)
fallTween.to({
x: bloodySprite.scale.x * game.rnd.realInRange(0.3, 0.5),
y: bloodySprite.scale.y * game.rnd.realInRange(1.5, 2.5)
}, 500, Phaser.Easing.Cubic.In, true)
fallTween.onComplete.add(() => {
// we're going to provide it a lifespan
// and affect it's alpha during the update
// this way we can have variety of solid or faded blood
// this also means that some of the blood may never land
bloodySprite.lifespan = game.rnd.between(500, 1000)
bloodySprite.totallife = bloodySprite.lifespan
// help gravity a little bit, so we don't have a sprite
// that slowly just starts it's movement
bloodySprite.body.velocity.y = game.rnd.between(100, 300)
// okay let physics system kick in
bloodySprite.body.enable = true
})
}
// drips the blood from the blood pool to the ground
dripBlood(x, y, scalex, scaley) {
let bloodySprite = game.add.sprite(x, y, this.bloodyCircleBitmap)
game.physics.arcade.enable(bloodySprite)
// stretch from the bottom
bloodySprite.anchor.set(0.5, 0)
// start with a short one
bloodySprite.scale.set(scalex, game.rnd.realInRange(0.05, 0.1))
// we're going to use the easing exponential-in
// because we want it to start travelling small and boxy for a time
// and quickly expand towards the end of the tween
game.add.tween(bloodySprite.scale).to({
x: scalex * game.rnd.realInRange(0.1, 0.3),
y: game.rnd.realInRange(0.8, 1.2) }, 500, Phaser.Easing.Exponential.In, true)
// boolean flag we can use so we know not to check collision with this
bloodySprite.dripper = true
// give it it's own life
bloodySprite.lifespan = game.rnd.between(1000, 3000)
bloodySprite.totallife = bloodySprite.lifespan
// so we can test against collision with the tilemap
this.bloodyDripGroup.add(bloodySprite)
}
update() {
super.update()
// no callback, we'll handle it with setTileIndexCallback
// so we can do different behaviour dependingon which tile index it landed in
game.physics.arcade.collide([this.bloodyDropGroup, this.bloodyDripGroup], this.layer)
if (game.input.activePointer.isDown) {
// prevent holding down of mouse
//game.input.activePointer.reset()
this.dropBlood(game.input.mousePointer.x, game.input.mousePointer.y)
}
this.bloodyDropGroup.forEachAlive(function(bloodySprite) {
// check if it began it's lifespan or is still tweening
if(bloodySprite.lifespan) {
// change it's alpha base on it's lifespan
bloodySprite.alpha = bloodySprite.lifespan / bloodySprite.totallife
if(bloodySprite.alpha < 0) {
bloodySprite.kill()
}
}
})
// similar to above but we don't need to check if physics has kick-in
// because it is when we create them
this.bloodyDripGroup.forEachAlive(function(bloodySprite) {
bloodySprite.alpha = bloodySprite.lifespan / bloodySprite.totallife
if(bloodySprite.alpha < 0) {
bloodySprite.kill()
}
})
}
render() {
super.render()
for(var j=0;j<this.bloodyDropGroup.children.length;j++){
let bloodySprite = this.bloodyDropGroup.children[j]
// game.debug.body(bloodySprite)
}
game.debug.text('group living count: ' + (this.bloodyDropGroup.countLiving() + this.bloodyDripGroup.countLiving()), 370, 20)
game.debug.text('sticky pools and drips: ' + this.bloodCount, 370, 40)
//game.debug.cameraInfo(game.camera, 250, 32)
}
}
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
DemoState.prototype.createBloodyDrop = function() {
let bitmapSize = 100
let bmd = game.add.bitmapData(bitmapSize, bitmapSize)
// Draw a triangle on top of a circle
bmd.ctx.fillStyle = 'rgb(131,3,3)'
bmd.ctx.beginPath()
bmd.ctx.moveTo(50,0)
bmd.ctx.lineTo(30,35)
bmd.ctx.lineTo(70,35)
bmd.ctx.fill()
// Draw a circle below the triangle
bmd.circle(50, 50, 25, 'rgb(131,3,3)')
return bmd
}
DemoState.prototype.createBloodyCircle = function() {
let circleSize = 30
let bmd = game.add.bitmapData(circleSize, circleSize)
bmd.circle(circleSize / 2, circleSize / 2,circleSize / 2, 'rgb(131,3,3)')
return bmd
}
DemoState.prototype.createTileMap = function(name, tileset) {
let tilemap = game.add.tilemap(name)
tilemap.addTilesetImage(tileset)
return tilemap
}
DemoState.prototype.createLayer = function(tilemap, name) {
let layer = tilemap.createLayer(name)
layer.setScale(1.7)
layer.resizeWorld()
return layer
}
// creates the blood pool effect
DemoState.prototype.createBloodPool = function(x, y, alpha) {
// by using our bloody circle shape and flatten it vertically
let bloodPoolSprite = game.add.sprite(x, y, this.bloodyCircleBitmap)
bloodPoolSprite.anchor.set(0.5)
// for variety, let's randomize it's size
// but make sure it's width is stretch further than it's height
bloodPoolSprite.scale.set(game.rnd.realInRange(0.3, 1.2), game.rnd.realInRange(0.1, 0.3))
// use the same alpha as when the blood drop make its impact
bloodPoolSprite.alpha = game.math.max(0.5, alpha)
// for further variety, 30% of the time we want to grow the blood pool
if(this.weightedRandom({0:0.7, 1:0.3})) {
// we just multiplay it's x scale so it stretches itself sideways
let scale = bloodPoolSprite.scale
game.add.tween(scale).to({x: scale.x * 1.5 }, game.rnd.between(500, 1500), Phaser.Easing.Linear.None, true)
}
// just keeping track
this.bloodCount++
return bloodPoolSprite
}
// creates the drip from the bloody pool
DemoState.prototype.createBloodDrip = function(x, y, alpha) {
// uses the bloody circle bitmap but this time we flatten it horizontally
let bloodDripSprite = game.add.sprite(x, y, this.bloodyCircleBitmap)
// make sure anchor y is 0, so it stretches from the bottom and not the center
bloodDripSprite.anchor.set(0.5, 0)
// so it doesn't look like it's forming a pattern
// i like to to only for a short percentage that a long drip is created
// the rest are all short ones
let randomScales = [
[game.rnd.realInRange(0.1, 0.15), game.rnd.realInRange(0.2, 0.3)],
[game.rnd.realInRange(0.1, 0.15), game.rnd.realInRange(0.3, 0.4)],
[game.rnd.realInRange(0.1, 0.15), game.rnd.realInRange(0.6, 0.9)]
]
let rndIndex = this.weightedRandom({0:0.7, 1:0.2, 2:0.1})
// pull the randomize scale out from the array
let randomSize = randomScales[rndIndex]
// now set the random size
bloodDripSprite.scale.set(randomSize[0], randomSize[1])
// same alpha from the bloody pool
bloodDripSprite.alpha = game.math.max(0.5, alpha)
// just keeping track
this.bloodCount++
return bloodDripSprite
}
// the weighted random algorithm so we can do things
// like only 20% of the sprites will be this big
DemoState.prototype.weightedRandom = function(spec) {
let i, sum=0, r=Math.random()
for (i in spec) {
sum += spec[i]
if (r <= sum) return parseInt(i)
}
}