Why does my canvas decrease in performance after a while? - javascript

I have the following code that makes the player jump when you press the up arrow key (sorry it's long):
class Vec {
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
}
class Rect {
constructor(w, h) {
this.pos = new Vec;
this.size = new Vec(w, h);
this.vel = new Vec;
this.last = new Vec;
}
}
class Player extends Rect {
constructor() {
super(40, 40);
}
}
// setup
const backgroundContext = document.getElementById("backgroundCanvas").getContext("2d");
const groundContext = document.getElementById("groundCanvas").getContext("2d");
const objectContext = document.getElementById("objectCanvas").getContext("2d");
const WIDTH = 600;
const HEIGHT = 400;
const GROUND_Y = 50;
const player = new Player;
player.pos.x = 100;
player.pos.y = 390;
player.last.x = player.pos.x;
player.last.y = player.pos.y;
player.vel.x = 0;
let isJumping = true;
const JUMP_STRENGTH = -300;
const GRAVITY = 10;
function update(dt) {
// update player
player.last.x = player.pos.x;
player.last.y = player.pos.y;
player.vel.y += GRAVITY;
player.pos.y += player.vel.y * dt;
player.pos.y = Math.round(player.pos.y);
document.addEventListener("keydown", (e) => {
if (e.keyCode === 38 && isJumping === false) {
isJumping = true;
player.vel.y = JUMP_STRENGTH;
}
}, false);
if (player.pos.y > HEIGHT - GROUND_Y - player.size.y) {
isJumping = false;
player.pos.y = HEIGHT - GROUND_Y - player.size.y;
player.vel.y = 0;
}
}
function draw() {
// draw background
backgroundContext.fillStyle = "#000";
backgroundContext.fillRect(0, 0, WIDTH, HEIGHT);
// draw ground
objectContext.clearRect(0, HEIGHT - GROUND_Y, WIDTH, GROUND_Y);
groundContext.fillStyle = "#00ff00";
groundContext.fillRect(0, HEIGHT - GROUND_Y, WIDTH, GROUND_Y);
// draw player
objectContext.clearRect(player.last.x, player.last.y, player.size.x, player.size.y);
objectContext.fillStyle = "#fff";
objectContext.fillRect(player.pos.x, player.pos.y, player.size.x, player.size.y);
}
// game loop
const TIMESTEP = 1 / 60;
let accumulator = 0;
let lastRender = 0;
function loop(timestamp) {
accumulator += (timestamp - lastRender) / 1000;
lastRender = timestamp;
while (accumulator >= TIMESTEP) {
update(TIMESTEP);
draw();
accumulator -= TIMESTEP;
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
canvas {
position: absolute;
left: 0;
top: 0;
}
<!DOCTYPE html>
<html>
<head>
<title>Jump_Over_It</title>
<link href="css/default.css" rel="stylesheet" />
</head>
<body>
<canvas style="z-index: 0;" id="backgroundCanvas" width="600" height="400"></canvas>
<canvas style="z-index: 1;" id="groundCanvas" width="600" height="400"></canvas>
<canvas style="z-index: 2;" id="objectCanvas" width="600" height="400"></canvas>
<script src="js/main.js"></script>
</body>
</html>
I have three questions:
1) If you wait for about a minute, you start to notice that the performance starts to decrease. Why is this happening?
2) Why does the performance significantly drop when you hold the up arrow key?
3) In the game loop, I did:
while (accumulator >= TIMESTEP) {
update(TIMESTEP);
draw();
accumulator -= TIMESTEP;
}
Is it OK to put the draw() function in the same while loop as the update() function?
If you know what to do, please let me know.

