3D Arcade Simplex Game: Consistency Refactoring

INTRODUCTION

Usually when you’re writing code for fun — off the top of your head (aka code noodling, code jamming, code improvisation) — your main concern is with things working. In the case of our simple game, “working” means everything moves and fires correctly.

Now, once everything works, your next step is to do what’s known as code refactoring. Basically this means the functionality stays the same, but the code is easier to read, easier to maintain, more consistent, and even more efficient—to name just a few ways of refactoring. Let’s do this.

 

ORIGINAL UNREFACTORED SOURCE CODE

<!doctype html>
<html>
<head>
<script src="babylon.max.js"></script>
<script>
window.addEventListener("DOMContentLoaded", init);
window.addEventListener("keydown", herocontrol);
function init()
{
    canvas=document.getElementById("cvGame");
    engine=new BABYLON.Engine(canvas);

    scene=new BABYLON.Scene(engine);
    light=new BABYLON.HemisphericLight("sun", new BABYLON.Vector3(0,1,0), scene);    
    ground=BABYLON.MeshBuilder.CreateGround("mGround", {width:1000,height:1000}, scene);   
    ground.material = new BABYLON.StandardMaterial("gMat", scene);
    ground.material.diffuseColor=new BABYLON.Color3(0,.5,0);

    hero = BABYLON.MeshBuilder.CreateCylinder("cHero", {height:10,diameterBottom:10,diameterTop:0}, scene);
    hero.material=new BABYLON.StandardMaterial("mHero", scene);
    hero.material.diffuseColor=new BABYLON.Color3(0,0,1);
    hero.position=new BABYLON.Vector3(-20, 5, 0);
    hero.rotation.z=-Math.PI/2;
    hero.computeWorldMatrix(true);

    mons= BABYLON.MeshBuilder.CreateCylinder("cMons", {height:10,diameterBottom:10,diameterTop:0}, scene);
    mons.material=new BABYLON.StandardMaterial("mMons", scene);
    mons.material.diffuseColor=new BABYLON.Color3(1,0,0);
    mons.position=new BABYLON.Vector3(20, 5, 0);
    mons.rotation.z=-Math.PI/2;    
    mons.computeWorldMatrix(true);

    boom = BABYLON.MeshBuilder.CreateCylinder("cBoom", {height:10,diameterBottom:4,diameterTop:0}, scene);
    boom.material=new BABYLON.StandardMaterial("mBoom", scene);
    boom.material.diffuseColor=new BABYLON.Color3(1,.5,0);
    boom.position=new BABYLON.Vector3(0,5,20);
    boom.rotation.z=-Math.PI/2;
    boomdx=boomdz=0;
    boom.computeWorldMatrix(true);

    camera =new BABYLON.FreeCamera("mCam", new BABYLON.Vector3(0,6,-50), scene);
    camera.attachControl(canvas);
    gameover=false;
    engine.runRenderLoop(gameloop);
}
function gameloop()
{
    if (!gameover) {
    monsdx=hero.position.x-mons.position.x;
    monsdz=hero.position.z-mons.position.z;
    monsang=Math.atan2(monsdz,monsdx);
    monsspd=0.05;
    mons.position.x+=Math.cos(monsang)*monsspd;
    mons.position.z+=Math.sin(monsang)*monsspd;
    mons.rotation.y=-monsang;
    if (mons.intersectsMesh(hero,true)) {
        gameover=true;
        spWinLose.innerHTML="YOU DIED!";
    }

    boom.position.x+=boomdx;
    boom.position.z+=boomdz;
    if (boom.intersectsPoint(mons.position)) {
        gameover=true;
        spWinLose.innerHTML="YOU WIN";
    }
    }
    scene.render();
}
function herocontrol(event)
{
    switch (event.key) {
        case 'a':
            hero.rotation.y-=Math.PI/180;          
        break;
        case 'd':
            hero.rotation.y+=Math.PI/180;          
        break;
        case 'w':
            heroang=-hero.rotation.y;
            herospd=1;
            herodx=Math.cos(heroang)*herospd;
            herodz=Math.sin(heroang)*herospd;
            hero.position.x+=herodx;
            hero.position.z+=herodz;
        break;
        case 's':
            heroang=-hero.rotation.y;
            herospd=1;
            herodx=Math.cos(heroang)*herospd;
            herodz=Math.sin(heroang)*herospd;
            hero.position.x-=herodx;
            hero.position.z-=herodz;
        break;
        case 't':
            camera.position.y=200;
            camera.position.x=camera.position.z=0;
            camera.rotation.x=Math.PI/2;
        break;
        case 'Enter':
            boom.position.x=hero.position.x;
            boom.position.z=hero.position.z;
            boom.rotation.y=hero.rotation.y;
            boomang=-boom.rotation.y;
            boomspd=1;
            boomdx=Math.cos(boomang)*boomspd;
            boomdz=Math.sin(boomang)*boomspd;
        break;
    }
}
</script>
<style>
body    {width:100%; height:100%; padding:0; margin:0; background:red;}
#cvGame {position:absolute;width:100%; height:100%; padding:0; margin:0; background:skyblue;}
</style>
</head>
<body>
<canvas id="cvGame"></canvas>
<span id="spWinLose" style="position:absolute;bottom:0;right:0;">SIMPLEX</span>
</body>
</html>

