Search code examples
jquerycanvaskeypresskeydownkeyup

Smooth tiled game character movement with $().keydown


I want to implement character movement in my tile game such that it does not spam the server with key events. Currently I have throttled the amount of events sent by using setInterval.

Problems with my idea:

  1. If the player does not tap a key at the right time, the movement might not work

  2. Continuously tapping a key(right arrow for example), would only send key events when the setInterval is picking up the event. This makes it look buggy.

Good things with my code:

  1. It works perfectly, if all you are doing is holding the key.

  2. You can move diagonally by holding two keys at a time. The code stores which keys are being pressed.

Things that might help:

  1. Instead of a throttle on the key events, a debounce would work nicely. I simply don't want players spamming the keys to move faster than they otherwise could by holding the key down. (I haven't had much success with debounce codes)

Here is a stripped down basic example of my code: http://jsfiddle.net/empXx/66/

var canvas = document.getElementById('c'),
    ctx = canvas.getContext("2d"),
    tileSize = 10;
var holdingUp = false;
var holdingDown = false;
var holdingLeft = false;
var holdingRight = false;
var cx = 150;
var cy = 150;

$(document).keydown(function(e) {
    if(e.which==37) { holdingLeft = true; }
    if(e.which==38) { holdingUp = true; }
    if(e.which==39) { holdingRight = true; }
    if(e.which==40) { holdingDown = true;  }
});
$(document).keyup(function(e) {
    if(e.which==37) { holdingLeft = false; }
    if(e.which==38) { holdingUp = false; }
    if(e.which==39) { holdingRight = false; }
    if(e.which==40) { holdingDown = false; }
});

ctx.fillRect(150, 150, tileSize, tileSize);
setInterval(function() {
    if(holdingUp) {
        cls();
        ctx.fillRect(cx, cy - tileSize, tileSize, tileSize);
        cy -= tileSize;
    } else if(holdingDown) { 
        cls();
        ctx.fillRect(cx, cy + tileSize, tileSize, tileSize);
        cy += tileSize;
    }
    if(holdingLeft) {
        cls();
        ctx.fillRect(cx - tileSize, cy, tileSize, tileSize); 
        cx -= tileSize;
    } else if(holdingRight) {
        cls();
        ctx.fillRect(cx + tileSize, cy, tileSize, tileSize);
        cx += tileSize;
    }
}, 120);

function cls() { ctx.clearRect(0, 0, canvas.width, canvas.height); }

How could I make this game interaction smoother, and more enjoyable for the user, while keeping down the spam?


Solution

  • Thanks to John S for the original fix. I altered the code a bit to make it even better. Currently if a user is holding both right and left, the player will not move. This is a simpler solution than handling key precedence depending on the order they where pressed. I also made it so that the upCount/downCount etc, cannot be altered when holdingUp & holdingDown are active. Same for left right motions.

    Simplified jsfiddle with bare basics here

    The production game with this code implemented + anti spam/cheat detection is here

    JSFiddle code, as required by SO:

    var canvas = document.getElementById('c'),
        ctx = canvas.getContext("2d"),
        tileSize = 10;
    var holdingUp = false;
    var holdingDown = false;
    var holdingLeft = false;
    var holdingRight = false;
    var cx = 150;
    var cy = 150;
    
    var upCount = 0;
    var downCount = 0;
    var leftCount = 0;
    var rightCount = 0;
    
    $(document).keydown(function(e) {
        if(e.which==37 && !holdingLeft) { holdingLeft = true; leftCount++; }
        if(e.which==38 && !holdingUp) { holdingUp = true; upCount++; }
        if(e.which==39 && !holdingRight) { holdingRight = true; rightCount++; }
        if(e.which==40 && !holdingDown) { holdingDown = true; downCount++;  }
    });
    $(document).keyup(function(e) {
        if(e.which==37) { holdingLeft = false; }
        if(e.which==38) { holdingUp = false; }
        if(e.which==39) { holdingRight = false; }
        if(e.which==40) { holdingDown = false; }
    });
    
    ctx.fillRect(150, 150, tileSize, tileSize);
    setInterval(function() {
        if(!(holdingUp && holdingDown)) {
            if((upCount > 0) || holdingUp) {
                if (upCount > 0) { upCount--; }
                cls();
                ctx.fillRect(cx, cy - tileSize, tileSize, tileSize);
                cy -= tileSize;
            } else if((downCount > 0) || holdingDown) { 
                if (downCount > 0) { downCount--; }
                cls();
                ctx.fillRect(cx, cy + tileSize, tileSize, tileSize);
                cy += tileSize;
            }
        } else {
            downCount = upCount = 0;
        }
        if(!(holdingLeft && holdingRight)) {
            if((leftCount > 0) || holdingLeft) {
                if (leftCount > 0) { leftCount--; }
                cls();
                ctx.fillRect(cx - tileSize, cy, tileSize, tileSize); 
                cx -= tileSize;
            } else if((rightCount > 0) || holdingRight) {
                if (rightCount > 0) { rightCount--; }
                cls();
                ctx.fillRect(cx + tileSize, cy, tileSize, tileSize);
                cx += tileSize;
            }
        } else {
            leftCount = rightCount = 0;
        }
    }, 120);
    
    function cls() { ctx.clearRect(0, 0, canvas.width, canvas.height); }