Monday, November 25, 2013

Color Canvas Collisions (Game Programming)

No, this is not a post about crazed artists whacking each other with wet canvases. I'm talking about Canvas, the strangely popular technique for drawing game graphics on an HTML5 web page. When it comes to collisions, Canvas is interesting. You can (and most people do) keep track of where your objects are flying, and you can calculate any possible collisions (as detailed in my post on CSS Collisions). Decide where you want to go, calculate if anyone or anything is in the way, make a decision and draw.

But what if you are battling 100 red space rabbits at once? You don't want to calculate the position of every rabbit, especially if they hop around (don't ask how rabbits hop around in space).


There's a technique that is unique to Canvas that can help you with collision detecting. When you copy an image to a canvas, the canvas doesn't remember what you put there. It only knows that it has pixels and it doesn't know where they came from. But they do have pixels. If you have red rabbits hopping around, you must erase the old rabbit, move to a new position, and draw the new rabbit. Clearing the canvas takes care of the erasing (unless you want to do exact erasing, as detailed in this blog post) in the example today, but you have to keep track of all the rabbits and make sure you hit one.

Well, there's something else notable about those rabbits. They are red! There is a Canvas method called getImageData that can go to a specific region of the canvas and capture what is there. Read all about it on MDN at https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D#getImageData. This cool method returns an ImageData object, which is documented at https://developer.mozilla.org/en-US/docs/Web/API/ImageData. (Isn't Mozilla wonderful, documenting all these things for us!) This method and object might seem complicated, but it isn't really. You just call getImageData with the top, left, width, and height of the area of the canvas you want to grab. The ImageData object is a bit more unusual, but really is easy to explain. If you look at the data property of the object, it an array of red, green, blue, and alpha that defines the actual color of that one pixel.

