top of page
Search
  • cedarcantab

Danmaku Interlude Part 7: Explosions using Sprites

Updated: Dec 20, 2021

In this post I look at how explosion effects can be achieved with sprite animation. At the same time I will dig a little deeper into the question of to pool or not to pool game objects.



Animated sprites

I have used various types of Phaser game objects in examples so far, including images and sprites. With sprites, there is the ability to add animations, rather than a still image as texture. As always, rather than explain concepts and build the code ground up, I will show the code and explain how it works.


The first thing we need is the 'images' or frames that the animation will cycle through. In Phaser, such collection of frames are called spritesheets, and are loaded with: spritesheet(key [, url] [, frameConfig] [, xhrSettings]).


I have, as always gone to the Phaser lab site, and found an explosion spritesheet, which I load with the following code inserted in the preload function.

this.load.spritesheet("explosion", "assets/games/lazer/explosion.png", {
    frameWidth: 16,
    frameHeight: 16
  });

Spritesheets are the first step (although not the only way) to creating animations. The texture loaded (in this case, explosion.png) must contain the frames to be cycled through, and the frames must be the same size. In this example, the file is 80x16 with 5 frames, each frame being 16x16 as shown below.



Create the animation sequence


Then we must create the animation, by inserting the following code in the create function.

// create explosion animation
  this.anims.create({
    key: "explosion",
    frames: this.anims.generateFrameNumbers("explosion"),
    frameRate: 10,
    repeat: 0,
    hideOnComplete: true
  });

The parameters are as follows:

  • key: the animation with a key "explosion",

  • frames: frames to be cycled through are generated using this.anims.generateFrameNumbers, which requires the key of the spritesheet that contains the frames. In this case, it is the previously loaded spritesheet with key="explosion"

  • frameRate: cycle through the frames at a frame rate of 10 frames per second; in our example we only have 5 frames so one loop of animation takes 500 milliseconds (ie 1000 milliseconds / 10 x 5) or a half of a second.

  • repeat: in this case, we only want the explosion animation to be played once, and

  • hideOnComplete: when the animation is complete, the animation image disappears.

Animations are played using the 'play' method, which requires the key of the animation to be passed to it.


Create an sprite object to "play" the animation


In this demo, I have created the explosion as an extended sprite and named it "Explosion" as follows. All it does is to set the scale of the image (original size is 16x16, ie very small) by 3 times and then play the animation created above, upon its own creation.

class Explosion extends Phaser.GameObjects.Sprite {
  constructor(scene, x, y) {
    super(scene, x, y);
    this.setScale(3);
    this.play("explosion");
  }
} //end of explosion class   

As we are likely to have more than one explosion 'happening' on the screen at once, groups will be used. The group can be set up with the following, in the create function.

//explosions group
  explosions = this.add.group({
    name: " Explosions",
    classType: Explosion,
    enable: false
  });

Then we can simply instantiate the Explosion object, every time a player bullet hits the enemy (or enemy bullet hits the player)


