I am working on my gaming skills (mainly with arrays) to generate enemies and now bullets to take them down. I was able to set-up bullets while testing, but were visible only when I had a key pressed (space bar let's say) and with no interval in between, so the browser was not able to take that many at one point.
Is there any simple way to make the ship fire bullets with interval in between (not to load the browser that much) and maybe upon going to the enemy[i].x / y location to delete an enemy and the bullet can disappear ?
Here is the cleaned as much as possible code I have for now (HTML and JS file. Have some images as well and will provide URL to the game to check it if needed - http://sarahkerrigan.biz/spaceship
<!DOCTYPE html>
<html>
<head>
<title>Space Ship</title>
</head>
<body>
<h3>Space Ship</h3>
<canvas id="canvas" width="1000" height="600"></canvas>
<script src="spaceship.js"></script>
</body>
</html>
And here is the spaceship.js file:
var cvs = document.getElementById("canvas");
var ctx = cvs.getContext("2d");
//-------------------------------
// load images
var player = new Image();
var enemy = new Image();
var bullet = new Image();
player.src = "images/player.png";
enemy.src = "images/enemy.png";
bullet.src = "images/fire.png";
//-------------------------------
// vars
var score = 0;
var pause = 0;
var playerY = 300;
var playerX = 100;
var upPressed = false;
var downPressed = false;
var leftPressed = false;
var rightPressed = false;
// audio
var fire = new Audio();
var hit = new Audio();
fire.src = "sounds/fire.mp3";
hit.src = "sounds/hit.mp3";
//-------------------------------
// on key down
document.addEventListener("keydown", keyDownHandler);
function keyDownHandler(e) {
if (e.keyCode == 87) {
upPressed = true;
}
if (e.keyCode == 83) {
downPressed = true;
}
if (e.keyCode == 65) {
leftPressed = true;
}
if (e.keyCode == 68) {
rightPressed = true;
}
}
// on key up
document.addEventListener("keyup", keyUpHandler);
function keyUpHandler(e) {
if (e.keyCode == 87) {
upPressed = false;
}
if (e.keyCode == 83) {
downPressed = false;
}
if (e.keyCode == 65) {
leftPressed = false;
}
if (e.keyCode == 68) {
rightPressed = false;
}
}
//-------------------------------
function moveUp() {
if (playerY <= canvas.height - canvas.height){
}
else{
playerY -= 6;
}
}
function moveDown() {
if (playerY >= canvas.height - player.height){
}
else{
playerY += 6;
}
}
function moveLeft() {
if (playerX <= canvas.width - canvas.width){
}
else{
playerX -= 6;
}
}
function moveRight() {
if (playerX >= canvas.width - player.width){
}
else{
playerX += 6;
}
}
//-------------------------------
// Enemy coordinates
var enemies = [];
enemies[0] = {
x: cvs.width,
y: 0
};
//-------------------------------
// reload page
function reLoad() {
location.reload(); // reload the page
}
//-------------------------------
// draw images
function draw() {
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (upPressed === true) {
moveUp();
}
if (downPressed === true) {
moveDown();
}
if (leftPressed === true) {
moveLeft();
}
if (rightPressed === true) {
moveRight();
}
//-------------------------------
for (var i = 0; i < enemies.length; i++) {
//draw the enemy
ctx.drawImage(enemy, enemies[i].x, enemies[i].y);
// enemy movement speed
enemies[i].x -= 3;
if (enemies[i].x == 880) {
enemies.push({
x: cvs.width,
y: Math.floor(Math.random() * enemy.height) * 10 - enemy.height
});
}
// detect collision
// if enemy hits player
if (playerX + player.width >= enemies[i].x && playerX <= enemies[i].x + enemy.width && (playerY <= enemies[i].y + enemy.height && playerY + player.height >= enemies[i].y)) {
pause = 1;
}
}
//-------------------------------
//draw the player
ctx.drawImage(player, playerX, playerY);
//draw score
ctx.fillStyle = "#fff";
ctx.font = "20px Verdana";
ctx.fillText("Destroyed ships : " + score + "$", 10, cvs.height - 20);
function onPause() {
if (pause >= 1) {
hit.play();
ctx.fillStyle = "#df8a62";
ctx.fillRect(150, 150, 280, 100);
ctx.fillStyle = "#000";
ctx.font = "20px Verdana";
ctx.fillText("You died:", 165, 170);
document.addEventListener("keydown", reLoad);
} else if (pause <= 0) {
requestAnimationFrame(draw);
}
}
onPause();
}
draw();
You want to use time intervals instead of listeners.
var myVar = setInterval(timeCycle, 50);
function timeCycle() {
//all the stuff you currently have listeners for.
}
This way when the time interval happens it will execute your key presses just once. Then if you want to change your rate of fire you add something like this:
setInterval(timeCycle, 50);
rateOfFire = 5;
shootCoolDown = 0;
function timeCycle() {
if (shootPressed === true) {
if(shootCoolDown === 0){
shootCoolDown = rateOfFire;
shoot();
}
}
if (shootCoolDown > 0){
shootCoolDown --;
}
}
This way it will shoot once every 5 game cycles (or 4-rounds per second in this case).
There are more fancy things you can do to create a delta-time system to offset lag by changing your sim rate based on the time it takes your timeCycle to execute, but that tends to be much more complex and easy to mess up, so I would not suggest going down that rabbit hole for beginners.
[EDIT]
So, I've seen several questions recently about deltaTime, but don't see any good examples of how to implement it; so, here is a basic example I threw together. To implement it, just replace the GAME STUFF part with your actual code of what happens in a game cycle, and run all of your time-based values through the delta() function, and it will convert your values from to units per second to units per currentFrame.
My game us under a load of <input type="text" id="lag" value="100000000"> operations per frame.<br>
My speed is = <input type="text" id="speed" value="500"> px per second<br>
I moved <span id="adjusted"></span>px this frame.<br>
FPS: <span id="fps"></span>
<script>
function wrapDelta(lastTime){
var d = new Date();
var n = d.getSeconds()*1000 + d.getMilliseconds();
if (lastTime >= n) {
lastTime -= 60000;
}
return n - lastTime;
}
function delta(input){
return input * deltaCoeff / 1000;
}
var d = new Date();
var ed = new Date();
var endTime = d.getSeconds()*1000 + d.getMilliseconds();
var startTime = d.getSeconds()*1000 + d.getMilliseconds();
var deltaCoeffMin = 25;
var deltaCoeff = deltaCoeffMin;
setInterval(function () {
d = new Date();
startTime = d.getSeconds()*1000 + d.getMilliseconds();
// START GAME STUFF
var lag = Math.round(Math.sqrt(document.getElementById('lag').value)); //because comparing large numbers caused a wierd lag spike at from 9999999 to 10000000
var speed = document.getElementById('speed').value;
document.getElementById('adjusted').innerHTML = delta(speed);
document.getElementById('fps').innerHTML = (1000/deltaCoeff).toFixed(2);
var i; var j; var k; for (i=0; i<lag; i++){ for (j=0; j<lag; j++){ k = 1234567*1.1;}} //This is just a random math loop to simulate the lag cause by actual game stuff
// END GAME STUFF
ed = new Date();
endTime = ed.getSeconds()*1000 + ed.getMilliseconds();
deltaCoeff = endTime - startTime;
if (deltaCoeff < deltaCoeffMin){deltaCoeff = deltaCoeffMin;}
} , deltaCoeffMin);
</script>
The call to requestAnimationFrame will run the draw function only at a rate supported by the monitor and only if the computer is fast enough. If the code is running slow, it will automatically skip a call to the draw function every now and then. Therefore, the draw function should only contain the rendering code and no logic.
You should first put any code that updates the game's state into another function called update. This function will be called at a consistent-ish rate using setInterval:
function update() {
// read inputs
// move objects
// detect collisions
// etc.
// render a new frame only if the browser is done drawing the previous one
requestAnimationFrame(draw);
}
// run the update function 60 times per second
var updateInterval = setInterval(update, 1000 / 60);
It's always good to store the updateInterval so that we can stop the game from running completely with clearInterval(updateInterval), but you might never need to use it.
Now that you have a somewhat consistent game speed, you can set a cooldown on the shooting like this:
if (fireCooldown > 0) {
fireCooldown -= 1;
}
if (/* holding the fire key */ && fireCooldown === 0) {
// create a projectile in front of the player ship
fireCooldown = 30;
}
You'll need to declare that variable somewhere first with var fireCooldown = 0; somewhere, but it should get you started.
As mentioned by Jake Holzinger in the comments, setInterval isn't 100% accurate and the update function might be called a few milliseconds later than expected. You'd have to check the time between two calls yourself using Date objects or other means if you wanted to time stuff perfectly, but I doubt this is necessary for a simple shooter game.
Related
There are two issues, I can't control the snake, and now it just resets constantly without giving the snake a chance to move. I figured that if i kept coding the problem would go away but sadly its not a perfect world.
I tried different key codes for controlling the snake but it didn't work.
window.onload = function() {
var cvs = document.getElementById("canvas");
var ctx = cvs.getContext("2d");
var cvsW = cvs.width;
var cvsH = cvs.height;
var snekW = 10;
var snekH = 10;
//score
var score = 0;
//default direction
var direction = "right";
//read users directions
document.addEventListener("keydown",getDirection);
//To my knowledge this function should control the snake with the
keyboard
function getDirection(e)
{
if(e.keyCode == 37 && direction != "right"){
direction = "left";
}else if(e.keyCode == 38 && direction != "down"){
direction = "up";
}else if(e.keyCode == 39 && direction != "left"){
direction = "right";
}else if(e.keycode == 40 && direction != "up"){
direction = "down";
}
}
function drawSnek(x,y)
{
ctx.fillStyle = "Lime";
ctx.fillRect(x*snekW,y*snekH,snekW,snekH);
ctx.fillStyle = "#000";
ctx.strokeRect(x*snekW,y*snekH,snekW,snekH);
}
var len = 4; //default length of the snake
var snek = []; //the snake is an array
for(var i = len-1; i>=0; i--)
{
snek.push({x:i,y:0});
}
//exceptions for direction of snake
if(direction == "left") snekx--;
else if( direction == "up") sneky--;
else if( direction == "right") snekx++;
else if( direction == "down") sneky++;
//Functions for creating snake food have been removed.
var newHead = { x: snekx, y: sneky};
snek.unshift(newHead);
drawScore(score);
}
setInterval(draw, 10); //I do not fully understand this function
}
There were several issues with your code:
You needed a draw function that could be called in an interval. In this draw function you could draw the individual parts of the snake.
You were unable to control the snake correctly as you had a typo in the down part of the callback function.
You have to either introduce x / y variables for the head of the snake or use the first element of the array to retrieve it.
Unshifting the new position using snek.unshift will cause the snake to grow indefinitely. Removing array elements using
snek.splice(len,snek.length - len);
solves this problem.
If you only draw new elements to the screen the old parts you drew will still exist in new frames. A good idea would be to clear the canvas using
ctx.clearRect(0, 0, cvsW, cvsH);
window.onload = function () {
var cvs = document.getElementById("canvas");
var ctx = cvs.getContext("2d");
var cvsW = cvs.width;
var cvsH = cvs.height;
var snekW = 10;
var snekH = 10;
//score
var score = 0;
//default direction
var direction = "right";
//read users directions
document.addEventListener("keydown", getDirection);
//To my knowledge this function should control the snake with the keyboard
function getDirection(e) {
if (e.keyCode == 37 && direction != "right") {
direction = "left";
} else if (e.keyCode == 38 && direction != "down") {
direction = "up";
} else if (e.keyCode == 39 && direction != "left") {
direction = "right";
} else if (e.keyCode == 40 && direction != "up") {
direction = "down";
}
}
function drawSnek(x, y) {
ctx.fillStyle = "Lime";
ctx.fillRect(x * snekW, y * snekH, snekW, snekH);
ctx.fillStyle = "#000";
ctx.strokeRect(x * snekW, y * snekH, snekW, snekH);
}
let snekx = 0;
let sneky = 0;
var len = 4; //default length of the snake
var snek = []; //the snake is an array
for (var i = len - 1; i >= 0; i--) {
snek.push({ x: i, y: 0 });
}
function draw() {
ctx.clearRect(0, 0, cvsW, cvsH);
//exceptions for direction of snake
if (direction == "left") snekx--;
else if (direction == "up") sneky--;
else if (direction == "right") snekx++;
else if (direction == "down") sneky++;
//Functions for creating snake food have been removed.
var newHead = { x: snekx, y: sneky };
snek.unshift(newHead);
for(let i = 0; i < snek.length; i++) {
drawSnek(snek[i].x, snek[i].y)
}
snek.splice(len,snek.length - len);
//drawScore(score);
}
setInterval(draw, 50); //I do not fully understand this function
}
CodePen with fixed code:
https://codepen.io/lukasmoeller/pen/PvVzqq?editors=1111
(boundaries are not checked - the snake will leave the canvas)
Explanation of setInterval(fn, interval)
setInterval calls the function specified using fn every interval ms until it is cleared. The interval can be cleared by using clearInterval, passing it the return value of setInterval. If you want to delay function execution at the beginning you could add setTimeout, add a callback that sets the interval:
setTimeout(function(){
setInterval(draw, 50);
}, 1000)
This would only start drawing the game every 50 ms after 1000ms have passed.
As I move the image of player.hero around the canvas I would like a variable that holds the current x and y pos of the hero. So I can make zombie image move towards current position of hero. Thanks and if my code so far is terrible please suggest amendments thanks.
(function() {
var canvas, context, width, height, speed = 8;
var interval_id;
var zombies = [];
var bullets = [];
var moveLeft = false;
var moveRight = false;
var moveUp = false;
var moveDown = false;
var player = {
x : 0,
y : 0,
width : 35,
height : 60,
hero : new Image(),
};
for (var i = 0; i < 10; i += 1){
var zombie = {
x : 10,
y : 10,
undead : new Image(),
targetToGox : 0,
targetToGoy : 0,
};
zombies.push(zombie);
}
var mouse = {
x : 0,
y : 0,
}
document.addEventListener('DOMContentLoaded', init, false);
function init() {
canvas = document.querySelector('canvas');
context = canvas.getContext('2d');
width = canvas.width;
height = canvas.height;
player.x = width / 2 - 18;
player.y = height / 2 - 30;
player.hero.src = 'hero.png';
zombie.undead.src = 'zombie.png';
//context.drawImage(player.hero, player.x, player.y);
window.addEventListener("keydown", activate,false);
window.addEventListener("keyup",deactivate,false);
//window.addEventListener("mouseover", drawImagePointingAt, false);
interval_player = window.setInterval(drawPlayer, 33);
}
function drawPlayer() {
context.clearRect(0 ,0 ,width, height);
context.drawImage(player.hero,player.x, player.y);
//******** need zombie to go to position of player.hero******///
context.drawImage(zombie.undead (somthing for x and y coordinats of player.hero);
// stops player moveing beyond the bounds of the canvas
if (player.x + player.width >= width) {
moveRight = false
}
if (player.y + player.height >= height) {
moveDown = false
}
if (player.x <= 0) {
moveLeft = false
}
if (player.y <= 0) {
moveUp = false
}
if (moveRight) {
player.x += speed;
}
if (moveUp) {
player.y -= speed;
}
if (moveDown) {
player.y += speed;
}
if (moveLeft){
player.x -= speed;
}
function activate(event) {
var keyCode = event.keyCode;
if (keyCode === 87){
moveUp = true;
}
else if (keyCode === 68){
moveRight = true;
}
else if (keyCode === 83){
moveDown = true;
}
else if (keyCode === 65){
moveLeft = true;
}
}
function deactivate(event) {
var keyCode = event.keyCode;
if (keyCode === 87){
moveUp = false;}
else if (keyCode === 68){
moveRight = false;}
else if (keyCode === 83){
moveDown = false;}
else if (keyCode === 65){
moveLeft = false;}
}
function getRandomNumber(min, max) {
return Math.round(Math.random() * (max - min)) + min;
}
function stop() {
clearInterval(interval_player);
}
})();
This is a pretty long wall of text about why I think it would be better that you restructured your code instead of "... getting the X and Y coordinates of an image that I move around the screen".
The end of this post contains a script that tries to show how you might go about doing that.
You asked about your code's quality. Your code is not terrible for a new programmer, but you are falling into some classic traps will be painful as your codebase gets larger.
An example of this might be keeping variables for each of the possible directions your player should move after a keypress (which is manipulated & used in separate functions). The problem with this is that when you decide to change any aspect of this system, it's going to crumble.
Instead, consider having an object representing the player which contains it's own internal logic for 'moving' by changing it's own coordinates.
I cannot emphasize this idea enough - Excellent hackers always give themselves a room to work. You shouldn't ever (for example), make the Player directly manipulate the game drawing routines. This is such a pervasive concept that there are actually words in software engineering for different facets of this organizational principle (words like 'Encapsulation' or 'Loose coupling' or 'Law of Demeter'). It's that important.
This leads me to another point: Global variables.
This is a tougher one because it's one where all programmers eventually make hypocrites of themselves if they are too critical of it (especially if they are doing low-level work). Still, it's best to consolidate whatever global variables you do have, and perhaps make functions that serve as their interface to the 'outside world'.
In your game, this would mean moving like moveLeft into a "game loop" that simply checks all of the 'objects' coordinates, clearing the screen & drawing those objects appropriately.
Another important idea here is that 'duplicate' functionality should share a single method. In your case, this would entail that both Player and Zombie become instances of some more abstract category which I'll name GameObject. This allows you to write all your major functions once for GameObject and 'inherit' their functionality inside of the Player and Zombie (there are many other, perhaps even better, ways to accomplish this without prototypes or inheritance at all)
In consideration of all of this, I tried to spend 20 minutes whipping up something that can hopefully give you something to work from. If this is totally unrelated to what you were going for, at the very least you can notch another round of possibly pointless internet pontification under your belt.
My code's "inheritance" is done in a very plain Javascript style, even in spite of the fact there are no less than a dozen 'new and improved' ways to share implementation details between code in JS, each with great variety in their depth of adherence to the principles of either prototypical or object oriented programming.
I cannot hope to cover Stamps, jSL, even Javascript's now infamous new 'class' keyword, so I would advise you read up about these and perhaps put them to profitable use yourself, but for now I'm sticking with the basics.
const ZOMBIE_COUNT = 10
function GameState() {
this.player = null;
this.enemies = []
}
var Game = new GameState() // our global game state
// An abstract 'game object' or character
function GameObject({x, y, image}) {
this.x = x
this.y = y
this.image = image
}
GameObject.prototype.draw = function() {
Game.ctx.fillStyle = this.color
Game.ctx.fillRect(this.x, this.y, 10, 10)
}
GameObject.prototype.moveLeft = function(n) { if(this.x > 0) this.x -= n }
GameObject.prototype.moveRight = function(n) { if(this.x < Game.width) this.x += n }
GameObject.prototype.moveDown = function(n) { if(this.y < Game.height) this.y += n}
GameObject.prototype.moveUp = function(n) { if(this.y > 0) this.y -= n }
function Player({x, y, width}) {
GameObject.call(this, {x, y}) // setup x, y & image
this.color = 'red'
}
Player.prototype = Object.create(GameObject.prototype, {})
function Zombie({x, y, target}) {
GameObject.call(this, {x, y}) // setup x, y & image
this.target = target // target contains the player
this.color = 'blue'
}
Zombie.prototype = Object.create(GameObject.prototype, {})
Zombie.prototype.moveToPlayer = function() {
let {x, y} = Game.player
// very basic 'movement' logic
if (this.x < x) {
this.moveRight(1/4)
} else if (this.x > x) {
this.moveLeft(1/4)
}
if (this.y > y) {
this.moveUp(1/4)
} else if (this.y < y) {
this.moveDown(1/4)
}
}
function init() {
var canvas = document.getElementById('canvas')
if (canvas.getContext) {
var ctx = canvas.getContext('2d')
} else {
console.log("No canvas")
return -1
}
let {width, height} = canvas
// Setup our game object
Game.width = width
Game.height = height
Game.ctx = ctx
// Create our player in the middle
Game.player = new Player({x: width / 2, y: height / 2})
// Create our enemies
for(let i = 0; i < ZOMBIE_COUNT; i++) {
Game.enemies.push(new Zombie({x: Math.random() * width | 0, // truncate our value
y: Math.random() * height | 0}))
}
game_loop()
}
function game_loop() {
window.requestAnimationFrame(game_loop)
Game.ctx.fillStyle = 'white'
Game.ctx.fillRect(0, 0, Game.width, Game.height);
Game.player.draw()
Game.enemies.map(enemy => {
enemy.moveToPlayer()
enemy.draw()
})
}
function process_key(ev) {
let speed = 3
let key = ev.keyCode
if (key === 68)
Game.player.moveRight(speed)
if (key === 87)
Game.player.moveUp(speed)
if (key === 65)
Game.player.moveLeft(speed)
if (key === 83)
Game.player.moveDown(speed)
}
window.addEventListener('keydown', process_key, false);
init()
canvas { border: 3px solid #333; }
<canvas id="canvas" width="400" height="400"></canvas>
I assume you mean this line?
//******** need zombie to go to position of player.hero******///
context.drawImage(zombie.undead (somthing for x and y coordinats of player.hero);
I would change the code to something like:
function init() {
...
interval_player = window.setInterval(updateGame, 33);
}
function updateGame() {
context.clearRect(0 ,0 ,width, height);
updatePlayer();
for (let zombie of zombies) {
updateZombie(zombie);
}
function updatePlayer() {
// stops player moveing beyond the bounds of the canvas
if (player.x + player.width >= width) {
moveRight = false
}
if (player.y + player.height >= height) {
moveDown = false
}
if (player.x <= 0) {
moveLeft = false
}
if (player.y <= 0) {
moveUp = false
}
if (moveRight) {
player.x += speed;
}
if (moveUp) {
player.y -= speed;
}
if (moveDown) {
player.y += speed;
}
if (moveLeft){
player.x -= speed;
}
context.drawImage(player.x, player.y);
}
function updateZombie(zombie) {
// Move zombie closer to player
if (zombie.x > player.x)
zombie.x -= zombieSpeed;
else if (zombie.x < player.x)
zombie.x += zombie.Speed;
if (zombie.y > player.y)
zombie.y -= zombieSpeed;
else if (zombie.y < player.y)
zombie.y += zombie.Speed;
context.drawImage(zombie.undead, zombie.x, zombie.y);
}
This line here:
zombie.undead.src = 'zombie.png';
will only change the last zombie created. You really need to move that:
for (var i = 0; i < 10; i += 1) {
var zombie = {
x : 10,
y : 10,
undead : new Image(),
targetToGox : 0,
targetToGoy : 0,
};
zombie.undead.src = 'zombie.png';
zombies.push(zombie);
}
UPDATE
changed it to the following and noticed speed improvements. The issue now is that the player will just slide without animating the frames.
var animator, frames;
animator = window.setInterval(function(){
if(currentFrame == totalFrames){
clearInterval(animator);
currentFrame = 0;
update();
isMoving = 0;
return;
}
xPosition += x;
yPosition += y;
frames = window.requestAnimationFrame(animator);
currentFrame++;
update();
},frames);
Some of the issues I am currently facing are: the map edges code section is completely broken. I am just trying to make it so that the player cannot move beyond the canvas.width/canvas.height. Also, my player movement is very sluggish and unresponsive. I think it's because of the isMoving check I added. I want to be able to move much smoother. Right now the character takes so long to move that I feel as if I am lagging. Also, for some reason, sometimes it'll move more than one time. It is completely random when it happens. Any help would be appreciated
var playerSprite = new Image();
playerSprite.src = "male.png";
var playerWidth = 64;
var playerHeight = 64;
var currentFrame = 0;
var totalFrames = 8;
var moveDistance = 4; // move 4 pixels
var xPosition = 300;
var yPosition = 200;
var direction = 2; // south, options: 0 - 3
var isMoving = 0;
var canvas, context;
window.addEventListener("load", function(){
canvas = document.getElementById('map');
context = canvas.getContext('2d');
})
function draw(){
context.drawImage(playerSprite,currentFrame * playerWidth, direction* playerHeight ,playerWidth,playerHeight,xPosition,yPosition,playerWidth,playerHeight);
}
function update()
{
clearMap();
draw();
}
function move(x, y){
if(isMoving)return;
isMoving = 1;
if(x > 0) direction = 3;
else if(x < 0) direction = 1;
if(y > 0) direction = 2;
else if(y < 0) direction = 0;
//update direction no matter what, implemented
// in order for directions to update
// when changing directions in map edges
//update();
/* Broken
if(xPosition + playerWidth + x > canvas.width)return; //works
else if(xPosition - x < 0)return; // player gets stuck
if(yPosition + playerHeight + y > canvas.height)return; //works
else if(yPosition - y < 0)return; // player gets stuck
//xPosition += x;
//yPosition += y;
*/
//actual animation update
var animator;
animator = window.setInterval(function(){
if(currentFrame == totalFrames){
clearInterval(animator);
currentFrame = 0;
update();
isMoving = 0;
return;
}
xPosition += x;
yPosition += y;
currentFrame++;
update();
},1000/16);
}
function clearMap(){
context.clearRect(0, 0, canvas.width, canvas.height);
}
function keyPress(e)
{
if(currentFrame == totalFrames){
currentFrame = 0;
}
switch(e.keyCode){
case 38: move(0, -moveDistance); break;
case 40: move(0, +moveDistance); break;
case 39: move(+moveDistance, 0); break;
case 37: move(-moveDistance, 0); break;
}
}
window.addEventListener("load", update, false);
window.addEventListener("keydown",keyPress);
Main points I changed:
No use of setInterval anywhere. Instead we let the browser handle the FPS at a rate it can handle using requestAnimationFrame.
One central game loop (update()). Before, you were doing a bunch of calculations and starting new background loops every time you press a key. That's bad. If someone were to mash the arrow keys, the browser would have to process 100+ setIntervals in the background.
Instead of doing any calculation in the key events, we're just using a variable to keep track of which buttons are pressed. Then in the game loop, which happens each frame, we can move the player a few pixels if an arrow key is held.
Exercises for you:
The animation is insanely fast because the player-frame is advanced every game-frame. Slow it down!
If a faster computer runs at 60fps, the player will move 60 * 4 = 240 pixels every second. If a slower computer runs at 20fps, the player will only move 20 * 4 = 80 pixels every second. That's actually a huge difference. To make your game run consistently regardless of platform, you should move the player more or less depending on how fast the game is running. Here's a good article to get you started. Also the requestAnimationFrame documentation will be helpful.
Here's the code:
var playerSprite = new Image();
playerSprite.src = "male.png";
var playerWidth = 64;
var playerHeight = 64;
var currentFrame = 0;
var totalFrames = 8;
var direction = 2; // south, options: 0 - 3
var moveDistance = 4; // move 4 pixels
var xPosition = 300;
var yPosition = 200;
var left = 0,
right = 0,
up = 0,
down = 0;
var canvas, context;
window.addEventListener("keydown", keyPress);
window.addEventListener("keyup", keyRelease);
window.addEventListener("load", function(){
canvas = document.getElementById('map');
context = canvas.getContext('2d');
// tells the browser to call update() as soon as it's ready
// this prevents lockups, and also the browser regulates the FPS
window.requestAnimationFrame(update);
});
function update() {
// EVERYTHING game related happens in update (except listening for key events).
// This keeps everything organized, and prevents any lag/slowdown issues
// handles player movement and animation
movePlayer();
// handles all drawing
draw();
// lets the browser know we're ready to draw the next frame
window.requestAnimationFrame(update);
}
function movePlayer() {
if(left) {
xPosition -= moveDistance;
direction = 1;
}
if(right) {
xPosition += moveDistance;
direction = 3;
}
if(up) {
yPosition -= moveDistance;
direction = 0;
}
if(down) {
yPosition += moveDistance;
direction = 2;
}
// all this code happens every frame
// in english: if we're moving, advance to the next frame
if(left || right || up || down) {
currentFrame ++;
if(currentFrame == totalFrames) currentFrame = 0;
}
}
function draw() {
// clear the map
context.clearRect(0, 0, canvas.width, canvas.height);
// draw the next frame
context.drawImage(playerSprite, currentFrame * playerWidth, direction * playerHeight,
playerWidth, playerHeight,
xPosition, yPosition,
playerWidth, playerHeight);
}
// keyPress and keyRelease ensure that the variables are
// equal to 1 if pressed and 0 otherwise.
function keyPress(e)
{
switch(e.keyCode){
case 38: up = 1; break;
case 40: down = 1; break;
case 39: right = 1; break;
case 37: left = 1; break;
}
}
function keyRelease(e)
{
switch(e.keyCode){
case 38: up = 0; break;
case 40: down = 0; break;
case 39: right = 0; break;
case 37: left = 0; break;
}
}
EDIT: Forgot to mention that I opted to include delta on my player's movement instead of on my update() redraws to prevent the whole game from running at 20 fps.
Forgot to post my revised and fully functional code. Thanks #Entity for helping me out <3. As you can see, I've begun to take more liberties and experimenting as well.
var fps = 20, fpsInterval = 1000/fps, now, then = Date.now(), delta;
var moving = {38:0, 40:0, 39:0, 37:0} // north, south, east, west
function move(direction, toggle){moving[direction] = toggle}
function keyDown(e){move(e.keyCode,1)}
function keyUp(e){move(e.keyCode,0)}
function playerMovement(){
now = Date.now();
delta = now - then;
if(delta > fpsInterval){
then = now - (delta % fpsInterval);
// north = 38, south = 40, east = 39, west = 37
// stop movement stall from opposite directions
if(moving[38] && moving[40]){move(40,0)}
if(moving[39] && moving[37]){move(37,0)}
// flip order to change diagonal rendering mode
if(moving[38]) {direction = 0; yPosition -= moveDistance};
if(moving[40]) {direction = 2; yPosition += moveDistance};
if(moving[39]) {direction = 3; xPosition += moveDistance};
if(moving[37]) {direction = 1; xPosition -= moveDistance};
if(moving[38] || moving[40] || moving[39] || moving[37]) currentFrame++;
if(currentFrame == totalFrames) currentFrame = 0;
}
}
I am trying to create a timer alongside some canvas animations. The animation is using a function loop set at 60 fps to refresh the canvas and redraw the objects. The only way I can think of making the stopwatch is by using the same loop to take the milliseconds per frame and add it to the text object. I'm just wondering if there is a more efficient way of doing this?
var frame = 0;
canvas.setLoop(function() {
if(particle.x < 1080 && particle.x > 0){
frame++;
particle.x = 540 + (acc*frame*frame)/120;
gField.t.text = "g = 9.81ms⁻²\nMass = "+particle.mass+"kg\nF = ma\nFrame: " + frame + "\nDistance: " + (particle.x - 540).toFixed(1);
stopwatch();
}else{
canvas.timeline.stop();
}
})
var sec = 0;
var tsec = 0;
var hsec = 0;
function stopwatch(){
hsec+= (5/3);
if(hsec >= 10){
tsec++;
hsec = hsec -10;
}
if(tsec >= 10){
sec++;
tsec = tsec-10;
}
time.text = (sec)+":"+(tsec)+(hsec).toFixed(0);
}
var clicks = 0
control.button.bind("click tap", function() {
clicks++;
if(clicks == 1){
canvas.timeline.start();
}else{
clicks = 0;
canvas.timeline.stop();
}
})
P.s. this is for a dynamics simulation program. I am using the oCanvas library for the canvas animation.
Use requestAnimationFrame as this is the most accurate timer you'll get with JavaScript, and bonus is it will provide you with a high-resolution time-stamp:
var ctx = canvas.getContext('2d'),
startTime = null,
lastTime = null, // for scale
isRunning = false,
FPS = 1000/60,
x = 0,
dx = 4; // ideal frame rate
function loop(timeStamp) {
if (!startTime) startTime = timeStamp;
var timeDiff = lastTime ? timeStamp - lastTime : FPS,
timeElapsed = timeStamp - startTime,
timeScale = timeDiff / FPS; // adjust variations in frame rates
lastTime = timeStamp;
ctx.clearRect(0,0,canvas.width, canvas.height);
// do your stuff using timeScale, ie:
// pos.x += velocity.x * timeScale
x += dx * timeScale;
if (x < 0 || x > canvas.width-1) dx = -dx;
ctx.fillRect(x,0,8,8);
ctx.fillText((timeElapsed*0.001).toFixed(4), 10, 50);
ctx.fillText(timeScale.toFixed(1), 10, 90);
if (isRunning) requestAnimationFrame(loop);
}
ctx.font = "40px sans-serif";
btnToggle.addEventListener("click", function() {
if (isRunning) {
isRunning = false;
this.innerHTML = "Start";
} else {
startTime = lastTime = null;
isRunning = true;
requestAnimationFrame(loop)
this.innerHTML = "Stop";
}
}, false);
<canvas id=canvas width=360 height=100></canvas>
<br><button id="btnToggle">Start</button>
To reset start time initialize it with null (or 0). isRunning is used here just as an example on how you can stop the loop (by setting it to false).
Notice that timeScale is used to compensate for frame rate variations. If the loop is not running at 60 FPS then timeScale will compensate for this, ie. if FPS is 30 timeScale would be 2 and so, so that you can update the parameters correctly based on time.
I want to modify existing code which uses an image to animate, and use a sprite sheet instead. I'm using the EaselJS library for this.
The piece of code that creates the objects for animation:
function initStageObjects(){
car = new Car('img/car.png',canvas.width()/2,canvas.height()/2);
}
And this is the complete code:
$(window).load(function(){
$(document).ready(function(){
// Canvas Variables
var canvas = $('#canvas');
var context = canvas.get(0).getContext('2d');
var canvasWidth = canvas.width();
var canvasHeight = canvas.height();
// Keyboard Variables
var leftKey = 37;
var upKey = 38;
var rightKey = 39;
var downKey = 40;
// Keyboard event listeners
$(window).keydown(function(e){
var keyCode = e.keyCode;
if(keyCode == leftKey){
car.left = true;
} else if(keyCode == upKey){
car.forward = true;
} else if(keyCode == rightKey){
car.right = true;
} else if (keyCode == downKey){
car.backward = true;
}
});
$(window).keyup(function(e){
var keyCode = e.keyCode;
if(keyCode == leftKey){
car.left = false;
} else if(keyCode == upKey){
car.forward = false;
} else if(keyCode == rightKey){
car.right = false;
} else if (keyCode == downKey){
car.backward = false;
}
});
// Start & Stop button controlls
var playAnimation = true;
var startButton = $('#startAnimation');
var stopButton = $('#stopAnimation');
startButton.hide();
startButton.click(function(){
$(this).hide();
stopButton.show();
playAnimation = true;
updateStage();
});
stopButton.click(function(){
$(this).hide();
startButton.show();
playAnimation = false;
});
// Resize canvas to full screen
function resizeCanvas(){
canvas.attr('width', $(window).get(0).innerWidth);
canvas.attr('height', $(window).get(0).innerHeight);
canvasWidth = canvas.width();
canvasHeight = canvas.height();
}
resizeCanvas();
$(window).resize(resizeCanvas);
function initialise(){
initStageObjects();
drawStageObjects();
updateStage();
}
// Car object and properties
function Car(src, x, y){
this.image = new Image();
this.image.src = src;
this.x = x;
this.y = y;
this.vx = 0;
this.vy = 0;
this.angle = 90;
this.topSpeed = 5;
this.acceleration = 0.1;
this.reverse = 0.1;
this.brakes = 0.3;
this.friction = 0.05;
this.handeling = 15;
this.grip = 15;
this.minGrip = 5;
this.speed = 0;
this.drift = 0;
this.left = false;
this.up = false;
this.right = false;
this.down = false;
}
// Create any objects needed for animation
function initStageObjects(){
car = new Car('img/car.png',canvas.width()/2,canvas.height()/2);
}
function drawStageObjects(){
context.save();
context.translate(car.x,car.y);
context.rotate((car.angle + car.drift) * Math.PI/180);
context.drawImage(car.image, -25 , (-47 + (Math.abs(car.drift / 3))));
// context.fillRect(-5, -5, 10, 10);
context.restore();
}
function clearCanvas(){
context.clearRect(0, 0, canvasWidth, canvasHeight);
context.beginPath();
}
function updateStageObjects(){
// Faster the car is going, the worse it handels
if(car.handeling > car.minGrip){
car.handeling = car.grip - car.speed;
}
else{
car.handeling = car.minGrip + 1;
}
// Car acceleration to top speed
if(car.forward){
if(car.speed < car.topSpeed){
car.speed = car.speed + car.acceleration;
}
}
else if(car.backward){
if(car.speed < 1){
car.speed = car.speed - car.reverse;
}
else if(car.speed > 1){
car.speed = car.speed - car.brakes;
}
}
// Car drifting logic
if(car.forward && car.left){
if(car.drift > -35){
car.drift = car.drift - 3;
}
}
else if(car.forward && car.right){
if(car.drift < 35){
car.drift = car.drift + 3;
}
}
else if(car.forward && !car.left && car.drift > -40 && car.drift < -3){
car.drift = car.drift + 3;
}
else if(car.forward && !car.right && car.drift < 40 && car.drift > 3){
car.drift = car.drift - 3;
}
if(car.drift > 3){
if(!car.forward && !car.left){
car.drift = car.drift - 4;
}
}
else if(car.drift > -40 && car.drift < -3){
if(!car.forward && !car.right){
car.drift = car.drift + 4;
}
}
// General car handeling when turning
if(car.left){
car.angle = car.angle - (car.handeling * car.speed/car.topSpeed);
} else if(car.right){
car.angle = car.angle + (car.handeling * car.speed/car.topSpeed);
}
// Use this div to display any info I need to see visually
$('#stats').html("Score: 0");
// Constant application of friction / air resistance
if(car.speed > 0){
car.speed = car.speed - car.friction;
} else if(car.speed < 0) {
car.speed = car.speed + car.friction;
}
// Update car velocity (speed + direction)
car.vy = -Math.cos(car.angle * Math.PI / 180) * car.speed;
car.vx = Math.sin(car.angle * Math.PI / 180) * car.speed;
// Plot the new velocity into x and y cords
car.y = car.y + car.vy;
car.x = car.x + car.vx;
}
// Main animation loop
function updateStage(){
clearCanvas();
updateStageObjects();
drawStageObjects();
if(playAnimation){
setTimeout(updateStage, 25);
}
}
// Initialise the animation loop
initialise();
});
});//]]>
// JavaScript Document
How could i replace the image being used by the sprite i created?
Even though you maybe could implement a createjs.SpriteSheet into a non-Createjs/non-EaselJS project I would strongly recommend you not to do that, EaselJS-classes where not designed to be used as single modular classes in any kind of project, they best work together in the framework. In the end you probably will need more time and probably end up with more code by trying get everything to work than with just converting your (yet still small) project to EaselJS all together. So learn something new today and refacture your project to EaselJS ;-)
but if you want more work, continue reading:
A createjs.SpriteSheet basically only handles SpriteSheet data and you can use mySpriteSheet.getFrame(int); to get the image and the source-rect to that frame so you can draw it to your canvas. But a SpriteSheet alone won't handle animations for you, so you would have to advance the "playhead" every frame yourself(go back to frame 0 if at the end ect...), or you can implement another createjs-class called createjs.BitmapAnimation - however, drawing this to your canvas would again require additional code or yet an additional createjs-class: you see where this is getting.