How can I fix keydown delays in Javascript game? - javascript

I am creating a javascript snake game, and have run into a problem when certain keys are pressed too fast in order. For example, (while going right) hitting the up arrow and then the left arrow key too fast will make my snake turn around completely and run into itself, ignoring the up arrow key press. Is there any code that would make sure that any key press will always be rendered? Thanks in advance.
let d = "RIGHT";
document.addEventListener("keydown", direction);
function direction(event) {
let key = event.keyCode;
if (key == 37 && d != "RIGHT" && d != "LEFT") {
d = "LEFT";
} else if (key == 38 && d != "DOWN" && d != "UP") {
d = "UP";
} else if (key == 39 && d != "LEFT" && d != "RIGHT") {
d = "RIGHT";
} else if (key == 40 && d != "UP" && d != "DOWN") {
d = "DOWN";
}
}
In separate function:
if (d == "LEFT") snakeX -= box;
if (d == "UP") snakeY -= box;
if (d == "RIGHT") snakeX += box;
if (d == "DOWN") snakeY += box;
You can also see this problem by going to https://jssnake.glitch.me/ and playing around a bit.

I briefly looked into your code. You render 10x a second, so if you manage to press more than one key during that interval, the described issue will occur:
For example, (while going right) hitting the up arrow and then the left arrow key too fast will make my snake turn around completely and run into itself, ignoring the up arrow key press.
There are two possible solutions:
Run the render loop faster, so that no one can ever press two keys during that interval.
Do not store only the last key, but all keys that were pressed between since the last render call.
Avoid half-turns.
I think solution 1 is not ideal as you should never say never. So let's continue with number 3 (a hack) and then with number 2 (the correct and clean way).
Avoid half-turns (alternative 3)
This little hack does not solve the root of the problem, but it will make the snake behave kind-of correct. The snake can move in 4 directions, but it can always turn only in two directions. You could either use a two-keys control to trigger CW/CCW change, something like
let currentDir = "RIGHT"; //note I renamed your d to currentDir
let nextDir = undefined;
document.addEventListener("keydown", direction);
function direction(event) {
const key = event.keyCode;
while (~currentDir) {}; //wait until the control function is finished
switch (currentDir) {
case "LEFT": nextDir = (key === 37 ? "DOWN" : (key === 39 ? "UP" : nextDir)); break;
case "UP": nextDir = (key === 37 ? "LEFT" : (key === 39 ? "RIGHT" : nextDir)); break;
case "RIGHT": nextDir = (key === 37 ? "UP" : (key === 39 ? "DOWN" : nextDir)); break;
case "DOWN": nextDir = (key === 37 ? "RIGHT" : (key === 39 ? "LEFT" : nextDir)); break;
}
}
//and later in the movement control function:
currentDir = undefined; //avoid overwriting nextDir during this update,
// i.e. the while-loop inside of direction() will wait
switch (tmp) {
case "LEFT": snakeX -= box; break;
case "UP": snakeY -= box; break;
case "RIGHT": snakeX += box; break;
case "DOWN": snakeY += box; break;
}
currentDir = nextDir;
nextDir = undefined;
The four-keys version would work in a similar way, you can easily intergrate it to your code. The key is to use the pair of currentDir and nextDir and keeping currentDir constant over the whole 0.1s time between the render calls. But your problem would kind-of stay. A snake heading right would only continue up if you would press ↑ and ← immediately after each other.
let currentDir = "RIGHT";
let nextDir = undefined;
document.addEventListener("keydown", direction);
function direction(event) {
const key = event.keyCode;
while (~currentDir) {}; //wait until the control function is finished
switch (currentDir) {
case "LEFT":
case "RIGHT":
nextDir = (key === 38 ? "UP" : (key === 40 ? "DOWN" : nextDir)); break;
case "UP":
case "DOWN":
nextDir = (key === 37 ? "LEFT" : (key === 39 ? "RIGHT" : nextDir)); break;
}
}
Keys buffer (alternative 2)
The correct solution is even easier, but requires an array. It stores all keys pressed since the last render call in a queue.
keysPressed = [];
document.addEventListener("keydown", event =>
keysPressed.push(event.keyCode); //enqueues the key pressed
Having two or three keys pressed, you could virtually update the snake position inside of the 0.1s interval applying one valid turn in each frame. This could lead to delayed snake movement if you would be able to fill the buffer quickly with commands. It can be interesting to try out as a fun excercise. The movement function for the four-keys control would look like this:
{
if (keysPressed.length > 0 {
const key = keysPresses.shift(); //dequeues the oldest key
//if there are more keys in the queue, they have to wait until next time
switch (d) {
case "LEFT":
case "RIGHT":
d = (key === 38 ? "UP" : (key === 40 ? "DOWN" : d)); break;
case "UP":
case "DOWN":
d = (key === 37 ? "LEFT" : (key === 39 ? "RIGHT" : d)); break;
}
}
switch (d) {
case "LEFT": snakeX -= box; break;
case "UP": snakeY -= box; break;
case "RIGHT": snakeX += box; break;
case "DOWN": snakeY += box; break;
}
}

What you need here is to delay the effect of a key press. You can achieve it by storing the last pressed key in a variable and only reading the key when snake is ready to turn.
let pressedKey;
document.addEventListener("keydown", event => {
pressedKey = event.keyCode;
});

Related

JavaScript precent multiple keypress action

I made a simple snake game. It works like a charm but there is a bug with multiple keypressing.
For example, when you are going to top direction and press left and down or right and down at the same time, the snake just simply dies on the same line.
So here is the code for the keypress
document.body.addEventListener('keydown', keyDown);
function keyDown(event){
//up
if(event.keyCode == 38){
if(yVelocity == 1)
return;
yVelocity = -1;
xVelocity = 0;
}
//down
if(event.keyCode == 40){
if(yVelocity == -1)
return;
yVelocity = 1;
xVelocity = 0;
}
//left
if(event.keyCode == 37){
if(xVelocity == 1)
return;
yVelocity = 0;
xVelocity = -1;
}
//right
if(event.keyCode == 39){
if(xVelocity == -1)
return;
yVelocity = 0;
xVelocity = 1;
}
}
Is there any way to prevent multiple keypressing or any other way to prevent this?
The problem is probably that the direction changes twice before the snake's shape is updated, and so the first of those two direction changes is actually ignored.
A way to overcome this, is to buffer the direction changes in a queue (implemented as array).
So in your key event handler you would not do this:
if (snake.direction != 1)
snake.direction = 3;
But instead:
if ((snake.queue.length ? snake.queue[0] : snake.direction) != 1)
snake.queue.unshift(3);
This queue should be initialised in the Snake constructor:
this.queue = [];
Then when you update the snake's position (at a time interval), you would consume that queue if there is something in it:
if (snake.queue.length)
snake.direction = snake.queue.pop();
// Now apply this direction:
// ... this would be code you already have...
You could set a maximum to this queue size, as it can get awkward if the user keeps pressing keys faster than the snake updates.

How to write a switch statement that compares multiple variables?

I'm a beginner in JS, I was hoping this code worked but it doesn’t, what would be an alternative way of accomplishing the result I'm expecting here? basically want to return something based on the combination of 3 variables/arguments. (btw, in some case statements I'm not mentioning the 'roomSize' value because I'm expecting the code to know that it's the same as the original value)
const roombaMovement = (direction , initialRoombaLocation, roomSize) => {
const roombaLocation = initialRoombaLocation
let wallHit = 0;
switch (roombaLocation && direction && roomSize) {
case [4,0] && "S" && [4,0]:
wallHit +=1;
break;
case [0,0] && "W":
wallHit +=1;
break;
case [1,0] && "S":
wallHit +=1;
break;
case [0,1] && "W":
wallHit +=1;
break;
default:
}
return wallHit;
}
Don't use switch for this - it's quite verbose and will be tedious to get the syntax to work properly for the logic you want. Since all the resulting statements to execute are the same (increment wallHit), all you need is a single (but large) if statement:
const arrsEqual = (a, b) => a.length === b.length && a.every((num, i) => num === b[i]);
const roombaMovement = (direction , initialRoombaLocation, roomSize) => {
const roombaLocation = initialRoombaLocation
let wallHit = 0;
if (
(direction === 'S' && arrsEqual(roombaLocation, [4, 0]) && arrsEqual(roomSize, [4, 0])) ||
(direction === 'W' && arrsEqual(roombaLocation, [0, 0])) ||
(direction === 'S' && arrsEqual(roombaLocation, [1, 0])) ||
(direction === 'W' && arrsEqual(roombaLocation, [0, 1]))
) {
wallHit +=1;
}
return wallHit;
}
you can also use JSON.stringify()
and compose a character string gathering all of your values:
const roombaMovement = (direction , initialRoombaLocation, roomSize) =>
{
let wallHit = 0
, testVal = `${JSON.stringify(initialRoombaLocation)} && "${direction}"`
;
if (testVal==='[4,0] && "S"')
{
testVal += ` && ${JSON.stringify(roomSize)}`;
}
switch (testVal)
{
case '[4,0] && "S" && [4,0]':
case '[1,0] && "S"':
case '[0,0] && "W"':
case '[0,1] && "W"':
++wallHit;
break;
}
return wallHit;
}
which is gives a little more readable code ;)
Taking a guess at the actual intent of the code…
Define what a wall is. For example:
const isWall = ([x, y], [roomWidth, roomHeight]) =>
x === -1
|| y === -1
|| x === roomWidth + 1 // check which of roomWidth or roomWidth + 1 is correct
|| y === roomHeight + 1; // same here
Check whether the movement brings you into one:
const roombaMovement = (direction, initialRoombaLocation, roomSize) => {
const [x, y] = initialRoombaLocation;
const [dx, dy] = direction;
const newLocation = [x + dx, y + dy];
return isWall(newLocation, roomSize);
};
here are some pointers for you:
don't change the order of your parameters. (direction , initialRoombaLocation, roomSize) vs (roombaLocation && direction && roomSize). It's just better coding to be consistent.
There's no reason to assign a variable into another variable: const roombaLocation = initialRoombaLocation, just use initialRoombaLocation.
This is not valid syntax: && "S". maybe you meant: && direction == "S"
roombaLocation == [1, 0] is not valid syntax. you will need to find a better was to check that condition.
There's no reason to use a case statement if there's only one possible result. just use a simple if statement.
good luck!

How to Make Time Buffering for keyPressed() Function in P5.js?

I made a simple Snake game in the P5.js environment,
and I have one thing that I want to fix.
So, I have this code for keyPressed() function
function keyPressed(){
if(keyCode === LEFT_ARROW) {
if(snake.yspeed == 1 || snake.yspeed == -1) {
snake.yspeed = 0;
snake.xspeed = -1;
}
}else if(keyCode === RIGHT_ARROW) {
if(snake.yspeed == 1 || snake.yspeed == -1) {
snake.yspeed = 0;
snake.xspeed = 1;
}
}else if(keyCode === UP_ARROW) {
if(snake.xspeed == 1 || snake.xspeed == -1) {
snake.xspeed = 0;
snake.yspeed = -1;
}
}else if(keyCode === DOWN_ARROW) {
if(snake.xspeed == 1 || snake.xspeed == -1) {
snake.xspeed = 0;
snake.yspeed = 1;
}
}
}
yspeed is the vertical movement, and xspeed is the horizontal movement.
So, if the snake is moving to the right, even if I pressed the LEFT_ARROW , the snake won't turn to the left. But, if I pressed UP_ARROW or DOWN_ARROW and quickly press the LEFT_ARROW, the snake will immediately turn left and hit itself.
So, I want to add a buffering time for the keyPressed() function after one of the arrow keys being pressed.
How can I do this in P5.js?
I also made a snake game and encountered this exact problem.
I wrote it in vanilla javascript and had set the fps to 12, which give the player enough time to change direction twice between two cycles.
Solution
My solution was to have a buffer variable that stores the last pressed direction. in your case, it could look like so
let dir = '';
function keyPressed(){
if(keyCode === LEFT_ARROW) dir = 'LEFT';
if(keyCode === RIGHT_ARROW) dir = 'RIGHT';
if(keyCode === UP_ARROW) dir = 'UP';
if(keyCode === DOWN_ARROW) dir = 'DOWN';
}
And actually change its direction only before I called the snake's update() method in draw():
function draw() {
...
snake.direction(dir);
snake.update();
snake.draw();
...
}
You only need to check whether or not the move is valid within the direction() method.
this.direction = function(d) {
switch(d) {
case 'LEFT':
if (this.xv != 1) {this.xv = -1; this.yv = 0;}
break;
case 'RIGHT':
if (this.xv != -1) {this.xv = 1; this.yv = 0;}
break;
case 'UP':
if (this.yv != 1) {this.xv = 0; this.yv = -1;}
break;
case 'DOWN':
if (this.yv != -1) {this.xv = 0; this.yv = 1;}
break;
default:
this.xv = this.yv = 0;
}
}
(I copied this from my code, but you get the idea)
How it works
Lets take your example.
In your game, what is happening is that it first changes the velocity of the snake to point UP, then to the LEFT, all before the next snake.update() is called.
Now all it does is change the dir variable to UP, then LEFT before the next snake.update() is called. So the next snake.direction() gets a dir argument containing LEFT, which is ignored (because the snake is pointing RIGHT) and the snake doesn't go backward on itself.
I hope this made some sense, you can check my code here if it helps.
You could try to add a time limit on the key presses, by doing something like checking that draw() was called at least once between key presses. Something like this:
var canPress = true;
function keyPressed(){
if(canPress){
// respond to key press
}
canPress = false;
}
function draw(){
// draw the frame
canPress = true;
}
But my guess is that this will make your game feel clunky and unresponsive.
Instead of restricting user input based on time, there are two general solutions for this:
Option 1: Move the snake immediately when the user presses the corresponding key. You'd do this by calling something like an update() function, both at the beginning of each frame, and when you press a key.
Option 2: Prevent the snake from moving in a direction that would kill itself immediately. You'd do this by checking whether the square you're about to go in is occupied, and ignoring the key press if so.
Which approach you take depends on how you want your game to work. You could even take a hybrid approach and do both.

Javascript switch and if statements with multiple conditions doing something really strange?

I'm trying to make a function that changes the value of the attribute x or y of the dot object four times, each time only if two conditions are met. The variable permissionSomething has to be 1, and x or y has to be more or less then a certain number, because in this case it is used for a moving dot on a canvas, which has to stay within the canvas. PermissionSomething is set to 1 if a certain key is pressed down, and changes back to the default value of -1 if that key goes up again.
var permissionLeft = -1;
var permissionRight = -1;
var permissionUp = -1;
var permissionDown = -1;
function update()
{
switch (true)
{
case permissionLeft = 1 && dot.x > 29:
dot.x--;
break;
case permissionRight = 1 && dot.x < 841:
dot.x++;
break;
case permissionUp = 1 && dot.y > 29:
dot.y--;
break;
case permissionDown = 1 && dot.y < 541:
dot.y++;
break;
}
}
If I run this, even without pressing any key, it starts to move in some strange way, stops and starts moving back and forth. If I don't press any keys permissionSomething should never be 1 so it should never move. The same thing happens if I write four if-statements like this:
if (permissionLeft = 1 && dot.x > 29) {
dot.x--};
if (.....

how to use two onkeydown events at the same time?

I'm trying to build a pong game, and I want the boards to be able to move simultaneously (one with the 's' and 'w' and the other with the up and down arrows).
function movePlayer1(event) {
if (player1.y > 7.5 && player1.y < 390) {
switch (event.keyCode) {
case 83: player1.y += player1.v;
break;
case 87: player1.y -= player1.v;
break;
}
}
else if (player1.y <= 7.5) {
switch (event.keyCode) {
case 83: player1.y += player1.v;
break;
}
}
else if (player1.y >= 390) {
switch (event.keyCode) {
case 87: player1.y -= player1.v;
break;
}
}
}
document.addEventListener("keydown", movePlayer1, false);
function movePlayer2() {
if (player2.y > 7.5 && player2.y < 390) {
switch (event.keyCode) {
case 40: player2.y += player2.v;
break;
case 38: player2.y -= player2.v;
break;
}
}
else if (player2.y <= 7.5) {
switch (event.keyCode) {
case 40: player2.y += player2.v;
break;
}
}
else if (player2.y >= 390) {
switch (event.keyCode) {
case 38: player2.y -= player2.v;
break;
}
}
}
document.addEventListener("keydown", movePlayer2, false);
I've tried putting them in one function instead but it didn't help.
There are two things make you code behave not as you expect.
Autorepeating works only for last pressed key. Say if you press "A" and hold it autorepeating will generate keydown events for "A". But if you press "B" and hold both autorepeating will generate sequential keydown automatically for "B" only. On the other side I believe for MacOS it will not autorepeat at all so better not rely on this.
But actually "keyup" are triggered correctly even if mutliple keys were pressed and are hold.
So you can refactor your code: instead of relying on keyup/keydown only you need some timer and each player model will be
{
directionIsUp: true | false,
isMoving: true | false
}
So on keydown you are setting appropriate direction and make isMoving to be true. And on keyup you are making isMoving to be false.
And timer will re-render your battlefield accordingly to those models - either moving player or keeping it at the same place.

Categories