Explanation. I’ve bolded the code that are candidates for refactoring.

Briefly, in refactoring, one of the first and easiest things I look for are variables that are initialized to constant values such as boomspd=1. This code should be moved to the init function.

I also look for code that operates similarly.  For example, the code that updates *.position.x and *.position.z is roughly the same for the hero, the monster, and the missile. They’re all variations of adding a dx and dz, multiplied by a speed. They should all be rewritten to use a consistent formula:

position.x=dx*spd;
position.z=dz*spd;

Here’s one way.

 

STEP 7: REFACTOR FOR INITIALIZATION AND CONSISTENCY

<!doctype html>
<html>
<head>
<script src="babylon.max.js"></script>
<script>
window.addEventListener("DOMContentLoaded", init);
window.addEventListener("keydown", herocontrol);
function init()
{
    canvas=document.getElementById("cvGame");
    engine=new BABYLON.Engine(canvas);

    scene=new BABYLON.Scene(engine);
    light=new BABYLON.HemisphericLight("sun", new BABYLON.Vector3(0,1,0), scene);    
    ground=BABYLON.MeshBuilder.CreateGround("mGround", {width:1000,height:1000}, scene);   
    ground.material = new BABYLON.StandardMaterial("gMat", scene);
    ground.material.diffuseColor=new BABYLON.Color3(0,.5,0);

    hero = BABYLON.MeshBuilder.CreateCylinder("cHero", {height:10,diameterBottom:10,diameterTop:0}, scene);
    hero.material=new BABYLON.StandardMaterial("mHero", scene);
    hero.material.diffuseColor=new BABYLON.Color3(0,0,1);
    hero.position=new BABYLON.Vector3(-20, 5, 0);
    hero.rotation.z=-Math.PI/2;
    herodx=herodz=0;
    herospd=1;
    herorotspd=5;
    herohit=herodead=false; // for future version
    hero.computeWorldMatrix(true);

    mons= BABYLON.MeshBuilder.CreateCylinder("cMons", {height:10,diameterBottom:10,diameterTop:0}, scene);
    mons.material=new BABYLON.StandardMaterial("mMons", scene);
    mons.material.diffuseColor=new BABYLON.Color3(1,0,0);
    mons.position=new BABYLON.Vector3(20, 5, 0);
    mons.rotation.z=-Math.PI/2;    
    monsdx=monsdz=0;
    monsspd=0.05;
    monsrotspd=1; // monsrotspd just in case
    monshit=monsdead=false; // for future version
    mons.computeWorldMatrix(true);

    boom = BABYLON.MeshBuilder.CreateCylinder("cBoom", {height:10,diameterBottom:4,diameterTop:0}, scene);
    boom.material=new BABYLON.StandardMaterial("mBoom", scene);
    boom.material.diffuseColor=new BABYLON.Color3(1,.5,0);
    boom.position=new BABYLON.Vector3(0,5,20);
    boom.rotation.z=-Math.PI/2;
    boomdx=boomdz=0;
    boomspd=boomrotspd=1; // boomrotspd just in case
    boomhit=boomdead=false; // for future version
    boom.computeWorldMatrix(true);

    camera =new BABYLON.FreeCamera("mCam", new BABYLON.Vector3(0,6,-50), scene);
    camera.attachControl(canvas);
    gameover=false;
    engine.runRenderLoop(gameloop);
}
function gameloop()
{
    if (!gameover) {
    monsdx=hero.position.x-mons.position.x;
    monsdz=hero.position.z-mons.position.z;
    monsang=Math.atan2(monsdz,monsdx); 
    // monsspd=0.05; move to init()
    
    monsdx=Math.cos(monsang); // added
    monsdz=Math.sin(monsang); // added
    mons.position.x+=monsdx*monsspd; // replaced Math.cos w/monsdx 
    mons.position.z+=monsdz*monsspd; // replaced Math.sin w monsdz
    mons.rotation.y=-monsang;
    if (mons.intersectsMesh(hero,true)) {
        gameover=true;
        spWinLose.innerHTML="YOU DIED!";
    }

    // boomspd=1; // added but init'ed at top
    boom.position.x+=boomdx*boomspd; // added boomspd
    boom.position.z+=boomdz*boomspd; // added boomspd
    if (boom.intersectsPoint(mons.position)) {
        gameover=true;
        spWinLose.innerHTML="YOU WIN";
    }
    }
    scene.render();
}
function herocontrol(event)
{
    switch (event.key) {
        case 'a':
            hero.rotation.y-=Math.PI/180*herorotspd; // added *herorotspeed also set in init
        break;
        case 'd':
            hero.rotation.y+=Math.PI/180*herorotspd; // added *herorotspeed also set in init
        break;
        case 'w':
            heroang=-hero.rotation.y;
            herospd=1;
            herodx=Math.cos(heroang); // moved *herospd
            herodz=Math.sin(heroang); // moved *herospd
            hero.position.x+=herodx*herospd; // moved it here
            hero.position.z+=herodz*herospd; // moved it here
        break;
        case 's':
            heroang=-hero.rotation.y;
            herospd=1;
            herodx=Math.cos(heroang); // moved *herospd
            herodz=Math.sin(heroang); // moved *herospd
            hero.position.x-=herodx*herospd; // moved it here
            hero.position.z-=herodz*herospd; // moved it here
        break;
        case 't':
            camera.position.y=200;
            camera.position.x=camera.position.z=0;
            camera.rotation.x=Math.PI/2;
        break;
        case 'Enter':
            boom.position.x=hero.position.x;
            boom.position.z=hero.position.z;
            boom.rotation.y=hero.rotation.y;
            boomang=-boom.rotation.y;
            // boomspd=1; // now set in gameloop
            boomdx=Math.cos(boomang); // *boomspd; now done in gameloop
            boomdz=Math.sin(boomang); // *boomspd; now done in gameloop
        break;
    }
}
</script>
<style>
body    {width:100%; height:100%; padding:0; margin:0; background:red;}
#cvGame {position:absolute;width:100%; height:100%; padding:0; margin:0; background:skyblue;}
</style>
</head>
<body>
<canvas id="cvGame"></canvas>
<span id="spWinLose" style="position:absolute;bottom:0;right:0;">SIMPLEX</span>
</body>
</html>

Explanation.  I moved any statements with hard-coded values (like boomspd=1) to the init(). I also added new variables like monsrotspd, boomrotspd, even though the original code never used them. I did this so that all things in the game have consistent variables, since this makes the game more flexible. Who knows? In a future update we may want to control how fast monsters rotate. For the same flexibility reason, I added *hit and *dead variables. Based on my experience, these two boolean (true/false) variables are extremely useful for playing different animations when hit or dead.

The other major change was to make all position updates the same. Hero, monster, and missile all follow a variation of the equation:

position.x=dx*spd;
position.z=dz*spd;

Why is this refactored code better? Two main reasons. First, because you can change a lot of the game properties in one place: the init function. For example you can change hero, monster, and missile speeds all in one convenient function: init(). But second, because it makes it easier for us to create objects.