Monday, December 2, 2013

SVG Collision Detection (Game Programming)

In the last two Firefox OS game programming posts covering collision detection, I wrote about simple collision detection using calculation (CSS Collisions) and using color to detect a collision (Canvas Collisions). The first is the most common, and the second takes advantage of a feature of Canvas I've only seen used a few times.

Unlike CSS, which has nothing built-in for collisions, and Canvas, which has something you can use (reading a color pixel on the canvas) but was probably not intended for collisions, the third technique uses a feature of SVG that seems to a perfect candidate for collision detection. The secret of this technique is to use the getBBox.method.


As I wrote about in the topic on Touch and SVG Spirals, SVG is made up of lines and shapes, and the shapes can be irregular. In that topic I created a spiral and used it to move to the touch of your finger. Because SVG shapes don't need to be regular, you might think it would be hard to know whether you are colliding with them or not. But by using getBBox, you can determine the size of the box that will exactly enclose that shape, no matter how goofy. If you'd like to see lots of unusual SVG shapes, I recommend you check out the Open Clipart site, which not only offers free clipart (like the box above), but all of their art is stored as SVG files.

So what about getBBox? First of all, as you might guess, it stands for "get bounding box" and that's all it does. But that's a lot. You can get bounding boxes of enemies so you can shoot them, and they can get your bounding box and shoot back. Whatever the shape, it has a bounding box. So what? Well, once you have the bounding box (which is a rectangle), you can determine the x and y coordinates of the top left corner, and the width and height of the box. From there you can decide if your bounding box overlaps the enemy bounding box. What is particularly cool about this for SVG is that one of the coolest game aspects of SVG is that your shapes can change size, and then they do, the bounding box changes also.

Where can you learn about bounding boxes for SVG. Well, unfortunately not a lot of places. For once, Mozilla is silent on the subject of getBBox. It doesn't even have links to SVGLocatable, which is where getBBox lives in the W3C specification for SVG (http://www.w3.org/TR/SVG/types.html#__svg__SVGLocatable__getBBox). I learned about it in a cool blog post from Erik Dahlstrom called How to get the boundingbox of an svg element.

Why isn't this more well known? Probably the easiest answer is that SVG wasn't designed for JavaScript; but because it has an object model you can get at with JavaScript, you can do cool stuff. You don't need to understand anything about SVGLocatable, it's just the object that getBBox method hangs off of. SVG has methods, but they can only be used in JavaScript. So use 'em! Make that SVG move!

And by the way, getBBox works in Firefox, Chrome, Opera (which is where Erik Dahlstrom hangs out), and even IE (starting with version 9). Why does all this scripting stuff work with SVG even though it isn't very well documented anywhere? The answer is that most of it is needed to make SVG work behind the scenes. Sometimes it is complicated, but once you know the basic tricks and can work your way through the SVG specification (can you say huge?), things work, and they work fast.

So the topic for today is using getBBox to detect collisions. A lot of the code will be similar in structure to the two previous collision detection posts, with only the drawing parts changed to SVG. I use an angry blue square trying to get an innocent red ball. The red ball can jump out of the way and avoid the blue box. And all this takes place on a chocolate square (a box of chocolate?).

The code runs fast on my ZTE Open with Firefox OS and it looks like this:

