The story of the Detectify Game

Did you also love the bug zapping game you played when signing up for Detectify the Beta? As there have been some buzz about it, we thought we would share the code and the story.

Update: As we are out of Beta we have temporarily moved the game to this page.

The game was created in between development and bug fixing on our other products during the span of 3-4 days so the code is no marvel of engineering, it works and is fairly free of bugs, which is all that matters in a quick and fun project like this.

So let’s start. The game is built with javascript and the svg html tag, the original idea was to make the Detectify eye have a searchlight, though after some testing we decided on a laser instead (that’s pretty neat too). In this article I will try and explain how some of the parts work and hopefully give you all a quick insight into how you can make simple and fun games using JS and HTML. You can view the complete code here. Also, if you do get inspired to create one, don’t forget to share it with us, we love a good game!

Basics

As I mentioned this game uses the svg tag to handle all the graphics this was mainly because I did not really know what I was doing when I started this and the svg graphics was an easy way to get the laser looking good. If I were to do this today I would start by checking out the canvas tag and functions and work with that, mostly because it has better support for sprites. One of my goals with the game was that it should be real easy to just add on to any page, the end result does require some tags and some JS code to be added to the page but it still is within reason.

Here you can see the game related HTML from the signup page. The GameArea tag is where the game will be drawn and the game-stats tag is the UI with points counter and health bar. It did feel right to have the sites designer be able to implement this to make it fit the sites design as well as possible.

<section id="game">
    <div id="GameArea" class="fullscreen"></div>
    <div id="game-stats">
     <div id="points">
     <h4>Game Points</h4>
     <div></div>
     <div id="counter">00000</div>
     <div id="tweet-it" class="game-but"></div>
     </div>
     <div id="restart">
     <h4>Restart</h4>
     <div class="game-but"></div>
     </div>
     <div id="life-stats">
     <h4>Life Left</h4>
     <div id="color-bar">
     <div id="life"></div>
     </div>
     </div>
    </div>
</section>


The necessary JS code is mostly to start the game and providing input and info on when the game should be paused, resumed and restarted.

$('#game').show();    
var touching = false;
$('#GameArea').on({
    //provide mouse input to the game
    mousemove: function(event) {
     Game.mouseCoords = [event.pageX, event.pageY];
    },
    mousedown: function(event) {
     event.preventDefault();
     Game.mouseCoords = [event.pageX, event.pageY];
     Game.onClick();
    },
    //Make it possible to play the game using touch devices.
    touchstart: function(){
     if(!touching) {
     Game.mouseCoords = [event.pageX, event.pageY];
     Game.onClick();
     touching = true;
     return false;
     }
    },
    touchend: function(){
     touching = false;
     return false;
    }
});
//pause and resume the game when focus is lost on the window.
$(window).on({
    focus: function(){
     Game.resume();
    },
    blur: function(){
     Game.pause();
    }
})
//setup and start the game.
Game.init();


To make the code easier to create and understand I started out by adding some functions that treats normal JS arrays as vectors to make the functions less cluttered with math. (You can view these at the top of the game file.) There is also a box object which helps when detecting collisions (In other words, when we check if a laser hits a bug). If you are interested in learning more Linear algebra I have a few tips for you; The Wolfire blog has a short series of posts that explain the basics. If you would like to dig even deeper Mathematics for 3D Game Programming and Computer Graphics is a great book. Or you could just google it :)

The Gun

We really wanted the eye in the logo to become more badass than just a still picture. As I mention earlier we first tried to make it a searchlight but we couldn’t make it feel right in a game setting so we decided to switch to the lasers that we now have. The laser beams are svg lines that get spawned in when clicking. To force some precision for the gamer (instead of just letting everyone shoot like a maniac) we limited the amount of lasers to 5 concurrent beams. Which also keep the performance of the game at a reasonable level.

The code for the laser object is made up of 4 functions:

  1. The setup creates the svg shape, gives the initial position and the speed values.
  2. The update function will be called every tick (the length of a tick is decided in the Game.init() function) and is responsible of moving and animating the increase/decrease effect of the beam.
  3. The explode function is called when the beam has reached it’s target and creates a small box in the impact zone which is tested against all the active critters, all critters that get hit are destroyed.
  4. The clear function handles the cleanup and removal of the beam and is called from the EffectManger object.
