3D Arcade Simplex Game: Variations

INTRODUCTION

The 3D Arcade Simplex Game: Final, will be the base from which we explore the implementation of many other game mechanisms in this article.

Now, I’ve separated these different mechanisms to make them easier to understand, so pick and choose what you need. One thing you SHOULD implement is the follow-behind and follow-from-top cameras, which is our first variation. But remember: All these variations build on 3D Arcade Simplex Game: Final, and I did not combine the variations.

Note: Sometimes if I copy and paste this code, it pastes invisible “A” characters in place of spaces. This causes the browser to crash, and some browser won’t show the invisible A characters.

 

VARIATION: FOLLOW-BEHIND & FOLLOW-TOP CAMERAS

<!doctype html>
<html>
<head>
<script src="babylon.max.js"></script>
<script>
window.addEventListener("DOMContentLoaded", init);

var things;
function init()
{
    things = new Array();
    
    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 = new ohero(scene);
    things.push(hero);
    maxmons = orgmons = 3; 
    for (i = 0; i < maxmons; i++) {
        mons = new omons(hero, scene);
        things.push(mons); // again, push any object if you want to see it
    }
    boom = null;
    
    camera =new BABYLON.FreeCamera("mCam", new BABYLON.Vector3(0,6,-50), scene);
    camera.attachControl(canvas);
    followcam=false; // follow behind cam
    followtop=false; // follow top cam

    gameover = false;
    starttime = Date.now();
    engine.runRenderLoop(gameloop);
}
function gameloop()
{
    if (!gameover) {
        for (i = 0; i < things.length; i++) {
            if ((things[i] instanceof ohero)==false)
                things[i].move();
        }

        spMons.innerHTML = (orgmons-maxmons);
        elapsed = Date.now() - starttime;
        spTime.innerHTML = (elapsed / 1000).toFixed(2);

        if (followtop) { // overrides follow behind cam
         cameraheight=200;
         camera.position.x=hero.mesh.position.x;
         camera.position.z=hero.mesh.position.z;
         camera.position.y=cameraheight;
         spWinLose.innerHTML="TOP VIEW";

        } 
        else if (followcam) {
         followdist=100;
         followheight=20;
         cameraang=-hero.mesh.rotation.y + Math.PI;
         camera.position.x=followdist*Math.cos(cameraang)+hero.mesh.position.x;
         camera.position.y=followheight;
         camera.position.z=followdist*Math.sin(cameraang)+hero.mesh.position.z;
         camera.rotation.y=hero.mesh.rotation.y+Math.PI/2;
         camera.rotation.x=0;
         spWinLose.innerHTML="FOLLOW CAM";
        } else
     spWinLose.innerHTML="SIMPLEX";

        if (maxmons == 0 || hero.hit) {
            gameover = true;
            if (maxmons == 0)
                spWinLose.innerHTML = "YOU WIN";
            else
                spWinLose.innerHTML = "YOU LOSE";
        }        
    }

    scene.render();
}

omons=function(h, scene) {
    this.hero = h.mesh;
    this.h = 10;
    this.maxw = this.maxd = 200;
    this.mesh = BABYLON.MeshBuilder.CreateCylinder("cMons", { height: this.h, diameterBottom: this.h, diameterTop: 0 }, scene);
    this.mesh.material=new BABYLON.StandardMaterial("mMons", scene);
    this.mesh.material.diffuseColor=new BABYLON.Color3(1,0,0);
    this.mesh.position = new BABYLON.Vector3(Math.random() * this.maxw - this.maxw / 2, this.h / 2, Math.random() * this.maxd - this.maxd / 2);
    this.mesh.rotation.z=-Math.PI/2;
    this.dx=this.dz=0;
    this.spd=0.05;
    this.rotspd=1;
    this.hit=this.dead=false;
    this.mesh.computeWorldMatrix(true);

    this.move = function () {
        this.dx=this.hero.position.x-this.mesh.position.x;
        this.dz=this.hero.position.z-this.mesh.position.z;
        this.ang=Math.atan2(this.dz,this.dx);

        this.dx = Math.cos(this.ang);
        this.dz = Math.sin(this.ang); 
        this.mesh.position.x+=this.dx*this.spd;
        this.mesh.position.z+=this.dz*this.spd; 
        this.mesh.rotation.y=-this.ang;
        if (this.mesh.intersectsMesh(this.hero,true)) {
            hero.hit = true;
        }
        if (this.hit) {
            this.monsindex = things.indexOf(this);
            things.splice(this.monsindex, 1);     
            this.mesh.dispose();
            maxmons = maxmons - 1; 
        }
    }

    this.draw = function () {
    }
}

