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();
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');
and give it some styles:
Object.assign(char.style, {
position: 'absolute',
width: '50px',
height: '80px',
background: 'black'
});
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();
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
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';
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';
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
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
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;
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;
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;
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;
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;
}
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;
}
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;
}
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)
})
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
}
})
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;
}
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;
}
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
}
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;
}
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;
});
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
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
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)