An object type of ball is created with a new constructor function ball(color).
Its properties include all that need to paint it on a canvas and to cause him to move in a random direction.
When one ball is created orangeBall = new ball('orange'), it shows up well including its movement on the canvas. But when another one is added, both of them start to blink.
How to solv it ?
Thanks.
<!DOCTYPE html>
<html lang="he">
<head>
<meta charset="utf-8" />
<title>
כדורים קופצים רנדומלית בצבעים שונים
</title>
<style>
html,body {
margin: 0;
padding: 0;
background: black;
}
.container { width:900px; margin:0 auto; }
#canvas { background:#8613eb; border:1px solid #cbcbcb; }
</style>
<script>
var ctx;
var H = 800;
var W = 800;
window.onload = function () {
ctx = canvas.getContext("2d");
canvas.width=W;
canvas.height=H;
function ball(color) {
//life - the amount of time to show the ball in the screen
this.life = 60*1000, //1 minute
this.color = arguments.length==1 ? color: 'white';
this.x= Math.round(Math.random()*W);
this.y= Math.round(Math.random()*H);
this.radius= 10 + Math.round(Math.random()*50);// between 10-60
this.dx=1+ Math.round(Math.random()*5); //between 1-6
this.dy= 2+ Math.round(Math.random()*4); //between 2-6
this.startAngel= 0;
this.endAngel= 2*Math.PI; //360deg
this.speed= 3+Math.round(Math.random()*50) //3-50msec
this.show = function() {
//first clear the previous ball
ctx.clearRect(0,0,canvas.width,canvas.height);
var xClear = (this.x-this.radius) <=0 ? 0:(this.x - this.radius);
var yClear = (this.y-2*this.radius) <=0 ? 0:(this.y - 2*this.radius);
ctx.clearRect(xClear,yClear,canvas.width,canvas.height);
//lets stroke the ball
ctx.beginPath();
ctx.fillStyle = this.color;
this.x+=this.dx;
this.y+=this.dy;
if (this.x<0 || this.x>W) {
this.dx=-this.dx;
}
if (this.y<0 || this.y>H) {
this.dy=-this.dy;
}
ctx.arc(this.x,this.y,this.radius,this.startAngel,this.endAngel);
ctx.closePath();
ctx.fill();
this.life-=this.speed;
var _this = this;
// creating new property in the ball 'timePointer'
this.timePointer = this.life<=0 ?
clearInterval(this.timePointer):
setTimeout(function() {
_this.show();
},this.speed);
}
this.show();
};
orangeBall = new ball('orange');
blackBall = new ball('black');
// whiteBall = new ball('white');
// yellowgeBall = new ball('yellow');
// pinkBall = new ball('pink');
// blueBall = new ball('blue');
// greenBall = new ball('green');
};
</script>
</head>
<body>
<div class="container">
<canvas id="canvas">Your browser doesn't support this game.</canvas>
</div>
</body>
</html>
You are clearing your whole canvas after every ball draws itself with its show function. Instead of having the balls clear the canvas, have an interval that will call a function that will clear the canvas once and then iterates all the balls and draw them.
Animations are divided up into frames. A frame is approx 1/60th of a second and in that time all of the animation is drawn.
To help with animations the browser has a function that you use to call the function that renders your frame. requestAnimationFrame(yourfunction)
requestAnimationFrame will tell the browser that you are making changes to an animation. This stops the canvas being presented to the display until the next vertical display refresh.
Using setInterval or setTimeout
function animateSomething(){
// draw something
} // as soon as this exits the content of the canvas is moved to the display
// there is no way to know where the display hardware is writing pixels to
// display, could be halfway through drawing a ball
setInterval(animateSomething,1000 / 60);
If you do this for many objects each time you exit the pixels are move to the display without regard to what the display hardware is doing. This causes flicker, shearing and other problems.
Using requestAnimationFrame
function animateSomething(){
// draw something
requestAnimationFrame(animateSomething)
} // The content of the canvas does not move to the display until the
// display hardware is getting ready to scan the next display refresh
requestAnimationFrame(animateSomething);
The best way to handle animations is to do all the rendering from one function.
Below i have modified your code to remove the flicker using requestAnimationFrame I have added a mainLoop function that draws all the balls. I let the browser handle the timing.
var ctx;
var H = 800;
var W = 800;
window.onload = function() {
ctx = canvas.getContext("2d");
canvas.width = W;
canvas.height = H;
function ball(color) {
//life - the amount of time to show the ball in the screen
this.life = 60 * 1000; //1 minute
this.color = arguments.length == 1 ? color : 'white';
this.x = Math.round(Math.random() * W);
this.y = Math.round(Math.random() * H);
this.radius = 10 + Math.round(Math.random() * 50); // between 10-60
this.dx = 1 + Math.round(Math.random() * 5); //between 1-6
this.dy = 2 + Math.round(Math.random() * 4); //between 2-6
this.startAngel = 0;
this.endAngel = 2 * Math.PI; //360deg
this.speed = 3 + Math.round(Math.random() * 50) //3-50msec
this.show = function() {
//first clear the previous ball
ctx.beginPath();
ctx.fillStyle = this.color;
this.x += this.dx;
this.y += this.dy;
if (this.x < 0 || this.x > W) {
this.dx = -this.dx;
}
if (this.y < 0 || this.y > H) {
this.dy = -this.dy;
}
ctx.arc(this.x, this.y, this.radius, this.startAngel, this.endAngel);
ctx.closePath();
ctx.fill();
this.life -= this.speed;
}
};
orangeBall = new ball('orange');
blackBall = new ball('black');
whiteBall = new ball('white');
yellowgeBall = new ball('yellow');
pinkBall = new ball('pink');
blueBall = new ball('blue');
greenBall = new ball('green');
var balls = [orangeBall, blackBall, whiteBall, yellowgeBall, pinkBall, blueBall, greenBall];
function mainLoop(){
ctx.clearRect(0, 0, canvas.width, canvas.height);
for(var i = 0; i < balls.length; i++){
if(balls[i].life > 0){
balls[i].show();
}
}
requestAnimationFrame(mainLoop);
}
requestAnimationFrame(mainLoop);
};
html,
body {
margin: 0;
padding: 0;
background: black;
}
.container {
width: 900px;
margin: 0 auto;
}
#canvas {
background: #8613eb;
border: 1px solid #cbcbcb;
}
<div class="container">
<canvas id="canvas">Your browser doesn't support this game.</canvas>
</div>
I changed the code to use prototype. Is that better ?
In addition it's not smooth enough.
Added also performance timeline for your help.
var ctx;
var H = window.innerHeight;
var W = window.innerWidth;
window.onload = function () {
ctx = canvas.getContext("2d");
canvas.width=W;
canvas.height=H;
function ball(color) {
//life - the amount of time to show the ball in the screen
this.life = 60*10, //duration of 600 X 16ms(fram per sec)
this.color = arguments.length==1 ? color: 'white';
this.x= Math.round(Math.random()*W);
this.y= Math.round(Math.random()*H);
this.radius= 10 + Math.round(Math.random()*50);// between 10-60
this.dx=1+ Math.round(Math.random()*5); //between 1-6
this.dy= 2+ Math.round(Math.random()*4); //between 2-6
this.startAngel= 0;
this.endAngel= 2*Math.PI; //360deg
this.start = null;
};
ball.prototype.show = function() {
//lets stroke the ball
ctx.beginPath();
ctx.fillStyle = this.color;
this.x+=this.dx;
this.y+=this.dy;
if (this.x<0 || this.x>W) {
this.dx=-this.dx;
}
if (this.y<0 || this.y>H) {
this.dy=-this.dy;
}
ctx.arc(this.x,this.y,this.radius,this.startAngel,this.endAngel);
ctx.closePath();
ctx.fill();
this.life--;
};
var arrBalls = [] ;
arrBalls.push(new ball('orange'));
arrBalls.push(new ball('black'));
arrBalls.push(new ball('white'));
arrBalls.push(new ball('pink'));
arrBalls.push(new ball('blue'));
arrBalls.push(new ball('green'));
// This loop shell be less then 10ms inorder not to maintain
// 60fps interval of screen hardware rendering
var balldeth = 0;
function paint() {
//clear the canvas once in xframes
ctx.clearRect(0,0,canvas.width,canvas.height);
//paint balls in the canvas
for (var i = 0; i < arrBalls.length; i++ ) {
arrBalls[i].life >= 0 ? arrBalls[i].show() : ++balldeth;
}
(balldeth == arrBalls.length) ? console.log("game over"):window.requestAnimationFrame(paint);
}
window.requestAnimationFrame(paint);
};
html,body {
margin: 0;
padding: 0;
background: black;
}
#canvas { background:#8613eb; border:1px solid #cbcbcb; }
<body>
<canvas id="canvas">Your browser doesn't support this game.</canvas>
</body>
Related
So I have this project I have been working on and the goal of it is to randomly generate terrain on a 2D plane, and put rain in the background, and I chose to use the html5 canvas element to accomplish this goal. After creating it I am happy with the result but I am having performance issues and could use some advice on how to fix it. So far I have tried to only clear the bit of the canvas that is needed, which is above the rectangles I drew under the terrain to fill it in, but because of this I have to redraw the circles. The rn(rain number) has already been lowered by about 2 times and it still lags, any suggestions?
Note - The code in the snippet does not lag due to it's small size, but if I was to run it in full screen with the actual rain number(800), it would lag. I have shrunk the values to fit the snippet.
var canvas = document.getElementById('gamecanvas');
var c = canvas.getContext('2d');
var ma = Math.random;
var mo = Math.round;
var wind = 5;
var rn = 100;
var rp = [];
var tp = [];
var tn;
function setup() {
//fillstyle
c.fillStyle = 'black';
//canvas size
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
//rain setup
for (i = 0; i < rn; i++) {
let x = mo(ma() * canvas.width);
let y = mo(ma() * canvas.width);
let w = mo(ma() * 1) + 1;
let s = mo(ma() * 5) + 10;
rp[i] = { x, y, w, s };
}
//terrain setup
tn = (canvas.width) + 20;
tp[0] = { x: -2, y: canvas.height - 50 };
for (i = 1; i <= tn; i++) {
let x = tp[i - 1].x + 2;
let y = tp[i - 1].y + (ma() * 20) - 10;
if (y > canvas.height - 50) {
y = tp[i - 1].y -= 1;
}
if (y < canvas.height - 100) {
y = tp[i - 1].y += 1;
}
tp[i] = { x, y };
c.fillRect(x, y, 4, canvas.height - y);
}
}
function gameloop() {
//clearing canvas
for (i = 0; i < tn; i++) {
c.clearRect(tp[i].x - 2, 0, 2, tp[i].y);
}
for (i = 0; i < rn; i++) {
//rain looping
if (rp[i].y > canvas.height + 5) {
rp[i].y = -5;
}
if (rp[i].x > canvas.width + 5) {
rp[i].x = -5;
}
//rain movement
rp[i].y += rp[i].s;
rp[i].x += wind;
//rain drawing
c.fillRect(rp[i].x, rp[i].y, rp[i].w, 6);
}
for (i = 0; i < tn; i++) {
//terrain drawing
c.beginPath();
c.arc(tp[i].x, tp[i].y, 6, 0, 7);
c.fill();
}
}
setup();
setInterval(gameloop, 1000 / 60);
body {
background-color: white;
overflow: hidden;
margin: 0;
}
canvas {
background-color: white;
}
<html>
<head>
<link rel="stylesheet" href="index.css">
<title>A Snowy Night</title>
</head>
<body id="body"> <canvas id="gamecanvas"></canvas>
<script src="index.js"></script>
</body>
</html>
Superimposing canvas
Like I suggested in my comment, the use of a second canvas point is to only have to draw the terrain once, and hence it could enhance the performance of your animation by saving a redraw on each new frame. This can be done with CSS by positioning one on the other (like layers).
#canvasBase {
position: relative;
}
#canvasLayer1 {
position: absolute;
top: 0;
left: 0;
}
#canvasLayer2 {
position: absolute;
top: 0;
left: 0;
}
// etc...
Also I advise you to use requestAnimationFrame over setinterval (see why).
requestAnimationFrame
However, by using requestAnimationFrame, we don't control the refresh rate, it's tied to the client hardware. So we need to handle it and for that, we will use the DOMHighResTimeStamp which is passed as an argument to our callback method.
The idea is to let it run at native speed and manage the fps by updating the logic (our calculs) only at desired time. For exemple, if we need a fps = 60; that means we need to update our logic every 1000 / 60 = ~16,67 ms. So we check if the deltaTime with the time of the last frame is equal or superior than ~16,67ms. If not enough time elapsed, we call a new frame & we return (important, otherwise the control we just did is useless as the code keeps going whatever the outcome of it).
let fps = 60;
/* Check if we need to update the logic */
/* if not request a new frame & return */
if(deltaLastUpdate <= 1000 / fps){ // 1000 / 60 = ~16,67ms
requestAnimationFrame(animate);
return;
}
Clearing canvas
As you need to erase all the past rain drops, the simplest & cheapest in ressources in to clear the whole context in one swoop.
ctxRain.clearRect(0, 0, rainCanvas.width, rainCanvas.height);
Path2D
As your drawing use the same color for the rain drops, you can as well group all these in one path:
rainPath = new Path2D();
...
So you will need only one instruction to draw them (same ressources saving type as the clearRect):
ctxRain.fill(rainPath);
Result
/* CANVAS "Terrain" */
const terrainCanvas = document.getElementById('gameTerrain');
const ctxTerrain = terrainCanvas.getContext('2d');
terrainCanvas.height = window.innerHeight;
terrainCanvas.width = window.innerWidth;
/* CANVAS "Rain" */
const rainCanvas = document.getElementById('gameRain');
const ctxRain = rainCanvas.getContext('2d');
rainCanvas.height = window.innerHeight;
rainCanvas.width = window.innerWidth;
/* Game Constants */
const wind = 5;
const rainMaxParticules = 100;
const rain = [];
let rainPath;
const terrainMaxParticules = terrainCanvas.width + 20;
const terrain = [];
let terrainPath;
/* Maths help */
const ma = Math.random;
const mo = Math.round;
/* Clear */
function clearTerrain(){
ctxTerrain.clearRect(0, 0, terrainCanvas.width, terrainCanvas.height);
}
function clearRain(){
ctxRain.clearRect(0, 0, rainCanvas.width, rainCanvas.height);
}
/* Logic */
function initTerrain(){
terrain[0] = { x: -2, y: terrainCanvas.height - 50 };
for (let i = 1; i <= terrainMaxParticules; i++) {
let x = terrain[i - 1].x + 2;
let y = terrain[i - 1].y + (ma() * 20) - 10;
if (y > terrainCanvas.height - 50) {
y = terrain[i - 1].y -= 1;
}
if (y < terrainCanvas.height - 100) {
y = terrain[i - 1].y += 1;
}
terrain[i] = { x, y };
}
}
function initRain(){
for (let i = 0; i < rainMaxParticules; i++) {
let x = mo(ma() * rainCanvas.width);
let y = mo(ma() * rainCanvas.width);
let w = mo(ma() * 1) + 1;
let s = mo(ma() * 5) + 10;
rain[i] = { x, y, w, s };
}
}
function init(){
initTerrain();
initRain();
}
function updateTerrain(){
terrainPath = new Path2D();
for(let i = 0; i < terrain.length; i++){
terrainPath.arc(terrain[i].x, terrain[i].y, 6, Math.PI/2, 5*Math.PI/2);
}
terrainPath.lineTo(terrainCanvas.width, terrainCanvas.height);
terrainPath.lineTo(0, terrainCanvas.height);
}
function updateRain(){
rainPath = new Path2D();
for (let i = 0; i < rain.length; i++) {
// Rain looping
if (rain[i].y > rainCanvas.height + 5) {
rain[i].y = -5;
}
if (rain[i].x > rainCanvas.width + 5) {
rain[i].x = -5;
}
// Rain movement
rain[i].y += rain[i].s;
rain[i].x += wind;
// Path containing all the drops
rainPath.rect(rain[i].x, rain[i].y, rain[i].w, 6);
}
}
/* Drawing */
function drawTerrain(){
ctxTerrain.fillStyle = 'black';
ctxTerrain.fill(terrainPath);
}
function drawRain(){
ctxRain.fillStyle = 'black';
ctxRain.fill(rainPath);
}
/* Animation Constant */
const fps = 60;
let lastTimestampUpdate;
let terrainDrawn = false;
/* Game loop */
function animate(timestamp){
/* Initialize rain & terrain particules */
if(rain.length === 0 || terrain.length === 0){
init();
}
/* Define "lastTimestampUpdate" from the first call */
if (lastTimestampUpdate === undefined){
lastTimestampUpdate = timestamp;
}
/* Check if we need to update the logic & the drawing, if not, request a new frame & return */
if(timestamp - lastTimestampUpdate <= 1000 / fps){
requestAnimationFrame(animate);
return;
}
if(!terrainDrawn){
/* Terrain --------------------- */
/* Clear */
clearTerrain();
/* Logic */
updateTerrain();
/* Draw */
drawTerrain();
/* ----------------------------- */
terrainDrawn = true;
}
/* --- Rain -------------------- */
/* Clear */
clearRain();
/* Logic */
updateRain();
/* Draw */
drawRain();
/* ----------------------------- */
/* Request another frame */
lastTimestampUpdate = timestamp;
requestAnimationFrame(animate);
}
/* Start the animation */
requestAnimationFrame(animate);
body {
background-color: white;
overflow: hidden;
margin: 0;
}
#gameTerrain {
position: relative;
}
#gameRain {
position: absolute;
top: 0;
left: 0;
}
<body>
<canvas id="gameTerrain"></canvas>
<canvas id="gameRain"></canvas>
</body>
Aside
This won't affect performance, however I encourage you to use const & let over var (What's the difference between using “let” and “var”?).
Generally, having more paint instructions will be what costs the most, the complexity of these paint instructions only comes to play when it's really complex.
Here you are spamming the GPU with paint instructions:
(canvas.width) + 20 calls to clearRect(). clearRect() is a paint instruction, and not a cheap one. Use it sporadically, but actually, you should use it only to clear the whole context.
One fillRect() per rain drop.. They're all the same color, they can be merged in a single sub-path and drawn in a single draw call.
One fill per circle composing the terrain.
So instead of this huge number of draw calls, we could make it in only two draw calls:
One clearRect, one fill() of one big subpath containing both the drops and
the terrain.
However it's certainly more practical to keep the terrain and the rain separated, so let's make it three draw calls, by keeping the terrain in its own Path2D object, which is more friendly for the CPU:
var canvas = document.getElementById('gamecanvas');
var c = canvas.getContext('2d');
var ma = Math.random;
var mo = Math.round;
var wind = 5;
var rn = 100;
var rp = [];
// this will hold our Path2D object
// which will hold the full terrain drawing
// set a 'let' because we will set it again on resize
let terrain;
var tp = [];
var tn;
function setup() {
//fillstyle
c.fillStyle = 'black';
//canvas size
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
//rain setup
for (let i = 0; i < rn; i++) {
let x = mo(ma() * canvas.width);
let y = mo(ma() * canvas.width);
let w = mo(ma() * 1) + 1;
let s = mo(ma() * 5) + 10;
rp[i] = { x, y, w, s };
}
//terrain setup
tn = (canvas.width) + 20;
tp[0] = { x: -2, y: canvas.height - 50 };
terrain = new Path2D();
for (let i = 1; i <= tn; i++) {
let x = tp[i - 1].x + 2;
let y = tp[i - 1].y + (ma() * 20) - 10;
if (y > canvas.height - 50) {
y = tp[i - 1].y -= 1;
}
if (y < canvas.height - 100) {
y = tp[i - 1].y += 1;
}
tp[i] = { x, y };
terrain.rect(x, y, 4, canvas.height - y);
terrain.arc(x, y, 6, 0, Math.PI*2);
}
}
function gameloop() {
// clear the whole canvas
c.clearRect(0, 0, canvas.width, canvas.height);
// start a new sub-path for the rain
c.beginPath();
for (let i = 0; i < rn; i++) {
//rain looping
if (rp[i].y > canvas.height + 5) {
rp[i].y = -5;
}
if (rp[i].x > canvas.width + 5) {
rp[i].x = -5;
}
//rain movement
rp[i].y += rp[i].s;
rp[i].x += wind;
//rain tracing
c.rect(rp[i].x, rp[i].y, rp[i].w, 6);
}
// paint all the drops in a single op
c.fill();
// paint the whole terrain in a single op
c.fill(terrain);
// loop at screen refresh frequency
requestAnimationFrame(gameloop);
}
setup();
requestAnimationFrame(gameloop);
onresize = () => setup();
body {
background-color: white;
overflow: hidden;
margin: 0;
}
canvas {
background-color: white;
}
<canvas id="gamecanvas"></canvas>
Further possible improvements:
Instead of making our terrain path a set of rectangles, using only lineTo to trace the actual outline would probably help a bit, some more calculations at init, but it's done only once in a while.
If the terrain becomes more complex, with more details, or with various colors and shadows etc. then consider painting it only once, and then produce an ImageBitmap from the canvas. Then in gameLoop you'll just have to drawImage that ImageBitmap (drawing bitmaps is super fast, but storing it consumes memory, so remember to .close() the ImageBitmap when you don't need it anymore).
I'm trying to make a little game with JavaScript (no engine) and I want to get rid of frame-based animation.
I successfully added delta time for horizontal movements (work fine with 60 or 144fps).
But I can't make it work with the jump, height (or the strength) isn't always the same, and I don't know why.
I already tried (And still had the exact same problem):
Passing Delta Time at the end of update(): x += Math.round(dx * dt)
Changing Date.now() to performance.now()
Not rounding DeltaY
Locking Jump Height
I made a simplified example with 2 jump type, height locked jump and a normal jump (IDK what to call it). Both have the same problem.
const canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d'),
canvas2 = document.getElementById('canvas2'),
ctx2 = canvas2.getContext('2d');
// CLASS PLAYER ------------------------
class Actor {
constructor(color, ctx, j) {
this.c = ctx
this.w = 20
this.h = 40
this.x = canvas.width /2 - this.w/2
this.y = canvas.height/2 - this.h/2
this.color = color
// Delta
this.dy = 0
// Movement
this.gravity = 25/1000
this.maxSpeed = 600/1000
// Jump Height lock
this.jumpType = (j) ? 'capedJump' : 'normalJump'
this.jumpHeight = -50
// Booleans
this.isOnFloor = false
}
// Normal jump
normalJump(max) {
if(!this.isOnFloor) return
this.dy = -max
this.isOnFloor = false
}
// Jump lock (locked max height)
capedJump(max) {
const jh = this.jumpHeight;
if(jh >= 0) return
this.dy += -max/15
if(jh - this.dy >= 0) {
this.dy = (jh - this.dy) + jh
this.jumpHeight = 0
} else {
this.jumpHeight += -this.dy
}
}
update(dt) {
const max = this.maxSpeed*dt,
gravity = this.gravity*dt;
// JUMP
this[this.jumpType](max)
// GRAVITY
this.dy += gravity
// TOP AND DOWN COLLISION (CANVAS BORDERS)
const y = this.y + this.dy,
h = y + this.h;
if (y <= 0) this.y = this.dy = 0
else if (h >= canvas.height) {
this.y = canvas.height - this.h
this.dy = 0
this.isOnFloor = true
this.jumpHeight = -50
}
// Update Y
this.y += Math.round(this.dy)
}
draw() {
const ctx = this.c
ctx.fillStyle = this.color
ctx.fillRect(this.x, this.y, this.w, this.h)
}
}
const Player = new Actor('brown', ctx, false)
const Player2 = new Actor('blue', ctx2, true)
// ANIMATE -----------------------------
let lastRender = 0
let currRender = Date.now()
function animate() {
// Set Delta Time
lastRender = currRender
currRender = Date.now()
let dt = currRender - lastRender
// CANVAS #1 (LEFT)
ctx.clearRect(0, 0, canvas.width, canvas.height)
background(ctx)
Player.update(dt)
Player.draw()
// CANVAS #2 (RIGHT)
ctx2.clearRect(0, 0, canvas2.width, canvas2.height)
background(ctx2)
Player2.update(dt)
Player2.draw()
window.requestAnimationFrame(animate)
}
animate()
// EVENT LISTENERS -----------------------
window.addEventListener('keydown', (e) => {
e.preventDefault()
if (Player.keys.hasOwnProperty(e.code)) Player.keys[e.code] = true
})
window.addEventListener('keyup', (e) => {
e.preventDefault()
if (Player.keys.hasOwnProperty(e.code)) Player.keys[e.code] = false
})
// Just a function to draw Background nothing to see here
function background(c) {
const lineNumber = Math.floor(canvas.height/10)
c.fillStyle = 'gray'
for(let i = 0; i < lineNumber; i++) {
c.fillRect(0, lineNumber*i, canvas.width, 1)
}
}
div {
display: inline-block;
font-family: Arial;
}
canvas {
border: 1px solid black;
}
span {
display: block;
color: gray;
text-align: center;
}
<div>
<canvas width="100" height="160" id="canvas"></canvas>
<span>Normal</span>
</div>
<div>
<canvas width="100" height="160" id="canvas2"></canvas>
<span>Locked</span>
</div>
Here's how I would refactor the code:
Don't use dy for both speed and position (which you seem to be doing). Rename it vy and use it purely as the vertical velocity.
Move isOnFloor to a function so that we can always check for collisions with the floor.
Decouple the jump functions from actual movement updates. Just make them set the vertical velocity if the player is on the floor.
Perform top / bottom collision checking separately depending on the direction of movement.
Don't round DeltaY - it'll mess up small movements.
With these changes in place, the movement behavior is correct and stable:
const canvas1 = document.getElementById('canvas1'),
ctx1 = canvas1.getContext('2d'),
canvas2 = document.getElementById('canvas2'),
ctx2 = canvas2.getContext('2d');
// Global physics variables
const GRAVITY = 0.0015;
const MAXSPEED = 0.6;
const MAXHEIGHT = 95;
// CLASS PLAYER ------------------------
class Actor {
constructor(C, W, H, J) {
// World size
this.worldW = W;
this.worldH = H;
// Size & color
this.w = 20;
this.h = 40;
this.color = C;
// Speed
this.vy = 0;
// Position
this.x = W/2 - this.w/2;
this.y = H/2 - this.h/2;
// Jump Height lock
this.jumpCapped = J;
this.jumpHeight = 0;
}
// move isOnFloor() to a function
isOnFloor() {
return this.y >= this.worldH - this.h;
}
// Normal jump
normalJump() {
if(!this.isOnFloor()) return;
this.vy = -MAXSPEED;
}
// Jump lock (locked max height)
cappedJump(max) {
if(!this.isOnFloor()) return;
this.vy = -MAXSPEED;
this.jumpHeight = max;
}
update(dt) {
// JUMP
if (this.jumpCapped)
this.cappedJump(MAXHEIGHT);
else
this.normalJump();
// GRAVITY
this.vy += GRAVITY * dt;
this.y += this.vy * dt;
// Bottom collision
if (this.vy > 0) {
if (this.isOnFloor()) {
this.y = this.worldH - this.h;
this.vy = 0;
}
}
else
// Top collision
if (this.vy < 0) {
const maxh = (this.jumpCapped) ? (this.worldH - this.jumpHeight) : 0;
if (this.y < maxh) {
this.y = maxh;
this.vy = 0;
}
}
}
draw(ctx) {
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, this.w, this.h);
}
}
const Player1 = new Actor('brown', canvas1.width, canvas1.height, false);
const Player2 = new Actor('blue', canvas2.width, canvas2.height, true);
// ANIMATE -----------------------------
let lastUpdate = 0;
let randomDT = 0;
function animate() {
// Compute delta time
let currUpdate = Date.now();
let dt = currUpdate - lastUpdate;
// Randomize the update time interval
// to test the physics' stability
if (dt > randomDT) {
randomDT = 35 * Math.random() + 5;
Player1.update(dt);
Player2.update(dt);
lastUpdate = currUpdate;
}
// CANVAS #1 (LEFT)
ctx1.clearRect(0, 0, canvas1.width, canvas1.height);
background(ctx1);
Player1.draw(ctx1);
// CANVAS #2 (RIGHT)
ctx2.clearRect(0, 0, canvas2.width, canvas2.height);
background(ctx2);
Player2.draw(ctx2);
window.requestAnimationFrame(animate);
}
animate();
// EVENT LISTENERS -----------------------
window.addEventListener('keydown',
(e) => {
e.preventDefault();
if (Player.keys.hasOwnProperty(e.code))
Player.keys[e.code] = true;
}
)
window.addEventListener('keyup',
(e) => {
e.preventDefault()
if (Player.keys.hasOwnProperty(e.code))
Player.keys[e.code] = false;
}
)
// Just a function to draw Background nothing to see here
function background(c) {
const lineNumber = Math.floor(canvas1.height/10)
c.fillStyle = 'gray'
for(let i = 0; i < lineNumber; i++) {
c.fillRect(0, lineNumber*i, canvas1.width, 1)
}
}
div {
display: inline-block;
font-family: Arial;
}
canvas {
border: 1px solid black;
}
span {
display: block;
color: gray;
text-align: center;
}
<div>
<canvas width="100" height="160" id="canvas1"></canvas>
<span>Normal</span>
</div>
<div>
<canvas width="100" height="160" id="canvas2"></canvas>
<span>Locked</span>
</div>
I have a game, and in it there is a main, controllable character and then enemies that shoot back at the character. For the enemies, when they shoot back I want them to shoot at intervals so that it isn't just one massive block of bullets, and it worked with a setInterval for one, but when a second enemy comes in they don't shoot. Only one of the two will. If anybody has a solution that would be great!
function enemies() {
if (enemy_soldiers.length == 0) {
level += 0.2;
for (var i = 0; i<(1 + Math.floor(Math.round(level))); i++) {
var gx = 1450
var gy = getRandom(430, 630);
enemy_soldiers.push({
x: gx,
y: gy,
l: gl,
d: getRandom(350, 600),
shooting: false,
interval: setInterval (function() {enemy.shooting = true;},fire_rate),
shoot: function() {
enemy_bullets.push({
x: enemy.x+40,
y: enemy.y+87,
vel: 10,
});
}
});
}
}
var enemy;
gctx.clearRect(0, 0, 1400, 800);
for (var i in enemy_soldiers) {
enemy = enemy_soldiers[i];
drawenemy(enemy.x, enemy.y, enemy.l);
//ai
if (distance(enemy.x, enemy.y, cx, cy) >= enemy.d && enemy.x>cx) {
enemy.x-=vel;
}
else if (distance(enemy.x, enemy.y, cx, cy) >= enemy.d && enemy.x<cx) {
enemy.x+=vel;
}
if (distance(enemy.x, enemy.y, cx, cy) <= 600) {
if (enemy.shooting == true) {
enemy.shoot(enemy.x,enemy.y);
enemy.shooting = false;
}
gbctx.clearRect(0, 0, 1400, 800);
for (var j in enemy_bullets) {
enemy_bullet = enemy_bullets[j];
enemy_bullet.x -= enemy_bullet.vel;
if (enemy_bullet.x > 1400 || enemy_bullet.x < -5 || enemy_bullet.y > 800 || enemy_bullet.y < -5) {
enemy_bullets.splice(j,1);
}
drawEnemyBullet(enemy_bullet.x, enemy_bullet.y);
}
}
}}
This solution relies on the function Date.now() which returns the current time in milliseconds. It's possible for each enemy to keep track of when they have to fire next and each one can have a different delay between shots.
I've only added a graphic to show when they are firing their weapons, but this can be easily altered to something like a raycast or spawn a projectile particle.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
background-color: black;
}
canvas {
position: absolute;
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
border: solid 1px white;
border-radius: 1px;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script type="application/javascript">
// Enemy constructor
function Enemy(x,y) {
this.x = x;
this.y = y;
this.isFiring = false;
this.fireDelay = (500 + Math.random() * 500) | 0; // Time between shots
this.lastFired = Date.now(); // Last time at which the enemy fired
}
/*
Enemy prototype
(All objects created using the constructor share these properties)
E.G.
var e1 = new Enemy(10,0);
var e2 = new Enemy(20,0);
if (e1.__proto__ === e2.__proto__) {
console.log("Match");
}
prints "Match"
*/
Enemy.prototype = {
WIDTH: 10,
HEIGHT: 20,
FIRE_DURATION: 100, // Amount of time 'fire' graphic is shown
FIRE_WIDTH: 10,
FIRE_HEIGHT: 10,
update: function() {
// If current time - time when I last fired > the amount of time between shots
if (Date.now() - this.lastFired > this.fireDelay) {
this.lastFired = Date.now();
this.isFiring = true;
// If you were using projectile particles, this is where you would spawn one
}
if (this.isFiring && Date.now() - this.lastFired > this.FIRE_DURATION) {
this.isFiring = false;
}
},
render: function(ctx) {
ctx.fillStyle = "darkred";
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.rect(
this.x,
this.y,
this.WIDTH,
this.HEIGHT
);
ctx.fill();
ctx.stroke();
if (this.isFiring) {
ctx.fillStyle = "yellow";
ctx.strokeStyle = "darkyellow";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.rect(
this.x + this.WIDTH,
this.y + this.HEIGHT * 0.5 - this.FIRE_HEIGHT * 0.5,
this.FIRE_WIDTH,
this.FIRE_HEIGHT
);
ctx.fill();
ctx.stroke();
}
}
};
var canvasWidth = 180;
var canvasHeight = 160;
var canvas = null;
var ctx = null;
var enemies = [];
window.onload = function() {
canvas = document.getElementById("canvas");
canvas.width = canvasWidth;
canvas.height = canvasHeight;
ctx = canvas.getContext("2d");
for (var i = 0; i < 5; ++i) {
enemies[i] = new Enemy(20 + i * 3,10 + i * 30);
}
loop();
}
function loop() {
// Update
for (var i = 0; i < enemies.length; ++i) {
enemies[i].update();
}
// Render
ctx.fillStyle = "gray";
ctx.fillRect(0,0,canvasWidth,canvasHeight);
for (var i = 0; i < enemies.length; ++i) {
enemies[i].render(ctx);
}
//
requestAnimationFrame(loop);
}
</script>
</body>
</html>
When I try to change the scoreboard using the computerCount variable (as you can see in my if statement in the update function for the ball variable) the other numbers aren't shown properly. Keep in mind that at first 0 works properly.
JSFiddle format - You can change code by going at top right corner that says "Edit" in JSFiddle.
Code:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Pong</title>
<!-- Basic styling, centering the canvas -->
<style>
canvas{
position: absolute;
margin: auto;
top:60px;
bottom:0;
right:0;
left:0;
}
#scoreBoard{
position: absolute;
margin: auto;
top:0;
bottom:640px;
right:720px;
left:20px;
}
</style>
</head>
<body>
<canvas id = "scoreBoard" width="500" height = "100"></canvas>
<script>
var WIDTH = 700
var HEIGHT = 600
var pi = Math.PI
var canvas
var ctx
var keystate
var upArrow = 38;
var downArrow = 40;
var computerCount = 0;
var playerCount = 0;
var player = {
x: null,
y: null,
width: 20,
height: 100,
/**
* Update the position depending on pressed keys
*/
update: function() {
if(keystate[upArrow]){
this.y-=7;
}
if(keystate[downArrow]){
this.y+=7;
}},
/**
* Draw the player paddle to the canvas
*/
draw: function() {
ctx.fillRect(this.x, this.y, this.width, this.height);
}
}
/**
* The ai paddle
*
* #type {Object}
*/
var ai = {
x: null,
y: null,
width: 20,
height: 100,
/**
* Update the position depending on the ball position
*/
update: function() {
var desty = ball.y-(this.height - ball.side)*0.5
this.y += (desty-this.y)*0.2;
},
draw: function() {
ctx.fillRect(this.x, this.y, this.width, this.height);
}
}
/**
* The ball object
*
* #type {Object}
*/
var ball = {
x: null,
y: null,
vel:null,
side: 20,
speed:5,
update: function() {
this.x += this.vel.x;
this.y += this.vel.y;
if(0>this.y || this.y+this.side>HEIGHT){
var offset = this.vel.y<0 ? 0-this.y : HEIGHT-(this.y+this.side);
this.y+=2*offset
this.vel.y*=-1;
}
var AABBIntersect = function(ax, ay, aw, ah, bx, by, bw, bh) {
return ax < bx+bw && ay < by+bh && bx < ax+aw && by < ay+ah;
};
var pdle = this.vel.x<0 ? player : ai;
if(AABBIntersect(pdle.x,pdle.y,pdle.width,pdle.height, this.x, this.y, this.side, this.side)){
this.x = pdle===player ? player.x + player.width : ai.x - this.side;
var n = (this.y+this.side-pdle.y)/(pdle.height+this.side)
var phi = 0.25*pi*(2*n - 1) // pi/4 = 45
this.x = pdle===player ? player.x+player.width : ai.x - this.side;
var n = (this.y+this.side - pdle.y)/(pdle.height+this.side);
var phi = 0.25*pi*(2*n - 1); // pi/4 = 45
this.vel.x = (pdle===player ? 1 : -1)*this.speed*Math.cos(phi);
this.vel.y = this.speed*Math.sin(phi);
}
if(ball.x<0){
computerCount =+1;
ball.x = (WIDTH - ball.side) / 2;
ball.y = (HEIGHT - ball.side) / 2;
}
},
// reset the ball when ball outside of the canvas in the
draw: function() {
ctx.fillRect(this.x, this.y, this.side, this.side);
}
}
function score(){
}
/**
* Starts the game
*/
function main() {
// create, initiate and append game canvas
canvas = document.createElement("canvas");
canvas.width = WIDTH;
canvas.height = HEIGHT;
ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
canvas2 = document.createElement("canvas");
canvas2.setAttribute("id", "scoreBoard");
canvas2.width = 200;
canvas2.height = 100;
ctx2 = canvas2.getContext("2d");
document.body.appendChild(canvas2);
keystate = {};
document.addEventListener("keydown" , function(event){
keystate[event.keyCode] = true;
})
document.addEventListener("keyup" , function(event){
delete keystate[event.keyCode];
})
init();
var loop = function() {
update();
draw();
window.requestAnimationFrame(loop, canvas);
};
window.requestAnimationFrame(loop, canvas);
}
/**
* Initatite game objects and set start positions
*/
function init() {
player.x = player.width;
player.y = (HEIGHT - player.height) / 2;
ai.x = WIDTH - (player.width + ai.width);
ai.y = (HEIGHT - ai.height) / 2;
ball.x = (WIDTH - ball.side) / 2;
ball.y = (HEIGHT - ball.side) / 2;
ball.vel = {
x:ball.speed,
y: 0
}
}
/**
* Update all game objects
*/
function update() {
ball.update();
player.update();
ai.update();
}
function draw(){
ctx.fillStyle = "black"
ctx.fillRect(0,0,WIDTH,HEIGHT)
ctx.save();
ctx2.fillStyle = "red"
ctx2.fillText(String(computerCount),100,100)
ctx2.fillText("VS",120,100)
ctx2.fillText(String(playerCount),175,100)
ctx2.font = "bold 40px monaco"
ctx.fillStyle = "white"
ball.draw();
player.draw();
ai.draw();
var w=4;
var x= (WIDTH-w)*0.5;
var y=2;
var step = HEIGHT/20;
while(y<HEIGHT){
ctx.fillRect(x,y+step*0.25,w,step*0.5)
y+=step;
}
ctx.restore();
}
// start and run the game
main();
</script>
</body>
</html>
You have two problems:
When you draw a number, you do not erase the previous number that is already printed. That is, when the score becomes 1, you render 1 on top of already rendered 0. The easiest way to address it is to do fillRect and draw a black rectangle on top of the previously written score before you draw a new score.
When you fix that, you will notice that your score never goes above 1. The reason is a typo in the code that increments it, replace
computerCount =+1;
with
computerCount += 1;
Mentioned by #Ishamael, line 139's computer count needs to be changed to computerCount += 1; //After this if statement add if the player scores.
In line 221,
function draw(){
ctx.fillStyle = "black";
ctx.fillRect(0,0,WIDTH,HEIGHT);
ctx.save();
ctx2.fillStyle = "black";
ctx2.fillRect(0,0,WIDTH, 100);
ctx2.save();
ctx2.fillStyle = "red";
ctx2.font = "bold 40px monaco";
ctx2.fillText(String(computerCount),100,100);
ctx2.fillText("VS",120,100);
ctx2.fillText(String(playerCount),175,100);
ctx2.save();
ctx.fillStyle = "white";
. . .
}
remember your semicolons and set text style before writing. I also added the repaint to erase the previous scoreboard.
How can I reset the velocity variable in my objects. I am making a canvas game, in which stars fall from the top of the Canvas. Problem is when I run the game in a setinterval() the velocity keeps getting greater and greater. What i want is the speed to stay the same unless i change it.
function Star(x, y, rad, velocity, fill){
this.x = Math.floor(Math.random() * 999);//this create a random number between 0 and 599 on the x axis
this.y = 0;
this.rad = Math.floor((Math.random() * 30) + 15);//this create a random number between 10 and 30 for the radius
this.velocity = 5;
this.fill = fill
this.draw = function(){
ctx.beginPath();
ctx.fillStyle = this.fill;
ctx.arc(this.x, this.y, this.rad, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
this.y += this.velocity;
}
}
function createMultipleStars(){
for (var i = 0; i <= numOfStars; i++)
stars[i] = new Star(i * 50, 10, i, i, "rgba(255,215,0,0.6)");
}
//createMultipleStars();
function step() {
ctx.clearRect(0,0,canvas.width, canvas.height);
for (var i = 0; i<= numOfStars; i++)
stars[i].draw();
requestAnimationFrame(step);
}
spaceShip.drawSpaceShip();
var myVar = setInterval(function(){ init() }, 4000);
function init(){
createMultipleStars();
step();
}
Your frames per second were increasing with each interval. Every four seconds another step function is added to the animation frame. To fix this I added an fps counter and singleton pattern. With the singleton pattern you shouldn't break the requestAnimationFrame max 60 fps. Without it you will see that the fps increases. Technically it can't go above 60 but the step function runs multiple times in the same frame increasing the velocity each time and making the stars run faster.
var canvas = document.getElementById('can');
var ctx = canvas.getContext('2d');
var stars = [];
var numOfStars = 10;
function Star(x, y, rad, velocity, fill) {
this.x = Math.floor(Math.random() * 999); //this create a random number between 0 and 599 on the x axis
this.y = 0;
this.rad = Math.floor((Math.random() * 30) + 15); //this create a random number between 10 and 30 for the radius
this.velocity = 5;
this.fill = fill
this.draw = function() {
ctx.beginPath();
ctx.fillStyle = this.fill;
ctx.arc(this.x, this.y, this.rad, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
this.y += this.velocity;
}
}
function createMultipleStars() {
for (var i = 0; i <= numOfStars; i++) {
stars[i] = new Star(i * 50, 10, i, i, "rgba(255,215,0,0.6)");
}
}
function fps() {
var now = (new Date()).getTime();
fps.frames++;
if ((now - fps.lastFps) >= 1000) {
fps.total = fps.frames;
fps.lastFps = now;
fps.frames = 0;
}
return fps.total;
}
fps.frames = 0;
fps.lastFps = (new Date()).getTime();
fps.total = 0;
// Step is a singleton. Only one instance can be created.
function Step() {
// comment out the line below to see what happens when not running
// singleton
if (Step.instance !== null) return Step.instance;
var self = this;
function frame() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i <= numOfStars; i++) {
stars[i].draw();
}
ctx.fillStyle = "red";
ctx.fillText("FPS: " + fps(), 10, 10);
requestAnimationFrame(frame);
}
frame();
Step.instance = this;
return this;
}
Step.instance = null;
//spaceShip.drawSpaceShip();
function init() {
var myVar = setInterval(function() {
createMultipleStars();
var step = new Step();
}, 4000);
createMultipleStars();
//
var step = new Step();
var step = new Step();
var step = new Step();
var step = new Step();
}
init();
#can {
border: 1px solid #FF0000;
}
<canvas id="can" width="400" height="200"></canvas>
Your issue is very simple : you are using both requestAnimationFrame and setInterval to drive the animation. More and more render loops get created and run at the same time, causing the issue
Separate the concerns :
have one render loop working for ever with RequestAnimationFrame
Have a setInterval-ed function inject some new stuff in your game.
So the only change you need to do is here :
var myVar = setInterval(createMultipleStars, 4000);