oboom = function (m, h, scene) {
    this.mons = m.mesh; 
    this.sx = h.mesh.position.x; 
    this.sz = h.mesh.position.z;
    
    this.h = 10;
    this.maxw = this.maxd = 100;
    this.mesh = BABYLON.MeshBuilder.CreateCylinder("cBoom", { height: this.h, diameterBottom: this.h/2.5, diameterTop: 0 }, scene);
    this.mesh.material = new BABYLON.StandardMaterial("mBoom", scene);
    this.mesh.material.diffuseColor = new BABYLON.Color3(1, .5, 0);
    this.mesh.position = new BABYLON.Vector3(20, this.h/2, 20);
    this.mesh.rotation.z = -Math.PI / 2;
    this.dx = this.dz = 0;
    this.spd = this.rotspd = 1;
    this.hit = this.dead = false;
    this.mesh.computeWorldMatrix(true);

    this.move = function () {
        if (Math.sqrt(Math.pow(this.mesh.position.x - this.sx, 2) + Math.pow(this.mesh.position.z - this.sz, 2)) < this.maxd) {
            this.mesh.position.x += this.dx * this.spd;
            this.mesh.position.z += this.dz * this.spd;
        } else {
            this.boomindex = things.indexOf(this); 
            things.splice(this.boomindex, 1);      
            boom = null;
            this.mesh.dispose();
        }

        for (this.i = 0; this.i < things.length; this.i++) {
            if ((things[this.i] instanceof omons) &&
                this.mesh.intersectsPoint(things[this.i].mesh.position)) {
                things[this.i].hit = true; 
                this.boomindex = things.indexOf(this); 
                things.splice(this.boomindex, 1);      
                boom = null;
                this.mesh.dispose();
            }
        }
    }

    this.draw = function () {
    }
}