function Laser(target, id) {
    //hardcoded position of the eye in the logo
    this.origin=[(Game.windim[0]/2)+5, 155];
    this.target=target;
    this.pos=this.origin;
    this.speed=30;
    
    this.id=0;
    
    this.shrinking=false;
    //set up an svg line object
    this.shape = document.createElementNS("http://www.w3.org/2000/svg", "line");
    this.shape.setAttribute("fill", "#ED1F24");
    this.shape.setAttribute("style", "stroke:#ED1F24;stroke-width:4;opacity:0.5");//stroke-linecap:round;
    this.shape.setAttribute("id", 'Laser'+id);
    
    //control var to start the shrinking effect.
    this.exploded = false;
    //add svg line to the game area
    document.getElementById('GameSVGArea').appendChild(this.shape);
    
    //tell the game we just added a new laser beam.. no this is not a good way of doing this :)
    Game.laser_count++;

    this.update=function(dt){
     //since we are only interested in measuring distances we can compare squared lengths which saves us a square root operation
     var distleft=Vec.lengthSq(Vec.sub(this.pos, this.target));
     var dir=Vec.sub(this.target, this.origin);
     dir=Vec.normalize(dir)
     dir=Vec.mulScalar(dir, this.speed);
     
     if(!this.exploded && Math.floor(distleft) <= this.speed*this.speed) {
     //We have reached our target explode and start shrinking
     this.pos=this.target;
     this.explode();
     this.shrinking=true;
     this.exploded=true;
     } else if(!this.exploded) {
     //move along
     this.pos=Vec.add(this.pos, dir);
     }
     if(!this.shrinking) {
     var disttraveled=Vec.lengthSq(Vec.sub(this.pos, this.origin));
     if(disttraveled > 100*100) {
 //actually means start moving the origin point as well as the position
    this.shrinking=true;
      }
     } else {
     var distleft_origin=Vec.lengthSq(Vec.sub(this.origin, this.target));
     if(Math.floor(distleft_origin) <= (this.speed*this.speed)) {
    this.pos=this.target;
 //returning false means we are done and the object will be removed
    return false;
     } else {
     this.origin=Vec.add(this.origin, dir);
     }
     }
     //update objects position
     this.shape.setAttribute('x1', this.origin[0]);
     this.shape.setAttribute('y1', this.origin[1]);
     this.shape.setAttribute('x2', this.pos[0]);
     this.shape.setAttribute('y2', this.pos[1]);
     return true;
    };
    this.explode=function() {
    //kill all critters in a 30x30 area around the target position
     CritterManager.kill(this.pos, 30);
    }
    this.clear=function() {
     Game.laser_count--;
     this.shape.parentNode.removeChild(this.shape);
    }
    this.setId=function(id) {this.id=id;}
    this.getId=function() {return this.id;}
}

Critters

The critters are animated gifs by our designer Desmond. I would have loved to animate them with code instead but this was faster to get going and in the end we did not have the time to do this properly. The motion of the critters are generated by a sin/cos function making them move in kinda jerky circles, when they hit the edge of the screen we simply invert the direction in which they are rotating.

this.update=function(dt){
    this.time+=0.01;
    //keep the critters on the screen inverting the x/y axis when they hit an edge
    if(this.pos[0] < 40) {
     this.dir=1;
     this.pos[0]=41;
    }
    if(this.pos[0] > Game.windim[0]-40) {
     this.dir=-1;
     this.pos[0]=Game.windim[0]-41;
    }
    if(this.pos[1] < 120) {
     this.diry=1;
     this.pos[1] = 121;
    }
    if(this.pos[1] > Game.windim[1]-60) {
     this.diry=-1;
     this.pos[1] = Game.windim[1]-61;
    }
    //after a random amount of time swap the rotational direction making the movement slightly more interesting
    if(this.time >= this.swaptime) {
     this.rot*=-1;
     this.time=0;
     this.swaptime = Math.random()*20;
    }
    if(this.rot == 1) {
     this.pos[0] += Math.cos(this.time)*this.speed*this.dir;
     this.pos[1] += Math.sin(this.time)*this.speed*this.diry;
    } else {
     this.pos[1] += Math.cos(this.time)*this.speed*this.diry;
     this.pos[0] += Math.sin(this.time)*this.speed*this.dir;
    }
    
    this.shape.setAttribute('x', Math.floor(this.pos[0]));
    this.shape.setAttribute('y', Math.floor(this.pos[1]));
};

