3d totem destroyer


How to create a simple totem destroyer prototype in 3d space with 2d physics. Actually it’s the same process as doing a totem destroyer in 2d, instead of 2d sprites we use 3d objects. First off, take a look at a totem destroyer game, this one is the most basic version TotemDestroyer. As you can see, a very basic game concept and still very enjoyable.
The P2.js physics engine will handle all the physics and collision detection events, all we have to do is to create a level to play with. In this case we will use a free vector drawing software called Inkscape to create many levels easy and fast.

 

prepare Inkscape

We use Inkscape as level editor, Inkscape will export an xml file with all the layers and objects we  have created. We can also add custom information to any object such as, is it destructible, is it a target or the ground plane. All this features make Inkscape perfect as level editor for a totem destroyer game.
There are a few things to keep in mind while using Inkscape as level editor.

inkscape_Pref
Change basic settings to optimized, which applies transformation direct to object
inkscape_properties
Change document properties width and height to match the 2d world size
inkscape_xmlEditor
Use the xml editor to edit object attributes such as the id

Once Inscape is prepared, it’s fairly easy to create a level. Each layer of the document defines a new level in the final game. Make sure you are on the right layer, enable snapping for precise positioning, start draw some rectangles and create a fun level. Define a target zone, in this case with an id called “ground”. Also create an object which should finally land and stop on that position with an id called “target”. We use this objects later on to define if the player finished the level or not.

LevelDesign
A simple level created on a layer called “level_1”, with a target and a ground to land on.

 

load levels

Load a SVG file with a simple loader, once loaded we can create levels based on that data. The SVG file has the center point on the bottom left, the 3d world has the center point top center. To align the objects properly we have to offset all the position values.

//ADD XML LOADER
    var xhttp = new XMLHttpRequest();
        xhttp.open("GET","assets/levels.svg",false);
        xhttp.onload = parseSVG;
        xhttp.send();
    
    
    //GET ALL LAYERS/LEVELS FROM SVG 
    function parseSVG()
    {
        //GET ALL LAYERS/LEVELS
        var layers = xhttp.responseXML.getElementsByTagName("g");
        //SELECT FIRST LEVEL
        var selected = layers[0]
        //GET AN ARRAY OF RECTS IN SELECTED LEVEL
        var level = selected.getElementsByTagName("rect");

        //NEXT -> CREATE 2D/3D OBJECTS
    };

//SVG OFFSET VALUES
    var mirrorY = xhttp.responseXML.getElementsByTagName("svg")[0].getAttribute("height"); //MIRROR Y VALUE
    var centerX = xhttp.responseXML.getElementsByTagName("svg")[0].getAttribute("width")/2; //MOVE TO CENTER X

 

user input

Handles user input for touch and mouse events.

//ADD CLICK EVENT
    renderer.domElement.addEventListener( 'mousedown', onDocumentMouseDown, false );
    renderer.domElement.addEventListener( 'touchstart', onDocumentMouseDown, false );
    
    var raycaster = new THREE.Raycaster(); 
    var mouse = new THREE.Vector2(); 

    function onDocumentMouseDown( event ) {

        event.preventDefault();
        
        //IS TOUCH OR CLICK EVENT
        if(event.type == 'mousedown')
        {
            mouse = new THREE.Vector2(
            ( event.clientX / window.innerWidth ) * 2 - 1,
          - ( event.clientY / window.innerHeight ) * 2 + 1
            );
        }
        else
        {
             mouse = new THREE.Vector2(
            ( event.touches[0].clientX / window.innerWidth ) * 2 - 1,
          - ( event.touches[0].clientY / window.innerHeight ) * 2 + 1
            )
        };
        
        //SEND RAY IN 3D SPACE
        raycaster.setFromCamera( mouse, camera );
        
        //GET OBJECTS IN TOUCH/CLICK POSITION
        var intersects = raycaster.intersectObjects( scene.children );

        if ( intersects.length > 0 ) {
            
            //CHECK IF OBJECT CAN BE DESTROYED
            if(intersects[0].object.name == "cube" && !intersects[0].object.data.target)
            {
                //PREPARE TO REMOVE 2D OBJECT
                removeBodys.push(intersects[0].object.data);
                //REMOVE 3D OBJECT
                scene.remove( intersects[0].object );
                
                //AWAKE EACH BODY IN P2 WORLD
                for (var i = 0; i < world.bodies.length; i++) 
                { 
                  world.bodies[i].wakeUp()
                }
            };
            
        };
    };

 

