My most basic movable blue square program slows down the longer the browser window is open.

I will paste my js code and a link to Drew Conley's code in a reply. ...... I made some optimizations like using push instead of unshift and calling from the end of the array instead of the beginning. I have integrated a delta in the game loop which I am still tweaking. Most basic setups for movement do not take in account remembering the second to last key you pressed and are still holding. Drew Conly posted a YT video of a basic 2D game working with the desired movement, but it uses the deprecated 'which' command, the updated e.key command broke Drew's code and I do not want to copy verbatim and plagiarize because I would like to the freedom to monetize what I build. I came up with code myself, but the longer the game is running, the frame drops get pretty noticeable the longer the game stays open without refreshing. There are two key differences between my code and Drew's code. Drew has one switch statement while I have three switch statements. Drew's code for the movement is wrapped in a single constructor and called once during the update() function. That is what I will be working on changing about my code next and may have more to report later. If I can get that to work, I will probably try again to reduce my three switch statements down to only one Later I will fixate my character to the center again and add back sprites and a map, but I want to get my basic movement system and game loop optimized before I go too far.
15 Replies
bluestreak711
bluestreak7118mo ago
Drew's Code: https://codepen.io/punkydrewster713/pen/WNrXPrb My JS Code:
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

const gameSpeed = 100000; // This is extra, but I have a purpose for it later.
let fps = 60;

canvas.width = 1024;
canvas.height = 576;

const player = {
x: 250,
y: 250,
width: 50,
height: 50,
color: 'blue',
speed: 72,
};

let held_keys = [];

const travel = { // I thought that storing ints in the array would be faster the strings, up down left right.
left: 91,
right: 92,
up: 93,
down: 94
}
document.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowLeft':
if (held_keys.indexOf(travel.left) === -1) {
held_keys.push(travel.left);
}
break;
case 'ArrowRight':
if (held_keys.indexOf(travel.right) === -1) {
held_keys.push(travel.right);
}
break;
case 'ArrowUp':
if (held_keys.indexOf(travel.up) === -1) {
held_keys.push(travel.up);
}
break;
case 'ArrowDown':
if (held_keys.indexOf(travel.down) === -1) {
held_keys.push(travel.down);
}
break;
}
});
document.addEventListener('keyup', (e) => {
switch (e.key) {
case 'ArrowLeft':
held_keys.splice(held_keys.indexOf(travel.left), 1);
break;
case 'ArrowRight':
held_keys.splice(held_keys.indexOf(travel.right), 1);
break;
case 'ArrowUp':
held_keys.splice(held_keys.indexOf(travel.up), 1);
break;
case 'ArrowDown':
held_keys.splice(held_keys.indexOf(travel.down), 1);
break;
}
});
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = player.color;
ctx.fillRect(player.x, player.y, player.width, player.height);

requestAnimationFrame(draw);
}
let lastTimestamp = 0;
let fpsInterval = 1000 / fps; // FPS as designated above.

function gameLoop(timestamp) {
let elapsed = timestamp - lastTimestamp;

if (elapsed > fpsInterval) {
lastTimestamp = timestamp - (elapsed % fpsInterval);

update(elapsed);
draw();
}
requestAnimationFrame(gameLoop);
}

function update(delta) {
let movement = gameSpeed * (delta / 100000000);

if (held_keys.length > 0) {
let direction = held_keys[held_keys.length - 1] // Get and remove the last input
switch (direction) {
case travel.left:
player.x -= movement * player.speed;
break;
case travel.right:
player.x += movement * player.speed;
break;
case travel.up:
player.y -= movement * player.speed;
break;
case travel.down:
player.y += movement * player.speed;
break;
}
}
}
requestAnimationFrame(gameLoop);
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

const gameSpeed = 100000; // This is extra, but I have a purpose for it later.
let fps = 60;

canvas.width = 1024;
canvas.height = 576;

const player = {
x: 250,
y: 250,
width: 50,
height: 50,
color: 'blue',
speed: 72,
};

let held_keys = [];

