DEV Community

Cover image for Character Controls
Zevan Rosser
Zevan Rosser

Posted on

Character Controls

What you'll be making:

Moving a character around the screen using arrow keys or gestures/taps is a key element of creating games. Let's dig in and see one way to do this...

let x = 300;
let y = 300;
let vx = 0;
let vy = 0;
function loop() {
  console.log('game loop');
  requestAnimationFrame(loop);
}
loop();
Enter fullscreen mode Exit fullscreen mode

The above is the core of what we'll be needing to make our character jump around the screen.

Let's pop it into codepen:

Let's add a div to represent the character:

const char = document.createElement('div');
Enter fullscreen mode Exit fullscreen mode

and give it some styles:

Object.assign(char.style, {
  position: 'absolute',
  width: '50px',
  height: '80px',
  background: 'black'
});
Enter fullscreen mode Exit fullscreen mode

Ok, let's make the div move and see what these four variables we defined are going to be used for.

In our loop, I'm going to do away with the test console log and replace it with some updates to the div styles. I'll also change the variables a bit:

let x = 100;
let y = 100;
let vx = 2;
let vy = 2;
function loop() {
  x += vx;
  y += vy;
  Object.assign(
    char.style, {
      left: x + 'px',
      top: y + 'px'
    }
  );
  requestAnimationFrame(loop);
}
loop();
Enter fullscreen mode Exit fullscreen mode

If you don't see anything, click the re-run button

Why does it move?

The x and y variables are where we want to store the location of the character (char div). In the above example we start x at 100. When the loop function is first run we then add vx to x:

x += vx;
// x = x + vx
Enter fullscreen mode Exit fullscreen mode

Since we've set vx to 2. This means that x becomes 102. We then set the left property on the div to be x + 'px' and we get our fist step of animation.:

left: x + 'px';
Enter fullscreen mode Exit fullscreen mode

It's pretty annoying to have to add px all the time like that. There are ways to get rid of it, but for simplicity I'm just going to leave it for now.

Object.assign is a pretty common, somewhat verbose function... If you don't know what that is, think of it as an easy way to define or update multiple properties of an object. That is an overly simplified description, so if you want more info go over to MDN and read up on it. I am just using here as an alternative to:

char.style.left = x + 'px';
char.style.top = y +'px';
Enter fullscreen mode Exit fullscreen mode

requestAnimationFrame is a way to call a function repeatedly at approximately 60 frames per second. If you want to use requestAnimationFrame, generally you call it at the end of the function you want to repeatedly call, passing it that same function that you wish to repeat:

function loop() {
  console.log('loop...');
  requestAnimationFrame(loop);
}
loop(); // start the loop
Enter fullscreen mode Exit fullscreen mode

You'll need to call the function once to start the loop off.

Controlling Velocity

With all that out of the way, try changing the initial values of vx and vy few times. See if you can intuit what they are doing and why they cause the char div to move in the direction that it does.

Velocity Directions

What you'll notice is a negative vx values moves the char left and a positive one moves it right. If vx is -5 and vy is just 0, the character will just move to the left:


If you don't see anything, click the re-run button

...and set vx to 5 to move to the right:


If you don't see anything, click the re-run button

A larger vx value will cause the char div to move faster and a smaller one will cause it to move slower. When vx and vy values are both set you can move things diagonally. Take our first example where the char moved down and to the right. This was because we had vx and vy both set to 2. If we were to look at these values we would see:

x += vx
100 = 100 + 2
// 102

x += vx
102 = 102 + 2
// 104

x += vx
106 = 106 + 2
// 106
Enter fullscreen mode Exit fullscreen mode

The y value would be identical, so each frame were are moving to the right 2 pixels and down 2 pixels. If you set vx and vy to -2 it would go up and to the left diagonally.

Why Velocity Variables?

You might wonder why we put velocity into variables. Why not just do:

x += 5;
y += 5;
Enter fullscreen mode Exit fullscreen mode

The answer is that having them in variables enables us to change them over time. For instance if we multiple the velocity values be a number less than 1, we will effectively shrink the velocity values over time:

x += vx;
y += vy;

vx *= .93;
vy *= .93;
Enter fullscreen mode Exit fullscreen mode


click re-run to watch again

In the same way, we could speed up velocity, reverse velocity etc... just by using a little math.

Here is how to add something like gravity:

let vx = 4;
let vy = -10;
Enter fullscreen mode Exit fullscreen mode

Starting off moving to the right with a vx of 4 and shooting up a little bit fast with a vy of -10. If we increase vy over time, by adding 1 each frame we will see a nice arc:

x += vx;
y += vy;

vy += 1;
Enter fullscreen mode Exit fullscreen mode


rerun to watch again

The vx value is starting off negative and slowly increasing -10, -9, -8, -7... 0, 1, 2, 3, 4 ... 99 forever. This is what causes the arc. Taking some time to play with this and understand it better might be useful.

The character currently shoots off the screen, let's prevent this by reversing vy:

if (y > innerHeight - 80) {
  vy *= -1;
  y = innerHeight - 80;
}
Enter fullscreen mode Exit fullscreen mode

The minus 80 is just from the original 80px height we gave the character div. We'll clean that code up in a bit, leaving this way for now.

Now the character is sort of jumping and going off the right hand side of the screen. Let's dampen the "bounciness" a bit by multiplying vy by a negative decimal value, this will reverse vy but also shrink it:

