Wednesday, April 22, 2015

Simple DOM Collision Tracking

Back on the 15th, I had this awesome frog dodgeball game right in the third frame of my Amphibian.com comic. While today's Chaucer-inspired tale told in Middle English is amusing, it lacks the technical complexity of the dodgeball game. One of the many pieces that came together to enable a game inside a webcomic was simple collision detection between DOM nodes.

Frog Dodgeball
The comic frames, even in the dodgeball comic, are just "regular" DIV tags. I didn't use an HTML5 Canvas or anything like that. The frogs and balls are just IMG tags. To determine if a ball hits a frog, I had to come up with some reasonably efficient way of detecting the collision between two elements in the DOM and then taking some action based on a possible collision.

I used a class-based detection strategy similar to what I learned while using Crafty to make my entry for the GitHub Game Off 2015. Any element that should be checked for collisions with other objects should have the "collision" class applied to it. There isn't necessarily any special CSS associated with that class - it just has to be on the element. The element should also define a value for the field "collidesWith" that will be the name of another class. Only elements that have that class will be checked for collisions. I used a simple circle-based intersection formula for speed, so elements that can collide with other should define a value for the "collisionRadius" field as well. Finally, the elements should specify a function, hit, to be called if a collision is detected. The function will be passed an array of objects that are in collision.

Here is an example of how I set up the ball elements.

var ball = $('#ball');
var newBall = ball.clone()[0];

$(newBall).addClass("collision");

newBall.collisionRadius = $(newBall).width() / 2;
newBall.collidesWith = "frog";
newBall.hit = handleHit;

All balls are clones of a master ball element (line 2). I add the "collision" class, set the radius, specify what kinds of elements with which the balls collide, and specify the function to call if there is a hit. Here is an example of the handleHit function:

function handleHit(elemArray) {

    $(this).removeClass("collision"); // this ball won't hit anything else

    // make the element hit (assumes only 1 in this case)
    // semi-transparent for half a second
    $(elemArray[0]).css("opacity", "0.5");
    setTimeout(function() {
        $(elemArray[0]).css("opacity", "1");
    }, 500);

}

So that's the setup. But of course I had to add the collision-checking code to the comic framework. I already had a function that handled the animation, so I added a call to checkCollisions after each position update.

function checkCollisions() {

    $("img.collision").each(function(idx, elem) {

        if (elem.collidesWith) {

            var cx1 = Number($(elem).css('left').replace('px', ''));
            var ox1 = cx1 + ($(elem).width() / 2);

            var cy1 = Number($(elem).css('top').replace('px', ''));
            var oy1 = cy1 + ($(elem).height() / 2);

            var hits = [];
            $("img." + elem.collidesWith).each(function(idx2, elem2) {

                var cx2 = Number($(elem2).css('left').replace('px', ''));
                var ox2 = cx2 + ($(elem2).width() / 2);

                var cy2 = Number($(elem2).css('top').replace('px', ''));
                var oy2 = cy2 + ($(elem2).height() / 2);

                var distX = (ox2) - (ox1);
                var distY = (oy2) - (oy1);
                var magSq = distX * distX + distY * distY;
                if (magSq < (elem.collisionRadius + elem2.collisionRadius) * (elem.collisionRadius + elem2.collisionRadius)) {
                    hits.push(elem2);
                }

            });

            if (hits.length > 0 && elem.hit) {
                elem.hit(hits);
            }

        }

    });

}

My function iterates over all images that have the "collision" class. If those elements have a "collidesWith" defined, I get the X and Y coordinates for the center of the image. I'll need these for the collision check. Then I iterate over all the image elements that have the class specified by the "collidesWith" field and get their centers. The collision check starts on line 22. Basically, it makes two circles at the center-points of the images with the specified radii. A collision is detected if the circles overlap at all. If they do, I add that element to the hits array. At the end I call the hit function, passing the array, if there were any hits.

It all worked pretty well, and now I'll have it all ready for the next time I want to add a pointless game to a comic!

Amphibian.com comic for 22 April 2015