Gaming: Battle on the High Seas, Part 4

Jeff Friesen
Tweet
This entry is part 4 of 5 in the series Battle on the High Seas

Battle on the High Seas

Last week, our gaming series dug deeper into SeaBattle’s architecture by discussing the SeaBattle object’s update() function along with its makeShip(x, y, bound1, bound2) constructor. This is the fourth article in our five-part series, and continues to explore this architecture by covering the constructors for submarines, depth charges, torpedoes, and explosions. It also discusses intersects(r1, r2) and collision detection.

Making a Submarine

The update() function is responsible for creating the submarine and other game objects. It accomplishes submarine creation with help from the makeSub(x, y, bound1, bound2) constructor. Listing 1 presents this constructor’s implementation.

makeSub: function(x, y, bound1, bound2) {
  this.x = x;
  this.y = y;
  this.bound1 = bound1;
  this.bound2 = bound2;
  this.bbox = { left: 0, top: 0, right: 0, bottom: 0 };
  this.LEFT = 0;
  this.RIGHT = 1;
  this.dir = (x >= SeaBattle.width) ? this.LEFT : this.RIGHT;
  this.exploded = false;
  this.height = SeaBattle.imgSubLeft.height;
  this.vx = SeaBattle.rnd(5)+2;
  this.width = SeaBattle.imgSubLeft.width;
  this.draw = function() {
    SeaBattle.ctx.drawImage((this.dir == this.LEFT)?
                             SeaBattle.imgSubLeft :
                             SeaBattle.imgSubRight,
                             this.x-this.width/2,
                             this.y-this.height/2);
  }
  this.getBBox = function() {
    this.bbox.left = this.x-this.width/2;
    this.bbox.top = this.y-this.height/2;
    this.bbox.right = this.x+this.width/2;
    this.bbox.bottom = this.y+this.height/2;
    return this.bbox;
  }
  this.move = function() {
    if (this.dir == this.LEFT)
    {
      this.x -= this.vx;
      if (this.x-this.width/2 < this.bound1)
      {
        this.x += this.vx;
        this.vx = SeaBattle.rnd(3)+1;
        this.dir = this.RIGHT;
      }
    }
    else
    {
      this.x += this.vx;
      if (this.x+this.width/2 > this.bound2)
      {
        this.x -= this.vx;
        this.vx = SeaBattle.rnd(3)+1;
        this.dir = this.LEFT;
      }
    }
  }
}

Listing 1: The move() function automatically switches the submarine’s direction after it passes the left or right edge.

Listing 1 first saves its arguments in submarine object properties, and then introduces 11 more object properties:

  • bbox references a rectangle object that serves as a bounding box for collision detection. This object is passed as an argument to the intersects(r1, r2) function.
  • LEFT is a pseudo-constant used in conjunction with the dir property.
  • RIGHT is a pseudo-constant used in conjunction with the dir property.
  • dir specifies the submarine’s current direction.
  • exploded indicates whether or not the submarine has exploded.
  • height specifies the height of the submarine image in pixels.
  • vx specifies the submarine’s horizontal velocity in terms of the number of pixels the submarine moves.
  • width specifies the width of the submarine image in pixels.
  • draw() draws the submarine image coinciding with the submarine’s x and y properties.
  • getBBox() returns an updated bbox object. This object is updated to accommodate a change in the submarine’s horizontal position.
  • move() moves the submarine left or right.

Making a Depth Charge

When the spacebar is pressed, update() attempts to create a depth charge object (only two depth charges can be in play at any one time). Listing 2’s makeDepthCharge(bound) constructor is used to create the depth charge.

makeDepthCharge: function(bound) {
  this.bound = bound;
  this.bbox = { left: 0, top: 0, right: 0, bottom: 0 };
  this.height = SeaBattle.imgDC.width;
  this.width = SeaBattle.imgDC.height;
  this.draw = function() {
    SeaBattle.ctx.drawImage(SeaBattle.imgDC, this.x-this.width/2, this.y-this.height/2);
  }
  this.getBBox = function() {
    this.bbox.left = this.x-this.width/2;
    this.bbox.top = this.y-this.height/2;
    this.bbox.right = this.x+this.width/2;
    this.bbox.bottom = this.y+this.height/2;
    return this.bbox;
  }
  this.move = function move() {
    this.y++;
    if (this.y+this.height/2 > this.bound)
      return false;
    return true;
  }
  this.setLocation = function(x, y) {
    this.x = x;
    this.y = y;
  }
}

Listing 2: The depth charge’s current location coincides with the center of its image.

Listing 2 first saves the argument passed to its bound parameter in a depth charge object property, and then introduces seven more object properties:

  • bbox references a rectangle object that serves as a bounding box for collision detection.
  • height specifies the height of the depth charge image in pixels.
  • width specifies the width of the depth charge image in pixels.
  • draw() draws the depth charge image.
  • getBBox() returns an updated bbox object centered on the object’s current x and y values.
  • move() advances the depth charge downward by a single pixel until the lower bound is passed.
  • setLocation(x, y) specifies the depth charge’s location, which coincides with the center of the depth charge image.

Making a Torpedo

When the center of the submarine is visible, a randomly generated integer equals a certain value, and less than 15 torpedoes are in play, update() creates a torpedo object. The actual work of creating this object is performed by Listing 3’s makeTorpedo(bound) constructor.

