top of page
Search
cedarcantab

Danmaku using Phaser 3 (lesson 19): Overtaking Bullets

Updated: Oct 16, 2021

If you play a lot of bullet hell type of games, you might have come across a bullet pattern whereby after a series of shots are fired, they bunch up, then start spreading again. This is achieved by firing the sequential shots at varying speeds, with the latter shots fired at higher speeds, so they catch-up with the shots fired earlier, then overtake them.


That is the subject of this post - Overtaking Bullets.



This type of danmaku works with intermittent firing patterns. In principle, the intermittent danmaku works on a timer with a repeat loop defined by "numberOfShots", and "stopShotsTime". For a "normal" intermittend danmaku, the successive shots (all shots) are fired at the same speed. This is quite different from the SPREAD danmaku, where the bullets are fired at different speeds but at the same time, ie in one "shot".


We can utilise the "counter" of the repeatShotsTimer (by using the Timer Event object property "repeatCount") to "vary" the bullet speeds of successive shots. We will take the bulletSpeed as the speed of the "first shot" and then add some kind of incremental speed for the subsequent shots - we will create a new parameter called bulletSpeedVariance for this speed increment.


For all the danmaku patterns up to now, the danmakuType varialble was used to define the "shape" of the danmaku - more specifically, it was used to define the direction of the cannons. With the exception of spread shot, the bullet speeds were consistent. In the previous version of the code, the SPREAD shot was actually defined as a separate danmakuType called "SPREAD" and the shape of the danmaku pattern (ie the cannon angles) was set as the default "NWAY" danmaku. This meant that you could not have, say a Multiple NWAY danmaku with spread type of bullets.


In this version of the code, we will create a new parameter to help define the shot or bullet speeds as opposed to the pattern of the cannon angles. For this, I have created a new parameter called cannonShotType, to indicate how the how the speed of bullets fired by each cannon in each Shot should be set up. I have now used this variable to indicate also"SPREAD" shots.


At the same time, I have changed the naming of the following to bring a bit more consistency:

  • setCannonAngles function --> setUpCannons. Whereas before, this function was purely to set up the cannonangles, now this function calls another function to set up bulletspeeds array.

  • setBulletSpeeds function --> shotSpeedArray

  • bulletSpeeds array ---> cannonShotSpeeds

The setBulletSpeed function now named shotSpeedArray does exactly that; set up the array of bulletspeeds. Before, this was either an array with a single bulletSpeed element (since all bullets were fired at the same speed), or an array of spreadGroupCount number of bulletSpeeds, spreadGroupCount being the number of different speeds that the bullest would be fired.


Now, I have restructured the code so that the function now returns a nested array of arrays. Each of the inner array would include bulletspeeds x numberOfCannons, and the outer array would include spreadGroupCount x the inner arrays. For example, The outer array so that for each "shot" (which may consist of n-cannons),


As the setUpCannons function has been significantly amended, it is reproduced in full below.