full prototype

<html>
<head>
    <meta charset="utf-8">
    <title>2d Physics in 3d Space</title>
    <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
    <meta name="viewport" content="user-scalable=0"/>
    <script src="lib/p2.min.js"></script>
    <script src="lib/three.min.js"></script>
    <script src="lib/OrbitControls.js"></script>
    <script src="main.js"></script>
</head>
<body onload="init()" style="margin:0px; padding:0px; overflow:hidden;">
    
    <select id="levelSelect" name="level" style="right: 10px;position:absolute; z-index:100; font-size: 20px; margin-top:10px;">
    </select>    
    
    
    <div id="winScreen" style="position:absolute;width:100%; height:100%; margin:0 auto; text-shadow: 0 0px 12px #000;visibility: hidden;">
         <p style="text-align: center;color:white;font-size: 50px;"><i class="fa fa-5x fa-trophy" ></i> <br>you Win</p>
         <p style="text-align: center;color:white;font-size: 20px;">please select a new level</p>
    </div>
    
    <div id="looseScreen" style="position:absolute;width:100%; height:100%; margin:0 auto; text-shadow: 0 0px 12px #000;visibility: hidden;">
        <p style="text-align: center;color:white;font-size: 50px;"><i class="fa fa-5x fa-thumbs-o-down" ></i> <br>you loose</p>
         <p style="text-align: center;color:white;font-size: 20px;">please select a new level</p>
    </div>
    
    
    <a class="fa fa-5x fa-external-link-square" href="http://inkfood.github.io/2dPhysics_in3dSpace/" target="_blank" style="color:inherit; position:absolute; min-width:100px; min-height:100px;text-align:center;bottom:20px; right:20px; cursor:pointer;"></a>

</body>
</html>