const travel = { // I thought that storing ints in the array would be faster the strings, up down left right.
left: 91,
right: 92,
up: 93,
down: 94
}
document.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowLeft':
if (held_keys.indexOf(travel.left) === -1) {
held_keys.push(travel.left);
}
break;
case 'ArrowRight':
if (held_keys.indexOf(travel.right) === -1) {
held_keys.push(travel.right);
}
break;
case 'ArrowUp':
if (held_keys.indexOf(travel.up) === -1) {
held_keys.push(travel.up);
}
break;
case 'ArrowDown':
if (held_keys.indexOf(travel.down) === -1) {
held_keys.push(travel.down);
}
break;
}
});
document.addEventListener('keyup', (e) => {
switch (e.key) {
case 'ArrowLeft':
held_keys.splice(held_keys.indexOf(travel.left), 1);
break;
case 'ArrowRight':
held_keys.splice(held_keys.indexOf(travel.right), 1);
break;
case 'ArrowUp':
held_keys.splice(held_keys.indexOf(travel.up), 1);
break;
case 'ArrowDown':
held_keys.splice(held_keys.indexOf(travel.down), 1);
break;
}
});
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = player.color;
ctx.fillRect(player.x, player.y, player.width, player.height);

requestAnimationFrame(draw);
}
let lastTimestamp = 0;
let fpsInterval = 1000 / fps; // FPS as designated above.

function gameLoop(timestamp) {
let elapsed = timestamp - lastTimestamp;

if (elapsed > fpsInterval) {
lastTimestamp = timestamp - (elapsed % fpsInterval);

update(elapsed);
draw();
}
requestAnimationFrame(gameLoop);
}

function update(delta) {
let movement = gameSpeed * (delta / 100000000);

if (held_keys.length > 0) {
let direction = held_keys[held_keys.length - 1] // Get and remove the last input
switch (direction) {
case travel.left:
player.x -= movement * player.speed;
break;
case travel.right:
player.x += movement * player.speed;
break;
case travel.up:
player.y -= movement * player.speed;
break;
case travel.down:
player.y += movement * player.speed;
break;
}
}
}
requestAnimationFrame(gameLoop);
Drew Conley
CodePen
Top Down 2D Game Camera
Here's a method for creating a camera illusion for top down web games. This Pen covers arrow key movement and camera following. Video Tutorial here: h...
ChooKing
ChooKing8mo ago
For key up, you only remove one item from the array, but key down repeatedly adds keys for the duration of the key press. The net effect is that the array keeps growing in size throughout the game.
MarkBoots
MarkBoots8mo ago
to prevent multiple keydown events on a longer press, you can use if (e.repeat) return;
bluestreak711
bluestreak7118mo ago
I tried inserting a console.log(held_keys"); command in different places, but it kept only showing me one item in the array. I thought it was fixed, but I was sus that my array was ever growing though I couldn't see it. Can one of you show me where to insert a simple console.log command in the current code to reveal this bug, so I can better troubleshoot myself next time?
Joao
Joao8mo ago
What I find odd is that you are calling requestAnimationFrame inside draw as well. You shouldn't need to do that and I suspect is probably responsible to some extend for the slow down. Apparently he has a Discord server as well: https://discord.gg/umD2GRy Maybe is worth asking there directly there? Not that is not ok to ask here, but I'd assume people there are more familiar with game dev
bluestreak711
bluestreak7118mo ago
Awesome, I always welcome more places from where I can ask for help and get advice. I commented out the requestAnimationFrame(); function out from the draw function and it still works. I have been going through so many revisions of this simple code just to see what works and what doesn't. There is nothing like continuously programming the same thing over and over from scratch until you become comfortable and it becomes ingrained in you. I feel really good about the above replies stating that the array is continuously growing until the script crashes. I have made some other optimizations to the code and hopefully in a few min or later tonight, I will start trying to incorporate the
js if (e.repeat) return;
js if (e.repeat) return;
part into my code. I still would like to know the appropriate place to put my consolelog("held_keys"); to see the array ever growing, so I can become a better debugger.
Joao
Joao8mo ago
Since you are updating the held_keys array on every game loop iteration I would just place that under the update function. It may be a bit too much information to spit to the console and despite what it may seem at first, logging output will definitely have negative performance impact, so make sure you delete this later. or you can even use a simple setInterval for this one time. But yeah, you are reading and writing the array multiple times per second so try to find a way to reduce that as it seems the bottleneck here.
bluestreak711
bluestreak7118mo ago
Did you look at the link to the Codepen? What is so different in the way that he sets up his code??? He also uses an array to make sure it is recorded if a key is still being pressed, so that the directions seem more fluid. Adding the
if (e.repeat) return;
if (e.repeat) return;
did prevent some slow downs. It is running better for longer, but I am still getting some slow downs. Here is my updated script.js by the way.
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

const gameSpeed = 1; // 1 = normal speed, 2 = double speed, etc.
const timeElapsed = 1000 // 1000 = miliseconds && 10000000 = nanoseconds
const gameSpeedMultiplier = timeElapsed / gameSpeed;
let fps = 60;

canvas.width = 1024;
canvas.height = 576;

const player = {
x: 250,
y: 250,
width: 50,
height: 50,
color: 'blue',
speed: 100,
};

let held_keys = [];
const keys = {
ArrowLeft: 'left',
ArrowRight: 'right',
ArrowUp: 'up',
ArrowDown: 'down'
};
let held_directions = [];

function characterMovement(delta) {
document.addEventListener('keydown', (e) => {
if (e.repeat) return;

var dir = keys[e.code];
if (dir && held_directions.indexOf(dir) === -1) {
held_directions.push(dir); // Changed unshift to push
console.log('Held directions:', held_directions);
}
});

document.addEventListener('keyup', (e) => {
// if (e.repeat) return; // I am not sure if I need this command for keyup

var dir = keys[e.code];
if (dir && held_directions.indexOf(dir) > -1) {
held_directions.splice(held_directions.indexOf(dir), 1);
console.log('Held directions:', held_directions);
}
});

}

function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = player.color;
ctx.fillRect(player.x, player.y, player.width, player.height);

// requestAnimationFrame(draw); Do NOT add this line of code here. It does not belong.
}

