Firstly, I have a major thanks to say to the members of Stack Overflow. Their contributions and helpfulness have been absolutely splendid in regards to this little demo.

In particular, one member suggested I take a look at finite state machines. I’ve tried to implement some of the concepts in the code, but I’m not entirely sure I’ve gotten the hang of them yet. They do interest me, and programming with finite state machine methodology is definitely something I find exciting.

Current Bugs/Issues

I’ll fix these at some point in the future.

  • Pressing two arrow keys at once causes the tank to move diagonally and out of control.
  • Holding down an arrow key for more than three seconds causes the tank to still be moving after the arrow key has lifted. I need to make sure subsequent arrow key presses have no effect until the current animation is complete.
  • No physics whatsoever. The tank can move through walls.
  • No border on the map. The tank can move off the canvas and never come back.

The JavaScript

This is the code taken from the canvasApp.js source:

$(document).ready(function() {  
  // Uses Modernizr.js to check for canvas support
  function canvasSupport() {
    return Modernizr.canvas;
  }

  canvasApp();

  function canvasApp() {
    // Check for canvas support
    if (!canvasSupport()) {
      return;
    }

    // Grab the canvas and set the context to 2d
    var theCanvas = $("#canvasOne");
    var context = theCanvas.get(0).getContext("2d");

    // Load the tile sheet
    var tileSheet = new Image();
    tileSheet.addEventListener('load', startUp, false);
    tileSheet.src = "./img/tanks_sheet.png";

    var mapIndexOffset = -1; // Our array is 0 relative while our tilesheet array is not
    var mapRows = 10;
    var mapCols = 10;
    var tileWidth = 32;
    var tileHeight = 32;
    var tilesPerRow = 8;

    // Tile map array data exported from Tiled as a tilesheet
    var tileMap = [
      [32,31,31,31,1,31,31,31,31,32],
      [1,1,1,1,1,1,1,1,1,1],
      [32,1,26,1,26,1,26,1,1,32],
      [32,26,1,1,26,1,1,26,1,32],
      [32,1,1,1,26,26,1,26,1,32],
      [32,1,1,26,1,1,1,26,1,32],
      [32,1,1,1,1,1,1,26,1,32],
      [1,1,26,1,26,1,26,1,1,1],
      [32,1,1,1,1,1,1,1,1,32],
      [32,31,31,31,1,31,31,31,31,32]
    ];

    // Tank animation frames
    var animationFrames = [1,2,3,4,5,6,7,8];
    // Counter to keep track of the current index of animationFrames
    var frameIndex = 0;
    // Tank tiles
    var tankSourceX = Math.floor(animationFrames[frameIndex] % tilesPerRow) * tileWidth;
    var tankSourceY = Math.floor(animationFrames[frameIndex] / tilesPerRow) * tileHeight; 
    // Tank position and movement
    var tankX = 32;
    var tankY = 32;
    var tankMoveX = 0;
    var tankMoveY = 0;
    // Tank state, direction, and rotation angle (used for rotation later)
    var tankState = "stopped";
    var tankDir = "up";
    var rotationAngle = 0;
    var rotationAngleRad = 0; // rotation angle in radians

    // Keyboard movement (arrow keys) event listener/handler
    document.onkeydown = function(e) {
      e = e?e:window.event;

      switch (e.keyCode) {
        // up
        case 38:
          if (tankState == "stopped") {
            if (tankDir != "up") {
              dirTank("up");
            }
            moveTank("up");
          }
          break;
        // down
        case 40:
          if (tankState == "stopped") {
           if (tankDir != "down") {
              dirTank("down");
            }
            moveTank("down");
          }
          break;
        // right
        case 39:
          if (tankState == "stopped") {
           if (tankDir != "right") {
              dirTank("right");
            }
            moveTank("right");
          }
          break;
        // left
        case 37:
          if (tankState == "stopped") {
           if (tankDir != "left") {
              dirTank("left");
            }
            moveTank("left");
          }
          break;
      }
    }

    function animateMovement() { 
      // Animation frames
      if (tankState == "moving") {
        frameIndex += 1;
        if (frameIndex == animationFrames.length) {
          frameIndex = 0;
        }
        tankSourceX = Math.floor(animationFrames[frameIndex] % tilesPerRow) * tileWidth;
        tankSourceY = Math.floor(animationFrames[frameIndex] / tilesPerRow) * tileHeight; 
      }
    }

    function moveTank(dir) {
      // Make sure tank only moves a certain number of animations per arrow press
      var steps = 0; 

      var int = setInterval(function() {
        tankState = "moving";
        steps += 1;

        if (dir == "up") {
          tankMoveY = -4;
        } else if (dir == "down") {
          tankMoveY = 4;
        } else if (dir == "left") {
          tankMoveX = -4;
        } else if (dir == "right") {
          tankMoveX = 4;
        }

        tankX += tankMoveX;
        tankY += tankMoveY; 

        animateMovement();
        drawScreen();
      },120);
      if (steps = 4) {
        setTimeout(function() {
          clearInterval(int);
          tankMoveX = 0;
          tankMoveY = 0;
          tankState = "stopped";
        }, 1000);
      }
    }

    function dirTank(dir) {
      switch (dir) {
        case "up":
          if (tankDir == "right") {
            rotationAngle += -90;
          } else if (tankDir == "down") {
            rotationAngle += 180;
          } else if (tankDir == "left") {
            rotationAngle += 90;
          }
          break;
        case "down":
          if (tankDir == "up") {
            rotationAngle += 180; 
          } else if (tankDir == "right") {
            rotationAngle += 90;
          } else if (tankDir == "left") {
            rotationAngle += -90;
          }
          break;
        case "left":
          if (tankDir == "up") {
            rotationAngle += -90;
          } else if (tankDir == "right") {
            rotationAngle += 180;
          } else if (tankDir == "down") {
            rotationAngle += 90;
          }
          break;
        case "right":
          if (tankDir == "up") {
            rotationAngle += 90;
          } else if (tankDir == "down") {
            rotationAngle += -90;
          } else if (tankDir == "left") {
            rotationAngle += 180;
          }
          break;
      }
      tankDir = dir;
      rotationAngle %= 360;
    }

    function startUp() {
      drawScreen();   
    }

    function drawScreen() {
      // Tile map
      for (var rowCtr = 0; rowCtr < mapRows; rowCtr += 1) {
        for (var colCtr = 0; colCtr < mapCols; colCtr += 1) {
          var tileId = tileMap[rowCtr][colCtr] + mapIndexOffset;
          var sourceX = Math.floor(tileId % tilesPerRow) * tileWidth;
          var sourceY = Math.floor(tileId / tilesPerRow) * tileHeight;

          context.drawImage(tileSheet, sourceX, sourceY, tileWidth, 
            tileHeight, colCtr * tileWidth, rowCtr * tileHeight, tileWidth, tileHeight);
        }
      }

      // Draw the tank
      context.save();
      context.setTransform(1,0,0,1,0,0); // identity matrix
      context.translate(tankX + tileWidth/2, tankY + tileHeight/2);
      rotationAngleRad = rotationAngle * Math.PI/180;
      context.rotate(rotationAngleRad);
      context.drawImage(tileSheet, tankSourceX, tankSourceY, tileWidth, tileHeight, -tileWidth/2, -tileHeight/2, tileWidth, tileHeight);
      context.restore();
    }    

  }

});

The Demo

You can find the current working version here: https://www.zesix.com/html5/movingTankExample