// create overlap event between the enemy and the player bullets
  this.physics.add.overlap(enemy, playerBullets, function (_enemy, _bullet) {
    const { x, y } = _bullet.body.center; // set x and y constants to the bullet's body (for use later)
    const explosion = explosions.create(x, y);
    explosion.once("animationcomplete", () => {
      explosion.destroy();
    });
...

With the above, everytime an "overlap" is detected by Phaser's physic engine, a new Explosion object is instantiated (which has the code to start playing the animation), and there is also a code to destroy the object once the animation is complete.


Doing away with the extended sprite object


As descirbed above, we created an extended sprite object whose sole job is to scale up the image, and 'play' the animation. This is probably a bit of an overkill. We can achieve exactly the same effect without using extended sprite object, but creating the explosion group as follows

//explosions group
  explosions = this.add.group({
    name: " Explosions",
    classType: Phaser.GameObjects.Sprite,
    enable: false
  });

And when we want to make the explosion happen, the following code can be used.

this.physics.add.overlap(player, enemyBullets, function (_player, _bullet) {
    const { x, y } = _bullet.body.center; // set x and y constants to the centre of the bullet's body (for use later)

    const explosion = explosions.create(x, y);
    explosion.setScale(3).play("explosion");
    explosion.once("animationcomplete", () => {
      explosion.destroy();
    });
 ...

And that does that job.


Use of object pools and object recycling


However, you would have noticed that we are creating a new Explosion object and destroy the same object everytime there is an explosion. However, creating objects and destroying objects apparently take up a lot of processor time. For a very simple program like ours, this really does not matter. But as you start getting thousands or tens of thousands of objects moving around the screen, then it will begin to matter. For this reason, object pools are used. We have in fact already been using object pools for the danmaku and player bullets. The concept is straight forward enough. Rather than creating a new game object everytime it is needed (eg when a bullet is fired) using create() and destroying it using destroy() when it is no longer needed (eg when the bullet flies off screen), a "pool" of objects (eg bullets) is initially created. In our example, for the enemy, a pool of 500 bullets are created.

// create bullets group to be utilised by danmaku spawner - make a pool of 500 bullets
  enemyBullets = this.physics.add.group({
    name: " Enemy bullets",
    enable: false
  });
  enemyBullets.createMultiple({
    classType: Bullet,
    frameQuantity: 500,
    active: false,
    visible: false,
    key: "bullet1" // set the default bulletType as "bullet1"
  });

The 500 bullets are initially crated with active and visible properties set to false; the objects are essentially asleep.


When a bullet is fired, we use the getFirdDead(false) method to get one sleeping object from the pool.

const bullet = enemyBullets.getFirstDead(false);

Then the objects are 'woken' up with the enableBody method: enableBody(reset, x, y, enableGameObject, showGameObject), the latter two parameters set to true.


Once the object has been enabled, then the various properties of a game object can be set so that the object does its thing, like flying around the screen.


And when the object has done its job, it is put to sleep again, by using

this.disableBody(true, true);

For our explosions, we could do it in exactly the same way, ie create a pool of a predetermined number of objects. However, for the sake of exploring some of Phaser's functionality, we'll do it a different way. We start by creating a group, but we unlike the bullet group, we do not use createMultiple to create "blanks". At this point in time, the explosions group is zero.

//explosions group
  explosions = this.add.group({
    name: " Explosions",
    classType: Phaser.GameObjects.Sprite
  });

Then in the overlap callback function, the following code is inserted.



this.physics.add.overlap(player, enemyBullets, function (_player, _bullet) {
    const { x, y } = _bullet.body.center; // set x and y constants to the centre of the bullet's body (for use later)

    const explosion = explosions.get(x, y, "explosion");
    explosion.setActive(true).setVisible(true).setScale(3).play("explosion");
    explosion.once("animationcomplete", () => {
      explosions.killAndHide(explosion);
    });

We use the get method, instead of getFirstDead. The get method is similar to getFirstDead except that when it finds no inactive members, and the group isn't full, it will create a new game object.

get( [x] [, y] [, key] [, frame] [, visible])

Initially, you will find that the size of the pool increases, if there are lots of explosions occurring on the screen at the same time. However, after a while, you will find that the explosion objects are recycled, and the total size of the pool no longer expands - ie object recycling happens.


The CodePen Pen of the entire code is below, for your perusal.



3 views0 comments

Recent Posts

See All

b2Distance

// MIT License // Copyright (c) 2019 Erin Catto // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"

p2 naive broadphase

var Broadphase = require('../collision/Broadphase'); module.exports = NaiveBroadphase; /** * Naive broadphase implementation. Does N^2 tests. * * @class NaiveBroadphase * @constructor * @extend

Extending Box2D-Lite in Javascript: Revolute Joint

/// Revolute joint definition. This requires defining an anchor point where the /// bodies are joined. The definition uses local anchor points so that the /// initial configuration can violate the con

記事: Blog2_Post
bottom of page