Gaming: Battle on the High Seas, Part 3

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

Battle on the High Seas

Last Friday, in our series on gaming, I started to explore SeaBattle’s architecture by focusing on the SeaBattle object’s init(width, height) function along with the related rnd(limit) and supports_html5_storage() helper functions. This article, part three of five, continues to explore game architecture by focusing on the update() function and makeShip(x, y, bound1, bound2) constructor.

Updating SeaBattle

Listing 1 presents the implementation of the update() function.

update: function() {
  if (SeaBattle.state == SeaBattle.STATE_INIT)
    return;

  if ((SeaBattle.state == SeaBattle.STATE_TITLE ||
       SeaBattle.state == SeaBattle.STATE_WINLOSE ||
       SeaBattle.state == SeaBattle.STATE_RESTART) && keydown.return)
  {
    if (SeaBattle.state == SeaBattle.STATE_RESTART)
    {
      SeaBattle.score = 0;
      SeaBattle.lives = 4;
    }
    SeaBattle.ship = new SeaBattle.makeShip(SeaBattle.width/2, SeaBattle.height/3, 0, SeaBattle.width-1);
    SeaBattle.sub = new SeaBattle.makeSub(SeaBattle.rnd(2) == 0
                          ? -50+SeaBattle.rnd(30)
                          : SeaBattle.width+SeaBattle.rnd(100),
                            2*SeaBattle.height/3-
                            SeaBattle.rnd(SeaBattle.height/6),
                            -100, SeaBattle.width+100);
    SeaBattle.state = SeaBattle.STATE_PLAY;
  }

  if (SeaBattle.state != SeaBattle.STATE_PLAY)
    return;

  if (SeaBattle.explosion != null)
  {
    if (SeaBattle.explosion.isShip)
      SeaBattle.sub.move();

    for (var i = 0; i < SeaBattle.MAX_DC; i++)
      if (SeaBattle.dc[i] != null)
        if (!SeaBattle.dc[i].move())
          SeaBattle.dc[i] = null;

    for (var i = 0; i < SeaBattle.MAX_TORP; i++)
      if (SeaBattle.torp[i] != null)
        if (!SeaBattle.torp[i].move())
          SeaBattle.torp[i] = null;

    if (!SeaBattle.explosion.advance())
    {
      SeaBattle.ship = null;
      SeaBattle.sub = null;
      for (var i = 0; i < SeaBattle.MAX_DC; i++)
        SeaBattle.dc[i] = null;
        for (var i = 0; i < SeaBattle.MAX_TORP; i++)
          SeaBattle.torp[i] = null;
        SeaBattle.state = SeaBattle.STATE_WINLOSE;
        if (SeaBattle.explosion.isShip)
        {
          SeaBattle.lives--;
          if (SeaBattle.lives == 0)
          {
            SeaBattle.state = SeaBattle.STATE_RESTART;
            SeaBattle.msg = "Game Over! Press RETURN to play "+"again!";
          }
        }
        else
        {
          SeaBattle.score += 100;
          if (SeaBattle.score > SeaBattle.hiScore)
          {
            SeaBattle.hiScore = SeaBattle.score;
            if (SeaBattle.supports_html5_storage())
              localStorage.setItem("hiScore", SeaBattle.hiScore);
          }
        }
        SeaBattle.explosion = null;
      }
    return;
  }

  if (keydown.left)
    SeaBattle.ship.moveLeft();

  if (keydown.right)
    SeaBattle.ship.moveRight();

  if (keydown.space)
  {
    for (var i = 0; i < SeaBattle.MAX_DC; i++)
      if (SeaBattle.dc[i] == null)
      {
        var bound = SeaBattle.hillTops[SeaBattle.ship.x];
        SeaBattle.dc[i] = new SeaBattle.makeDepthCharge(bound);
        SeaBattle.dc[i].setLocation(SeaBattle.ship.x, SeaBattle.ship.y);
        break;
      }
      keydown.space = false;
  }

  SeaBattle.sub.move();
  if (SeaBattle.sub.x > 0 && SeaBattle.sub.x < SeaBattle.width && SeaBattle.rnd(15) == 1)
    for (var i = 0; i < SeaBattle.MAX_TORP; i++)
      if (SeaBattle.torp[i] == null)
      {
        SeaBattle.torp[i] = new SeaBattle.makeTorpedo(SeaBattle.height/3);
        SeaBattle.torp[i].setLocation(SeaBattle.sub.x, SeaBattle.sub.y-SeaBattle.imgTorpedo.height);
        break;
      }

  for (var i = 0; i < SeaBattle.MAX_DC; i++)
    if (SeaBattle.dc[i] != null)
      if (!SeaBattle.dc[i].move())
        SeaBattle.dc[i] = null;
      else
      {
        if (SeaBattle.intersects(SeaBattle.dc[i].getBBox(), SeaBattle.sub.getBBox()))
        {
          SeaBattle.explosion = new SeaBattle.makeExplosion(false);
          SeaBattle.explosion.setLocation(SeaBattle.dc[i].x, SeaBattle.dc[i].y);
          SeaBattle.msg = "You win! Press RETURN to keep playing!";
          SeaBattle.dc[i] = null;
          return;
        }
      }

  for (var i = 0; i < SeaBattle.MAX_TORP; i++)
    if (SeaBattle.torp[i] != null)
      if (!SeaBattle.torp[i].move())
        SeaBattle.torp[i] = null;
      else
      {
        if (SeaBattle.intersects(SeaBattle.torp[i].getBBox(), SeaBattle.ship.getBBox()))
        {
          SeaBattle.explosion = new SeaBattle.makeExplosion(true);
          SeaBattle.explosion.setLocation(SeaBattle.torp[i].x, SeaBattle.torp[i].y);
          SeaBattle.msg = "You lose! Press RETURN to keep playing!";
          SeaBattle.torp[i] = null;
          return;
        }
      }
}

