Bloody Drippy Scene using Phaser

Last modified: 25 Jan 2019

Summary

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

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:

  1. pull a bloody drop sprite from a Phaser.Group
  2. reset a few stuff
  3. tween it so it expands down
  4. after the tween finishes, enable physics
  5. put a random life, we will use this to alter it’s alpha over the course of it’s fall during update()
  6. apply some downward velocity, just to get the sprite moving fast
  7. let gravity do its thing
  8. 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:

  1. create a sprite from the bitmap circle shape, squeeze vertically so it looks like a flat disc which would form our bloody pool sprite
  2. create another sprite from the bitmap circle shape, now squeeze horizontally, this will form our bloody drip sprite
  3. 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.

  1. createBloodPool - this is the flattened disc shape sprite
  2. createBloodDrip - this is the drips that hangs within the width of the disc shape sprite
  3. dripBlood - this is the sprite that would continue to fall down to the ground if a blood drops to our second floors.

The bloody pool

  1. create a sprite that uses the bloody circle shape bitmap data we created
  2. flatten it so it would look like a bloody pool by scaling up it’s x and scaling down it’s y
  3. use the same alpha from the bloody drop so it shares the same opacity with the blood drop impacts the floor
  4. 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

  1. create a sprite that uses the bloody circle shape bitmap data we created
  2. flatten it the other way around so it would look like it’s dripping down from the bloody pool.
  3. 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

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

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)
  }
}

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