Saturday, December 14, 2013

HTML5 Game #2 : The Tank Battle


In my previous article I have shown how to use a vehicle as "1st person navigator" in 2D.
After a second thought I found that there are more things to tell regarding HTML5game dev. So I decided to continue the series hoping that at the end of which I can come up with a minimum playable game. It would be a good practice for me and others.
During this series if you have remarks, or suggestions feel free to drop a line in the comments.

First I will redefine the game story.
Starting this article the game will be about a tank, navigating and and shooting  on a map. The enemies are not very specific for the time being, they can be computer animated ones or other user(s) in a multiplayer game. Future articles will help determining them, since it is all about what can be achieved in a reasonable amount of time and efforts.

So in this article I will do the following:
1- add a tank with a moving turret
2- control the tank via keyboard instead of push buttons for better playability. 
3- add some quick objects to the map
4- some technical stuff

As a matter of fact this article is more about polishing than anything else. It doesn't require any head scratching.
However before I start, I would like to point that the code has suffered a bit and needs some refactoring. I hope to be able to do this in the next articles.

1. The tank
In order to simulate a tank I have used a  sprite PNG format with a transparent background. See image
The chassis and the turret are separate since the turret should be able to turn 360°.
Placing the chassis and the turret on the screen is straight forward. First you have to load the image then you have to draw each part alone.




It is worth noting that the tank is positioned upwards which is opposed to the relative axis system initial direction. This means that the turret along with its gun as the trajectory of the shell evolve in the negative values of the y-axis.

As I said earlier the chassis is fixed, but the turret can rotate. The good news is that since we are in a 2D and Top view, all objects are symmetric thus we don't need to draw every shape that the turret takes as it rotates. So delegating the job to the graphic engine is sufficient.

Tank forward, reverse, rotation speed as well as the rotation speed of the turret are controlled by the following variables in the code.
var ctDefaultForward = 5;
var ctDefaultBackward = 3;
var ctDefautltTurning = 2;
var ctDefautltTurretTurning = 2;

Firing is also constrained by the loading time. Meaning that after every shot there is a certain amount of time to wait until the next shell is loaded. This time is controlled by the variable.
var ctDefaultLoadTime = 20;

Explosion of the shell is a sequence of sprites that display one after the other.
Each of these sprites is contained in a  64x64 rectangle, so it suffice to know the number of the sequence in order to deduce the sprite position on the sprite sheet.
The variable explosionSeq keeps track of the explosion sequence. A value of -1 means there is no explosion.

Upon firing the gun recoils and this is reflected by move back of the turret. The variable recoil tells if there has been a recoiling and recoilDist controls the amount.

2. Tank control

Unlike in the previous article, the tank is now controlled via keyboard instead of push buttons. This is will allow a greater maneuverability.
The logic of the keyboard controls lies in the existence of a keyDown array. This array will take as index the key code and as value true in case the key is down and false in case the key is up. This keyDown array is essential when several keys are pressed down.
The variables that set the key controls are listed in here:
var ctKeyMoveUp = 69; /*E*/
var ctKeyMoveDown = 68; /*D*/
var ctKeyTurnLeft = 83; /*S*/
var ctKeyTurnRight = 70; /*F*/
var ctKeyTurretLeft = 37; /*left arrow*/
var ctKeyTurretRight = 39; /*right arrow*/
var ctKeyFire = 16; /*shift*/

In order to avoid the differences between QWERTY and AZERTY keyboards, I have set the control keys to match both layouts, whereas E = Forward, D = Backwards, S = TurnLeft, F = TurnRight, Left Arrow = Turret Left, Right Arrow = Turret Right, Shift = Fire.

3. Buildings

For the time being other objects on the map, particularly buildings are no more than rectangles of different dimensions and colors spread randomly.
As of this article, there are no collisions detection implemented in the source code.


4. Technical stuff

The most important technical issues to mentions are:
1- The game loop that maintain the game running. It is called every 100ms and consists of function readInput() for reading and analyzing the key strokes, the update() function that make some calculations regarding the positioning of objects, the drawAll() function that draws all the objects on the screen and finally the postUpdate() method that perform computations for the after drawing phase (useful in case of shell firing).
2- Another important issue, the rotating turret. The turret rotates within a relative axis system. This is the easiest way to do it, since the graphics api is able to rotate the turret sprite without deteriorating its quality.





5. The demo

Here is a live demo of this new version. Give it a try!