if (y > innerHeight - 80) {
  vy *= -.33;
  y = innerHeight - 80;
}
Enter fullscreen mode Exit fullscreen mode

We can also kill off some x velocity by halfing vx each time the character hits the ground.

if (y > innerHeight - 80) {
  vy *= -.33;
  vx *= .5;
  y = innerHeight - 80;
}
Enter fullscreen mode Exit fullscreen mode

Key Controls

Ok! You may want to take some time and play with what you've learned so far, but if you are feeling like everything makes sense, let's add some key controls.

One super annoying thing about key listeners in the browser is that if you say hit the SPACE key, you'll notice that the keydown event fires once, then there is a delay and then it continues to fire at equal intervals. This doesn't cut it for games at all, as it adds an annoying lag. We can avoid this by keeping track of which keys are down and updating our graphics in our game loop, instead of when the keydown event fires.

document.addEventListener('keydown', e => {
  console.log(e.key)
})
Enter fullscreen mode Exit fullscreen mode

The above will show us a string version of the name of the key that is down. In this case we want to use the arrow keys, so we'll look for ArrowLeft, ArrowRight etc..

If we were to hardcode some checks for these it looks like this:

let leftKey;
let rightKey;
let downKey;
let upKey;
document.addEventListener('keydown', e => {
  e.preventDefault();

  if (e.key === 'ArrowLeft') {
    leftKey = true
  } 
  if (e.key === 'ArrowRight') {
    rightKey = true
  }
  if (e.key === 'ArrowDown') {
    downKey = true
  }
  if (e.key === 'ArrowUp') {
    upKey = true
  }
})
document.addEventListener('keyup', e => {
  e.preventDefault();

  if (e.key === 'ArrowLeft') {
    leftKey = false
  } 
  if (e.key === 'ArrowRight') {
    rightKey = false
  }
  if (e.key === 'ArrowDown') {
    downKey = false
  }
  if (e.key === 'ArrowUp') {
    upKey = false
  }
})
Enter fullscreen mode Exit fullscreen mode

I'll show in a bit how to make that less repetitive/ugly. For now, I'm just hardcoding it so it's easy to understand. The preventDefault method is preventing any keys from doing normal browser behaviors like scrolling the page etc...

Armed with our arrow key variables. We can now check if a key is down during the main loop using:

if (rightKey) {
  vx += 3;
}
Enter fullscreen mode Exit fullscreen mode

Here we check if the right key is down and alter the x velocity to move the character to the right. All they keys follow this pattern except the up key, which needs some special logic. Have a look, you may need to click the area where the character is in order to give the keyboard focus:

Fully Working Demo

The only trick here is handling the ground. We don't want to be able to cause the character to jump, unless it is on the floor (otherwise the character will sort of be able to fly). To achieve this we add an additional check when looking at the state of the up key:

if (upKey && y >= innerHeight - 80) {
  vy -= 15;
}
Enter fullscreen mode Exit fullscreen mode

This really highlights the fact that we want to put things like innerHeight - 80 into variables. In this case a variable called floor. NOTE: you can resize the window and things will still work, the character will drop or rise up to level with the floor. Have a look on codepen to see what I mean.

That's the main part of this tutorial. Now it's about making the code a bit more realistic. I'm also going to allow the character to go off the right of the screen and re-appear on the left etc...

if (x > innerWidth + 50) {
  x = -50
}
Enter fullscreen mode Exit fullscreen mode

That will handle going off the right side of the screen and shooting back from the left... for going off the left hand side of the screen:

if (x < -50) { 
  x = innerWidth + 50;
}
Enter fullscreen mode Exit fullscreen mode

Now I'm going to tidy everything up with variables and a few tricks and then walk through the key aspects of the changes I've made. Have a look at the new version:

Read through that code, a fair amount has changed. Mostly just moving things into variables for readability. The main change/improvement is the way the keys are handled now. Instead of a bunch of if statements, I use an object to keep track of which keys are down:

// dynamically handle keys instead of explicitly
// checking each one
const keys = {}
document.addEventListener('keydown', e => {
  e.preventDefault();

  // store a boolean on the `keys` object
  // to keep track of whick keys are down
  keys[e.key] = true;
});

document.addEventListener('keyup', e => {
  e.preventDefault();
   keys[e.key] = false;
});
Enter fullscreen mode Exit fullscreen mode

If a key is down, I set keys[e.key] = true. So in the case of ArrowLeft. This is equivalent to saying:

keys.ArrowLeft = true
Enter fullscreen mode Exit fullscreen mode

If you don't already know, you can use strings to reference properties on an object using an associative array type syntax:

keys['ArrowLeft'] = true
Enter fullscreen mode Exit fullscreen mode

This is the same as using the "dot" syntax keys.ArrowLeft = true... but allows for a property of keys to be reference dynamically. If the property is not there, the first time we set it, it gets created.

Enjoy playing around with this - there is much more that can be done, platforms, enemies, power-ups etc... most of that stuff can be done with variations on what I've shown here...

Header Image Version

I added some trails to this, just to make a more interesting screenshot for the article header.

If you're feeling creative - see if you can adjust the above pen so that the trails don't just disappear when the character goes from one side of the screen to another...

See more code over @ Snippet Zone

Top comments (0)