<!DOCTYPE HTML>
<html>  
  <head>
      <meta charset="utf-8"> 
    <title>
    SVG Collision
    </title>

    <script>
       
          // Global variables
      var boardWidth = 320;
      var boardHeight = 460;
      var bwPixels = boardWidth + "px";
      var bhPixels = boardHeight + "px";
        var redBallHor = 60;
      var redBallVer = 160;
      var redBallRad = 10;
      var howFast = 3;
        var blueBoxHor = 270;
      var blueBoxVer = 150;
   
          // Namespace for SVG.
          svgNS = "http://www.w3.org/2000/svg";
     
      // Covers all bases for various browser support.
        var requestAnimationFrame =
          window.requestAnimationFrame ||
          window.mozRequestAnimationFrame ||
          window.webkitRequestAnimationFrame ||
          window.msRequestAnimationFrame; 

      // Event listeners
      // Page load event listener
      window.addEventListener("load", getLoaded, false);
      // Mouse down event listener
      window.addEventListener(
          "mousedown", redBallJump, false);
         
      // Run this when the page loads.
      function getLoaded(){

        // Make sure we are loaded.
        console.log("Page loaded!");    
    
        // Create SVG parent element.
        myBoard = document.createElementNS(svgNS, "svg");
        myBoard.style.setProperty("width",bwPixels);
        myBoard.style.setProperty("height",bhPixels);
        myBoard.style.setProperty("top","0px");
        myBoard.style.setProperty("left","0px");
        myBoard.style.setProperty("position","absolute");
        
        // You must append the board to the body.
        document.getElementById("pageBody").
             appendChild(myBoard);
            
        // Create blue box.
        blueBox =
          document.createElementNS(svgNS, "rect");
         
        // Width,  height, radius, and color of box.
        blueBox.x.baseVal.valueAsString =
          blueBoxHor + "px";
        blueBox.y.baseVal.valueAsString =
          blueBoxVer + "px";
        blueBox.width.baseVal.valueAsString =
          "20px";
        blueBox.height.baseVal.valueAsString =
          "20px";       
        blueBox.style.
          setProperty("fill","blue","");
         
        // Attach the box to the game board.
        myBoard.appendChild(blueBox);

        // Create red ball.
        redBall =
          document.createElementNS(svgNS, "circle");
         
        // Width,  height, radius, and color of ball.
        redBall.cx.baseVal.valueAsString =
          redBallHor + "px";
        redBall.cy.baseVal.valueAsString =
          redBallVer + "px";
        redBall.r.baseVal.valueAsString =
          redBallRad + "px";
        redBall.style.
          setProperty("fill","red","");
         
        // Attach the ball to the game board.
        myBoard.appendChild(redBall);
       
        // Create ground.
        myGround =
          document.createElementNS(svgNS, "rect");
         
        // Width,  height, radius, and color of box.
        myGround.x.baseVal.valueAsString =
          "0px";
        myGround.y.baseVal.valueAsString =
          "170px";
        myGround.width.baseVal.valueAsString =
          "320px";
        myGround.height.baseVal.valueAsString =
          "230px";       
        myGround.style.
          setProperty("fill","chocolate","");
         
        // Attach the box to the game board.
        myBoard.appendChild(myGround);

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

      // Game loop
      function doMainLoop() {
     
        // Loop within the loop.
        // 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
              console.log("Moving the box.");
         
              // Move the box
              moveBlueBox();
         
        }, 1000 / howFast); // Delay / how fast     
      }
           
      // Move the blue box here.
      function moveBlueBox() {
     
          // Subtract 10 from box horizontal value.
          blueBoxHor = blueBoxHor - 10;
     
      // Draw the new blue box.
      blueBox.x.baseVal.valueAsString =
          blueBoxHor + "px";
   
          // If the blue box hits the left edge, restart.
          if (blueBoxHor < 10) blueBoxHor = 270;   

          // Get bounding box of box.
      bbBox = blueBox.getBBox();
      console.log(bbBox.x);

      // Get bounding box of ball.
      bbBall = redBall.getBBox();
      console.log(bbBall.x);

      // Is there an x collision?
      if (bbBall.x + 20 == bbBox.x) {
     
        // Is there a y collision?
        if (bbBall.y == bbBox.y) {
       
          // Collision!
          blueBoxHor = 270;
          alert("Bang");
        }
      }  
    }

    // Make the red ball jump up.
    function redBallJump() {

      // Calculate red ball jump and move it.
      redBallVer = redBallVer - 50;  

      // Draw the new red ball.
      redBall.cy.baseVal.valueAsString =
          redBallVer + "px";      
   
      console.log("Ball up.");
   
      // Make the red ball fall after one second.
      redBallTimer = setTimeout(redBallFall, 1000);     
    } 

    // Make the red ball fall down.
    function redBallFall() {
   
      // Calculate the redBox fall and move it.
      redBallVer = redBallVer + 50; 
     
      // Draw the new red box.
      redBall.cy.baseVal.valueAsString =
          redBallVer + "px";    
      console.log("Ball down.");  
    }

    </script>
  </head>
   
  <body id="pageBody">
  </body>

</html>


Here is what it looks like. You have a blue box marching across the screen from right to left. There is a red ball just sitting there on the left.


You can make the red ball jump out of the way by touching the screen.


Whee!

If the ball doesn't jump out of the way fast enough, the box catches the ball and an alert is displayed.



Maybe not very exciting, but my philosophy is to show the least amount of code to illustrate a concept. Here the concept is to have three different drawing techniques (CSS, Canvas, SVG) doing the same thing. In this case, detecting a collision between two objects.

Most of the code is the same as the other two examples, so you can read about it there (requestAnimationFrame, setTimeout, eventListener, etc). Here are the SVG parts.