let lastTimestamp = 0;
let fpsInterval = gameSpeedMultiplier / fps;

function gameLoop(timestamp) {
let elapsed = timestamp - lastTimestamp;

if (elapsed > fpsInterval) {
lastTimestamp = timestamp - (elapsed % fpsInterval);

update(elapsed);
draw();
}

requestAnimationFrame(gameLoop);
}

function update(delta) {
characterMovement(delta);
// Character Functions
if (held_directions.length > 0) {
let direction = held_directions[held_directions.length - 1]; // Changed held_directions[0] to held_directions[held_directions.length - 1 ]
let movement = delta / gameSpeedMultiplier;

switch (direction) {
case 'left':
player.x -= movement * player.speed;
break;
case 'right':
player.x += movement * player.speed;
break;
case 'up':
player.y -= movement * player.speed;
break;
case 'down':
player.y += movement * player.speed;
break;
}
}
// End Character Functions
}

requestAnimationFrame(gameLoop);
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

const gameSpeed = 1; // 1 = normal speed, 2 = double speed, etc.
const timeElapsed = 1000 // 1000 = miliseconds && 10000000 = nanoseconds
const gameSpeedMultiplier = timeElapsed / gameSpeed;
let fps = 60;

canvas.width = 1024;
canvas.height = 576;

const player = {
x: 250,
y: 250,
width: 50,
height: 50,
color: 'blue',
speed: 100,
};

let held_keys = [];
const keys = {
ArrowLeft: 'left',
ArrowRight: 'right',
ArrowUp: 'up',
ArrowDown: 'down'
};
let held_directions = [];

function characterMovement(delta) {
document.addEventListener('keydown', (e) => {
if (e.repeat) return;

var dir = keys[e.code];
if (dir && held_directions.indexOf(dir) === -1) {
held_directions.push(dir); // Changed unshift to push
console.log('Held directions:', held_directions);
}
});

document.addEventListener('keyup', (e) => {
// if (e.repeat) return; // I am not sure if I need this command for keyup

var dir = keys[e.code];
if (dir && held_directions.indexOf(dir) > -1) {
held_directions.splice(held_directions.indexOf(dir), 1);
console.log('Held directions:', held_directions);
}
});

}

function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = player.color;
ctx.fillRect(player.x, player.y, player.width, player.height);

// requestAnimationFrame(draw); Do NOT add this line of code here. It does not belong.
}

let lastTimestamp = 0;
let fpsInterval = gameSpeedMultiplier / fps;

function gameLoop(timestamp) {
let elapsed = timestamp - lastTimestamp;

if (elapsed > fpsInterval) {
lastTimestamp = timestamp - (elapsed % fpsInterval);

update(elapsed);
draw();
}

requestAnimationFrame(gameLoop);
}