Listing 1: SeaBattle doesn’t update the game in the initialization state.

Listing 1 first examines the state property to learn if it equals STATE_INIT. If so, the update() function returns. There’s no point in executing update() any further while game resources are still loading.

Next, state is compared with STATE_TITLE, STATE_WINLOSE, and STATE_RESTART. The game is not in play when in this state. To get it into play, it’s necessary for the user to press the Return key (keydown.return exists and is true).

If the game is being restarted (state equals STATE_RESTART), the score is reset to zero and the number of ship lives is set to four. Regardless of restart, win/lose, or title state, ship and sub objects are created, and STATE_PLAY is assigned to state.

The makeShip(x, y, bound1, bound2) constructor is called to create the ship. This object is horizontally centered and vertically positioned one third of the canvas height below the top of the canvas. The bounds prevent the ship from being moved beyond canvas limits.

A similar constructor creates the submarine. This object is horizontally and randomly positioned beyond the left or right canvas edge. It’s also vertically and randomly positioned in the middle third of the canvas. Bounds are chosen so that the submarine can move beyond canvas limits.

At this point, state is compared to STATE_PLAY to determine if SeaBattle is in the game play state. The previous comparison with STATE_TITLE, STATE_WINLOSE, and STATE_RESTART may have fallen through because of keydown.return evaluating to false.

The possibility of an exploding ship or submarine must be tested before Listing 1 can proceed to check for user input that controls the ship. There’s no point moving or firing depth charges from an exploding ship, or moving or launching torpedoes from an exploding submarine.

When an explosion is in progress, the explosion property references an explosion object. This object’s isShip property is set to true when the ship is exploding. In this case, the submarine can still move; this task is handled by invoking the sub object’s move() function.

Any depth charges or torpedoes that were in play before the ship or submarine started to explode are moved by invoking each of their object’s move() functions. When a depth charge or torpedo can no longer move, move() returns false and the object is nullified.