To start with, you need to define the board, the box, the ball, and the box of chocolates. There are two different ways to define SVG objects using the SVG object model. Here is the one I used to define the board:

       // Create SVG parent element.
        myBoard = document.createElementNS(svgNS, "svg");
        myBoard.style.setProperty("width",bwPixels);
        myBoard.style.setProperty("height",bhPixels);
        myBoard.style.setProperty("top","0px");
        myBoard.style.setProperty("left","0px");
        myBoard.style.setProperty("position","absolute");


I created an SVG element that is the container and parent for the other SVG objects that are on it. Note that svgNS (the SVG namespace) is defined by

          svgNS = "http://www.w3.org/2000/svg";

After creating the SVG container, I defined the properties of that container by using setProperty. I set the width and height to match the ZTE Open screen size. Height and width are obvious, but I needed to set the top and left as well, and make them 0,0. And to make the position absolute. If I don't do these three, Firefox thinks I want the SVG container to be offset by a few pixels to leave a margin.

Is this a bug or a feature? Only time will tell, but if you use SVG, you want to make sure the all your objects are firmly placed exactly on the screen where you want them to be. The container is your board, but that board doesn't come into full existence until you add it to the HTML5 page. (Does an unattached SVG container exist if no one can find it in the forest?)

At the bottom of the page, I assigned an id of pageBody to the body of the document. Then this code attaches the SVG container to the page.

        // You must append the board to the body.
        document.getElementById("pageBody").
             appendChild(myBoard);


You don't see anything yet, because you haven't assigned a color to the container and you don't need to here.

But you want to see something, so code is added for the box, ball,and ground. Here is the code for the box:

        // Create blue box.
        blueBox =
          document.createElementNS(svgNS, "rect");
         
        // Width,  height, radius, and color of box.
        blueBox.x.baseVal.valueAsString =
          blueBoxHor + "px";
        blueBox.y.baseVal.valueAsString =
          blueBoxVer + "px";
        blueBox.width.baseVal.valueAsString =
          "20px";
        blueBox.height.baseVal.valueAsString =
          "20px";       
        blueBox.style.
          setProperty("fill","blue","");
         
        // Attach the box to the game board.
        myBoard.appendChild(blueBox);


Note that this code starts out similar to the container, but then uses a different style of SVG object model definitions. Instead of setting properties, you actually use code to assign values. As I explained in an earlier post, you need to define your data carefully. All of this because when they created SVG (almost 15 years ago), they thought they would use animated lengths so they made it all too complicated, especially because (not thinking of JavaScript), they decided that data types were important. So you can't feed in a number if it wants a string (defining pixels). But if you follow the patterns in this post, SVG is fast and fun.

Essentially you can set properties on objects using assignment or setProperty. The blue box is a rect, the ball is a circle, and the ground is another rect. Set the colors and locations, and your objects appear on the screen, ready to go.

By the way, if you are working with the SVG object model, you'll be safe if you think in terms of objects and properties. That's the terminology for manipulating the SVG object model. But you may remember my post on part 2 of the bouncing ball in SVG, which used the HTML Document Object Model. In that post, my code for a ball looked like this:

        // Width,  height, and radius of the ball
        paddleBall.setAttribute("cx", ballHor);
        paddleBall.setAttribute("cy", ballVer);
        paddleBall.setAttribute("r", 10);


This used setAttribute, not setProperty. If you are in the world of attributes, you are talking to HTML elements and the HTML Document Object Model. You are still working with SVG object model, but you are doing it by going through the HTML DOM. Doing it with setAttribute makes your code run a little slower because it has to tunnel through a layer of translation. But you want to play with the big programmers, who use objects and properties. Leave the elements and attributes for the markup monkeys!

The rest of the code is pretty simple. The box will move toward the ball using two timing loops, and the box will jump out of the way using the mousedown event. But as the box moves forward, each time it uses getBBox to determine its own bounding box and the bounding box of the ball.

If the x value of both bounding boxes match, there might be a collision. But if the ball has jumped out of the way, there won't be a collision because the y values of both won't be the same. If there is a collision, bang!

So there you have it. Using getBBox lets you detect collisions easily. Especially because SVG offers a whole array of ways to stretch, resize, and change the shapes of your objects, and yet you can always find them!

Next time (after a game review or two), I'll compare the pros and cons of the three drawing techniques (CSS, Canvas, and SVG) and then move on to other game programming topics. I'm ignoring a fourth technique called WebGL which is catching on fast and which Firefox OS supports, but since it requires a 3D editor and may need more horsepower than my little ZTE Open has, I'll let it sit on the sidelines now.

But as you can guess by now (spoiler alert!), I'm biased toward SVG and there are plenty of books and articles on Canvas and a few on CSS, but almost none on SVG. So I'll keep writing about it because SVG is definitely fun.

No comments:

Post a Comment