Your browser does not support the HTML5 canvas tag.





Keys: E = Forward, D = Backward, S = Left, F = Right, Left Arrow = Turret Left, Right Arrow = Right Turret, Shift = Fire


Please find below the source code of this version:
<!DOCTYPE html>
<html>
<body>
<canvas id="myCanvas2" width="600" height="480" style="border:1px solid #d3d3d3;">
Your browser does not support the HTML5 canvas tag.</canvas>
<br/>
<span id="loadingInfo"></span><br/>
<span id="info">Keys: E = Forward, D = Backward, S = Left, F = Right, Left Arrow = Turret Left, Right Arrow = Right Turret, Up Arrow = Fire</span><br/>

<script>
var timeIntervalID = -1;
var canvas=document.getElementById("myCanvas2");
var ctx=canvas.getContext("2d");
var info=document.getElementById("info");
var loadingInfo=document.getElementById("loadingInfo");
var imgSprites = new Image();
imgSprites.src="file:///C:/projects/tank/t2_1.png";
var imgExplo = new Image();
imgExplo.src="file:///C:/projects/tank/explo2.png";
var keyDown = [];
var ctDefaultLoadTime = 20;
var ctDefaultForward = 5;
var ctDefaultBackward = 3;
var ctDefautltTurning = 2;
var ctDefautltTurretTurning = 2;
var ctTotalExplosionSeq = 5;
var ctShellMaxRange = 300;

var ctKeyMoveUp = 69; /*E*/
var ctKeyMoveDown = 68; /*D*/
var ctKeyTurnLeft = 83; /*S*/
var ctKeyTurnRight = 70; /*F*/
var ctKeyTurretLeft = 37; /*left arrow*/
var ctKeyTurretRight = 39; /*right arrow*/
var ctKeyFire = 16; /*shift*/

// coordinates of the center of the map
// they are also those of the vehicle since it does not physically move on the map.
var canvasWidth = parseInt(canvas.width);
var canvasHeight = parseInt(canvas.height);
var centerX = canvasWidth/2;
var centerY = canvasHeight/2;

// the rotation angle by which the relative system will rotate
var rotAngle = 0;

// the rotation angle of the turret
var rotTurret = 0;

// movements
var deltaAngle = 0;
var deltaMove = 0;
var deltaTurret = 0;
var explosionSeq = -1;

// tank dimensions
var tankWidth = 40;
var tankHeight = 86;
var tankHalfWidth = 20;
var tankHalfHeight = 43;
var realTankPoints = [];



// this is the PI/2 which is 90 in radians
var PIover2 = Math.PI/2;

// coordinates of the objects in the relative system
var buildings = createBuildings();

var shellY = 0;
var fire = false;
var recoil = false;
var recoilDist = 2;
var loadingTime = 0;

// draw everything at start with no translation nor rotation
//drawAll(0, 0);
imgSprites.addEventListener("load", function(){
drawAll(0,0, 0);
});

window.addEventListener("keydown", function(ev){

if(ev.keyCode == ctKeyMoveUp || ev.keyCode == ctKeyTurnLeft|| ev.keyCode == ctKeyMoveDown|| ev.keyCode == ctKeyTurnRight || ev.keyCode == ctKeyTurretLeft || ev.keyCode == ctKeyTurretRight
|| ev.keyCode == ctKeyFire){
keyDown[ev.keyCode] = true;
}
});

window.addEventListener("keyup", function(ev){
if(ev.keyCode == ctKeyMoveUp || ev.keyCode == ctKeyTurnLeft || ev.keyCode == ctKeyMoveDown || ev.keyCode == ctKeyTurnRight || ev.keyCode == ctKeyTurretLeft || ev.keyCode == ctKeyTurretRight){
keyDown[ev.keyCode] = false;
}
});



function readInput(){
deltaAngle = 0;
deltaMove = 0;
deltaTurret = 0;

if(keyDown[ctKeyMoveUp]){
deltaMove = -ctDefaultForward;
}
if(keyDown[ctKeyMoveDown]){
deltaMove = ctDefaultBackward;
}
if(keyDown[ctKeyTurnLeft]){
deltaAngle = ctDefautltTurning;
}
if(keyDown[ctKeyTurnRight]){
deltaAngle = -ctDefautltTurning;
}
if(keyDown[ctKeyTurretLeft]){
deltaTurret = -ctDefautltTurretTurning;
}
if(keyDown[ctKeyTurretRight]){
deltaTurret = ctDefautltTurretTurning;
}

if(loadingTime > 0){
keyDown[ctKeyFire] = false;
}
if(keyDown[ctKeyFire]){
fire = true;
recoil = true;
keyDown[ctKeyFire] = false;
loadingTime = ctDefaultLoadTime;
}
}
function update(){

rotAngle = (rotAngle + deltaAngle) % 360;
}

