We will maintain all the code for implementing firing functionality in a file named fire.js
To create the firing effect, we will first create a path of the projectile using physics and store the points in an array like we did for terrain. We will use the formulae of projectile motion and will define an object named fireInfo which holds all the information about the projectile.
var fireInfo = {is_on: true,path: []};
One property inside the object is an array named path that holds the points generated for the projectile motion of bullet and another for specifying whether we should draw the path of fire or not (to be used later). We will add more properties to this object as we proceed further.
Next we will create a function fireByLeftTank() which generates points for the path of fire if the left tank has fired. The function for the right tank will be similar to this one with some minor changes.
function fireByLeftTank(){var beta = tankleft.theta + tankleft.nozzle.alpha,initVel = tankleft.power,acceleration = 8;for(var x=0; x<=canvas.width; x+=20){var newx = 0,newy = 0;var y = x*Math.tan(beta) - (((0.5)*acceleration*x*x)/(Math.pow(initVel*Math.cos(beta),2)));var tankx_i = points[tankright.pos_index].x,tankx_f = tankx_i + tankWidth*Math.cos(tankright.theta);newx = tankleft.nozzle.x + x;newy = tankleft.nozzle.y - y;if(newx>=canvas.width || newy>=points[Math.floor(newx)].y || collisionDetection(newx, newy, tankx_i, tankx_f)){fireInfo.is_on = true;return;}fireInfo.path.push({x: newx, y: newy});}}
We have used the following formula for projectile motion to generate the coordinates (x,y) of the path of the projectile:
But these coordinates are relative to the tip of nozzle of the tank. So to calculate their absolute position we need to add x coordinate of nozzle to x coordinate of path of fire and subtract y coordinate of path of fire from y coordinate of nozzle (remember when we draw on canvas (0,0) is top left so we need to subtract y coordinate).
Then, for each coordinate check if that point has crossed our terrain or has collided with the opposite tank. If so, we would like to end our function at that point and start drawing the path of fire by turning the fireInfo.is_on to true. The is_on is a property that keeps track of when we want to start drawing the path of fire (will be used later).
We will now create the collisionDetection function which detects if the bullet has struck the opposite tank or not.
function collisionDetection(newx, newy, tankx_i, tankx_f){if(newx>=tankx_i && newx<=tankx_f){if(newy>=points[Math.floor(newx)].y - 45){if(tankx_i<canvas.width/2)tankright.score += 25;elsetankleft.score += 25;return true;}}}
The basic idea is to check if the given bullet point lies in the range of opposite tank or not. If the tank hits the opponent then we should increment the score of the attacker. To implement that, check if the tank which is going to be hit lies in first half of the screen (which means it’s the left tank) so we increment the score of right tank as it would have been the attacker.
Just to test if this function works we’ve written a test function which iterates over the coordinates of path of fire and draws them. (Note: This is just a temporary function to make you understand the concept of frames, we will draw the path of fire in a more appropriate way later) Have a look at it:
function drawPathOfFire(){fireByLeftTank();for(var i = 0; i<fireInfo.path.length; i++){ctx.beginPath();ctx.arc(fireInfo.path[i].x, fireInfo.path[i].y, 5, 0, 2*Math.PI);ctx.fillStyle = 'black';ctx.fill();}}var tempEventListener = document.querySelector("#fireAndPower button.statBox");tempEventListener.addEventListener("click", drawPathOfFire);
In the above function, we first call the fireByLeftTank() function to generate the projectile points and then we iterate through them to draw them on the canvas. You may have seen a new canvas property ctx.arc(), it is used to draw a circular arc on the canvas. It’s syntax is (x, y, radius, startAngle, endAngle) where (x, y) are the coordinates of the center and startAngle specifies the angle to start and endAngle specifies at what angle to stop.
We have added a temporary eventListener on the fire button, so when a you click on it, it will call the drawPathOfFire() function.
Now your output should look like this :
You may see the path of fire by the left tank. But we don’t want the tanks to fire like this, leaving a trail behind. To remove it we will update frames every 60 ms (ms - milliseconds). Updating frames just means that we will clear the whole canvas and redraw all the components every 60ms. This will remove the impression of bullets and will show a more realistic animation of bullets firing.
Let’s see how this can be implemented :
For the fire.js file, remember how we said that the drawPathOfFire() function is just temporary, now we will remove it and make a few changes in order to implement it with updating frames. We have added some new properties to our fireInfo object.
var fireInfo = {is_on: false,path: [],pathIterator: 0,drawPath(){if(this.is_on === true && this.pathIterator<this.path.length){ctx.beginPath();ctx.arc(this.path[this.pathIterator].x, this.path[this.pathIterator].y, 5, 0, 2*Math.PI);ctx.fillStyle = 'black';ctx.fill();this.pathIterator++;}else if(this.is_on === true && this.pathIterator>=this.path.length){this.is_on = false;this.pathIterator = 0;this.path = [];}}};
You may notice that we have added a property named pathIterator, which will act just like our variable 'i' in the for loop which we had earlier for looping through all the points of path of fire. There is a method named drawPath which is similar to earlier drawPathOfFire, the difference here is that it doesn’t loop at once and draw all the points of the path. Instead this method will be called every 60ms so we can see a smooth transition of points being drawn. That is why we had kept the property is_on to keep track of whether we need to start drawing or not. Also if the is_on property is true and the pathIterator has finished looping through all points we want to turn the is_on property off and set the path array to be empty.
For your main.js file,
startGame();function startGame(){tankleft.turn = true;generate_terrain_pts();draw_terrain();generateTankPoints();setInterval(updateFrames, 60);}function updateFrames(){ctx.clearRect(0, 0, canvas.width, canvas.height);draw_terrain();goAhead();}function goAhead(){if(loadedImages == 2){draw_tank();draw_nozzle();fireInfo.drawPath();}}
We have removed all the previous content of main.js and refactored it so that now we have a startGame function which initially gives turn to the left tank (which we will switch after each fire) then generates terrain points and draws them and generates points for the tanks. Notice the setInterval method which we have used, it calls a function every specified milliseconds. In our case it calls the updateFrames function every 60 ms (1000ms = 1s).
The updateFrames function clears the whole canvas, draws the terrain and calls the goAhead function. This will happen every 60 ms. We have called the fireInfo.drawPath() method inside goAhead function which will draw the path of fire.
Also change the callback function of tempEventListener to fireByLeftTank()
tempEventListener.addEventListener("click", fireByLeftTank);
Go ahead and run your project and have a look at the output.
Great, now you are able to see a smooth transition in the firing. Now let’s add some more visual aid to the firing like explosion. For this we will use an image sprite animation. An image sprite is a collection of images put into a single image. We will use the following image to show explosion effect.
The top left corner of image has the coordinate (0px,0px). The image is divided into 25 equal parts so consider it like a 2d matrix.
We have added the logic for explosion inside the fireInfo object.
var fireInfo = {is_on: false,path: [],pathIterator: 0,explosion: {gridX: 0,gridY: 0,bomb: new Image()},drawPath(){if(this.is_on === true && this.pathIterator<this.path.length){ctx.beginPath();ctx.arc(this.path[this.pathIterator].x, this.path[this.pathIterator].y, 5, 0, 2*Math.PI);ctx.fillStyle = 'black';ctx.fill();this.pathIterator++;}else if(this.is_on === true && this.pathIterator>=this.path.length && this.explosion.gridY<=64*4){if(this.explosion.gridX>64*4){ this.explosion.gridX = 0; this.explosion.gridY += 64; }var xPos = this.path[this.path.length-1].x - 32;var yPos = this.path[this.path.length-1].y - 32;ctx.drawImage(this.explosion.bomb, this.explosion.gridX, this.explosion.gridY, 64,64,xPos,yPos,64,64);this.explosion.gridX += 64;}else if(this.is_on === true && this.pathIterator>=this.path.length){this.explosion.gridX = 0;this.explosion.gridY = 0;this.is_on = false;this.pathIterator = 0;this.path = [];}}};fireInfo.explosion.bomb.onload = function(){ loadedImages++; goAhead(); }fireInfo.explosion.bomb.src = 'css/images/explosion.png';
Previously we were loading only 2 images, now we have one more so change the if condition to loadImages === 3 in goAhead function in main.js. Replace the previous fireInfo object with current one, add source and onload to the bomb image and run the project.
Wrapping it all up, now you’ve seen that the fireByLeftTank() function works fine, let’s get rid of all temporary variables and functions like tempEventListener and make a final fire.js file.
var fireInfo = {is_on: false,path: [],pathIterator: 0,explosion: {gridX: 0,gridY: 0,bomb: new Image()},drawPath(){if(this.is_on === true && this.pathIterator<this.path.length){ctx.beginPath();ctx.arc(this.path[this.pathIterator].x, this.path[this.pathIterator].y, 5, 0, 2*Math.PI);ctx.fillStyle = 'black';ctx.fill();this.pathIterator++;}else if(this.is_on === true && this.pathIterator>=this.path.length && this.explosion.gridY<=64*4){if(this.explosion.gridX>64*4){ this.explosion.gridX = 0; this.explosion.gridY += 64; }var xPos = this.path[this.path.length-1].x - 32;var yPos = this.path[this.path.length-1].y - 32;ctx.drawImage(this.explosion.bomb, this.explosion.gridX, this.explosion.gridY, 64,64,xPos,yPos,64,64);this.explosion.gridX += 64;}else if(this.is_on === true && this.pathIterator>=this.path.length){this.explosion.gridX = 0;this.explosion.gridY = 0;this.is_on = false;this.pathIterator = 0;this.path = [];switchTurn();}}};fireInfo.explosion.bomb.onload = function(){ loadedImages++; goAhead(); }fireInfo.explosion.bomb.src = 'css/images/explosion.png';function fire(){if(tankleft.turn === true)fireByLeftTank();else if(tankright.turn === true)fireByRightTank();}function fireByLeftTank(){var beta = tankleft.theta + tankleft.nozzle.alpha,initVel = tankleft.power,acceleration = 8;for(var x=0; x<=canvas.width; x+=20){var newx = 0,newy = 0;var y = x*Math.tan(beta) - (((0.5)*acceleration*x*x)/(Math.pow(initVel*Math.cos(beta),2)));var tankx_i = points[tankright.pos_index].x,tankx_f = tankx_i + tankWidth*Math.cos(tankright.theta);newx = tankleft.nozzle.x + x;newy = tankleft.nozzle.y - y;if(newx>=canvas.width || newy>=points[Math.floor(newx)].y || collisionDetection(newx, newy, tankx_i, tankx_f)){fireInfo.is_on = true;return;}fireInfo.path.push({x: newx, y: newy});}}function fireByRightTank(){var beta = -tankright.theta + tankright.nozzle.alpha,initVel = tankright.power,acceleration = 8;for(var x=0; x<=canvas.width; x+=20){var newx = 0,newy = 0;var y = x*Math.tan(beta) - (((0.5)*acceleration*x*x)/(Math.pow(initVel*Math.cos(beta),2)));var tankx_i = points[tankleft.pos_index].x,tankx_f = tankx_i + tankWidth*Math.cos(tankleft.theta);newx = tankright.nozzle.x - x;newy = tankright.nozzle.y - y;if(newx>=canvas.width || newy>=points[Math.floor(newx)].y || collisionDetection(newx, newy, tankx_i, tankx_f)){fireInfo.is_on = true;return;}fireInfo.path.push({x: newx, y: newy});}}function collisionDetection(newx, newy, tankx_i, tankx_f){if(newx>=tankx_i && newx<=tankx_f){if(newy>=points[Math.floor(newx)].y - 45){if(tankx_i<canvas.width/2)tankright.score += 25;elsetankleft.score += 25;return true;}}}
We’ve added fireByRightTank() function which is similar to fireByLeftTank() and a new function fire() which we call on clicking the fire button(implemented later by adding an eventListener). There is also a function switchTurn() inside drawPath method of fireInfo object. It switches the turn property of the tanks, for eg. if currently tankleft.turn = true and it fires, it’s turn will become false and tankright.turn will become true.
The above code is the final code of fire.js
We’ve defined the switchTurn() function inside main.js,
startGame();function startGame(){tankleft.turn = true;generate_terrain_pts();draw_terrain();generateTankPoints();setInterval(updateFrames, 60);}function updateFrames(){ctx.clearRect(0, 0, canvas.width, canvas.height);draw_terrain();goAhead();}function goAhead(){if(loadedImages == 3){draw_tank();draw_nozzle();fireInfo.drawPath();}}function switchTurn(){if(tankleft.turn === true){tankleft.numOfTurns++;tankright.turn = true;tankleft.turn = false;}else if(tankright.turn === true){tankright.numOfTurns++;tankright.turn = false;tankleft.turn = true;}}
This should be your current main.js file.
You would’ve noticed that our angle + and - buttons, fire button and power slider don’t work. Let’s add some eventListeners to them and get them going.