function update(delta) {
characterMovement(delta);
// Character Functions
if (held_directions.length > 0) {
let direction = held_directions[held_directions.length - 1]; // Changed held_directions[0] to held_directions[held_directions.length - 1 ]
let movement = delta / gameSpeedMultiplier;

switch (direction) {
case 'left':
player.x -= movement * player.speed;
break;
case 'right':
player.x += movement * player.speed;
break;
case 'up':
player.y -= movement * player.speed;
break;
case 'down':
player.y += movement * player.speed;
break;
}
}
// End Character Functions
}

requestAnimationFrame(gameLoop);
I will also see about adding back that one part of the code back to the update(); function. I did have it there in a previous version of code, but it was before I made other changes, so it will be good to try that again. Should I include the entire character movement function inside the update function or can I get away with just moving over the 'if' statement that is constantly being looped to update the character's movement? So I moved the if statement for movement that is looped from the character movement function from characterMovement to the update function. So far the slow down doesn't happen unless I am holding down more than one direction for five to ten seconds. The game does not recover until the page is refreshed, but overall performance has improved a little. If I mvoed all of the character movement under the update function, would it get messy in the code if I programmed NPC's and other things that move all under the same update code? I guess I am attempting to separate all functions that update in different categories like Character, NPC's Game Events etc the same what I separate all of that in different classes in the Java language as a way to keep the code cleaner Is that the wrong way to think about it in Javascript even though it would technially be right if I were programming Java?
Joao
Joao8mo ago
That's just OOP and makes perfect sense in any language, Java is just more oriented towards that while JS is meant to be more flexible in that regard. I would very much encourage you to use this approach for games though, at least until you reach a certain size which is where you might benefit from some optimization techniques like Object Pools or ECS design. Not sure what the difference is, could you upload your code to codepen as well? Maybe there's something you are not showing here that is responsible for the slow down.
bluestreak711
bluestreak7118mo ago
I showed all of my js code and my html and css are just bare basic.
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}

canvas {
border: 1px solid black;
}
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}

canvas {
border: 1px solid black;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JJv3</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<canvas id="gameCanvas"></canvas>
<span id="fpsCounter" style="position: absolute; top: 10px; left: 10px; font-size: 20px;"></span> <!--FPS MeterNot yet finished-->
<script src="script.js"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JJv3</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<canvas id="gameCanvas"></canvas>
<span id="fpsCounter" style="position: absolute; top: 10px; left: 10px; font-size: 20px;"></span> <!--FPS MeterNot yet finished-->
<script src="script.js"></script>
</body>
</html>
I am so glad I posted my code here. I had a virus attack yesterday and after doing a windows repair and opting to keep personal files, my jscode was missing out of all my backup folders.
Joao
Joao8mo ago
You are creating the event listeners on every frame, inside character movement function. You only need to ever declare those once, maybe at the start of the file. The e.repeat trick probably prevents them from firing all at once but that must be the issue for sure.
bluestreak711
bluestreak7118mo ago
I commented out the characterMovement tags and commented out the character movement function and everything seems to be working much better!!!! I am going to do some testing on it for a while. If it works, I am going to try to move all of the character related code to player.js for organization. I will wrap only the code, that is currently located in the update function, in the characterMovement function, so I can call that code inside of the update function from player.js. I plan to put the active listerners inside the player.js file also, but not in a loop. I will do some testing and report back in a little bit. If this solves me problem, do I mark this problem as solved and create a new post if I have an entirely different problem with the code? Now if I do wrap just the portion of the code I want to loop in a characterMovement function and just call characterMovement inside of the update function, do I still need to pass delta through the characterMovement function or should that be left out since I am already passing delta through the update function?
Joao
Joao8mo ago
If it works, I am going to try to move all of the character related code to player.js for organization.
That would be ideal, break things down into units of logic. This alone will make it much easier to spot this type of errors and help you add new features.
do I still need to pass delta through the characterMovement function
Are you actually using delta inside of character movement though? But yes, update should receive the delta and then use that inside Player wherever you need to. And yes, if there are no more issues with performance please tag this as solved and open a new thread 👍
bluestreak711
bluestreak7118mo ago
It is marked solved and I updated my code in code pen if anybody is interested. I even overlayed a fps meter over the canvas.