The death of the critters

When it was time to do the death animation of the critters animated gifs did not work since there is no way to start, stop or replay them so here we will see an example of animating using code.

this.update=function(dt) {
    //advance animation at a set speed
    this.animtimer+=(dt);
    this.endtime-=dt;
    //if it is time to switch frame
    if(this.animtimer > this.frame_switch_time) {
     var skip = false;
     if(!this.rev && this.anim_frame >= (this.type=='pow'?3:5)) {
     //this makes the animation pause for a it on the end frame before
 //it starts reversing
     this.pausetime -= dt;
     if(this.pausetime <= 0) {
     this.rev = true;
     }
     skip = true;
     }
     if(!skip) {
     //change frame.
     if(this.rev){this.anim_frame--;} else {this.anim_frame++;}
     this.animtimer=0;
     this.shape.setAttributeNS('http://www.w3.org/1999/xlink', "href", "#BugDie_"+this.type+this.anim_frame);
     }
    }
    if(this.endtime<=0) {//this.anim_frame > (this.type=='pow'?5:8)) {
     //this is also handled by the EffectManager object and returning false 
 //tells it to remove this object.
     return false;
    }
    return true; //ie keep going
}

Game over / Game finished

I wanted to do something a little fun with the game over screen and got the idea to move the critters so they form the phrase ‘game over’. To make this happen i assign a target position to all the critters and run a cosine interpolation function, this makes the critters accelerate in a straight line towards the target and then decelerate when they get close.

startGameover: function() {
    Game.is_gameover=true;
    //make sure we have 100 critters on the screen
    var spawn=100-CritterManager.critters.length;
    for(var i=0; i<spawn; ++i) {
     CritterManager.spawn();
    }
    //change update function
    window.clearInterval(Game.updateId);
    Game.updateId=window.setInterval(Game.gameover, 20);
    EffectManager.clearEffects();
}
gameover: function() {
    var dt= Game.getDT();
    //let all explosions and lasers play out
    EffectManager.update(dt);
    //the tile map used to decide each critters position
    var gameovermap = [
    ' 11    1    1 1  1111 ',
    '1     1 1  1 1 1 1    ',
    '1 11  111  1 1 1 111  ',
    '1  1 1   1 1 1 1 1    ',
    ' 11  1   1 1 1 1 1111 ',
    '                      ',
    ' 11  1   1 1111  111  ',
    '1  1 1   1 1     1  1 ',
    '1  1 1   1 111   111  ',
    '1  1  1 1  1     1  1 ',
    ' 11    1   1111  1   1',
    ];
    var crit=0;
    var tilesize=38;
    var startx=Game.windim[0]/2-((gameovermap[0].length/2)*tilesize);
    var starty=(Game.windim[1]/2-((gameovermap.length/2)*tilesize))*0.9;
    for(var o=0;o<gameovermap.length;++o) {
     for(var it=0;it<gameovermap[0].length;++it) {
     if(gameovermap[o][it]=='1' && crit in CritterManager.critters) {
     //assign a target position
     CritterManager.critters[crit].target=[startx+(it*tilesize), starty+(o*tilesize)];
     ++crit;
     }
     }
    }
    //remove all unnecessary critters
    if(crit <= CritterManager.critters.length) {
     var crit_start=crit;
     for(;crit<CritterManager.critters.length;++crit) {
     CritterManager.critters[crit].shape.parentNode.removeChild(CritterManager.critters[crit].shape);
     }
     CritterManager.critters.splice(crit_start, CritterManager.critters.length-crit_start);
    }
    
    Game.gameovertimer += 0.01;
    if(Game.gameovertimer >= 1) {
     Game.gameovertimer=1;
     Game.gameoverdone=true;
    }
    //move critters toward their targets
    for(var c=0;c<CritterManager.critters.length;++c) {
     var newpos = Vec.cosInterpolate(CritterManager.critters[c].pos, CritterManager.critters[c].target, Game.gameovertimer);
     CritterManager.critters[c].shape.setAttribute('x', newpos[0]);
     CritterManager.critters[c].shape.setAttribute('y', newpos[1]);
    }
}

That’s it

So that’s basically the story on how we created the Detectify Bug Zapping game. Don’t forget to email me a link so I can play yours.

Questions? Email us at hello@detecitfy.com or hit us on Twitter @detectify.

The post and the game is created by Jonas Erixon

comments powered by Disqus