Moving around like a platformer
So now let's discuss moving our player around in a 2d platformer style and dealing with some common forces. The player needs some boundaries, so they can't just wander off the board. Let's draw them a ground box.
let GROUND_HEIGHT = 10
const ground = (color) => {
box(color, 0, CANVAS.height - GROUND_HEIGHT, CANVAS.width, CANVAS.height)
}
Now we're going to use a very simple collision detection for the ground. Since we know the position of our hero in it's x and y data, we can just check if it's on or past the ground plane and if so, it's .vy
becomes 0, stopping gravity like the ground should. We also place the Hero on the ground exactly, so there's no chance it'll drift. If the ground was quicksand, you might add a multiplier to the velicity that's quite low to stop it as it sticks inside.
const move = () => {
if (KEYS_PRESSED["ArrowUp"]) {
HERO.vy -= ACCEL;
}
if (KEYS_PRESSED["ArrowLeft"]) {
HERO.vx -= ACCEL;
}
if (KEYS_PRESSED["ArrowRight"]) {
HERO.vx += ACCEL;
}
if (KEYS_PRESSED["ArrowDown"]) {
HERO.vy += ACCEL;
}
HERO.vy += 1; // Gravity
// Collision
const groundPosition = CANVAS.height - GROUND_HEIGHT - HERO.h
if (HERO.y > groundPosition) {
HERO.vy = 0;
HERO.vx *= .8 // Ground friction
HERO.y = groundPosition
}
else {
HERO.vy *= .9 // Air friction
HERO.vx *= .9
}
HERO.x += HERO.vx;
HERO.y += HERO.vy;
}
The ground and air have multipliers as well. We can see that if they are under 1, they eventually bring the player to a stop. If they're over one, they'll add velocity to the player, like those arrows in Mario Kart. The closer the number is to 1, the less it'll take away and multiplying by 1, of course does nothing to the velocity. So we can mimic a kind of air resistance with something over .9 and under 1. The ground has more friction than the air, so I set it at .8.
Tweaking the Numbers
Right from the start, you can see how much fun messing with the numbers is going to be. This course takes an intentional deep look at movement and with movement especially you'll find that the whole game feels different, better or worse for these numbers. So, we need a way to adjust them as the game is happening to find exactly the numbers you'll hard cord in later. Let's set that up with a great little library called dat.gui.js.
Dat GUI is a self-contained javascript library that is most often paired with Three.JS, which we'll cover later. Right now we're going to make some of our variables, updatable as the game plays. The best way to dial in the numbers as you play. In the same javascript file we can quickly add user interface sliders to control nearly everything if we want.
import * as dat from "/js/lib/three/modules/libs/dat.gui.module.js"
const gui = new dat.GUI();
var folder1 = gui.addFolder('World');
const WORLD = {
gravity: 1,
airFriction: .9,
groundFriction: .8,
}
gui.add(WORLD, 'gravity', 0, 2);
gui.add(WORLD, 'airFriction', 0.0, 1.0);
gui.add(WORLD, 'groundFriction', 0.0, 1.0);
var folder2 = gui.addFolder('Hero');
gui.add(HERO, 'radius', 1, 100);
Now let's update the move function, so it can use the WORLD object instead. Notice how we're turning those "magic numbers" of the world into proper self-documenting code with these descriptors.
HERO.vy += WORLD.gravity;
if (HERO.y > groundPosition) {
HERO.vy = 0;
HERO.vx *= WORLD.groundFriction
HERO.y = groundPosition
}
else {
HERO.vy *= WORLD.airFriction
HERO.vx *= WORLD.airFriction
}
So now we see the ball is affected by our sliders. And if we make our player's acceleration high enough to beat gravity and airFriction, well, we have a flying player, ever pulled down. If we lose gravity all together but up the airFriction, we have movement through water. I mean, we've alrady got something pretty fun here. We just need to keep the player on the screen. Lots of ways of doing that, like following him with the Camera and having the player appear on the other side, like Pacman or Joust. But for now we'll just keep it inside the screen with the following simple check.
// Collision
const ground = CANVAS.height - WORLD.groundHeight - HERO.radius
HERO.x += HERO.vx;
HERO.y += HERO.vy;
if (HERO.y > ground) {
HERO.vy = 0;
HERO.vx *= WORLD.groundFriction // ground friction
HERO.y = ground
}
if (HERO.y < HERO.radius) {
HERO.y = HERO.radius
}
if (HERO.x < HERO.radius) {
HERO.x = HERO.radius
}
if (HERO.x > (CANVAS.width - HERO.radius)) {
HERO.x = (CANVAS.width - HERO.radius)
}
else {
HERO.vy *= WORLD.airFriction // Air friction
HERO.vx *= WORLD.airFriction
}
In Sum
Okay, so on the one hand, it's just a circle on a box, but now the world has forces that push and pull the player around the board. We've added a simple user interface that allows us to adjust the world and invent new kinds of movement. We also boxed our player in, so they don't fly off the screen. There's a lot more to cover, but I hope this gives you a better idea of how simple something like wind or motion in water can be to recreate in game code.