Danmaku is all about shooting bullets in pretty patterns. A lot of bullets. A curtain of bullets!
Directional Danmaku (方向弾)
In this blog, we start with a Directional Danmaku, or firing a continuous bullets in a straight line. Probably the simplest of all danmaku patterns. In Japanese this is referred to as 方向弾.
The basic elements to achieve this (and in any danmaku patterns) are:
'Bullet' class: basically a single bullet which flies according to prescribed parameters
Group of bullets to hold the bullets to be recycled by the 'Danmaku'
'Danmaku' class: a class that makes a lot of bullets fly in prescribed patterns
But before we get to the important bits...
To keep the post (as much as possible) focused on the Danmaku mechanics, I will keep the 'other bits' as standard as possible (ie try to use it consistently in following posts).
We will use the following game config (note, the images used in this example are also 'poached' from Phaser.Discourse example).
const config = {
width: 600,
height: 700,
backgroundColor: 0x000000,
pixelArt: true,
physics: {
default: "arcade",
arcade:{
debug: true
}
},
loader: {
baseURL: "https://labs.phaser.io",
crossOrigin: "anonymous"
},
scene: {
preload: preload,
create: create,
update: update
}
};
const WIDTH = config.width; // declare constants WIDTH = canvas width
const HEIGHT = config.height; // declare constants HEIGHT = canvas height
const game = new Phaser.Game(config);
In addition to the 'standard' preload, create, and update functions, we'll create one function to get the information about the bullet pool (or any groups) - this is taken straight out of an Phaser.Discourse example referred to below.
function poolInfo(group) {
return `${group.name} ${group.getLength()} (${group.countActive(
true
)}:${group.countActive(false)})`;
}
We will load some images in the preload function (again, poached from Phaser.Discourse example). The following code loads images for 10 different types of bullets, and a 'enemy ship' image. We only use one of the bullet images for this example, and the enemy ship is put on the screen as an image but does not encompass any of the danmaku mechanics. It is simply an image from where the bullets will be fired, since it would look odd to have bullets flying out from nothing.
function preload() {
for (var i = 1; i <= 11; i++) {
this.load.image('bullet' + i, 'assets/sprites/bullets/bullet' + i + '.png');
}
this.load.image('enemyShip', "assets/sprites/bsquadron2.png");
} // end of preload function
Put an enemy character on the screen for show
And within function create(), include the following:
function create() {
// place the enemy image on the screen (this does not do anything)
enemy = this.add.image(WIDTH/2, HEIGHT/4, 'enemyShip');
// create text object to hold the information about bullet pool
text = this.add.text(0, 16, "", { font: "16px monospace" });
}
In the bold highlighted line, we are using the add.image method to place a Phaser 'image object' using the picture defined by the key 'enemyShip' onto the scene (which is the 'this'). The picture 'enemyShip' is a called "bsquadron2.png" that was loaded in the preload function, using Phaser's load.image method. It is a picture of 58 pixels width and 50 pixels height.
With that out of the way, we can go onto the 'meat and potatoes'.
Bullet class and Bullets Group
I have poached many aspects of bullet firing mechanics from an example mentioned above.
My version of the Bullet class is as below. We are making judicious use of functionalities offered by Phaser 3 physics engine (acceleration is not used in this example - ignore for now).
class Bullet extends Phaser.Physics.Arcade.Image {
fire(config) {
this.enableBody(true, config.x, config.y, true, true);
this.body.collideWorldBounds = true;
this.body.onWorldBounds = true;
this.shootAngle = config.shootAngle || 0; // default is 0
this.bulletSpeed = config.bulletSpeed || 300; // default is 300
this.acceleration = config.acceleration || 0; // default is 0
this.scene.physics.velocityFromAngle(this.shootAngle, this.bulletSpeed, this.body.velocity);
this.scene.physics.velocityFromAngle(this.shootAngle, this.acceleration, this.body.acceleration)
}
onWorldBounds() {
this.disableBody(true, true);
}
}
Create Bullets Group
Within the create function, we create the bullets group:
// create bullets group - make a pool of 500 bullets
bullets = this.physics.add.group({
name: "bullets",
enable: false
});
bullets.createMultiple({
classType: Bullet,
frameQuantity: 500,
active: false,
visible: false,
key: "bullet7",
});
// set worldbounds event so that gameobjects disappear when go outside of screen
this.physics.world.on("worldbounds", (body) => {
body.gameObject.onWorldBounds();
});
Create Danmaku Engine!
Now comes the danmaku 'engine' that controls the bullet firing mechanism. I have created it as a Phaser arcade sprite object with a physics body. For this particular example this is necessary but in later examples we will make use of the attributes associated with game objects to make the bullet firing happen.
I have also split the danmaku engine into a DanmakuCore class, and a DanmakuWrapper class which extends the DanmakuCore class. For this simple example, splitting the class into 2 makes it longer than necessary but this makes it easier to develop more complicated danmaku spawners in later examples.
class DanmakuCore extends Phaser.Physics.Arcade.Image {
constructor(scene, x, y, bulletSpeed, timeBetweenBullets) {
super(scene, x, y);
scene.add.existing(this);
scene.physics.add.existing(this);
this.visible = false; // delete this to show the arrow that the cannon is pointing.
this.danmakuPosX = x;
this.danmakuPosY = y;
this.bulletSpeed = bulletSpeed;
this.timeBetweenBullets = timeBetweenBullets;
this.timeLastBulletFired = 0; // set default parameter
} // end of constructor
} // end of basic Danmaku
class DanmakuWrapper extends DanmakuCore {
constructor(scene, config) {
super(
scene,
config.x,
config.y,
config.bulletSpeed,
config.timeBetweenBullets
);
this.angle = config.shootAngle || 0;
}
fireweapon(elapsedTime) {
if (elapsedTime > this.timeLastBulletFired + this.timeBetweenBullets) {
this.timeLastBulletFired = elapsedTime;
const bullet = bullets.getFirstDead(false);
if (bullet) {
bullet.fire({
x: this.danmakuPosX,
y: this.danmakuPosY,
shootAngle: this.angle,
bulletSpeed: this.bulletSpeed
}); // end of code to call the bullet.fire method
} // end of if statement to check if a bullet to fire exists
} // end of if statement to check if sufficient time has elapsed since last bullet fired
} // end of fireweapon method
} // end of DanmakuWrapper class
The fireweapon method is the one that fires the bullet. This method will be called in the update() function, i.e. every frame. However, we definitely don't want the bullet to be fired every frame. Hence we control that by using comparing timeLastBulletFired + this.timeBetweenBullets against the current elapsed time. All this does is to act as a timer to ensure that we wait for a period as defined by this.timeBetweenBullets (which is in milli seconds) before the next bullet is fired. There is definitely a much easier way to do this, using functionality offered by Phaser 3. We will come to re-write this piece of code in future posts.
Now we instantiate the Directional danmaku class within the create() function.
danmaku = new Directional(this,{
x: enemy.x, // origin of the danmaku spawn
y: enemy.y, // origin of the danmaku spawn
shootAngle: 90, // set shoot angle to 90, which is downwards
bulletSpeed: 300, // bullet speed
timeBetweenBullets: 200 // time between bullets in ms
});
All that's left to do is to make the danmaku come alive by calling the fireweapon method of the danmaku class, from the update() function!
function update(elapsedTime) {
danmaku.fireweapon(elapsedTime)
text.setText(poolInfo(bullets)); // show the latest bullet pool info
} // end of update function
And that's it! If you type in the entire code into CodePen, you will see the Directional Danmaku fire consecutive bullets in a straight line, as like shown below!
The entire code can be accessed below.
Comments