In lesson 10, we implemented code to make the danmaku "aim" its cannon at the player. The aiming of the cannon was performed once at the time of firing the bullet. The bullet would be fired in the direction of the player at the time of firing, but after firing, the bullet would move in the predetermined straight line (or predetermined curve, if bending bullet). The player could move and avoid the bullet quite easily.
In this post we implement code to make the bullets "home-in" on the player, or "seek" the player as it flies.
Theory behind automous objects seeking targets
The theory behind making objects seek a target is very well explained in this Coding Train youtube video here. The video refers to a fascinating paper on the subject called "Steering Behavior of Automous Character" by Chris W Reynods, available here. You do not need to read the paper to get through this post, but it is a fascinating paper and well worth a read. In a nut-shell the key equation is:
steering = desired velocity - velocity
In otherwords;
"desired velocity" is the vector which defines where the object wants to go.
the difference between the "desired velocity" and the (current) "velocity" = "steering", is the "force" that should be applied to the object to make it move in the direction of the target.
At its simplest, the desired velocity would be a vector from the object to the target. This would take the object immediately to the target. To make the steering a bit more "realistic", a max-speed is applied to desired velocity, and also the "steering" force is constrained by some kind of maximum turning ability, otherwise the object can "turn on a dime".
The video explains how to implement the above in all by manipulating 2D vectors.
In terms of the maximum speed, we already have that in our bulletSpeed parameter. Phaser's body.velocity is actually a 2D vector. There are Phaser functions to limit (limit) the magnitude of a vector, or set the magnitude of a vector (setLength). Hence we could very easily implement the code in exactly the same way, using Phaser 2D vectors, and I suspect that would be the most elegant.
However, rather than work directly with vectors, we will work in angles, as per the Ourcade tutorial here, as (i) all the danmaku functionalities created so far, have not worked directly with vectors, and (ii) passing the "turning" constraint in terms of degrees is intuitively easier to understand, than say passing a "length" of the vector.
I have created 2 new parameters:
new bulletType = "HOMING"
degreesPerTurn: this is the maximum angle that the direction of the bullet can change per frame. It is passed to the danmaku object in degrees, but within the bullet class, it is converted into radians.
bulletTarget: this is the target for the bullet to seek. It would in passed on from the danmaku object parameter, target, which is used to aim the cannon in the direction of the target. If the bulletType is "HOMING", then the code will automatically seek the target.
The mechanism to adjust the direction to make the bullet "seek" the target is driven by the preUpdate function.
preUpdate(time, delta) {
switch (this.bulletLife) {
case -1:
break;
default:
this.timeSinceFired += delta;
if (this.timeSinceFired > this.bulletLife) {
this.disableBody(true, true);
}
break;
} ;
if (this.outOfScreen(this)) {
this.disableBody(true, true);
}
switch (this.bulletType) {
case "NORMAL":
break;
case "BEND":
this.adjustVelocity()
break;
case "HOMING":
this.seek();
this.adjustVelocity();
break;
}
} // end of preUpdate
With the BEND bulletType, we already have a code that changes the velocity direction of the bullet according to its body angle. We have moved that piece of code to a new function called adjustVelocity, so that we can also call upon it for our HOMING bullet.
The piece of code to make the bullet "seek" the target is contained in a new function called "seek". The algorithm is pretty much taken straight out of the Ourcade tutorial mentioned above.
seek() {
const targetAngle = Phaser.Math.Angle.Between(this.x, this.y, this.bulletTarget.x, this.bulletTarget.y);
let diff = Phaser.Math.Angle.Wrap(targetAngle - this.rotation);
// to to targetAngle if less than degrees per turn
if (Math.abs(diff) < this.degreesPerTurn) {
this.rotation = targetAngle;
} else
{
let angle = this.rotation;
if (diff > 0) {
angle += this.degreesPerTurn
} else
{
angle -= this.degreesPerTurn
}
this.setRotation(angle);
}
} // end of seek function
Another change, on-the-side
I used to have a danmakuType called "SINGLE" specifically for danmaku with 1 cannon, so that in the set cannons function we would not end up dividing by zero. I have no incorporated a switch statement to check for numberOfCannons = 1 to check for this condition, instead of using the danmakuType.
Danmaku configuration to test the new feature
The following configuration was used to create the pictured homing bullet.
danmakuPattern.push({
name: "HOMING SINGLE INTERMITTENT DIRECTIONAL",
danmakuType: "SINGLE",
numberOfCannons: 1,
cannonAngle: 90,
numberOfShots: 3,
stopShotsTime: 1000,
bulletType: "HOMING",
bulletSpeed: 200,
timeBetweenShots: 100,
bulletTexture: "bullet7",
degreesPerTurn: 3,
target: player,
bulletLife: 3000
});
And that's pretty much it! Here is the CodePen for your perusal.
Comments