So, if I want to find out whether part of a canvas is red, I just have to use getImageData and see if the first part of the array is solid red. Of course, to be careful, you want to make sure that none of the rest of your objects are 100% red in the RGBA breakdown of the color spot you are looking at. And you also want to be careful if your pixel is on the edge of an area where there are other different-colored pixels because sometimes Canvas smears (I'm not kidding).  But if you want to kill 100 rabbits, you have to be efficient about it.

Here is some sample code, written in the same style as the CSS Collision code. Instead of a box and a ball, I change the code to two squares, an angry blue square and a scared red square (sorry, bunnies). The blue square marches across the screen from right to left, attempting to collide with the red square.

It looks like this:


The red square can jump out of the way (with a tap on the screen).


If the red square doesn't jump out of the way in time, a collision takes place and a message is displayed showing this:



So here is the code, modeled after the CSS Collision example, but using Canvas and color detection. Tested, run, and screenshots on Firefox OS on ZTE Open.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>
      Canvas Collision
    </title>
 
    <script>
      // Global variables
      var canvas;
      var ctx;
      var boardWidth = 320;
      var boardHeight = 460;
      var redBoxHor = 50;
      var redBoxVer = 150;
      var howFast = 3;
      var blueBoxHor = 270;
      var blueBoxVer = 150;
   
      // Covers all bases for various browser support.
      var requestAnimationFrame =
          window.requestAnimationFrame ||
          window.mozRequestAnimationFrame ||
          window.webkitRequestAnimationFrame ||
          window.msRequestAnimationFrame; 

      // Event listener
      window.addEventListener("load", getLoaded, false);
      window.addEventListener(
          "mousedown", redBoxJump, false);

    // Run this when the page loads.
    function getLoaded(){

      // Make sure we are loaded.
      console.log("Page loaded!");
 
      // Load the canvas.
      loadMyCanvas();   

      // Start the main loop.
      doMainLoop();
    }

    // Game loop
    function doMainLoop(){

      // Outer loop
      // Runs every 1/3 second.
          loopTimer = setTimeout(function() {
       
        // Inner loop
        // Runs as fast as it can.
        animTimer = requestAnimationFrame(doMainLoop);
         
        // Drawing code goes here
       
          // Clear the canvas.
        ctx.clearRect(0,0,320,460);   
         
        // Draw the ground.
        ctx.fillStyle="chocolate";
        ctx.fillRect(0,170,320,230);
        ctx.fill();

        // Draw the red box
        ctx.fillStyle="#FF0000";
        ctx.fillRect(redBoxHor,redBoxVer,20,20);
        ctx.fill(); 

        // Move the blue box
            moveBlueBox();
         
        }, 1000 / howFast); // Delay timing
      }
  
      // Move the blue box here.
      function moveBlueBox() {
     
          // Subtract 10 from box horizontal value.
          blueBoxHor = blueBoxHor - 10;
     
          // Draw the blue box with new coordinates.
          ctx.fillStyle="#0000FF";
          ctx.fillRect(blueBoxHor,blueBoxVer,20,20);
          ctx.fill(); 
   
          // If the blue box hits the left edge, restart.
          if (blueBoxHor < 10) blueBoxHor = 270;   

          // Report position of blue box.
          console.log(blueBoxHor);

          // See what is to the left of the box.
          // We're looking for the red box!
          p = ctx.getImageData(blueBoxHor - 5,
             blueBoxVer + 10, 1, 1).data;
     
          // Get first value of array (red value).
          p =  p[0];
     
          // If the first part of the color is red,
          // then the color was hit.
          if (p == 255) {
     
             blueBoxHor = 270;
             alert ("Bang");  
          }    
      }
      
      // Make the redBox jump up.
      function redBoxJump() {
   
      // Calculate redBox jump and move it.
      redBoxVer = redBoxVer - 50;   
   
      console.log("Ball up.");
   
      // Make the redBox fall after one second.
      redBoxTimer = setTimeout(redBoxFall, 1000);     
    }
   
    // Make the redBox fall down.
    function redBoxFall() {
   
      // Calculate the redBox fall and move it.
      redBoxVer = redBoxVer + 50; 
   
      console.log("Ball down.");  
    }

    // Load the image and the canvas.
    function loadMyCanvas() {

      // Get the canvas element.
      canvas = document.getElementById("myCanvas");

      // Make sure you got it.
      if (canvas.getContext) {

        // Specify 2d canvas type.
        ctx = canvas.getContext("2d");      
      }
    }

</script>

</head>
<body>

  <canvas id="myCanvas" width="320" height="460">
  </canvas>

</body>
</html>


This is simple Canvas code. In this example I didn't import the bitmaps, but just created squares of red, blue, and chocolate using the fillRect method. When you use fillRect, you need to also define the fillStyle as a color before you call fillRect, and then you need to use fill afterward.
  1. Define your style.
  2. Define your fill object.
  3. Do the actual fill.
This example uses the same double loop as the CSS Collision example: a slow outer loop that runs every 1/3 second and a faster inner loop (using requestAnimationFrame) that moves the blue box and draws it. The red box moves with its own loop, which is triggered by a mousedown event (which becomes a simple tap on the screen).  Move, jump, and ... collide (or not).

The actual collision test is done every time the blue box moves. Here is the code:

      // See what is to the left of the box.
      // We're looking for the red box!
      p = ctx.getImageData(blueBoxHor - 5,
             blueBoxVer + 10, 1, 1).data;
     
      // Get first value of array (red value).
      p =  p[0];
     
      // If the first part of the color is red,
      // then the color was hit.
      if (p == 255) {
     
        blueBoxHor = 270;
        alert ("Bang");  
      }     


getImageData looks 5 pixels to the left and 10 pixels down, so it is looking inside any potential red box that is sitting around. If the red box jumps up, there won't be any red pixels there. If the red box is too slow, red will be detected and ... "Bang".

The value of the data property (p) is an array of length 4. I only tested for the first array value (p[0]) and all I was looking for was the value of 255. The value of red is, in RBG values, 100% red, 0% green, 0% blue. Or in the actual array: [255, 0, 0]. Ignore the 4th part of the array, which has to do with transparency/opacity. For more about colors than you ever want to know, see https://developer.mozilla.org/en-US/docs/Web/CSS/color_value. Essentially I grabbed the pixel, tore it apart into its three color values, and wanted to see if red was 100% red. Other colors might also have this, so I made sure I only had an 100% red square to look for.

In some cases, using getImageData is considered to be a slow method to call on Canvas, but if you use it for just looking at one pixel and define your pixels carefully, it can be a lot faster than calculating the position of every other object on the screen.

But there's one other method of collision that is very cool, and you may not be surprised to learn that it involves the mysterious (but not really that hard) world of SVG. Next time, unless something else gets in the way, and of course, not before I do another game review.

Stay tuned, but not iTuned!

No comments:

Post a Comment