makeTorpedo: function(bound) {
  this.bound = bound;
  this.bbox = { left: 0, top: 0, right: 0, bottom: 0 };
  this.height = SeaBattle.imgTorpedo.height;
  this.width = SeaBattle.imgTorpedo.width;
  this.draw = function() {
    SeaBattle.ctx.drawImage(SeaBattle.imgTorpedo, this.x-this.width/2, this.y);
  }
  this.getBBox = function() {
    this.bbox.left = this.x-this.width/2;
    this.bbox.top = this.y;
    this.bbox.right = this.x+this.width/2;
    this.bbox.bottom = this.y+this.height;
    return this.bbox;
  }
  this.move = function move() {
    this.y--;
    if (this.y < this.bound)
      return false;
    return true;
  }
  this.setLocation = function(x, y) {
    this.x = x;
    this.y = y;
  }
}

Listing 3: The torpedo’s current location coincides with the top-center of its image.

Listing 3 first saves the argument passed to its bound parameter in a same-named torpedo object property, and then introduces seven more object properties:

  • bbox references a rectangle object that serves as a bounding box for collision detection.
  • height specifies the height of the torpedo image in pixels.
  • width specifies the width of the torpedo image in pixels.
  • draw() draws the torpedo image.
  • getBBox() returns an updated bbox object centered around the object’s current x value.
  • move() advances the torpedo upward by a single pixel. This function returns true until the top of the torpedo’s image passes its upper bound, at which point it returns false.
  • setLocation(x, y) specifies the torpedo’s location, which coincides with the top-center of the torpedo image. Its arguments are stored in the x and y properties of the torpedo object.

Detecting a Collision

Part 3’s update() function relies on an intersects(r1, r2) function to determine whether or not a collision between a torpedo and the ship or between a depth charge and the submarine has occurred. Listing 4 presents this function’s implementation.

intersects: function(r1, r2) {
  return !(r2.left > r1.right ||
           r2.right < r1.left ||
           r2.top > r1.bottom ||
           r2.bottom < r1.top);
}

Listing 4: Two rectangles are tested for intersection.

Listing 4 determines if its two rectangle arguments (returned from getBBox() calls) intersect by first determining if the second rectangle (r2) lies completely to the right or left of, below, or above the first rectangle (r1) and then negating the result.

If you recall from Part 3, the ship’s bounding box is not fully vertically centered around the object’s current y location. Although the top part is vertically centered, the bottom is not because I assign this.y+2 instead of this.y+this.height/2 to this.bbox.bottom.

Figure 1: The ship image is outlined with a red border to clearly show the extent of the empty vertical space.

Why the difference? Each of the left and right ship images reveals a lot of empty vertical space below the ship. Figure 1 shows the image of the ship facing left.

If I specified this.y+this.height/2 as the bottom bound, an intersecting torpedo would explode too far from the ship’s bottom to look believable. This problem is not present with the submarine, whose images don’t have an excessive amount of empty vertical space.

Making an Explosion

The update() function responds to a collision by calling the makeExplosion(isShip) constructor to create an explosion object. The passed Boolean argument is true when the ship is exploding and false otherwise. Listing 5 shows how this constructor is implemented.

makeExplosion: function(isShip) {
  this.isShip = isShip;
  this.counter = 0;
  this.height = SeaBattle.imgExplosion[0].height;
  this.imageIndex = 0;
  this.width = SeaBattle.imgExplosion[0].width;
  this.advance = function() {
    if (++this.counter < 4)
      return true;
    this.counter = 0;

    if (++this.imageIndex == 8)
    {
      if (this.isShip)
        SeaBattle.ship.exploded = true;
      else
        SeaBattle.sub.exploded = true;
    }
    else
      if (this.imageIndex > 16)
      {
        this.imageIndex = 0;
        return false;
      }
    return true;
  }
  this.draw = function() {
    SeaBattle.ctx.drawImage(SeaBattle.imgExplosion[this.imageIndex],
                            this.x-this.width/2, this.y-this.height/2);
  }
  this.setLocation = function(x, y) {
    this.x = x;
    this.y = y;
    try
    {
      SeaBattle.audBomb.play();
    }
    catch (e)
    {
      // Safari without QuickTime results in an exception
    }
  }
}

Listing 5: An explosion starts to play its audio as soon as its location is specified.

Listing 5’s makeExplosion(isShip) constructor first saves the argument passed to parameter isShip in the explosion object’s isShip property, and then introduces seven additional object properties:

  • counter is used to slow down the explosion’s advance so that it doesn’t disappear too quickly.
  • height specifies the height of each explosion image in pixels.
  • imageIndex specifies the zero-based index of the next explosion image to display.
  • width specifies the width of each explosion image in pixels.
  • advance() advances the explosion each time counter equals four. When imageIndex equals eight, almost half of the explosion is finished, and the exploding ship or submarine is removed.
  • draw() draws the next explosion image.
  • setLocation(x, y) specifies the explosion’s location, which coincides with the center of each explosion image. Its arguments are stored in the x and y properties of the explosion object.

After setting the explosion’s location, an explosion sound effect is played via SeaBattle.audBomb.play();. If you’re using the Safari browser without Quicktime, this browser throws an exception. An exception handler could display a message or take some other action. Currently, we ignore the exception.

Conclusion

Our exploration of SeaBattle’s architecture is nearly complete. Next Friday, Part 5 completes this exploration by first showing you how the game’s scene is drawn on the canvas. Next, it briefly reviews HTML5’s Audio, Canvas, and Web Storage APIs to help newcomers to these APIs better understand SeaBattle. After providing ideas for enhancing this game, Part 5 ends this series by taking SeaBattle beyond the desktop.

Battle on the High Seas

<< Gaming: Battle on the High Seas, Part 3Gaming: Battle on the High Seas, Part 5 >>

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • evanxg

    Hi , can we have the game asset to follow along and maybe do some changes ?

  • Jeff Friesen

    Hi Evan,

    The assets will be made available in Part 5, which is due to be published on Friday.

    All the best.

    Jeff