function postUpdate(){

// loading time
if(loadingTime > 0){
loadingTime --;
loadingTime = Math.max(0, loadingTime);
loadingInfo.innerHTML= "Reloading: " + loadingTime;
}else{
loadingInfo.innerHTML = "Loaded";
}
}

// the drawAll function draws all the map and its objects
// parameters:

function drawAll(){
// compute the total rotation angle of the vehicle in degrees and radians
var rad = rotAngle*Math.PI/180;

// compute the forward vector coordinates in the relative system
var rotdy = Math.round(deltaMove * Math.sin(PIover2-rad));
var rotdx = Math.round(deltaMove * Math.cos(PIover2 - rad));

// clear the map
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvasWidth, canvasHeight);

// save the context in order to start the transformation
ctx.save();

// move the coordinates system to the center (this is the relative system)
ctx.translate(centerX, centerY);

// apply the rotation to the relative system
ctx.rotate(rad);


for(var i = 0; i < buildings.length; i++){
// make the necessary translation in the relative system
buildings[i].x += -rotdx;
buildings[i].y += -rotdy;

// draw the objects on the map in the relative system
ctx.fillStyle = buildings[i].c;
ctx.fillRect(buildings[i].x, buildings[i].y ,buildings[i].w, buildings[i].h);
if(buildings[i].hilit){
ctx.strokeStyle="#000000";
ctx.lineWidth=4;
ctx.strokeRect(buildings[i].x, buildings[i].y ,buildings[i].w, buildings[i].h);
}
}

// restore the previous context (return to the fixed system)
ctx.restore();

// draw the vehicle at the center of the map using the fixed system
ctx.drawImage(imgSprites, 1, 0, tankWidth, tankHeight, centerX - tankHalfWidth, centerY - tankHalfHeight, tankWidth, tankHeight);


// draw turrent
rotTurret = (rotTurret + deltaTurret) % 360;
var radDeltaTurret = rotTurret*Math.PI/180;
ctx.save();
ctx.translate(centerX, centerY);

// apply the rotation to the relative system
ctx.rotate(radDeltaTurret);
ctx.drawImage(imgSprites, 75, 0, 27, 99, - 15, - 80 + (recoil ? recoilDist : 0), 28, 99);

//fire
var fired = fire || false;
if(fired || shellY != 0){
if(shellY == 0){
shellY = -107;
}
recoil = false;
ctx.beginPath();
ctx.fillStyle = '#ff0000';
ctx.strokeStyle = '#ff0000';
ctx.arc(0,shellY,2,0,2*Math.PI);
ctx.fill();
ctx.stroke();

shellY += -50;

if(Math.abs(shellY) > ctShellMaxRange){
fire = false;
explosionSeq = 0;
explosionY = shellY;
shellY = 0;
}
}

// explosion
if(explosionSeq > -1){
var eX = (explosionSeq * 64) % 320;
var eY = 0;//(explosionSeq * 64) % 320;
ctx.drawImage(imgExplo, eX, eY, 64, 64, - 32, explosionY - 32, 64, 64);
explosionSeq++;
if(explosionSeq >= ctTotalExplosionSeq){
explosionSeq = -1;
}
}
ctx.restore();
}

timeIntervalID = setInterval(function(){gameLoop();}, 100);
function gameLoop(){
readInput();
update();
drawAll();
postUpdate();
}

function stopGame(){
clearInterval(timeIntervalID);
timeIntervalID = -1;
}

function createBuildings(){
var temp = [];
for(var i = 0; i < 20; i++){
var tx = getRandomInt(-200, 200);
var ty = getRandomInt(-200, 200);
var tw = getRandomInt(50, 150);
var th = getRandomInt(50, 150);
temp[i] = {
x: tx,
y:ty,
w:tw,
h:th,
c:'#'+Math.floor(Math.random()*16777215).toString(16),
}
}
return temp;
}

function getRandomInt (min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}

</script>

</body>
</html>

No comments:

Post a Comment