function init()
{
    //CREATE NEW THREE JS SCENE
    var scene = new THREE.Scene();
    
    //INIT AND SET UP RENDER
    var SCREEN_WIDTH = window.innerWidth, SCREEN_HEIGHT = window.innerHeight;
    var renderer = new THREE.WebGLRenderer({antialias:true});
        renderer.setClearColor(new THREE.Color('lightgrey'), 1)
        renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT); // SET RENDER SIZE
        document.body.appendChild( renderer.domElement ); // APPEND RENDER CANVAS TO BODY
        renderer.domElement.id = "canvas_threeJS"; // SET CANVAS ID
    
    //ADD CAMERA TO THE SCENE
    var VIEW_ANGLE = 45, ASPECT = SCREEN_WIDTH / SCREEN_HEIGHT, NEAR = 0.1, FAR =1000;
    var camera = new THREE.PerspectiveCamera( VIEW_ANGLE, ASPECT, NEAR, FAR);
        camera.position.set(-4,6,12);
        scene.add(camera);
    
    
    //HANDLE WINDOW RESIZE
	window.addEventListener('resize', function(){
		renderer.setSize( window.innerWidth, window.innerHeight )
		camera.aspect	= window.innerWidth / window.innerHeight
		camera.updateProjectionMatrix();
	}, false);
    
    
    //ADD AMBIENT LIGHT
    var ambientLight = new THREE.AmbientLight(0x404040);
        scene.add(ambientLight)
    
    // 3d PAN AND ZOOM CONTROLS
    var controls = new THREE.OrbitControls(camera,renderer.domElement);
        controls.maxDistance = 14;
        controls.noPan = true;
        controls.maxPolarAngle = 1.4;
    
    //ADD POINT LIGHT
    var light = new THREE.PointLight(0xffffff,.7);
        light.position.set(0,7,3);
        scene.add(light);
    
    //INIT PHYSICS
    var world = new p2.World();
        world.sleepMode = p2.World.BODY_SLEEPING;
        world.setGlobalStiffness(1e6);
        world.solver.iterations = 30;
        world.defaultContactMaterial.relaxation= 2;
        world.defaultContactMaterial.restitution= 0.001;
    
        world.on('beginContact', function (e) 
        {
            //LAND ON TARGET
            if(e.bodyA.name == "ground" && e.bodyB.target)
            {
                e.bodyB.on('sleep',winner)
            }
            
            //LAND OUTSIDE
            if(e.bodyA.name == "backGround" && e.bodyB.target)
            {
                e.bodyB.on('sleep',loose)
            }
            
            
        });
    
    //GET THE DROPDOWN MENU 
    var levelSelect =  document.getElementById("levelSelect");
    //ADD EVENT TO THE DROPDOWN MENU
    levelSelect.addEventListener("change", loadLevel);
    
    //ADD XML LOADER
    var xhttp = new XMLHttpRequest();
        xhttp.open("GET","assets/level_1.svg",false);
        xhttp.onload = parseSVG;
        xhttp.send();
    
    
    //GET ALL LAYERS/LEVELS FROM SVG 
    function parseSVG()
    {
        //GET ALL LAYERS
        var layers = xhttp.responseXML.getElementsByTagName("g");
        
        //ADD NEW OPTION TO THE SELECT MENU FOR EACH LAYER
        for (var i = 0; i < layers.length; i++) 
        { 
            var option = document.createElement("option");
                option.text = layers[i].getAttribute("inkscape:label"); //GET LAYER NAME
                option.id = layers[i].getAttribute("id"); //ADD ID
                levelSelect.add(option);    
        };
    };
    
    //SVG OFFSET VALUES
    var mirrorY = xhttp.responseXML.getElementsByTagName("svg")[0].getAttribute("height"); //MIRROR Y VALUE
    var centerX = xhttp.responseXML.getElementsByTagName("svg")[0].getAttribute("width")/2; //MOVE TO CENTER X
    
    //LOAD LEVEL SELECTED BY DROPDOWN MENU
    function loadLevel(e)
    {
        
        var selected = xhttp.responseXML.getElementById(levelSelect[levelSelect.selectedIndex].id);
        var level = selected.getElementsByTagName("rect");
        
        //DESTROY CURRENT LEVEL
        destroyLevel();
        
        //REMOVE WIN/LOOSE SCREEN
        document.getElementById("winScreen").style.visibility ="hidden";
        document.getElementById("looseScreen").style.visibility ="hidden";
        
        //PARSE LOADED SVG FILE
        for (var i = 0; i < level.length; i++) 
        { 
            if(level[i].getAttribute("id").indexOf("ground") > -1)
            {
                addGround(level[i])
            }
            else
            {
                addBox(level[i])
            };
        };
    };
    
    //LOAD FIRST LEVEL
    loadLevel();
    
    //ARRAY HOLDS OBJECTS TO REMOVE
    var removeBodys = [];
    
    //REMOVE LEVEL
    function destroyLevel()
    {
        //GET ALL BODIES IN THE WORLD
        for (var i = 0; i < world.bodies.length; i++) 
        { 
            if(world.bodies[i].name == "box" || world.bodies[i].name == "ground")
            {
                removeBodys.push(world.bodies[i]);
                scene.remove( world.bodies[i].data );
            };
        };  
        
    };
    
    //YOU TOTALLY WON
    function winner(e)
    {
        e.target.off('sleep',winner);
        document.getElementById("winScreen").style.visibility ="visible";
    }
    
    //YOU SCREWED UP
    function loose(e)
    {
        e.target.off('sleep',loose);
        document.getElementById("looseScreen").style.visibility ="visible";
    }
    
    
    //ADD CLICK EVENT
    renderer.domElement.addEventListener( 'mousedown', onDocumentMouseDown, false );
    renderer.domElement.addEventListener( 'touchstart', onDocumentMouseDown, false );
    
    var raycaster = new THREE.Raycaster(); 
    var mouse = new THREE.Vector2(); 

    function onDocumentMouseDown( event ) {

        event.preventDefault();
        
        //IS TOUCH OR CLICK EVENT
        if(event.type == 'mousedown')
        {
            mouse = new THREE.Vector2(
            ( event.clientX / window.innerWidth ) * 2 - 1,
          - ( event.clientY / window.innerHeight ) * 2 + 1
            );
        }
        else
        {
             mouse = new THREE.Vector2(
            ( event.touches[0].clientX / window.innerWidth ) * 2 - 1,
          - ( event.touches[0].clientY / window.innerHeight ) * 2 + 1
            )
        };
        
        //SEND RAY IN 3D SPACE
        raycaster.setFromCamera( mouse, camera );
        
        //GET OBJECTS IN TOUCH/CLICK POSITION
        var intersects = raycaster.intersectObjects( scene.children );

        if ( intersects.length > 0 ) {
            
            //CHECK IF OBJECT CAN BE DESTROYED
            if(intersects[0].object.name == "cube" && !intersects[0].object.data.target)
            {
                //PREPARE TO REMOVE 2D OBJECT
                removeBodys.push(intersects[0].object.data);
                //REMOVE 3D OBJECT
                scene.remove( intersects[0].object );
                
                //AWAKE EACH BODY IN P2 WORLD
                for (var i = 0; i < world.bodies.length; i++) 
                { 
                  world.bodies[i].wakeUp()
                }
            };
            
        };
    };
    
    //ADD SKYBOX/BACKGROUND
    addBackground();
    
    
    //FUNCTION TO ADD SKYBOX/BACKGROUND
    function addBackground()
    {
        //CREATE NEW TEXTURE FROM IMAGE
        var skyBoxTexture = new THREE.ImageUtils.loadTexture( 'assets/grid.png' );
            skyBoxTexture.wrapS = skyBoxTexture.wrapT = THREE.RepeatWrapping; 
            skyBoxTexture.repeat.set( 4, 4 );

        //CREATE NEW MATERIAL WITH TEXTURE APPLIED
        var floorMat =  new THREE.MeshPhongMaterial( { map: skyBoxTexture, side: THREE.BackSide,shininess: 1} );

        //CREATE NEW 3d BOX MESH WITH MATERIAL
        var skyBox = new THREE.Mesh(new THREE.BoxGeometry(20,20,20), floorMat);
            skyBox.position.y += 10;
            scene.add(skyBox);
        
        //CREATE NEW P2 PHYSICS BODY -> 2d GROUNDPLANE
        var planeShape = new p2.Plane();
        var planeBody = new p2.Body({position:[0,0]});
            planeBody.name = "backGround"; //ADD NAME TO 2d OBJECT
            planeBody.data = skyBox; //ADD MESH OBJECT
            planeBody.addShape(planeShape);
            world.addBody(planeBody);
        
    }
    
    //FUNCTION TO ADD NEW GROUND TARGET
    function addGround(boxData)
    {
        var x = boxData.getAttribute("x")-centerX+(boxData.getAttribute("width")/2);
        var y = mirrorY-boxData.getAttribute("y")-(boxData.getAttribute("height")/2)
        var w = boxData.getAttribute("width");
        var h = boxData.getAttribute("height");
        var color = boxData.style.fill;
        //CREATE NEW MATERIAL WITH RANDOM COLOR
        var material = new THREE.MeshPhongMaterial( { color: color } );
        
        
        //CREATE NEW 3d BOX MESH WITH MATERIAL
        var cube = new THREE.Mesh(new THREE.BoxGeometry(w, h, w), material);
            cube.position.set(x,y,0);
            cube.name = "ground"
            scene.add(cube);
        
        //CREATE NEW P2 PHYSICS BODY -> 2d BOX
        var boxShape = new p2.Rectangle(w,h);
        var boxBody = new p2.Body({ mass:0, position:[x,y]});
            boxBody.data = cube;
            boxBody.name="ground";
            boxBody.allowSleep = true;
            boxBody.sleepSpeedLimit = 1; // Body will feel sleepy if speed<1 (speed is the norm of velocity)
            boxBody.sleepTimeLimit =  2; // Body falls asleep after 1s of sleepiness
            boxBody.addShape(boxShape);
            world.addBody(boxBody);
            cube.data = boxBody;
        
    }
    
    
    //FUNCTION TO ADD NEW RANDOM BOX
    function addBox(boxData)
    {
        var x = boxData.getAttribute("x")-centerX+(boxData.getAttribute("width")/2);
        var y = mirrorY-boxData.getAttribute("y")-(boxData.getAttribute("height")/2)
        var w = boxData.getAttribute("width");
        var h = boxData.getAttribute("height");
        var color = boxData.style.fill;
        //CREATE NEW MATERIAL WITH RANDOM COLOR
        var material = new THREE.MeshPhongMaterial( { color: color } );
        
        
        //CREATE NEW 3d BOX MESH WITH MATERIAL
        var cube = new THREE.Mesh(new THREE.BoxGeometry(w, h, .75), material);
            cube.position.set(x,y,0);
            cube.name = "cube"
            scene.add(cube);
        
        //CREATE NEW P2 PHYSICS BODY -> 2d BOX
        var boxShape = new p2.Rectangle(w,h);
        var boxBody = new p2.Body({ mass:1, position:[x,y]});
            boxBody.data = cube;
            boxBody.name="box";
            boxBody.allowSleep = true;
            boxBody.sleepSpeedLimit = 1; // Body will feel sleepy if speed<1 (speed is the norm of velocity)
            boxBody.sleepTimeLimit =  2; // Body falls asleep after 1s of sleepiness
            boxBody.addShape(boxShape);
            world.addBody(boxBody);
            cube.data = boxBody;
        
        
            if(boxData.id.indexOf("target") > -1)
            {
                boxBody.target = true;
            }
    }
    
    
    //START RENDER
    var clock = new THREE.Clock();
    update();
    
    function update(nowMsec)
    {
        
        //REMOVEVE FROM PHYSICS WORLD
        if(removeBodys.length > 0)
        {
            for (var i = 0; i < removeBodys.length; i++) 
            { 
                world.removeBody(removeBodys[i]);
            }
            
            removeBodys = []
        }
        
        //GET DELTA TIME
        var  delta = clock.getDelta();
        
        //KEEP FRAMES ABOVE 24
//        if(delta > 1/24){delta = 1/30}
        
        //UPDATE PHYSICS
        world.step(1/60,delta);
        
        //FOR EACH 2d BODY IN THE WORLD
        for (var i = 0; i < world.bodies.length; i++) 
        { 
            //IF 2d BODY HAS NAME BOX
            if(world.bodies[i].name == "box")
            {
                //UPDATE 3d MESH BODY POSITION AND ROTATION ACCORDINGLY
                world.bodies[i].data.position.set(world.bodies[i].position[0],world.bodies[i].position[1],0);
                world.bodies[i].data.rotation.z = world.bodies[i].angle; 
            }
        };  
        
        //UPDATE 3D SCENE
        renderer.render( scene, camera );
        
        //KEEP UPDATING
        requestAnimationFrame( update );
    };
};

 

conclusion

While this is still a raw prototype, it already has a lot of code to study and understand. Once the code is separated into render, game logic and object scripts, the actual game script will be just a few line of code.
It’s still a really simple script and if everything is set up correctly, creating multiple levels is fast and easy.