setUpCannons(danmakuType) {

    this.cannonAngles = [];
    this.cannonShotSpeeds = [];
    let speedsArray = []
    
    // check first if there is only 1 cannon. then set 1 cannon angle, then return
    if (this.numberOfCannons === 1) {
      this.cannonAngles.push(this.angle); 
      this.cannonShotSpeeds = this.shotSpeedArray(this.cannonShotType);
      return 
    }
    
    switch (danmakuType) {
        
      case "MULTI_NWAY":
        for (let j = 0; j < this.numberOfNWAY; j++) {
          let nwaycentre = this.angle + j * (360 / this.numberOfNWAY);
          for (let i = 0; i < this.numberOfCannons; i++) {
            this.cannonAngles.push(nwaycentre + (i / (this.numberOfCannons - 1) - 0.5) * this.cannonAngleRange);
          } 
          speedsArray = speedsArray.concat(this.shotSpeedArray(this.cannonShotType)[0]);
        }
        this.cannonShotSpeeds.push(speedsArray);
        break;
      case "GAP_CIRCLE":       
        for (let i = 0; i < this.numberOfCannons; i++) {
          this.cannonAngles.push((this.angle+180) + (i / (this.numberOfCannons - 1) - 0.5) * this.cannonAngleRange);
        } // end of for loop where cannon range is less than 360
        this.cannonShotSpeeds = this.shotSpeedArray(this.cannonShotType);
        break;
      case "BI_DIRECTIONAL":  
        for (let i = 0; i < this.numberOfCannons; i++) {
          this.cannonAngles.push(this.angle + i * (360 / this.numberOfCannons));
          this.cannonAngles.push((360 - this.angle) + i * (360 / this.numberOfCannons));
       
        } // end of for loop
        this.cannonShotSpeeds.push(
            this.shotSpeedArray(this.cannonShotType)[0].concat(this.shotSpeedArray(this.cannonShotType)[0])
          );
        break;
      
      default: // default is NWAY    
        if (this.cannonAngleRange < 360) {
          for (let i = 0; i < this.numberOfCannons; i++) {
            this.cannonAngles.push(this.angle + (i / (this.numberOfCannons - 1) - 0.5) * this.cannonAngleRange);
          } // end of for loop where cannon range is less than 360
          this.cannonShotSpeeds = this.shotSpeedArray(this.cannonShotType);
        } // end of if statement cannonAngleRange < 360
        else 
        {
          // get here if cannon angle range is 360
          for (let i = 0; i < this.numberOfCannons; i++) {
            this.cannonAngles.push(i * (360 / this.numberOfCannons) + this.angle);
          } // end of for loop where cannonAngleRange = 360
          this.cannonShotSpeeds = this.shotSpeedArray(this.cannonShotType);
        } // end of case where cannonAngleRange = 360
    } // end of switch statement
    
  } // end of setCannonAngle function

And more importantly, the completely re-written setSpeedArray function

shotSpeedArray(cannonShotType) {
    // this function returns just the inner array if whole=false, otherwise returns the nested array
 
    const cannonShotSpeeds = []
    let cannonsInGroup = []
    
    switch (cannonShotType) {
      case "SPREAD":
        for (let i = 0; i < this.spreadGroupCount; i++) {
          cannonsInGroup = [];
          for (let j = 0; j < this.numberOfCannons; j++ ) {
            cannonsInGroup.push(this.bulletSpeed + this.bulletSpeedVariance * i);
          }          
          cannonShotSpeeds.push(cannonsInGroup);
        }
        break;
      case "OVERTAKE":
        for (let j = 0; j < this.numberOfCannons; j++ ) {
          cannonsInGroup.push(this.bulletSpeed + (this.numberOfShots - this.repeatShotsTimer.repeatCount) * this.bulletSpeedVariance);
        }
        cannonShotSpeeds.push(cannonsInGroup);
        break;
      default:        
        for (let j = 0; j < this.numberOfCannons; j++ ) {
          cannonsInGroup.push(this.bulletSpeed);
        }
        cannonShotSpeeds.push(cannonsInGroup)
    }
    return cannonShotSpeeds
    
  }  // end of setSpeedArray functinon

The configuration to produce the pictured danmaku is as below. The key parameters are highlighted.


By its nature, overtaking bullets type only works with intermittent danmaku.

 danmakuPattern.push({
    name: "4 x 3-WAY INTERMITTENT WITH OVERTAKING BULLETS",
    danmakuType: "MULTI_NWAY",
    numberOfCannons: 3, // number of (pair in case of bi-directional danmaku) cannons
    cannonAngle: 90, // direction in which the primary cannon faces.
    cannonAngleRange: 25, // used to n-way type. angle range across which cannons are positioned.
    numberOfNWAY: 4,   
    numberOfShots: 5, // how many times to shoot the weapon in succession
    stopShotsTime: 200,
    // properties for individual bullets
    bulletType: "NORMAL",
    bulletSpeed: 200, // speed of bullets fired from cannon
    bulletSpeedVariance: 30,
    timeBetweenShots: 100, // time between bullets in ms
    bulletTexture: "bullet7", // specify the bullet image to be used,
    cannonShotType: "OVERTAKE"
  });

And that's it! As always, the CodePen is below, for your perusal.



Comments


記事: Blog2_Post
bottom of page