For questions 1 and 2:
This is Because you are adding a new EventListener in a while loop, itself in a requestAnimationFrame loop.
I won't even calculate the number of event handlers that are attached, nor run this snippet, but do not touch your keyboard because there could be Zillions handlers executing serially there.
To fix this properly, move your addEventListener call out of these loops, you only need to call it once.
For question 3:
It is not clear why you even need this while loop at all. Would be better to actually calculate the new position directly rather than updating in a loop like that.
But at least, no. You should not call draw inside this while loop since every call will negate previous ones. So call it only once at the end of your rAF handler.
Also, note that instead of clearing on the part where your object is, you'd be better clearing the whole canvas everytime, and you might even consider moving all your drawings on a single canvas, since the compositing you thought you'd win by having three canvases actually still happens when painting the 3 DOM Elements on screen.
class Vec {
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
}
class Rect {
constructor(w, h) {
this.pos = new Vec;
this.size = new Vec(w, h);
this.vel = new Vec;
this.last = new Vec;
}
}
class Player extends Rect {
constructor() {
super(40, 40);
}
}
// setup
const backgroundContext = document.getElementById("backgroundCanvas").getContext("2d");
const groundContext = document.getElementById("groundCanvas").getContext("2d");
const objectContext = document.getElementById("objectCanvas").getContext("2d");
const WIDTH = 600;
const HEIGHT = 400;
const GROUND_Y = 50;
const player = new Player;
player.pos.x = 100;
player.pos.y = 390;
player.last.x = player.pos.x;
player.last.y = player.pos.y;
player.vel.x = 0;
let isJumping = true;
const JUMP_STRENGTH = -300;
const GRAVITY = 10;
function update(dt) {
// update player
player.last.x = player.pos.x;
player.last.y = player.pos.y;
player.vel.y += GRAVITY;
player.pos.y += player.vel.y * dt;
player.pos.y = Math.round(player.pos.y);
if (player.pos.y > HEIGHT - GROUND_Y - player.size.y) {
isJumping = false;
player.pos.y = HEIGHT - GROUND_Y - player.size.y;
player.vel.y = 0;
}
}
document.addEventListener("keydown", (e) => {
if (e.keyCode === 38 && isJumping === false) {
e.preventDefault();
isJumping = true;
player.vel.y = JUMP_STRENGTH;
}
}, false);
function draw() {
// draw background
backgroundContext.fillStyle = "#000";
backgroundContext.fillRect(0, 0, WIDTH, HEIGHT);
// draw ground
groundContext.clearRect(0, 0, WIDTH, HEIGHT);
groundContext.fillStyle = "#00ff00";
groundContext.fillRect(0, HEIGHT - GROUND_Y, WIDTH, GROUND_Y);
// draw player
objectContext.clearRect(0, 0, WIDTH, HEIGHT); objectContext.fillStyle = "#fff";
objectContext.fillRect(player.pos.x, player.pos.y, player.size.x, player.size.y);
}
// game loop
const TIMESTEP = 1 / 60;
let accumulator = 0;
let lastRender = 0;
function loop(timestamp) {
accumulator += (timestamp - lastRender) / 1000;
lastRender = timestamp;
while (accumulator >= TIMESTEP) {
accumulator -= TIMESTEP;
update(TIMESTEP);
}
draw();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
canvas {
position: absolute;
left: 0;
top: 0;
}
<!DOCTYPE html>
<html>
<head>
<title>Jump_Over_It</title>
<link href="css/default.css" rel="stylesheet" />
</head>
<body>
<canvas style="z-index: 0;" id="backgroundCanvas" width="600" height="400"></canvas>
<canvas style="z-index: 1;" id="groundCanvas" width="600" height="400"></canvas>
<canvas style="z-index: 2;" id="objectCanvas" width="600" height="400"></canvas>
<script src="js/main.js"></script>
</body>
</html>

Related

Motion effect for standing object

I have trying to achieve motion effect for the standing object.
I assume that my object will be able to go to the right or the left for example.
And I wanna make illusion like that object is still moving. Even if currently is not moving. (Meanwhile background can moving still...)
'use strict';
const pressedKeys = [];
const canvas = document.querySelector('#game');
const ctx = canvas.getContext('2d');
canvas.width = 300;
canvas.height = 150;
class Player
{
xPosition = 150;
yPosition = 50;
speed = 5;
isMoving = false;
update(pressedKeys)
{
if (pressedKeys['ArrowLeft']) {
this.xPosition -= this.speed;
} else if (pressedKeys['ArrowRight']) {
this.xPosition += this.speed;
}
}
draw(ctx)
{
ctx.fillStyle = '#2976f2';
ctx.fillRect(this.xPosition, this.yPosition, 30, 30);
}
}
const player = new Player();
function animate()
{
window.requestAnimationFrame(animate);
ctx.clearRect(0, 0, canvas.width, canvas.height)
if (player.isMoving) {
player.update(pressedKeys);
}
player.draw(ctx);
}
animate();
window.addEventListener('keyup', function (event) {
delete pressedKeys[event.key];
player.isMoving = false;
})
window.addEventListener('keydown', function (event) {
switch (event.key) {
case 'ArrowLeft':
case 'ArrowRight':
pressedKeys[event.key] = true;
player.isMoving = true;
break;
}
})
canvas {
border: 1px solid blue;
}
<canvas id="game"></canvas>
Usually this kind of effect is done by constantly duplicating the desired object, move it to the exact same screen position and ultimately fade it out over time e.g. within one second.
In your case though we can simplify things a bit since you want to keep that "motion blurred" look even if it ain't moving.
So first we need to another property to your Player class oldX. It holds the position of the object before a movement occured. By subtracting oldX from x we can determine if the object is moving to the left or to the right - so we know where to put the trailing duplicates.
If we know the direction, we can start creating duplicates using a simple for loop like:
for (var a = 0; a < 7; a++) {
ctx.fillRect(this.x - (this.x - this.oldX) / this.speed * a * 2, this.y, 30, 30);
}
this will create seven equal looking squares - so it won't look good yet. The duplicate next to the original should have almost the same color while the last one should almost blend with the background. To do this we can use the globalAlpha property of the canvases context. A value of 1 is opaque while 0 is completely transparent.
Putting it all together:
const keys = [];
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
canvas.width = 300;
canvas.height = 150;
class Player {
x = 150;
y = 50;
oldX = 150;
speed = 5;
moving = false;
update(keys) {
this.oldX = this.x;
if (keys['ArrowLeft']) {
this.x -= this.speed;
} else if (keys['ArrowRight']) {
this.x += this.speed;
}
}
draw(ctx) {
ctx.fillStyle = '#2976f2';
ctx.fillRect(this.x, this.y, 30, 30);
ctx.save();
for (var a = 0; a < 7; a++) {
ctx.globalAlpha = 0.5 - (a / 7) * 0.5;
ctx.fillRect(this.x - (this.x - this.oldX) / this.speed * a * 2, this.y, 30, 30);
}
ctx.restore();
}
}
const player = new Player();
function animate() {
window.requestAnimationFrame(animate);
ctx.clearRect(0, 0, canvas.width, canvas.height)
if (player.moving) {
player.update(keys);
}
player.draw(ctx);
}
animate();
window.addEventListener('keyup', function(event) {
delete keys[event.key]
player.moving = false;
})
window.addEventListener('keydown', function(event) {
switch (event.key) {
case 'ArrowLeft':
case 'ArrowRight':
keys[event.key] = true;
player.moving = true;
break;
}
})
canvas {
border: 1px solid blue;
}
<canvas id="game"></canvas>

rendering huge number of elements in html5 canvas

Assume you have a 500x500 2D canvas and you want to animate 100000 of elements in it, for example you want to create noise effects.
consider code bellow :
const canvas = document.getElementById("plane");
let animatelist = [];
animate = function() {
animatelist.forEach((e) => {
e.render();
});
setTimeout(animate, 1000 / 30);
}
animate();
let point = function(plane, x, y, size) {
animatelist.push(this);
this.plane = plane;
this.x = x;
this.y = y;
this.size = size;
this.render = () => {
const context = this.plane.getContext("2d");
this.x = Math.random() * 500;
this.y = Math.random() * 500;
context.fillStyle = "#000";
context.fillRect(this.x, this.y, this.size, this.size);
}
}
for (let i = 0;i < 100000;i++) {
new point(canvas, Math.random() * 500, Math.random() * 500, 0.3);
}
it barely gives you 2 or 3 fps and it is just unacceptable, i was wondering if there is a trick a about these kinda of animations or something to render massive amounts of elements smoothly!
You can play in memory and after that draw on an invisuble canvas. And when you are ready, copy all of bytes into visible canvas.
And i see, you use a lot of random. This is slow instruction. Try to make a random table and implement your own random function
Here is an 12-15 fps version but I think you can reach better performance by pixel manipulating. So this code based on your solution, but I cannot increase fps because too many function calls, object manipulating and similar baklava. (a code below reach over 100 fps)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>sarkiroka</title>
</head>
<body>
<canvas id="plane" width="500" height="500"></canvas>
<script>
// variable and function for speedup
const randomTable = [];
const randomTableLength = 1000007;
const fpsMinimum = 1000 / 30;
for (let i = 0; i < randomTableLength; i++) {
randomTable.push(Math.random() * 500);
}
let randomSeed = 0;
function getNextRandom() {
if (++randomSeed >= randomTableLength) {
randomSeed = 0;
}
return randomTable[randomSeed];
}
// html, dom speedup
const canvas = document.getElementById("plane");
const context = canvas.getContext("2d");
const drawCanvas = document.createElement('canvas');
drawCanvas.setAttribute('width', canvas.getAttribute('width'));
drawCanvas.setAttribute('height', canvas.getAttribute('height'));
const drawContext = drawCanvas.getContext('2d');
drawContext.fillStyle = "#000";
let animatelist = [];
let point = function (x, y, size) {
animatelist.push(this);
this.x = x;
this.y = y;
this.size = size;
this.render = () => {
this.x = getNextRandom();
this.y = getNextRandom();
drawContext.fillRect(this.x, this.y, this.size, this.size);
}
}
for (let i = 0; i < 100000; i++) {
new point(getNextRandom(), getNextRandom(), 0.3);
}
//the animation
let lastBreath = Date.now();
const animateListLength = animatelist.length;
let framesDrawed = 0;
let copied = false;
const maximumCallstackSize = 100;
function continouslyAnimation(deep) {
if (copied) {
drawContext.clearRect(0, 0, 500, 500);
for (let i = 0; i < animateListLength; i++) {
animatelist[i].render();
}
copied = false;
}
framesDrawed++;
let now = Date.now();
if (lastBreath + 15 > now && deep < maximumCallstackSize) {
continouslyAnimation(deep + 1);
} else { // to no hangs browser
lastBreath = now;
setTimeout(continouslyAnimation, 1, 1);
}
}
setInterval(() => {
console.log(framesDrawed);
framesDrawed = 0;
}, 1000);
continouslyAnimation(0);
function copyDrawToVisible() {
context.putImageData(drawContext.getImageData(0, 0, 499, 499), 0, 0);
copied = true;
}
setInterval(copyDrawToVisible, fpsMinimum);
</script>
</body>
</html>
And here is a pixel manipulation solution, with much better performance (over 100 fps, 220-245 fps in my computer):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>sarkiroka</title>
</head>
<body>
<canvas id="plane" width="500" height="500"></canvas>
<script>
// variable and function for speedup
const randomTable = [];
const randomTableLength = 1000007;
for (let i = 0; i < randomTableLength; i++) {
randomTable.push(Math.random());
}
let randomSeed = 0;
function getNextRandom() {
if (++randomSeed >= randomTableLength) {
randomSeed = Math.round(Math.random() * 1000);
}
return randomTable[randomSeed];
}
// html, dom speedup
const canvas = document.getElementById("plane");
const context = canvas.getContext("2d");
let framesDrawed = 0;
function drawNoise() {
context.clearRect(0, 0, 500, 500);
let imageData = context.createImageData(499, 499);
let data = imageData.data;
for (let i = 0, length = data.length; i < length; i += 4) {
if (0.1 > getNextRandom()) {
data[i] = 0;
data[i + 1] = 0;
data[i + 2] = 0;
data[i + 3] = 255;
}
}
context.putImageData(imageData, 0, 0);
framesDrawed++;
}
setInterval(drawNoise, 0);
setInterval(() => {
console.log('fps', framesDrawed);
framesDrawed = 0;
}, 1000)
</script>
</body>
</html>
Explanataion: for noise, you don't need a function / object for every colored pixel. Trust to statistics and the random. In my example 10% of pixels are colored but we don't know how many pixel before render. But this is not important. From afar it is just like that perfect. And the most important thing: it can reach more fps.
General advice:
Which code is repeated many times, organize out of it whatever you can
Draw only on the canvas when you are done drawing in memory
Measure what is slow and optimize it, and only it

I Want To add a image instead of triangle in my code in p5.js

i tried doing 'loadImage()' using preload() but it stuck in loading only
it did not help me because
i just copied code from a source
now it is working but if i add image using loadImage() it doesn't work
i am new to it please help me
html code
<!DOCTYPE html>
<html>
<head >
<meta charset="UTF-8">
<title> Kill Corona Virus </title>
<script src="libraries/p5.js" type="text/javascript"></script>
<script src="libraries/p5.dom.js" type="text/javascript"></script>
<script src="libraries/p5.sound.js" type="text/javascript"></script>
<script src="game.js" type="text/javascript"></script>
<script src="scoreboard.js" type="text/javascript"></script>
<script src="ship.js" type="text/javascript"></script>
<script src="bubble.js" type="text/javascript"></script>
<script src="sketch.js" type="text/javascript"></script>
<style>
body {
padding: 0;
margin: 0;
}
canvas {
vertical-align: top;
}
</style>
</head>
<body style="background-color:grey;">
</body>
</html>
bubble.js
function Bubble(x, y, size, speed, col, crazyness) {
// position
this.x = x;
this.y = y;
this.size = size;
this.speed = speed;
this.col = col;
// should we delete?
this.alive = true;
// crazy fx
this.crazyness = crazyness;
// schedule ball for destroyal
this.destroy = function() {
this.alive = false;
}
this.setSpeed = function(newSpeed) {
this.speed = newSpeed;
}
this.display = function() {
strokeWeight(1);
stroke(0);
fill(this.col);
ellipse(this.x, this.y, this.size, this.size);
};
// move the ball down (towards the ground)
this.move = function() {
this.y += this.speed;
this.size += random(-this.crazyness, this.crazyness);
};
// detects intersection with another Bubble()
this.intersects = function(other) {
d = dist(this.x, this.y, other.x, other.y);
r1 = this.size / 2;
r2 = other.size / 2;
if (d < r1 + r2)
return true
else
return false
}
}
game.js
function Game(ship, scoreboard) {
this.debug = false;
this.ship = ship;
this.scoreBoard = scoreboard;
var canvas = document.querySelector("canvas")
this.reset = function() {
this.gameActive = false;
this.scoreBoard.reset();
this.meteors = [];
this.projectiles = [];
this.meteorsDensity = 0.985;
this.meteorsDensityInc = 0.0001;
this.meteorsMinSpeed = 0.25;
this.meteorsMinSpeedInc = 0.0001;
this.meteorsMaxSpeed = 2;
this.meteorsMaxSpeedInc = 0.0001;
this.meteorsMinSize = 25;
this.meteorsMaxSize = 125;
}
this.isActive = function() {
return this.gameActive;
}
this.start = function() {
this.gameActive = true;
}
this.showWelcomeScreen = function() {
background(255);
textFont("Courier New");
fill(0);
noStroke();
textAlign(CENTER);
welcome_msg = "Kill Corona-virus";
textSize(random(65, 68));
text(welcome_msg, width / 2, height / 2);
action_msg = "Click to start, click to play.";
textSize(25);
text(action_msg, width / 2, height / 4);
score_msg = "Your previous score was " + scoreBoard.score + ".";
textSize(25);
text(score_msg, width / 2, height / 4 * 3);
credits_msg = "(c) 2020 - Haseef Azhaan";
textSize(15);
text(credits_msg, width / 2, height / 4 * 3.75);
}
this.createNewMeteor = function() {
if (random() > this.meteorsDensity) {
// pick random color, speed, size and horizontal position
col = color(random(255), random(255), random(255), 50);
speed = random(this.meteorsMinSpeed, this.meteorsMaxSpeed);
size = random(this.meteorsMinSize, this.meteorsMaxSize);
x = random(0 + size / 2, width - size / 2);
// vertical position is fixed
y = -size / 2;
// crzyness is just a visual FX
crazyness = random(0.5, 1.5);
//create a new "meteor" (a Bubble)
this.meteors.push(new Bubble(x, y, size, speed, col, crazyness));
}
};
this.updateAndDisplayMeteors = function() {
for (var i = this.meteors.length - 1; i >= 0; i--) {
this.meteors[i].move();
this.meteors[i].display();
}
};
this.updateAndDisplayProjectiles = function() {
for (var i = this.projectiles.length - 1; i >= 0; i--) {
this.projectiles[i].move();
this.projectiles[i].display();
}
};
this.updateAndDisplayShip = function() {
this.ship.updatePosition();
this.ship.display();
};
this.displayScoreboard = function() {
this.scoreBoard.display();
};
canvas.addEventListener("keyup",(e)=>{
if(e.key==='Enter'){alert("p")}
})
this.shoot = function() {
this.projectiles.push(this.ship.shoot());
}
this.stopIfMeteorHitGround = function() {
// iterate through all the meteors
for (var i = this.meteors.length - 1; i >= 0; i--) {
// when a meteor hits the ground, it's game over
if (this.meteors[i].y > height) {
this.gameActive = false;
}
}
};
this.removeLostProjectiles = function() {
// iterate through all the projectiles
for (var i = this.projectiles.length - 1; i >= 0; i--) {
// if a projectile passes the screen top, it's lost (can delete it)
if (this.projectiles[i].y < 0)
this.projectiles.splice(i, 1);
}
};
this.detectSuccessfullShots = function() {
// iterate through all the meteors
for (var i = this.meteors.length - 1; i >= 0; i--) {
// for each meteor, now consider all projectiles
for (var j = this.projectiles.length - 1; j >= 0; j--) {
// is there a hit?
if (this.meteors[i].intersects(this.projectiles[j])) {
// destroy both projectile and meteor
this.meteors[i].destroy();
this.projectiles[j].destroy();
// increment score!
this.scoreBoard.incrementScore();
// increment game difficulty! :)
this.meteorsMinSpeed += this.meteorsMinSpeedInc;
this.meteorsMaxSpeed += this.meteorsMaxSpeedInc;
this.meteorsDensity -= this.meteorsDensityInc;
}
}
}
};
this.removeKilledMeteors = function() {
// remove meteors scheduled for removal
for (var i = this.meteors.length - 1; i >= 0; i--) {
if (!this.meteors[i].alive)
this.meteors.splice(i, 1);
}
};
this.removeUsedProjectiles = function() {
for (var i = this.projectiles.length - 1; i >= 0; i--) {
if (!this.projectiles[i].alive)
this.projectiles.splice(i, 1);
}
};
this.setDebug = function(v) {
this.debug = v;
}
this.showDebugInfo = function() {
if (this.debug == true) {
print("# meteors: " + this.meteors.length);
print("# projectiles: " + this.projectiles.length);
}
}
}
scoreboard.js
function ScoreBoard(x,y) {
// position
this.x = x;
this.y = y;
// initial score
this.score = 0;
this.display = function() {
noStroke();
fill(0);
textAlign(RIGHT);
textFont("Courier new");
textSize(22);
text("score: " + this.score,this.x,this.y);
};
this.incrementScore = function() {
this.score++;
};
this.reset = function() {
this.score = 0;
}
}
ship.js
,
in the place of this triangle i want a image
function Ship(x,y) {
// ship position
this.x = x;
this.y = y;
// width and height
this.width = 25;
this.height = 50;
this.display = function() {
fill(color(255,0,0,50));
stroke(0);
strokeWeight(1);
triangle(this.x - this.width, this.y,this.x, this.y - this.height,this.x + this.width, this.y);
};
// update position based on mouseX
this.updatePosition = function() {
this.x = mouseX;
this.y = height - 10;
};
// shoot a projectile
this.shoot = function(){
projectile = new Bubble(this.x, this.y - 50, 10,-10,0,0);
return projectile;
}
}
sketch.js
var game;
var ship_im;
function setup() {
var a = createCanvas(windowWidth, windowHeight);
ship = new Ship(width / 2, height / 2);
var b = createCanvas(windowWidth, windowHeight);
ship_im = loadImage("ship.png")
scoreBoard = new ScoreBoard(width - 10, 20);
game = new Game(ship, scoreBoard);
game.reset();
game.setDebug(true);
}
function draw() {
if (game.isActive()) {
background(255);
// create new meteors
game.createNewMeteor();
// update position of and display stuff (meteors, projectiles, ship)
game.updateAndDisplayMeteors();
game.updateAndDisplayProjectiles();
game.updateAndDisplayShip();
// display the scoreboard
game.displayScoreboard();
// remove projectiles that passed the top of screen
game.removeLostProjectiles();
// detect successfull shots (projectile hits meteor)
// after a successfull shoot, projectile and meteor will be marked as "dead"
game.detectSuccessfullShots();
// remove "dead" meteors and projectiles
game.removeKilledMeteors();
game.removeUsedProjectiles();
// if a meteor hits the ground, it's game over.
game.stopIfMeteorHitGround();
// show debug info when enables
//game.showDebugInfo();
} else {
game.showWelcomeScreen();
}
}
function mouseClicked() {
// when the game is active, clicking the mouse shots
if (game.gameActive)
game.shoot();
// when the game is inactive, clicking the mouse restarts the game
else {
game.reset();
game.start();
}
}
function keyPressed(){
if(keyCode===ENTER){
if (game.gameActive)
game.shoot();
// when the game is inactive, clicking the mouse restarts the game
else {
game.reset();
game.start();
}
}
}
please ignore bad indentation

AI follow and avoid collision with obstacle

I'm making an AI navigation system based on polar coordinates. The purpose is to move an actor to a position, while also moving away from a possible obstacle on its path.
The code works perfectly most of the time but after testing, I discovered this: when the player, obstacle and actor are all perfectly aligned in either the X or Y direction or diagonally, the actor gets stuck in the obstacle. It's mostly noticeable when the player is "hugging" a wall because the actor's movement vector is clipped by the walls, making them aligned.
Click the buttons in the snippet to see what I'm on about.
Is there a way to prevent this?
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let playerX = 100;
let playerY = 200;
let obstacleX = 200;
let obstacleY = 200;
let actorX = 300;
let actorY = 201;
function loop() {
// Wall clipping
if (actorX > 490) {
actorX = 490;
} else if (actorX < 10) {
actorX = 10;
}
if (actorY > 490) {
actorY = 490;
} else if (actorY < 10) {
actorY = 10;
}
// Vector between player and actor
let vectorPlayerX = playerX - actorX;
let vectorPlayerY = playerY - actorY;
// Vector between obstacle and actor
let vectorObstacleX = obstacleX - actorX;
let vectorObstacleY = obstacleY - actorY;
// Where to move, defaults to player's position
const anglePlayer = Math.atan2(vectorPlayerY, vectorPlayerX);
let moveAngle = anglePlayer;
// If near obstacle, adjust course and try to avoid it
if (Math.sqrt(vectorObstacleX * vectorObstacleX + vectorObstacleY * vectorObstacleY) < 50) {
const angleObstacle = Math.atan2(vectorObstacleY, vectorObstacleX);
moveAngle += anglePlayer - angleObstacle;
}
// Move the vector to desired location
actorX += Math.cos(moveAngle);
actorY += Math.sin(moveAngle);
//Drawing
ctx.clearRect(0, 0, 500, 500);
ctx.beginPath();
ctx.fillStyle = "gray";
ctx.arc(actorX, actorY, 10, 0, Math.PI * 2, true);
ctx.fill();
ctx.beginPath();
ctx.fillStyle = "orange";
ctx.arc(obstacleX, obstacleY, 10, 0, Math.PI * 2, true);
ctx.fill();
ctx.beginPath();
ctx.fillStyle = "blue";
ctx.arc(playerX, playerY, 10, 0, Math.PI * 2, true);
ctx.fill();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
function nonAligned() {
playerX = 100;
playerY = 200;
obstacleX = 200;
obstacleY = 200;
actorX = 300;
actorY = 201;
}
function alignedY() {
playerX = 100;
playerY = 490;
obstacleX = 200;
obstacleY = 490;
actorX = 300;
actorY = 490;
}
function alignedBoth() {
playerX = 200;
playerY = 200;
obstacleX = 300;
obstacleY = 300;
actorX = 400;
actorY = 400;
}
#options {
position: fixed;
top: 5px;
left: 5px;
}
<!DOCTYPE html>
<html>
<body>
<canvas id="canvas" width="500" height="500"></canvas>
<div id="options">
<button onclick="nonAligned()">Spawn non-aligned</button>
<button onclick="alignedY()">Spawn Y aligned</button>
<button onclick="alignedBoth()">Spawn diagonally aligned</button>
</div>
</body>
</html>
If the angle to the player and the obstacle are the same then we continue the course, as the variables cancel each other out.
moveAngle += anglePlayer - angleObstacle;
If anglePlayer is 117 and angleObstacle is 117 and your moveAngle is 117 you get
117 + 117 -117 = 117 ...
You might want something like this (pseudo code)
moveAngle += anglePlayer + random(90)-45;
Or if hitting an obstacle move left or right
moveAngle += anglePlayer - 90;
if (random(2)==1 moveAngle += 180
The issue is indeed that moveAngle is unchanged when it is pointed directly at the obstacle. A small modification checks whether the moveAngle is clockwise or counter-clockwise from the obstacle, and veers further away (note: my code breaks the wall hugging and behaves badly in the "aligned in Y" case for that reason, which is fixable but I don't care to):
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let playerX = 100;
let playerY = 200;
let obstacleX = 200;
let obstacleY = 200;
let actorX = 300;
let actorY = 201;
function loop() {
// Wall clipping
if (actorX > 490) {
actorX = 490;
} else if (actorX < 10) {
actorX = 10;
}
if (actorY > 490) {
actorY = 490;
} else if (actorY < 10) {
actorY = 10;
}
// Vector between player and actor
let vectorPlayerX = playerX - actorX;
let vectorPlayerY = playerY - actorY;
// Vector between obstacle and actor
let vectorObstacleX = obstacleX - actorX;
let vectorObstacleY = obstacleY - actorY;
// Where to move, defaults to player's position
const anglePlayer = Math.atan2(vectorPlayerY, vectorPlayerX);
let moveAngle = anglePlayer;
// If near obstacle, adjust course and try to avoid it
obs_distance = Math.sqrt(vectorObstacleX * vectorObstacleX + vectorObstacleY * vectorObstacleY);
if (obs_distance < 100) {
const angleObstacle = Math.atan2(vectorObstacleY, vectorObstacleX);
delta = Math.PI/2.0;
if (obs_distance > 100.0/32.0) { delta = (100.0/32.0)*Math.PI/obs_distance; }
cross = Math.sin(moveAngle-angleObstacle);
if (cross>0) { moveAngle += delta; }
if (cross<0) { moveAngle -= delta; }
if (cross==0) {
if (Math.random(2)==1) {
moveAngle += delta;
} else {
moveAngle -= delta;
}
}
}
// Move the vector to desired location
actorX += Math.cos(moveAngle);
actorY += Math.sin(moveAngle);
//Drawing
ctx.clearRect(0, 0, 500, 500);
ctx.beginPath();
ctx.fillStyle = "gray";
ctx.arc(actorX, actorY, 10, 0, Math.PI * 2, true);
ctx.fill();
ctx.beginPath();
ctx.fillStyle = "orange";
ctx.arc(obstacleX, obstacleY, 10, 0, Math.PI * 2, true);
ctx.fill();
ctx.beginPath();
ctx.fillStyle = "blue";
ctx.arc(playerX, playerY, 10, 0, Math.PI * 2, true);
ctx.fill();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
function nonAligned() {
playerX = 100;
playerY = 200;
obstacleX = 200;
obstacleY = 200;
actorX = 300;
actorY = 201;
}
function alignedY() {
playerX = 100;
playerY = 490;
obstacleX = 200;
obstacleY = 490;
actorX = 300;
actorY = 490;
}
function alignedBoth() {
playerX = 200;
playerY = 200;
obstacleX = 300;
obstacleY = 300;
actorX = 400;
actorY = 400;
}
#options {
position: fixed;
top: 5px;
left: 5px;
}
<!DOCTYPE html>
<html>
<body>
<canvas id="canvas" width="500" height="500"></canvas>
<div id="options">
<button onclick="nonAligned()">Spawn non-aligned</button>
<button onclick="alignedY()">Spawn Y aligned</button>
<button onclick="alignedBoth()">Spawn diagonally aligned</button>
</div>
</body>
</html>

Canvas code not updating and I'm not sure why

I'm trying to make a simple canvas program where the user clicks to create bouncing moving circles. It keeps freezing but still creates the circles without updating. I'm not sure whats going on, please help!
I'm adding each circle to an array of circles with the constructor
The setInterval loop seems to be freezing but the circles are still created even when this is happening
I'm having a hard time debugging this, any advice is greatly appreciated
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Background Test</title>
<style>
* { margin: 0; padding: 0; overflow: hidden; }
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
// Request animation frame -> Optimizes animation speed
const requestAnimationFrame = window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame;
const c = document.getElementById('canvas');
const ctx = c.getContext('2d');
// Fullscreen
c.width = window.innerWidth;
c.height = window.innerHeight;
ctx.fillStyle = 'red';
let fps = 60;
// FOR MOBILE DEVICES
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent))
fps = 29;
// Options
const background = '#333';
const circleMinSpeed = 3;
const circleMaxSpeed = 6;
const circleMinSize = 3;
const circleMaxSize = 10;
const circles = [];
let circlesCounter = 0;
const circlesTimeAlive = 20 * fps; // seconds
let i = 0;
const interval = 1000 / fps;
let now, delta;
let then = Date.now();
// Coordinate variables
let mouseX, mouseY, clickX, clickY;
// Tracks mouse movement
c.onmousemove = function(event)
{
mouseX = event.clientX;
mouseY = event.clientY;
};
// Tracks mouse click
c.onmousedown = function(event)
{
clickX = event.clientX;
clickY = event.clientY;
circle(clickX, clickY);
};
function draw()
{
// Loop
requestAnimationFrame(draw);
// Set NOW and DELTA
now = Date.now();
delta = now - then;
// New frame
if (delta > interval) {
// Update THEN
then = now - (delta % interval);
// Our animation
// Clear canvas then draw
ctx.clearRect(0, 0, c.width, c.height);
drawBackground();
drawCos();
drawCircles();
drawTest();
}
}
// Circle constructor
function circle(x, y)
{
// Pick random color
let r = Math.floor(Math.random() * 255);
let g = Math.floor(Math.random() * 255);
let b = Math.floor(Math.random() * 255);
self.color = 'rgb(' + r + ', ' + g + ', ' + b + ')';
self.xCo = x;
self.yCo = y;
// Pick random size within ranges
self.size = circleMinSize + Math.floor(Math.random() *
(circleMaxSize - circleMinSize));
// Pick random direction & speed (spdX spdY)
self.speed = circleMinSpeed + Math.floor(Math.random() *
(circleMaxSpeed - circleMinSpeed));
self.spdX = self.speed * (Math.random() * 2) - 1; // picks -1 to 1
self.spdY = self.speed * (Math.random() * 2) - 1;
self.draw = function()
{
ctx.beginPath();
ctx.arc(self.xCo, self.yCo, self.size, 0, 2*Math.PI);
ctx.fillStyle = self.color;
ctx.fill();
};
circles[circlesCounter++] = self;
}
// Draw the background
function drawBackground()
{
ctx.fillStyle = background;
ctx.fillRect(0, 0, c.width, c.height);
}
function drawCircles()
{
for (let i = 0; i < circles.length; i++)
circles[i].draw();
}
function drawTest()
{
ctx.fillStyle = 'red';
ctx.fillRect(i++, i, 5, 5);
}
function drawCos()
{
ctx.fillStyle = 'white';
ctx.fillText("X: " + mouseX + " Y:" + mouseY, 10, 10, 200);
}
// Main loop
setInterval(function()
{
// Loop through circles and move them
for (let i = 0; i < circles.length; i++)
{
if (circle[i])
{
// Check left and right bounce
if (circle[i].xCo <= 0 || circle[i].xCo >= c.width)
circle[i].spdX = -circle[i].spdX;
circle[i].xCo += circle[i].spdX;
// Check left and right bounce
if (circle[i].yCo <= 0 || circle[i].yCo >= c.height)
circle[i].spdY = -circle[i].spdY;
circle[i].yCo += circle[i].spdY;
}
}
// Draw Everything
draw();
}, interval);
</script>
</body>
</html>
This code:
self.draw = function()
{
ctx.beginPath();
ctx.arc(self.xCo, self.yCo, self.size, 0, 2*Math.PI);
ctx.fillStyle = self.color;
ctx.fill();
};
Is overriding this function:
function draw()
{
// Loop
requestAnimationFrame(draw);
// Set NOW and DELTA
now = Date.now();
delta = now - then;
// New frame
if (delta > interval) {
// Update THEN
then = now - (delta % interval);
// Our animation
// Clear canvas then draw
ctx.clearRect(0, 0, c.width, c.height);
drawBackground();
drawCos();
drawCircles();
drawTest();
}
}
You need to rethink how you want to draw your circles because you're re-drawing the black canvas every time a click event is triggered. I mean, when a click is triggered, you're applying new coordinates, color, Etc, and probably that's not what you want to do.
My suggestion is create canvas per circle and append them into a DIV.
Hope it helps!

Categories