ohero = function (scene) {
    this.h = 10;
    this.maxw = this.maxd = 100;
    this.mesh = BABYLON.MeshBuilder.CreateCylinder("cHero", { height: this.h, diameterBottom: this.h, diameterTop: 0 }, scene);
    this.mesh.material = new BABYLON.StandardMaterial("mHero", scene);
    this.mesh.material.diffuseColor = new BABYLON.Color3(0, 0, 1);
    this.mesh.position = new BABYLON.Vector3(0, this.h/2, 0);
    this.mesh.rotation.z = -Math.PI / 2;
    this.dx = herodz = 0;
    this.spd = 1;
    this.rotspd = 5;
    this.hit = herodead = false;
    this.mesh.computeWorldMatrix(true);

    this.move = function (event) {
        switch (event.key) {
            case 'a':
                this.mesh.rotation.y -= Math.PI / 180 * this.rotspd;
                break;
            case 'd':
                this.mesh.rotation.y += Math.PI / 180 * this.rotspd;
                break;
            case 'w':
                this.ang = -this.mesh.rotation.y;
                this.spd = 1;
                this.dx = Math.cos(this.ang);
                this.dz = Math.sin(this.ang);
                this.mesh.position.x += this.dx * this.spd;
                this.mesh.position.z += this.dz * this.spd;
                break;
            case 's':
                this.ang = -this.mesh.rotation.y;
                this.spd = 1;
                this.dx = Math.cos(this.ang);
                this.dz = Math.sin(this.ang);
                this.mesh.position.x -= this.dx * this.spd;
                this.mesh.position.z -= this.dz * this.spd;
                break;
            case 't':
                followtop=!followtop; // toggle follow top
                cameraheight=200;
                 camera.position.y = cameraheight;
                //camera.position.x = camera.position.z = 0;
                camera.position.x=hero.mesh.position.x;
                camera.position.z=hero.mesh.position.z;
                camera.rotation.x = Math.PI / 2;
                break;
            case 'f':
                followcam=!followcam; // toggle folow behind cam
                break;
            case 'Enter':
                if (boom == null) {
                    boom = new oboom(mons, hero, scene);
                    things.push(boom);
                    boom.mesh.position.x = this.mesh.position.x;
                    boom.mesh.position.z = this.mesh.position.z;
                    boom.mesh.rotation.y = this.mesh.rotation.y;
                    boom.ang = -boom.mesh.rotation.y;
                    boom.dx = Math.cos(boom.ang);
                    boom.dz = Math.sin(boom.ang);
                }
                break;
        }
    }

    window.addEventListener("keydown", this.move.bind(this)); // this is magic

    this.draw = function () {
    }
}
</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;}
.score  {position: absolute; top: 0; right: 0; width: 100%; font:bold 24pt Arial; color:white; text-shadow:2px 2px #000000}
</style>
</head>
<body>
<canvas id="cvGame"></canvas>
<div id="spWinLose" class="score" style="text-align:center;">SIMPLEX</div>
<div id="spTime" class="score" style="text-align:left;">TIME</div>
<div id="spMons" class="score" style="text-align:right;">MONSTERS</div>
</body>
</html>

Explanation. A straightforward change. You have switches (boolean variables) FollowTop and FollowCam which are toggled in the keydown handler. The main code is in the gameloop(), which monitors these switches and moves the camera accordingly.  One quirky implementation detail: I made the follow-top camera override the follow-behind camera.

For the follow behind camera, the basic idea is to construct a vector that is 180 degrees away from the direction the hero is facing at some fixed distance (like 100 units, followdist). The math is somewhat weird because the rotation stored in the hero mesh is opposite the true angle, so we must negate it first before adding 180.  The camera rotation angle is also weird: 0 degrees is 90 degrees!

 

VARIATION: OBSTACLES

<!doctype html>
<html>
<head>
<script src="babylon.max.js"></script>
<script>
window.addEventListener("DOMContentLoaded", init);

var things;
function init()
{
    things = new Array();
    
    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 = new ohero(scene);
    things.push(hero);
    maxmons = orgmons = 3; 
    for (i = 0; i < maxmons; i++) {
        mons = new omons(hero, scene);
        things.push(mons); // again, push any object if you want to see it
    }
    boom = null;
    
    block = new oblock(scene);
    block.mesh.position.x=50;block.mesh.position.z=50;
    things.push(block);

    block = new oblock(scene);
    block.mesh.position.x=-50;block.mesh.position.z=50;
    things.push(block);

    camera =new BABYLON.FreeCamera("mCam", new BABYLON.Vector3(0,6,-50), scene);
    camera.attachControl(canvas);

    gameover = false;
    starttime = Date.now();
    engine.runRenderLoop(gameloop);
}

function gameloop()
{
    if (!gameover) {
        for (i = 0; i < things.length; i++) {
            if ((things[i] instanceof ohero)==false)
                things[i].move();
        }

        spMons.innerHTML = (orgmons-maxmons);
        elapsed = Date.now() - starttime;
        spTime.innerHTML = (elapsed / 1000).toFixed(2);

        if (maxmons == 0 || hero.hit) {
            gameover = true;
            if (maxmons == 0)
                spWinLose.innerHTML = "YOU WIN";
            else
                spWinLose.innerHTML = "YOU LOSE";
        }        
    }
    scene.render();
}

omons=function(h, scene) {
    this.hero = h.mesh;
    this.h = 10;
    this.maxw = this.maxd = 200;
    this.mesh = BABYLON.MeshBuilder.CreateCylinder("cMons", { height: this.h, diameterBottom: this.h, diameterTop: 0 }, scene);
    this.mesh.material=new BABYLON.StandardMaterial("mMons", scene);
    this.mesh.material.diffuseColor=new BABYLON.Color3(1,0,0);
    this.mesh.position = new BABYLON.Vector3(Math.random() * this.maxw - this.maxw / 2, this.h / 2, Math.random() * this.maxd - this.maxd / 2);
    this.mesh.rotation.z=-Math.PI/2;
    this.dx=this.dz=0;
    this.spd=0.05;
    this.rotspd=1;
    this.hit=this.dead=false;
    this.mesh.computeWorldMatrix(true);

    this.move = function () {
        this.dx=this.hero.position.x-this.mesh.position.x;
        this.dz=this.hero.position.z-this.mesh.position.z;
        this.ang=Math.atan2(this.dz,this.dx);

        this.dx = Math.cos(this.ang);
        this.dz = Math.sin(this.ang); 
        this.mesh.position.x+=this.dx*this.spd;
        this.mesh.position.z+=this.dz*this.spd; 
        this.mesh.rotation.y=-this.ang;
        if (this.mesh.intersectsMesh(this.hero,true)) {
            hero.hit = true;
        }
        for (this.i=0;this.i<things.length;this.i++) {
            if (things[this.i] instanceof oblock) {
                if (this.mesh.intersectsMesh(things[this.i].mesh)) {
                    this.mesh.position.x-=this.dx*this.spd; // back up
                    this.mesh.position.z-=this.dz*this.spd;                     
                }
            }
        }
        if (this.hit) {
            this.monsindex = things.indexOf(this);
            things.splice(this.monsindex, 1);     
            this.mesh.dispose();
            maxmons = maxmons - 1; 
        }
    }

    this.draw = function () {
    }
}

oboom = function (m, h, scene) {
    this.mons = m.mesh; 
    this.sx = h.mesh.position.x; 
    this.sz = h.mesh.position.z;
    
    this.h = 10;
    this.maxw = this.maxd = 100;
    this.mesh = BABYLON.MeshBuilder.CreateCylinder("cBoom", { height: this.h, diameterBottom: this.h/2.5, diameterTop: 0 }, scene);
    this.mesh.material = new BABYLON.StandardMaterial("mBoom", scene);
    this.mesh.material.diffuseColor = new BABYLON.Color3(1, .5, 0);
    this.mesh.position = new BABYLON.Vector3(20, this.h/2, 20);
    this.mesh.rotation.z = -Math.PI / 2;
    this.dx = this.dz = 0;
    this.spd = this.rotspd = 1;
    this.hit = this.dead = false;
    this.mesh.computeWorldMatrix(true);

    this.move = function () {
        if (Math.sqrt(Math.pow(this.mesh.position.x - this.sx, 2) + Math.pow(this.mesh.position.z - this.sz, 2)) < this.maxd) {
            this.mesh.position.x += this.dx * this.spd;
            this.mesh.position.z += this.dz * this.spd;
        } else {
            this.boomindex = things.indexOf(this); 
            things.splice(this.boomindex, 1);      
            boom = null;
            this.mesh.dispose();
        }

        for (this.i = 0; this.i < things.length; this.i++) {
            if (((things[this.i] instanceof omons) || (things[this.i] instanceof oblock)) &&
                this.mesh.intersectsMesh(things[this.i].mesh)) {
                things[this.i].hit = true; 
                this.boomindex = things.indexOf(this); 
                things.splice(this.boomindex, 1);      
                boom = null;
                this.mesh.dispose();
            }
        }
    }

    this.draw = function () {
    }
}

ohero = function (scene) {
    this.h = 10;
    this.maxw = this.maxd = 1000;
    this.mesh = BABYLON.MeshBuilder.CreateCylinder("cHero", { height: this.h, diameterBottom: this.h, diameterTop: 0 }, scene);
    this.mesh.material = new BABYLON.StandardMaterial("mHero", scene);
    this.mesh.material.diffuseColor = new BABYLON.Color3(0, 0, 1);
    this.mesh.position = new BABYLON.Vector3(0, this.h/2, 0);
    this.mesh.rotation.z = -Math.PI / 2;
    this.dx = this.dz = 0;
    this.spd = 1;
    this.rotspd = 5;
    this.hit = herodead = false;
    this.mesh.computeWorldMatrix(true);

    this.move = function (event) {
        switch (event.key) {
            case 'a':
                this.mesh.rotation.y -= Math.PI / 180 * this.rotspd;
                break;
            case 'd':
                this.mesh.rotation.y += Math.PI / 180 * this.rotspd;
                break;
            case 'w':
                this.ang = -this.mesh.rotation.y;
                this.spd = 1;
                this.dx = Math.cos(this.ang);
                this.dz = Math.sin(this.ang);
                this.mesh.position.x += this.dx * this.spd;
                this.mesh.position.z += this.dz * this.spd;
                for (this.i=0;this.i<things.length;this.i++) {
                    if (things[this.i] instanceof oblock 
                    &&  this.mesh.intersectsMesh(things[this.i].mesh)) { // if hit block
                        this.mesh.position.x -= this.dx * this.spd; // backup
                        this.mesh.position.z -= this.dz * this.spd;                    
                    }
                }
                break;
            case 's':
                this.ang = -this.mesh.rotation.y;
                this.spd = 1;
                this.dx = Math.cos(this.ang);
                this.dz = Math.sin(this.ang);
                this.mesh.position.x -= this.dx * this.spd;
                this.mesh.position.z -= this.dz * this.spd;
                for (this.i=0;this.i<things.length;this.i++) {
                    if (things[this.i] instanceof oblock 
                    &&  this.mesh.intersectsMesh(things[this.i].mesh)) { // if hit block
                        this.mesh.position.x += this.dx * this.spd; // backup
                        this.mesh.position.z += this.dz * this.spd;                    
                    }
                }
                break;
            case 't':
                camera.position.y = 200;
                camera.position.x = camera.position.z = 0;
                camera.rotation.x = Math.PI / 2;
                break;
            case 'Enter':
                if (boom == null) {
                    boom = new oboom(mons, hero, scene);
                    things.push(boom);
                    boom.mesh.position.x = this.mesh.position.x;
                    boom.mesh.position.z = this.mesh.position.z;
                    boom.mesh.rotation.y = this.mesh.rotation.y;
                    boom.ang = -boom.mesh.rotation.y;
                    boom.dx = Math.cos(boom.ang);
                    boom.dz = Math.sin(boom.ang);
                }
                break;
        }
    }

    window.addEventListener("keydown", this.move.bind(this)); // this is magic

    this.draw = function () {
    }
}

oblock=function (scene)
{
    this.h = 20;
    this.maxw = this.maxd = 1000;
    this.mesh = BABYLON.MeshBuilder.CreateBox("cBox", { height: this.h, width: this.h, depth: this.h }, scene);
    this.mesh.material = new BABYLON.StandardMaterial("mHBox", scene);
    this.mesh.material.diffuseColor = new BABYLON.Color3(1, .5, 0);
    this.mesh.position = new BABYLON.Vector3(this.maxw*Math.random()-this.maxw/2, this.h/2, this.maxd*Math.random()-this.maxd/2);
    this.mesh.rotation.z = -Math.PI / 2;
    this.dx = this.dz = 0;
    this.spd = 1;
    this.rotspd = 5;
    this.hit = herodead = false;
    this.mesh.computeWorldMatrix(true);
    this.move=function() {}
    this.draw=function() {}
}

</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;}
.score  {position: absolute; top: 0; right: 0; width: 100%; font:bold 24pt Arial; color:white; text-shadow:2px 2px #000000}
</style>
</head>
<body>
<canvas id="cvGame"></canvas>
<div id="spWinLose" class="score" style="text-align:center;">SIMPLEX</div>
<div id="spTime" class="score" style="text-align:left;">TIME</div>
<div id="spMons" class="score" style="text-align:right;">MONSTERS</div>
</body>
</html>
Explanation. First I created an object named oblock—this is just an orange box 20x20x20 units, which is double the height of the cylinders. I tried 10x10x10, but they were too small. The move and draw functions were empty because these objects don’t move. Next
  • In init(), I placed two boxes for test purposes. Now you can loop through and create boxes, liked I did the monsters and it will place them in random positions throughout the 1000×1000 plane.
  • In the hero, monster, and missile .move() functions, I tested for a hit with an oblock. If there was a hit, I added code to back up by dx and dz.

 

VARIATION: POWER PELLETS

<!doctype html>
<html>
<head>
<script src="babylon.max.js"></script>
<script>
window.addEventListener("DOMContentLoaded", init);

var things;
function init()
{
    things = new Array();
    
    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 = new ohero(scene);
    things.push(hero);
    maxmons = orgmons = 3; 
    for (i = 0; i < maxmons; i++) {
        mons = new omons(hero, scene);
        things.push(mons); // again, push any object if you want to see it
    }
    boom = null;

    maxsensu=3; // I could loop, but I want to position senzu beans
    
    senzu = new osenzu(scene);
    senzu.mesh.position.x=-50;
    senzu.mesh.position.z= 50;
    things.push(senzu);

    senzu = new osenzu(scene);
    senzu.mesh.position.x= 0;
    senzu.mesh.position.z= 50;
    things.push(senzu);

    senzu = new osenzu(scene);
    senzu.mesh.position.x= 50;
    senzu.mesh.position.z= 50;
    things.push(senzu);
    
    
    camera =new BABYLON.FreeCamera("mCam", new BABYLON.Vector3(0,6,-50), scene);
    camera.attachControl(canvas);

    gameover = false;
    starttime = Date.now();
    engine.runRenderLoop(gameloop);
}

function gameloop()
{
    if (!gameover) {
        for (i = 0; i < things.length; i++) {
            if ((things[i] instanceof ohero)==false)
                things[i].move();
        }

        spMons.innerHTML = (orgmons-maxmons);
        elapsed = Date.now() - starttime;
        spTime.innerHTML = (elapsed / 1000).toFixed(2);

        if (maxmons == 0 || hero.hit) {
            gameover = true;
            if (maxmons == 0)
                spWinLose.innerHTML = "YOU WIN";
            else
                spWinLose.innerHTML = "YOU LOSE";
        }        
    }
    scene.render();
}

omons=function(h, scene) {
    this.hero = h.mesh;
    this.h = 10;
    this.maxw = this.maxd = 200;
    this.mesh = BABYLON.MeshBuilder.CreateCylinder("cMons", { height: this.h, diameterBottom: this.h, diameterTop: 0 }, scene);
    this.mesh.material=new BABYLON.StandardMaterial("mMons", scene);
    this.mesh.material.diffuseColor=new BABYLON.Color3(1,0,0);
    this.mesh.position = new BABYLON.Vector3(Math.random() * this.maxw - this.maxw / 2, this.h / 2, Math.random() * this.maxd - this.maxd / 2);
    this.mesh.rotation.z=-Math.PI/2;
    this.dx=this.dz=0;
    this.spd=0.05;
    this.rotspd=1;
    this.hit=this.dead=false;
    this.mesh.computeWorldMatrix(true);

    this.move = function () {
        this.dx=this.hero.position.x-this.mesh.position.x;
        this.dz=this.hero.position.z-this.mesh.position.z;
        this.ang=Math.atan2(this.dz,this.dx);

        this.dx = Math.cos(this.ang);
        this.dz = Math.sin(this.ang); 
        this.mesh.position.x+=this.dx*this.spd;
        this.mesh.position.z+=this.dz*this.spd; 
        this.mesh.rotation.y=-this.ang;
        if (this.mesh.intersectsMesh(this.hero,true)) {
            hero.hit = true;
        }
        if (this.hit) {
            this.monsindex = things.indexOf(this);
            things.splice(this.monsindex, 1);     
            this.mesh.dispose();
            maxmons = maxmons - 1; 
        }
    }

    this.draw = function () {
    }
}

oboom = function (m, h, scene) {
    this.mons = m.mesh; 
    this.sx = h.mesh.position.x; 
    this.sz = h.mesh.position.z;
    
    this.h = 10;
    this.maxw = this.maxd = 100;
    this.mesh = BABYLON.MeshBuilder.CreateCylinder("cBoom", { height: this.h, diameterBottom: this.h/2.5, diameterTop: 0 }, scene);
    this.mesh.material = new BABYLON.StandardMaterial("mBoom", scene);
    this.mesh.material.diffuseColor = new BABYLON.Color3(1, .5, 0);
    this.mesh.position = new BABYLON.Vector3(20, this.h/2, 20);
    this.mesh.rotation.z = -Math.PI / 2;
    this.dx = this.dz = 0;
    this.spd = this.rotspd = 1;
    this.hit = this.dead = false;
    this.mesh.computeWorldMatrix(true);

    this.move = function () {
        if (Math.sqrt(Math.pow(this.mesh.position.x - this.sx, 2) + Math.pow(this.mesh.position.z - this.sz, 2)) < this.maxd) {
            this.mesh.position.x += this.dx * this.spd;
            this.mesh.position.z += this.dz * this.spd;
        } else {
            this.boomindex = things.indexOf(this); 
            things.splice(this.boomindex, 1);      
            boom = null;
            this.mesh.dispose();
        }

        for (this.i = 0; this.i < things.length; this.i++) {
            if ((things[this.i] instanceof omons) &&
                this.mesh.intersectsPoint(things[this.i].mesh.position)) {
                things[this.i].hit = true; 
                this.boomindex = things.indexOf(this); 
                things.splice(this.boomindex, 1);      
                boom = null;
                this.mesh.dispose();
            }
        }
    }

    this.draw = function () {
    }
}

ohero = function (scene) {
    this.h = 10;
    this.maxw = this.maxd = 100;
    this.mesh = BABYLON.MeshBuilder.CreateCylinder("cHero", { height: this.h, diameterBottom: this.h, diameterTop: 0 }, scene);
    this.mesh.material = new BABYLON.StandardMaterial("mHero", scene);
    this.mesh.material.diffuseColor = new BABYLON.Color3(0, 0, 1);
    this.mesh.position = new BABYLON.Vector3(0, this.h/2, 0);
    this.mesh.rotation.z = -Math.PI / 2;
    this.dx = this.dz = 0;
    this.spd = 1;
    this.rotspd = 5;
    this.hit = this.dead = false;
    this.senzu=0; // # of senzu beans for bullets
    this.mesh.computeWorldMatrix(true);

    this.move = function (event) {
        switch (event.key) {
            case 'a':
                this.mesh.rotation.y -= Math.PI / 180 * this.rotspd;
                break;
            case 'd':
                this.mesh.rotation.y += Math.PI / 180 * this.rotspd;
                break;
            case 'w':
                this.ang = -this.mesh.rotation.y;
                this.spd = 1;
                this.dx = Math.cos(this.ang);
                this.dz = Math.sin(this.ang);
                this.mesh.position.x += this.dx * this.spd;
                this.mesh.position.z += this.dz * this.spd;
                break;
            case 's':
                this.ang = -this.mesh.rotation.y;
                this.spd = 1;
                this.dx = Math.cos(this.ang);
                this.dz = Math.sin(this.ang);
                this.mesh.position.x -= this.dx * this.spd;
                this.mesh.position.z -= this.dz * this.spd;
                break;
            case 't':
                camera.position.y = 200;
                camera.position.x = camera.position.z = 0;
                camera.rotation.x = Math.PI / 2;
                break;
            case 'Enter':
                if (boom == null) {
                    if (this.senzu>0) {
                        boom = new oboom(mons, hero, scene);
                        things.push(boom);
                        boom.mesh.position.x = this.mesh.position.x;
                        boom.mesh.position.z = this.mesh.position.z;
                        boom.mesh.rotation.y = this.mesh.rotation.y;
                        boom.ang = -boom.mesh.rotation.y;
                        boom.dx = Math.cos(boom.ang);
                        boom.dz = Math.sin(boom.ang);
                        this.senzu--;
                        if (this.senzu==0) { // always add another sensu to world
                            senzu=new osenzu(scene); // random position
                            things.push(senzu);
                        }
                    }
                }
                break;
        }
    }

    window.addEventListener("keydown", this.move.bind(this)); // this is magic

    this.draw = function () {
    }
}

osenzu=function(scene) {
    this.h = 4;
    this.maxw = this.maxd = 100;
    this.mesh = BABYLON.MeshBuilder.CreateSphere("cBall", { diameter:4 }, scene);
    this.mesh.material = new BABYLON.StandardMaterial("mBall", scene);
    this.mesh.material.diffuseColor = new BABYLON.Color3(1, 1, 0);
    this.mesh.position = new BABYLON.Vector3(Math.random()*this.maxw-this.maxw/2, this.h/2, Math.random()*this.maxd-this.maxd/2);
    this.mesh.rotation.z = -Math.PI / 2;
    this.dx = this.dz = 0;
    this.spd = 1;
    this.rotspd = 5;
    this.hit = this.dead = false;
    this.mesh.computeWorldMatrix(true);

    this.move=function() {
        if (this.mesh.intersectsMesh(hero.mesh)) { // remove if senzu intersects hero
            this.senzuindex = things.indexOf(this); 
            things.splice(this.senzuindex, 1);      
            this.mesh.dispose();
            hero.senzu++;
        }
    }
    this.draw=function() {}
}
</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;}
.score  {position: absolute; top: 0; right: 0; width: 100%; font:bold 24pt Arial; color:white; text-shadow:2px 2px #000000}
</style>
</head>
<body>
<canvas id="cvGame"></canvas>
<div id="spWinLose" class="score" style="text-align:center;">SIMPLEX</div>
<div id="spTime" class="score" style="text-align:left;">TIME</div>
<div id="spMons" class="score" style="text-align:right;">MONSTERS</div>
</body>
</html>

Explanation. Like all changes. The first step is to add a new object. We could have named it power ball, or power pellet, but I liked osenzu, because of (DragonBall Z Senzu Beans). In osenzu, the sphere is yellow and diameter 4.  The osenzuo.move() function only checks if the senzu intersects the hero—monsters will move right over it with no effect. If the senzu intersects the hero, then it removes itself from the thing list.  It also increments hero.senzu (this violates our rule of not allowing objects to affect the state of other objects except through flags like hit, thus you should refactor this eventually).

  • In init(), I created three senzu beans and placed them in specific position in the world. If you don’t position them, the code will place the beans randomly.
  • In ohero, I created a new variable senzu, initializing it to zero.
  • In ohero.move(), a missile is fired only if the senzu count is non-zero. Once it reaches zero, another senzu bean is put somewhere in the world in a random position.

 

VARIATION: MONSTERS FIRING

<!doctype html>
<html>
<head>
<script src="babylon.max.js"></script>
<script>
window.addEventListener("DOMContentLoaded", init);

var things;
function init()
{
    things = new Array();
    
    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 = new ohero(scene);
    things.push(hero);
    maxmons = orgmons = 3; 
    for (i = 0; i < maxmons; i++) {
        mons = new omons(hero, scene);
        things.push(mons); // again, push any object if you want to see it
    }
    boom = null;
    
    camera =new BABYLON.FreeCamera("mCam", new BABYLON.Vector3(0,6,-50), scene);
    camera.attachControl(canvas);

    gameover = false;
    starttime = Date.now();
    engine.runRenderLoop(gameloop);
}

function gameloop()
{
    if (!gameover) {
        for (i = 0; i < things.length; i++) {
            if ((things[i] instanceof ohero)==false)
                things[i].move();
        }

        spMons.innerHTML = (orgmons-maxmons);
        elapsed = Date.now() - starttime;
        spTime.innerHTML = (elapsed / 1000).toFixed(2);

        if (maxmons == 0 || hero.hit) {
            gameover = true;
            if (maxmons == 0)
                spWinLose.innerHTML = "YOU WIN";
            else
                spWinLose.innerHTML = "YOU LOSE";
        }        
    }
    scene.render();
}

omons=function(h, scene) {
    this.hero = h;
    this.h = 10;
    this.maxw = this.maxd = 200;
    this.mesh = BABYLON.MeshBuilder.CreateCylinder("cMons", { height: this.h, diameterBottom: this.h, diameterTop: 0 }, scene);
    this.mesh.material=new BABYLON.StandardMaterial("mMons", scene);
    this.mesh.material.diffuseColor=new BABYLON.Color3(1,0,0);
    this.mesh.position = new BABYLON.Vector3(Math.random() * this.maxw - this.maxw / 2, this.h / 2, Math.random() * this.maxd - this.maxd / 2);
    this.mesh.rotation.z=-Math.PI/2;
    this.dx=this.dz=0;
    this.spd=0.05;
    this.rotspd=1;
    this.hit=this.dead=false;
    this.boom=null; // the monster's current missile
    this.close=50; // fires a missile after 50 units
    this.mesh.computeWorldMatrix(true);

    this.move = function () {
        this.dx=this.hero.mesh.position.x-this.mesh.position.x;
        this.dz=this.hero.mesh.position.z-this.mesh.position.z;
        this.dh=Math.sqrt(this.dx*this.dx+this.dz*this.dz); // calc dist
 
        this.ang=Math.atan2(this.dz,this.dx); 
        this.dx = Math.cos(this.ang);
        this.dz = Math.sin(this.ang); 
        this.mesh.position.x+=this.dx*this.spd;
        this.mesh.position.z+=this.dz*this.spd; 
        this.mesh.rotation.y=-this.ang;
        if (this.mesh.intersectsMesh(this.hero.mesh,true)) {
            hero.hit = true;
        }
        if (this.hit) {
            this.monsindex = things.indexOf(this);
            things.splice(this.monsindex, 1);     
            this.mesh.dispose();
            maxmons = maxmons - 1; 
        }
        // fire a missile if close
        if (this.dh<=this.close) {      // fire if close
            if (this.boom==null) {      // & no missile
                this.boom=new omboom(this, this.hero, scene); 
                things.push(this.boom);
            }
        }

    }

    this.draw = function () {
    }
}

oboom = function (m, h, scene) {
    this.mons = m.mesh; 
    this.sx = h.mesh.position.x; 
    this.sz = h.mesh.position.z;
    
    this.h = 10;
    this.maxw = this.maxd = 100;
    this.mesh = BABYLON.MeshBuilder.CreateCylinder("cBoom", { height: this.h, diameterBottom: this.h/2.5, diameterTop: 0 }, scene);
    this.mesh.material = new BABYLON.StandardMaterial("mBoom", scene);
    this.mesh.material.diffuseColor = new BABYLON.Color3(1, .5, 0);
    this.mesh.position = new BABYLON.Vector3(20, this.h/2, 20);
    this.mesh.rotation.z = -Math.PI / 2;
    this.dx = this.dz = 0;
    this.spd = this.rotspd = 1;
    this.hit = this.dead = false;
    this.mesh.computeWorldMatrix(true);

    this.move=function() {
        if (Math.sqrt(Math.pow(this.mesh.position.x - this.sx, 2) + Math.pow(this.mesh.position.z - this.sz, 2)) < this.maxd) {
            this.mesh.position.x += this.dx * this.spd;
            this.mesh.position.z += this.dz * this.spd;
        } else {
            this.boomindex = things.indexOf(this); 
            things.splice(this.boomindex, 1);      
            boom = null;
            this.mesh.dispose();
        }

        for (this.i = 0; this.i < things.length; this.i++) {
            if ((things[this.i] instanceof omons) &&
                this.mesh.intersectsPoint(things[this.i].mesh.position)) {
                things[this.i].hit = true; 
                this.boomindex = things.indexOf(this); 
                things.splice(this.boomindex, 1);      
                boom = null;
                this.mesh.dispose();
            }
        }
    }

    this.draw = function () {
    }
}

ohero = function (scene) {
    this.h = 10;
    this.maxw = this.maxd = 100;
    this.mesh = BABYLON.MeshBuilder.CreateCylinder("cHero", { height: this.h, diameterBottom: this.h, diameterTop: 0 }, scene);
    this.mesh.material = new BABYLON.StandardMaterial("mHero", scene);
    this.mesh.material.diffuseColor = new BABYLON.Color3(0, 0, 1);
    this.mesh.position = new BABYLON.Vector3(0, this.h/2, 0);
    this.mesh.rotation.z = -Math.PI / 2;
    this.dx = this.dz = 0;
    this.spd = 1;
    this.rotspd = 5;
    this.hit = this.dead = false;
    this.mesh.computeWorldMatrix(true);

    this.move = function (event) {
        switch (event.key) {
            case 'a':
                this.mesh.rotation.y -= Math.PI / 180 * this.rotspd;
                break;
            case 'd':
                this.mesh.rotation.y += Math.PI / 180 * this.rotspd;
                break;
            case 'w':
                this.ang = -this.mesh.rotation.y;
                this.spd = 1;
                this.dx = Math.cos(this.ang);
                this.dz = Math.sin(this.ang);
                this.mesh.position.x += this.dx * this.spd;
                this.mesh.position.z += this.dz * this.spd;
                break;
            case 's':
                this.ang = -this.mesh.rotation.y;
                this.spd = 1;
                this.dx = Math.cos(this.ang);
                this.dz = Math.sin(this.ang);
                this.mesh.position.x -= this.dx * this.spd;
                this.mesh.position.z -= this.dz * this.spd;
                break;
            case 't':
                camera.position.y = 200;
                camera.position.x = camera.position.z = 0;
                camera.rotation.x = Math.PI / 2;
                break;
            case 'Enter':
                if (boom == null) {
                    boom = new oboom(mons, hero, scene);
                    things.push(boom);
                    boom.mesh.position.x = this.mesh.position.x; 
                    boom.mesh.position.z = this.mesh.position.z; 
                    boom.mesh.rotation.y = this.mesh.rotation.y;
                    boom.ang = -boom.mesh.rotation.y;
                    boom.dx = Math.cos(boom.ang);
                    boom.dz = Math.sin(boom.ang);
                }
                break;
        }
    }

    window.addEventListener("keydown", this.move.bind(this)); // this is magic

    this.draw = function () {
    }
}

omboom = function (m, h, scene) {
    this.source = m;
    this.target = h; 
    this.sx = this.source.mesh.position.x; 
    this.sz = this.source.mesh.position.z;

    this.h = 10;
    this.maxw = this.maxd = 100;
    this.mesh = BABYLON.MeshBuilder.CreateCylinder("cmBoom", { height: this.h, diameterBottom: this.h/2.5, diameterTop: 0 }, scene);
    this.mesh.material = new BABYLON.StandardMaterial("mmBoom", scene);
    this.mesh.material.diffuseColor = new BABYLON.Color3(1, .5, .5);
    this.mesh.position = new BABYLON.Vector3(this.sx, this.h/2, this.sz);
    this.mesh.rotation.z = -Math.PI / 2;
    // now fix missile positions and rotations
    this.mesh.position.x=m.mesh.position.x;
    this.mesh.position.z=m.mesh.position.z;
    this.mesh.rotation.y=m.mesh.rotation.y;    
    this.ang=-m.mesh.rotation.y;
    this.dx=Math.cos(this.ang);
    this.dz=Math.sin(this.ang);

    // this.dx = this.dz = 0;
    this.spd = 0.25; // this is a good debug speed 
    this.rotspd = 1;
    this.hit = this.dead = false;
    this.mesh.computeWorldMatrix(true);

    this.move = function () {
        if (Math.sqrt(Math.pow(this.mesh.position.x - this.sx, 2) + Math.pow(this.mesh.position.z - this.sz, 2)) < this.maxd) {
            this.mesh.position.x += this.dx * this.spd;
            this.mesh.position.z += this.dz * this.spd;
        } else {
            this.boomindex = things.indexOf(this); 
            things.splice(this.boomindex, 1);      
            this.source.boom = null;
            this.mesh.dispose();
        }

        if (this.mesh.intersectsMesh(this.target.mesh)) {
            this.target.hit = true; 
            this.boomindex = things.indexOf(this); 
            things.splice(this.boomindex, 1);      
            this.boom = null;
            this.mesh.dispose();
        }
    }

    this.draw = function () {
    }
}
</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;}
.score  {position: absolute; top: 0; right: 0; width: 100%; font:bold 24pt Arial; color:white; text-shadow:2px 2px #000000}
</style>
</head>
<body>
<canvas id="cvGame"></canvas>
<div id="spWinLose" class="score" style="text-align:center;">SIMPLEX</div>
<div id="spTime" class="score" style="text-align:left;">TIME</div>
<div id="spMons" class="score" style="text-align:right;">MONSTERS</div>
</body>
</html>

Explanation. As usual, we create a new missile object, but for the monster: omboom. This missile takes on the source monster’s position and rotation. I set a test spd of 0.25, which you’ll have to balance for the actual game. The hero is passed as a target parameter. The missile ignores all other objects. If the missile hits the hero, it removes itself from the thing list, and sets the hero’s hit flag. If the missile travels more than a certain distance from the monster (maxd), it vanishes.

The tricky part is the monster code. In omons, the code checks if the hero is within a certain distance from the monster and, if there is no current missile pending, the monster fires a missile (new omboom).