The explosion object’s advance() function returns true to indicate that the explosion is advancing. When it returns false, the explosion is finished, relevant game objects are nullified, and state is set to STATE_WINLOSE.

If the ship has exploded, the number of lives is decremented. When this value reaches zero, the game is over and a suitable message is prepared. However, if the submarine has exploded, the score increments by 100 points and the high score is modified and saved (when necessary).

In the absence of an explosion, Listing 1′s next task is to check for left arrow, right arrow, or spacebar key presses. A left or right arrow key press results in a call to ship‘s moveLeft() or moveRight() function.

In contrast, pressing the spacebar results in an attempt to fire a depth charge, but only if the maximum number of depth charges isn’t in play. A depth charge’s initial location is the center of the ship, and its lower bound is the hill top that coincides with the ship’s x coordinate.

The submarine is now moved, firing a torpedo if not off the screen, a randomly chosen integer equals a specific value, and the maximum number of torpedoes isn’t in play. A torpedo’s initial location is the submarine’s center, less torpedo height, and its upper bound is the water line.

Listing 1 lastly checks for a collision between a depth charge and the submarine, or between a torpedo and the ship. A collision results in an explosion object being created with a location set to depth charge/torpedo coordinates, and a suitable message being assigned to msg.

Making a Ship

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

makeShip: 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 = this.LEFT,
  this.exploded = false;
  this.height = SeaBattle.imgShipLeft.height;
  this.vx = 2;
  this.width = SeaBattle.imgShipLeft.width;
  this.draw = function() {
    SeaBattle.ctx.drawImage((this.dir == this.LEFT)?
                            SeaBattle.imgShipLeft :
                            SeaBattle.imgShipRight,
                            this.x-this.width/2,
                            this.y-this.height/2);
    return;
  }

  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+2;
    return this.bbox;
  }

  this.moveLeft = function() {
    this.dir = this.LEFT;
    this.x -= this.vx;
    if (this.x-this.width/2 < this.bound1)
    {
      this.x += this.vx;
      this.vx = SeaBattle.rnd(4)+1;
    }
  }

  this.moveRight = function() {
    this.dir = this.RIGHT;
    this.x += this.vx;
    if (this.x+this.width/2 > this.bound2)
    {
      this.x -= this.vx;
      this.vx = SeaBattle.rnd(4)+1;
    }
  }
}

Listing 2: The bottom of a ship’s bounding box is raised so that a torpedo explodes closer to the ship’s bottom.

Listing 2 first saves its arguments in same-named ship object properties, and then introduces 12 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 ship’s current direction (facing left or right). The ship initially faces left.
  • exploded indicates whether (when assigned true) or not (when assigned false) the ship has exploded.
  • height specifies the height of the ship image in pixels.
  • vx specifies the ship’s horizontal velocity in terms of the number of pixels the ship moves. The default value is two.
  • width specifies the width of the ship image in pixels.
  • draw() draws the ship left or right image. The image is drawn so that its center location coincides with the ship object’s x and y property values.
  • getBBox() returns an updated bbox object. This object is updated to accommodate a change in the ship’s horizontal position.
  • moveLeft() moves the ship left by the number of pixels specified by vx. When the ship reaches the canvas’s left edge, it’s prevented from moving any further left and its velocity changes.
  • moveRight() moves the ship right by the number of pixels specified by vx. When the ship reaches the canvas’s right edge, it’s prevented from moving any further right and its velocity changes.

Conclusion

The update() function relies on makeShip(x, y, bound1, bound2) along with other make-prefixed constructors to create the various game objects. The fourth part of this gaming series continues to explore SeaBattle’s architecture by focusing on these other constructors along with the intersects(r1, r2) function, which enables collision detection. See you next Friday!

Battle on the High Seas

<< Gaming: Battle on the High Seas, Part 2Gaming: Battle on the High Seas, Part 4 >>

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.

No Reader comments

Comments on this post are closed.