How to create a simple platformer prototype called “up”, only with SVG elements. This prototype is entirely made with code, no visual level designer was involved. All elements have certain values which can be changed to change the whole level/game behavior. Currently some of these values set to random, such as platform speed, platform width and platform position. Others are static like player speed, level speed, time, background etc..
Keep in mind, this is just a prototype, a visualization of a concept. There are no graphical elements which represents the player, enemies and other assets. The code is not perfect as well, but it is as easy as possible, which really helps to understand what happens and where it needs some optimization. Each line of the code is commented to help to understand whats happening there.
overview
Once you have downloaded the project, these are all the project files from the “/js” folder inside the project folder. The rest of the project files are fairly simple and basic, you should check them as well. The game is created an controlled entirely with these javascript files.
/js
Initializes the game and keeps the render alive, Get delta time for animations.
main.js
var time; function init() { initLevel(); //INITIALIZE THE LEVEL STRUCTURE initPlayer(); //INITIALIZE THE PLAYER initControll(); //INITIALIZE CONTROLLS render(); //START RENDER } function render() { var now = new Date().getTime(), //GET CURRENT TIME dt = now - (time || now); //FRAME TIME time = now; updateLevel(dt); //UPDATE LEVEL updatePlayer(dt); //UPDATE PLAYER requestAnimationFrame(render); //CONTINUE RENDER }
Creates the level and keeps it updated. Takes care of platform position,speed, update.
level.js
var SVG; //MAIN SVG ELEMENT var w; //WIDTH OF THE SCREEN var h; //HEIGHT OF THE SCREEN var ground; //GROUND ELEMENT var score; //SCORE ELEMENT var maxPlatforms = 20; //MAX PLATFORMS TO PREPARE var platformsArray = []; //STORES ALL PLATFORMS IN A ARRAY var activePlatforms = 0; //PLATFORMS CURRENTLY ON THE SCREEN var maxActivePlatforms = 10; //MAX PLATFORMS ON THE SCREEN -> DIFFICULTY function initLevel() { SVG = document.getElementById("SVG_scene"); //GET SVG ELEMENT w = window.innerWidth; //GET WINDOW WIDTH h = window.innerHeight; //GET WINDOW HEIGHT createLevel(); //CREATE LEVEL score(); } function createLevel() { var svgNS = "http://www.w3.org/2000/svg"; //DEFINE THE namespaceURI var level = document.createElementNS(svgNS,"g"); //CREATE A GROUP FOR THE ENTIRE LEVEL ground = document.createElementNS(svgNS,"rect"); //CREATE A RECT WHICH REPRESENT THE GROUND ground.setAttributeNS(null,"x",0); //START X ground.setAttributeNS(null,"y",h-100); //START Y ground.setAttributeNS(null,"width",w); //WIDTH ground.setAttributeNS(null,"height",100); //HEIGHT ground.setAttributeNS(null,"fill","white"); //FILLCOLOR ground.position = { x: 0, y: 0 }; //TRANSFORM VALUE OF THE GROUND var midLine = document.createElementNS(svgNS,"line"); //CREATE A RECT WHICH REPRESENT THE GROUND midLine.setAttributeNS(null,"x1",0); //START X midLine.setAttributeNS(null,"y1",h/2-100); //START Y midLine.setAttributeNS(null,"x2",w); //WIDTH midLine.setAttributeNS(null,"y2",h/2-100); //HEIGHT midLine.setAttributeNS(null,"stroke","white"); //FILLCOLOR midLine.setAttributeNS(null,"stroke-width","5"); //FILLCOLOR midLine.setAttributeNS(null,"stroke-dasharray","10 10"); //FILLCOLOR //CREATE "maxPlatforms" PLATFORMS for (var i = 0; i < maxPlatforms; i++) { var platform = document.createElementNS(svgNS,"rect"); //CREATE A RECT WHICH REPRESENT ONE PLATFORM platform.setAttributeNS(null,"x",-100); //START X -> STARTS OFFSCREEN platform.setAttributeNS(null,"y",0); //START Y platform.setAttributeNS(null,"width",100); //WIDTH platform.setAttributeNS(null,"height",30); //HEIGHT platform.setAttributeNS(null,"fill","#4274a8"); //FILLCOLOR platform.position = { x: 0, y: 0 }; //TRANSFORM VALUE OF THE PLATFORM platform.velocity = { x: 3, y: 0 }; //VELOCITY OF THE PLATFORM level.appendChild(platform); //PUT THE PLATFORM INSIDE THE "level" GROUP platformsArray.push(platform); //PUT THE PLATFORM INSIDE THE "platformsArray" } level.appendChild(ground);//PUT THE GROUND INSIDE THE "LEVEL" GROUP level.appendChild(midLine);//PUT THE GROUND INSIDE THE "LEVEL" GROUP SVG.appendChild(level) //APPEND LEVEL TO THE SVG ELEMENT } function setPlatform() { //SET A NEW RANDOM PLATFORM POSITION var side = Math.random(); //RANDOM CHOOSE LEFT OR RIGHT SIDE platformsArray[activePlatforms].position.y = (h+200)/maxActivePlatforms*activePlatforms+Math.random()*10; //SET Y POSITION if(side < 0.5) { platformsArray[activePlatforms].position.x = (Math.random()*400)*-1; //SET RANDOM START POSITION X LEFT platformsArray[activePlatforms].velocity.x = Math.random()*4+1; //SET RANDOM START VELOCITY } else { platformsArray[activePlatforms].position.x = w+200+(Math.random()*400);//SET RANDOM START POSITION X RIGHT platformsArray[activePlatforms].velocity.x = Math.random()*4+1;//SET RANDOM START VELOCITY } activePlatforms +=1; } function score() { var svgNS = "http://www.w3.org/2000/svg"; //DEFINE THE namespaceURI score = document.createElementNS(svgNS,"text"); //CREATE A RTEXT NODE score.setAttributeNS(null,"x",w-60); //START X score.setAttributeNS(null,"y",h/2-110); //START Y score.setAttributeNS(null,"fill","white"); //FILLCOLOR score.setAttributeNS(null,"font-family","helvetica"); //FONT score.setAttributeNS(null,"font-weight","bold"); //FONT-WEIGHT score.textContent ="0"; //TEXT score.current = 0; //CURRENT SCORE SVG.appendChild(score) //APPEND LEVEL TO THE SVG ELEMENT } function updateScore() { score.current += 1/10; //ADD TO SCORE score.textContent =Math.round(score.current); //UPDATE SCORE } function updateLevel(dt) { //UPDATE LEVEL ON EACH FRAME AND GET FRAMETIME "dt" if(activePlatforms < maxActivePlatforms) // SET PLATFORMS { setPlatform(); } if(player.position.y < h/2 && player.velocity.y <= 0) //MOVE LEVEL DOWN WHILE PLAYER MOVES UP { var pv = player.velocity.y; //GET PLAYER VELOCITY for (var i = 0; i < activePlatforms; i++) { platformsArray[i].velocity.y = pv*-1; //PASS PLAYER VELOCITY TO WORLD } player.position.y -=pv* dt * 60/1000; //RESET PLAYER VELOCITY updateScore(); if(ground.position.y < 110)//IF GROUND IS STILL IN SCENE -> MOVE IT DOWN { ground.position.y -= pv;//GET THE NEW Y POSITION ground.setAttribute("transform", "translate(" +0 + " "+ ground.position.y +" )");//TRANSFORM TO THE NEXT Y POSITION } } else//MOVE LEVEL DOWN WHILE PLAYER MOVES UP { for (var i = 0; i < activePlatforms; i++) { platformsArray[i].velocity.y = 0;} } for (var i = 0; i < activePlatforms; i++) { if(platformsArray[i].velocity.x > 0 && platformsArray[i].position.x > w) //CHECK FOR RIGHT BOUNDRIES { platformsArray[i].velocity.x = platformsArray[i].velocity.x *-1; //INVERT PLATFORM VELOCITY } if(platformsArray[i].velocity.x < 0 && platformsArray[i].position.x < 100) //CHECK FOR LEFT BOUNDRIES { platformsArray[i].velocity.x = platformsArray[i].velocity.x *-1; //INVERT PLATFORM VELOCITY } if(platformsArray[i].position.y > h)//BLOCK IS OUT OF SCREEN { platformsArray[i].position.y = (Math.random()*100)*-1; //PUT IT BACK ON TOP platformsArray[i].velocity.x = Math.random()*4+1; //SET NEW VELOCITY } platformsArray[i].position.x += platformsArray[i].velocity.x * dt * 60/1000;//GET THE NEW X POSITION platformsArray[i].position.y += platformsArray[i].velocity.y * dt * 60/1000;//GET THE NEW X POSITION platformsArray[i].setAttribute("transform", "translate(" + platformsArray[i].position.x + " "+ platformsArray[i].position.y +" )");//TRANSFORM TO THE NEXT X POSITION } }
Creates the player, checks for collision on each frame, respond to collision.
player.js
var player; //PLAYER ELEMENT var target; //CURRENT PLATFORM function initPlayer() { createPlayer(); //CREATE THE PLAYER } function createPlayer() { var svgNS = "http://www.w3.org/2000/svg"; //DEFINE THE namespaceURI player = document.createElementNS(svgNS,"g"); //CREATE GROUP FOR THE PLAYER playerBody = document.createElementNS(svgNS,"rect"); //CREATE RECT WHICH REPRESENTS THE PLAYER playerBody.setAttributeNS(null,"x",w/2-25); //START X = CENTER OF SCREEN playerBody.setAttributeNS(null,"y",-100); //START Y -> OFFSCREEN playerBody.setAttributeNS(null,"width",50); //WIDTH playerBody.setAttributeNS(null,"height",50); //HEIGHT playerBody.setAttributeNS(null,"fill","black"); //FILLCOLOR player.appendChild(playerBody) //PUT THE PLAYER INSIDE THE "player" GROUP player.velocity = { x: 0, y: 0.5 }; //VELOCITY OF THE PLAYER player.position = { x: 0, y: 0 }; //TRANSFORM VALUE OF THE PLAYER player.onFloor = false; SVG.appendChild(player); //APPEND THE "player" GROUP TO THE SVG ELEMENT } function intersectRect(r1, r2) { //CHECK IF THE TWO RECTANGLES OVERLAP return !(r2.left > r1.right || r2.right < r1.left || r2.top > r1.bottom || r2.bottom < r1.top); } function updatePlayer(dt) { player.velocity.x = 3 * (!!keys[39] - !!keys[37]) // right - left player.velocity.y += 0.1 // Acceleration due to gravity var nextY = player.position.y + player.velocity.y * dt * 60/1000; //GET THE NEXT POSSIBLE Y POSITION var BB_Player = player.getBoundingClientRect(); //GET PLAYER BOUNDING BOX BB_Player.bottom += player.velocity.y * dt * 60/1000; BB_Player.top += player.velocity.y * dt * 60/1000; //CHECK COLLISION FOR EACH PLATFORM ON SCREEN for (var i = 0; i < maxActivePlatforms; i++) { var BB_Platform = platformsArray[i].getBoundingClientRect(); //GET PLATFORM BOUNDING BOX if(intersectRect(BB_Player,BB_Platform)) //CHECK IF TWO BOUNDING BOXES OVERLAP { //IF PLAYER IS JUMPING if(player.velocity.y < 0 && !player.onFloor) {player.velocity.y = (BB_Platform.bottom - BB_Player.top);} //IF PLAYER IS FALLING else if(player.velocity.y > 0) {player.velocity.y=(BB_Platform.top - BB_Player.bottom)/4;target = platformsArray[i];} } } //CHECK COLLISION FOR THE GROUND if(intersectRect(BB_Player,ground.getBoundingClientRect()) && !player.onFloor) { player.velocity.y = ground.getBoundingClientRect().top-BB_Player.bottom; target = null; if(ground.position.y > 100) //IF GROUND IS ALREADY OFFSCREEN -> PLAYER DEAD { //RESET LEVEL activePlatforms = 0; score.current = 0; score.textContent =Math.round(score.current); player.position = {x:0,y:0}; ground.position = {x:0,y:0}; } } if(target) //IF ON PLATFORM { player.velocity.x += target.velocity.x; } player.position.y += player.velocity.y * dt * 60/1000; //GET NEW Y POSITION player.position.x += player.velocity.x * dt * 60/1000; //GET NEW X POSITION player.setAttribute("transform", "translate(" + player.position.x + " "+player.position.y+" )"); //TRANSFORM TO NEW POSITION player.onFloor = (nextY > player.position.y); if (nextY != player.position.y) {player.velocity.y = 0}; if(keys[38] && player.onFloor) { player.velocity.y = -10 // Acceleration due to gravity target = null; } }
Check for user input, apply input correct to player.
controll.js
var keys = {} function initControll() { document.onkeydown=function(e) { keys[e.which] = true return; }; document.onkeyup=function(e){keys[e.which] = false} document.addEventListener('touchstart', function(e) { if(e.touches[0].pageX < window.innerWidth/2)//LEFT { keys[37] = true; } else { keys[39] = true; }; if(e.touches.length >= 2) { keys[38] = true; } },false); document.addEventListener('touchend', function(e) { keys[39] = false;keys[37] = false; keys[38] = false; },false); }
Basic render tick with requestAnimationFrame.
requestAnimationFrame.js
// http://paulirish.com/2011/requestanimationframe-for-smart-animating/ // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating // requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel // MIT license (function() { var lastTime = 0; var vendors = ['ms', 'moz', 'webkit', 'o']; for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame']; } if (!window.requestAnimationFrame) window.requestAnimationFrame = function(callback, element) { var currTime = new Date().getTime(); var timeToCall = Math.max(0, 16 - (currTime - lastTime)); var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); lastTime = currTime + timeToCall; return id; }; if (!window.cancelAnimationFrame) window.cancelAnimationFrame = function(id) { clearTimeout(id); }; }());
conclusion
Creating a basic, fully computed platformer only with SVG elements worked fine. It is really easy to create basic game prototypes in pure SVG. Moving objects along x and y coordinates and checking for collision, is the same as on any other platform. While the prototype “UP” is really simple and basic, its a good starting point for a more complex and fun game. A game with different platforms, enemies, certain task etc., as well as good artwork.
Anyway there is clearly a performance problem on mobile devices, most of the devices the game was tested on, are running below 30 fps while playing. This is a common problem while moving too much DOM related elements, at the same time on the screen. There are already some workarounds, which should help increase the fps. Unless SVG elements are GPU accelerated on mobile devices, there is no chance to get more complex games running.