making games - leanpubsamples.leanpub.com/making-games-sample.pdf · introduction...
TRANSCRIPT
Making Games
Christopher Pitt
This book is for sale at http://leanpub.com/making-games
This version was published on 2015-09-14
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishingprocess. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools andmany iterations to get reader feedback, pivot until you have the right book and build traction onceyou do.
©2015 Christopher Pitt
Contents
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
The Game Loop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2Setting the stage for our game . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2Creating sprites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4The Game Loop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Player Input . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8Making modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8Detecting input . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9Natural player movement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
Collision Detection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14Creating boxes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14Detecting circle collisions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17Detecting rectangle collisions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Gravity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21Cleaning up our existing code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21Adding gravity to the world . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28Allowing players to jump . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
Ladders . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34Creating our first ladder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34Allowing players to climb ladders . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37Allowing players to stand on ladders . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
IntroductionI’m a gamer. I’ve been a gamer since before I was a programmer. And yet I’ve never tried to builda game…until now. A few weeks ago, a coworker sketched a simple, beautiful platform game. Themoment I saw it, I knew I wanted to build it.
Knowing where to begin is always hard. Many books and tutorials about making games are oftenwritten in Java, C++ or some other language you haven’t learned yet. So the complexity of makinggames is increased with the need to learn new languages.
Instead I’m going to use modern JavaScript to make games for web browsers. They’re effortless touse and they work everywhere. If you’ve ever wanted to make a game, join me. We’ll start withnothing and build fun games, in no time at all.
If you have questions, feel free to ask me on Twitter¹.
¹https://twitter.com/assertchris
The Game LoopGame loops are an essential part of every game. In this post we’re going to set the stage for ourgame, by creating a solid workflow and environment. We’ll see a few helpful libraries, and renderour first game character. This is gonna be fun!
..
You can find this code at https://github.com/assertchris/making-games/tree/the-game-loop
Setting the stage for our game
There is an lot of stuff to think about, when creating a browser-based game. We’re going to learntechniques that work in many platforms and languages.
We’re also going to use a 2D rendering engine called Pixi.js². It’s not a game engine or a physicsengine. We’re still going to have to learn how physics and gameplay work, but at least we won’t bewasting time on browser inconsistencies. We can pick those apart when we have nothing better todo. For now, we’re going to create an environment for Pixi to operate in:
1 {
2 "scripts": {
3 "build": "browserify main.js -t babelify --outfile main.dist.js",
4 "watch": "watchify main.js -t babelify --outfile main.dist.js"
5 },
6 "dependencies": {
7 "pixi.js": "^3.0"
8 },
9 "devDependencies": {
10 "babel": "^5.4",
11 "babelify": "^6.1",
12 "browserify": "^10.2",
13 "watchify": "^3.2"
14 }
15 }
²http://www.pixijs.com
The Game Loop 3
..
This is from package.json
I want to be able to use ES6 features. At the time of writing, ES6 isn’t supported in any stable browser.So we’re going to cross-compile ES6 code to ES5 code that browsers can understand. The tools we’reusing for this are Babel³, Browserify⁴, and Watchify⁵.
Babel will cross-compile ES6 to ES5 for us. Browserify will connect all our ES6 modules (JavaScriptfiles) together, using import syntax. Watchify will trigger Babel/Browserify builds when our fileschange.
We’re also including the 3.0+ version of Pixi and defining two scripts. These NPM scripts⁶are just shorthand for commands we would otherwise enter in a terminal. Instead of typingbrowserify...main.dist.js we type npm run build.
1 <!doctype html>
2 <html>
3 <head>
4 <style>
5 html, body {
6 margin: 0;
7 padding: 0;
8 cursor: default;
9 background: #000;
10 overflow: hidden;
11 }
12 </style>
13 </head>
14 <body>
15 <script src="node_modules/pixi.js/bin/pixi.js"></script>
16 <script src="main.dist.js"></script>
17 </body>
18 </html>
..
This is from index.html
³https://babeljs.io/⁴http://browserify.org/⁵https://github.com/substack/watchify⁶https://docs.npmjs.com/misc/scripts
The Game Loop 4
You might be wondering if we will need to include every JavaScript file we make. We won’t.Browserify connects together all the JavaScript files imported into main.js. It then runs a Babeltransformation on them, resulting in a single file. We only need to import that file.
You may also be wondering what elements we’re going to render our game to. The code we writeto render our game will embed itself in the document body.
..
You can serve these files by going to their folder, in terminal, and running php -S 127.0.0.1:8000.This spins up a PHP test server, at a URL you can put into your browser. You’ll need that beforeyou can load in sprite images, so try it now…
Creating sprites
Sprites are a common name for visual objects in a game. We can move them around (though oftenthey are responsible for moving themselves around) on the screen. We can interact with them
Mario is a sprite, and the platforms he walks on are sprites, and the clouds in the background aresprites. Think of sprites as slices of a design file, which we paint over abstract data structures. Thoseabstract data structures have a position, and sometimes a velocity. Those abstract data structuresare what we apply game rules to. They are our power-ups and enemies.
So how do we make them? Let’s start by creating a small PNG image. We’re using PNG because itallows us to make parts of the sprite texture transparent. You could use JPEG images for your sprites,if you want to.
Then we need to create a renderer, a stage, and a sprite:
1 var renderer = new PIXI.autoDetectRenderer(
2 window.innerWidth,
3 window.innerHeight,
4 {
5 "antialias": true,
6 "autoResize": true,
7 "transparent": true,
8 "resolution": 2
9 }
10 );
11
12 document.body.appendChild(renderer.view);
13
14 var sprite = new PIXI.Sprite.fromImage("player.png");
15 sprite.x = window.innerWidth / 2;
The Game Loop 5
16 sprite.y = window.innerHeight / 2;
17
18 var stage = new PIXI.Container();
19 stage.addChild(sprite);
20
21 animate();
22
23 function animate() {
24 requestAnimationFrame(animate);
25
26 renderer.render(stage);
27 }
..
This is from main.js
Ok, there’s a lot going on here! Let’s look at this code in steps:
1. We create a renderer. Renderers are what convert Pixi abstractions (sprites, textures etc.) intocanvas or WebGL graphics. We don’t have to interact with them, but we always have to havea renderer for our Pixi stuff to show up.
We tell the renderer to take up the full width and height of the browser window. We alsotell it to anti-alias our graphics, and bump them up to retina resolution. We tell it to have atransparent background, and to resize all graphics to fit on the screen.
2. Then we create a new instance of PIXI.Sprite, using the PNG image we created earlier. Bydefault, it has a position of x = 0 and y = 0. We can position it in the centre of the screeninstead.
We have to create a root sprite container, often called a stage, and append the sprite to thestage. It’s less complicated than it sounds. Think of HTML documents. They have a root htmlelement which we add all other elements to. This is the same kind of thing.
3. Finally we create an animate function and make sure that the renderer renders our stage andsprite.
We’re using the requestAnimationFrame() function as a way rendering without blockingthe JavaScript thread. There’s a long discussion we could have about that. For now, it’s onlyimportant to know that requestAnimationFrame() happens many times a second.
The goal is for our game is to render between 30 and 60 frames per second, while doing all sorts ofplayer and world calculations in the background. This is the function we need to use for that all togo smoothly.
The Game Loop 6
The Game Loop
The game loop will control the flow of our game. It’s a repetitive process of reading input, calculatingchanges in state and rendering output to the screen.
So far we’ve set up a lot of scaffolding and all we’re doing is rendering a static image. Let’s move itaround a bit! To begin with, we’re going to create a Player class, so we can track the position of theplayer:
1 class Player {
2 constructor(sprite, x, y) {
3 this.sprite = sprite;
4 this.x = x;
5 this.y = y;
6
7 this.sprite.x = this.x;
8 this.sprite.y = this.y;
9 }
10
11 animate(state) {
12 this.x += 5;
13
14 if (this.x > window.innerWidth) {
15 this.x = 0;
16 }
17
18 this.sprite.x = this.x;
19 this.sprite.y = this.y;
20 }
21 }
..
This is from main.js
This is what JavaScript classes look like, in ES6. The Player class has a constructor, which we useto store a reference to the sprite, and the initial x and y position.
It also has an animate() method, which we will call many times a second. We can use the methodto figure out if the player needs to change position or do something else. In this case, we incrementthe sprite x position a bit. If the player/sprite moves off the right-hand side of the screen, we moveit back to the left-hand side of the screen.
We also have to change how we create the sprite:
The Game Loop 7
1 var sprite = new PIXI.Sprite.fromImage("player.png");
2
3 var player = new Player(
4 sprite,
5 window.innerWidth / 2,
6 window.innerHeight / 2
7 );
8
9 stage.addChild(sprite);
10
11 var state = {
12 "renderer": renderer,
13 "stage": stage
14 };
15
16 animate();
17
18 function animate() {
19 requestAnimationFrame(animate);
20
21 player.animate(state);
22
23 renderer.render(stage);
24 }
..
This is from main.js
Now we’re beginning to see the loop in our game. Many times a second, we’re telling our playerto animate. The player takes into account some internal logic, and adjusts its sprite. The rendererrenders the output, and what we see is a smooth animation.
..
You can see a video of this code at https://vimeo.com/137681095.
The only thing we’re lacking here is input, but we’ll get to that soon!
Player InputWhat’s the difference between a movie and a game? Player input! In fact, it’s such a critical pieceof game design that games are often defined by how they take player input.
Racing games rely on constant, subtle input. If you lift your fingers from the keyboard, or lift yourfoot off the pedal, you lose momentum. If you don’t enter the right combination of keys, your flightsimulation will crash.
Platform games (like the one we’re building) need input from both hands. Often the right-hand sideof the keyboard is for movement. Often the left-hand side of the keyboard is for player actions, likeshooting or opening doors.
Perhaps you want to design a platform game like Terraria⁷, which uses the mouse for player actionsand projective aim. Perhaps you want to use WASD to move your player around the screen. Let’sget to work!
..
You can find this code at https://github.com/assertchris/making-games/tree/player-input
Making modules
Last time we create the Player class, and made it responsible for the movement of the player. We leftit in main JavaScript file, but that’ll become messy once we have more classes. Let’s make a moduleof it:
1 class Player {
2 constructor(sprite, x, y) {
3 this.sprite = sprite;
4 this.x = x;
5 this.y = y;
6
7 this.sprite.x = this.x;
8 this.sprite.y = this.y;
9 }
10
11 animate(state) {
⁷https://terraria.org
Player Input 9
12 this.x += 5;
13
14 if (this.x > window.innerWidth) {
15 this.x = 0;
16 }
17
18 this.sprite.x = this.x;
19 this.sprite.y = this.y;
20 }
21 }
22
23 export default Player;
..
This is from player.js
The Player class is identical to the one we created before. The important bit is export default
Player. This makes the player class importable in the following way:
1 import Player from "./player";
2
3 var player = new Player(
4 sprite,
5 window.innerWidth / 2,
6 window.innerHeight / 2
7 );
This is a common pattern for splitting code (and often single classes) into their own files. It keepsthings lean, and we gain the ability to reuse code without copy and paste.
..
Remember to check that the sprite still moves across the screen. As before, you can start a testingserver with php -S 127.0.0.1:8000.
Detecting input
When I first tried detecting input, I tried to add event listeners to everything. I tried adding eventlisteners to the player and event listeners to the stage. Aaaand things got out of hand.
Player Input 10
The trick is to detect events at the highest level, and save details of them to a game state object.Last time we created this kind of state object, and passed it to the player’s animate() method. Let’sexpand on that state:
1 var state = {
2 "renderer": renderer,
3 "stage": stage,
4 "keys": {},
5 "clicks": {},
6 "mouse": {}
7 };
8
9 window.addEventListener("keydown", function(event) {
10 state.keys[event.keyCode] = true;
11 });
12
13 window.addEventListener("keyup", function(event) {
14 state.keys[event.keyCode] = false;
15 });
16
17 window.addEventListener("mousedown", function(event) {
18 state.clicks[event.which] = {
19 "clientX": event.clientX,
20 "clientY": event.clientY
21 };
22 });
23
24 window.addEventListener("mouseup", function(event) {
25 state.clicks[event.which] = false;
26 });
27
28 window.addEventListener("mousemove", function(event) {
29 state.mouse.clientX = event.clientX;
30 state.mouse.clientY = event.clientY;
31 });
32
33 animate();
34
35 function animate() {
36 requestAnimationFrame(animate);
37
38 player.animate(state);
39
40 renderer.render(stage);
41 }
Player Input 11
..
This is from main.js
We began by adding the renderer and state to the game state object. Now we’ve added keys, clicks,and mouse properties which track buttons and movement.
..
For this next part, I had to adjust the CSS of index.html so that the canvas is absolutely positioned.This made it possible to track the mouse movement more accurately. Check out the source code tosee the change.
As we add more objects to the game, we’ll pass the game state object to each of them. They canuse it to work out what they should do when the player interacts with the game. Let’s remove thecurrent player movement and add input-driven movement:
1 animate(state) {
2 if (state.keys[37]) { // left
3 this.x = Math.max(
4 0, this.x - 5
5 );
6 }
7
8 if (state.keys[39]) { // right
9 this.x = Math.min(
10 window.innerWidth - 64, this.x + 5
11 );
12 }
13
14 if (state.clicks[1]) { // left click
15 this.x = state.clicks[1].clientX;
16 }
17
18 this.sprite.x = this.x;
19 this.sprite.y = this.y;
20 }
..
This is from player.js
Player Input 12
Little actually changes in the animate() method. We check for two arrow keys, and move if theplayer and pressed one of them. We we also check if the player has clicked, and immediately movethe player if so.
..
Pay no attention to the Math functions and magic number 64. These are just there to prevent theplayer sprite from going off-screen. We’ll remove these things when we start building walls…
Natural player movement
The player can now move a bit. It doesn’t feel great though. The moment we let go of an arrow key,it just grinds to a halt. And it has only one speed: slow.
What we need is some acceleration, to allow the player to speed up in a direction. We could also usesome friction, to slow the player down when they are no longer accelerating. Let’s add these:
1 class Player {
2 constructor(sprite, x, y) {
3 this.sprite = sprite;
4 this.x = x;
5 this.y = y;
6 this.velocityX = 0;
7 this.maximumVelocityX = 12;
8 this.accelerationX = 2;
9 this.frictionX = 0.9;
10
11 this.sprite.x = this.x;
12 this.sprite.y = this.y;
13 }
14
15 animate(state) {
16 if (state.keys[37]) { // left
17 this.velocityX = Math.max(
18 this.velocityX - this.accelerationX,
19 this.maximumVelocityX * -1
20 );
21 }
22
23 if (state.keys[39]) { // right
24 this.velocityX = Math.min(
25 this.velocityX + this.accelerationX,
26 this.maximumVelocityX
Player Input 13
27 );
28 }
29
30 this.velocityX *= this.frictionX;
31
32 this.x += this.velocityX;
33
34 this.sprite.x = this.x;
35 this.sprite.y = this.y;
36 }
37 }
..
This is from player.js
Woah there, tiger! Let’s review this in steps:
1. We’ve created a handful of properties: velocity, maximum velocity, acceleration, and friction.We’ve set them to reasonable defaults. Feel free to experiment with their values until youappreciate what each of them do.
2. We can begin to track the acceleration of our player, in either direction. If we’re pressing theleft key, we start to accelerate the player in that direction. There’s a maximum accelerationthat the player can have, so they don’t continue to gain speed.
3. Without some kind of counter-force, the player will continue to move in the same direction.This is like what happens in space, where there is no air resistance or gravity to counteractmomentum.
We define this counter-force as friction, and multiply the velocity by it. Without acceleration,this means the momentum tends towards zero. It gives the appearance that the player hasstopped.
We define this counter-force as friction, and multiply the velocity by it. Without acceleration, thismeans the momentum tends towards zero. It gives the appearance that the player has stopped.
..
You can see a video of this code at https://vimeo.com/137681201.
Collision DetectionIt’s time for us to talk about collision detection. It affects obvious parts of our game, like walls wecan’t walk through. Like floors we can’t fall through. It also affects obscure parts of our game likeweapon projectiles, and checkpoints.
We’re not going to get to gravity yet. Nor are we going to look at player health or respawning. Floors,projectiles and checkpoints are interesting, but they deserve sections of their own. We’re going tocreate impassable objects. We’re going to learn ways of knowing whether two object occupy thesame space.
I’ve spent a bit of time researching this topic. It seems there are many ways of working out whethertwo things occupy the same space. Some of them are easy to explain and implement. We’ll look atthose. Other ways are not easy. They’re still cool though.
..
You can find this code at https://github.com/assertchris/making-games/tree/collision-detection
Creating boxes
The player is only one of many objects that will exist on the screen at one time. It’s a platform game,so we can expect at least one platform on screen, at a time.
Platforms have some interesting characteristics though. Sometimes they allow players to fall downthrough them. Like when you’re standing on a platform and you hold down and press jump (at thesame time). Some games take that sequence to mean that you want to fall through the platform.
Some games allow players to jump through the bottom of platforms. This enables vertical movementwithout gaps in overhead platforms.
Sometimes platforms move!
Platforms are so special that we’ll spend a few sections just implementing their different behaviours.Now we’re going to focus on another common object. The generic box.
Think of this box as an ancestor to the platform. It may share some platform functionality, but themain reason it exists to for things to collide with it. Especially things like the player.
The boxes we’re going tomakemight not even look like boxes.Whenwe get to implementing gravity,we’ll need a wide, thin box to keep the player from falling out of the world. We’ll need tall, thin
Collision Detection 15
boxes to stop the player from running off the side of the floor. We’ll make walls out of them. Wemay even make boxes out of the boxes. Big, wooden, “jump on me to get to higher things” boxes.
Ok, enough talking.
1 class Box {
2 constructor(sprite, x, y, width, height) {
3 this.sprite = sprite;
4 this.x = x;
5 this.y = y;
6 this.width = width;
7 this.height = height;
8
9 this.sprite.x = this.x;
10 this.sprite.y = this.y;
11 }
12
13 animate(state) {
14 this.sprite.x = this.x;
15 this.sprite.y = this.y;
16 }
17 }
18
19 export default Box;
..
This is from box.js
To make this class, I copy and pasted the Player class and deleted a bunch of stuff. I did have to adda width and height property to it. We’ll get to that in a bit.
Next, we need to add two of these boxes to the stage:
1 var player_sprite = new PIXI.Sprite.fromImage(
2 "player.png"
3 );
4
5 var player = new Player(
6 player_sprite,
7 window.innerWidth * 2/4,
8 200,
9 64,
10 64
Collision Detection 16
11 );
12
13 var barrel_sprite1 = new PIXI.Sprite.fromImage(
14 "barrel.png"
15 );
16
17 var barrel1 = new Box(
18 barrel_sprite1,
19 window.innerWidth * 1/4,
20 152,
21 64,
22 64
23 );
24
25 var barrel_sprite2 = new PIXI.Sprite.fromImage(
26 "barrel.png"
27 );
28
29 var barrel2 = new Box(
30 barrel_sprite2,
31 window.innerWidth * 3/4,
32 248,
33 64,
34 64
35 );
36
37 stage.addChild(player_sprite);
38 stage.addChild(barrel_sprite1);
39 stage.addChild(barrel_sprite2);
40
41 var state = {
42 "renderer": renderer,
43 "stage": stage,
44 "keys": {},
45 "clicks": {},
46 "mouse": {},
47 "objects": [
48 player,
49 barrel1,
50 barrel2
51 ]
52 };
Collision Detection 17
..
This is from main.js
That’s strange! I’ve created two new Box instances, and called them barrel. That’s because we’reabout to look at…
Detecting circle collisions
I want you to create circles for these first few boxes. The type of collision detection we’re going todo first, is with circles. It’s ok that the box has a width and height, instead of a radius. You won’tuse this kind of collision detection often, unless your platform game has a lot of circles in it.
Let’s see how this kind of detection works:
1 class Player {
2 constructor(sprite, x, y, width, height) {
3 this.sprite = sprite;
4 this.x = x;
5 this.y = y;
6 this.width = width;
7 this.height = height;
8 this.velocityX = 0;
9 this.maximumVelocityX = 12;
10 this.accelerationX = 2;
11 this.frictionX = 0.9;
12
13 this.sprite.x = this.x;
14 this.sprite.y = this.y;
15 }
16
17 animate(state) {
18 if (state.keys[37]) { // left
19 this.velocityX = Math.max(
20 this.velocityX - this.accelerationX,
21 this.maximumVelocityX * -1
22 );
23 }
24
25 if (state.keys[39]) { // right
26 this.velocityX = Math.min(
27 this.velocityX + this.accelerationX,
28 this.maximumVelocityX
Collision Detection 18
29 );
30 }
31
32 this.velocityX *= this.frictionX;
33
34 var move = true;
35
36 state.objects.forEach((object) => {
37 if (object === this) {
38 return;
39 }
40
41 var deltaX = this.x - object.x;
42 var deltaY = this.y - object.y;
43
44 var distance = Math.sqrt(
45 deltaX * deltaX + deltaY * deltaY
46 );
47
48 if (distance < this.width / 2 + object.width / 2) {
49 if (this.velocityX < 0 && object.x <= this.x) {
50 move = false;
51 }
52
53 if (this.velocityX > 0 && object.x >= this.x) {
54 move = false;
55 }
56 }
57 });
58
59 if (move) {
60 this.x += this.velocityX;
61 }
62
63 this.sprite.x = this.x;
64 this.sprite.y = this.y;
65 }
66 }
67
68 export default Player;
..
This is from player.js
Collision Detection 19
The first thing we need to do is define a width and height. While we’re pretending that our playersand boxes are circles, we only need half a width as a radius…
Next we check each object in the state. We can ignore any the player object because we don’t needto know when something collides with itself. We do check everything else though.
Circles collide when the distance between their origin is less than their combined radii. Their middlepoints are so close that their lines have to be overlapping.
We do a quick check to see whether the direction the player is moving in is where the box is. If that’sthe case then we prevent the player from moving in that direction.
Give it a go. Its pretty fun to see how non-squares block each other. Of course they all have to beperfect circles for this simple algorithm to work.
Detecting rectangle collisions
Detecting collisions of rectangles is almost as easy as circles. Go ahead and swap the barrel imagefor a box image. You can even adjust the bootstrap code to reflect the squareness of your boxes.
This time round, we’ll treat the player like a rectangle. Instead of radii, we need to check if there aregaps between the player rectangle and either box. We call this axis-aligned bounding box collisiondetection (or AABB for short).
If there is no gap, and the player wants to move in the direction of the box, then we prevent thatfrom happening:
1 var move = true;
2
3 state.objects.forEach((object) => {
4 if (object === this) {
5 return;
6 }
7
8 if (this.x < object.x + object.width &&
9 this.x + this.width > object.x &&
10 this.y < object.y + object.height &&
11 this.y + this.height > object.y) {
12
13 if (this.velocityX < 0 && object.x <= this.x) {
14 move = false;
15 }
16
17 if (this.velocityX > 0 && object.x >= this.x) {
18 move = false;
19 }
Collision Detection 20
20 }
21 });
22
23 if (move) {
24 this.x += this.velocityX;
25 }
..
This is from player.js
These are simple methods for detecting collisions, but there are others. There’s one that usesprojection-based vector mathematics⁸ to determine overlap. There’s one that checks each line, ina couple of polygons, to see if any lines intersect⁹. It’s crazy.
You could even experiment with groups of circles colliding together. That might be fun. I’m goingto run this little green circle into these little red boxes for a bit…
..
You can see a video of this code at https://vimeo.com/137681307.
⁸http://www.sevenson.com.au/actionscript/sat⁹http://stackoverflow.com/questions/9043805/test-if-two-lines-intersect-javascript-function
GravityThis time we’re going to work on our code structure, and add gravity to our game. We’ve alreadydone most of the work for gravity.
..
You can find this code at https://github.com/assertchris/making-games/tree/gravity
Cleaning up our existing code
We need to clean up a few things! First, let’s swap out x, y, width, and height (both in Box andPlayer) with PIXI.Rectangle. They have these properties, but they also interact with the rest ofPIXI in interesting ways.
1 class Box {
2 constructor(sprite, rectangle) {
3 this.sprite = sprite;
4 this.rectangle = rectangle;
5 }
6
7 animate(state) {
8 if (this.sprite) {
9 this.sprite.x = this.rectangle.x;
10 this.sprite.y = this.rectangle.y;
11 }
12 }
13 }
14
15 export default Box;
..
This is from box.js
Gravity 22
1 class Player {
2 constructor(sprite, rectangle) {
3 this.sprite = sprite;
4 this.rectangle = rectangle;
5
6 this.velocityX = 0;
7 this.maximumVelocityX = 12;
8 this.accelerationX = 2;
9 this.frictionX = 0.9;
10 }
11
12 animate(state) {
13 if (state.keys[37]) {
14 this.velocityX = Math.max(
15 this.velocityX - this.accelerationX,
16 this.maximumVelocityX * -1
17 );
18 }
19
20 if (state.keys[39]) {
21 this.velocityX = Math.min(
22 this.velocityX + this.accelerationX,
23 this.maximumVelocityX
24 );
25 }
26
27 this.velocityX *= this.frictionX;
28
29 var move = true;
30
31 state.objects.forEach((object) => {
32 if (object === this) {
33 return;
34 }
35
36 var me = this.rectangle;
37 var you = object.rectangle;
38
39 if (me.x < you.x + you.width &&
40 me.x + me.width > you.x &&
41 me.y < you.y + you.height &&
42 me.height + me.y > you.y) {
43
44 if (this.velocityX < 0 && you.x <= me.x) {
45 move = false;
Gravity 23
46 }
47
48 if (this.velocityX > 0 && you.x >= me.x) {
49 move = false;
50 }
51 }
52 });
53
54 if (move) {
55 this.rectangle.x += this.velocityX;
56 }
57
58 this.sprite.x = this.rectangle.x;
59 this.sprite.y = this.rectangle.y;
60 }
61 }
62
63 export default Player;
..
This is from player.js
Notice how much code we can delete? The comparisons get a little verbose, but nothing a localvariable or two can’t fix. I also realised that we canmove the initial x and y set operations to animate.
Next, I want to encapsulate the event, renderer and stage logic into a Game class:
1 class Game {
2 constructor() {
3 this.state = {
4 "keys": {},
5 "clicks": {},
6 "mouse": {},
7 "objects": []
8 };
9 }
10
11 get stage() {
12 if (!this._stage) {
13 this._stage = this.newStage();
14 }
15
16 return this._stage;
Gravity 24
17 }
18
19 set stage(stage) {
20 this._stage = stage;
21 }
22
23 newStage() {
24 return new PIXI.Container();
25 }
26
27 get renderer() {
28 if (!this._renderer) {
29 this._renderer = this.newRenderer();
30 }
31
32 return this._renderer;
33 }
34
35 set renderer(renderer) {
36 this._renderer = renderer;
37 }
38
39 newRenderer() {
40 return new PIXI.autoDetectRenderer(
41 window.innerWidth,
42 window.innerHeight,
43 this.newRendererOptions()
44 );
45 }
46
47 newRendererOptions() {
48 return {
49 "antialias": true,
50 "autoResize": true,
51 "transparent": true,
52 "resolution": 2
53 };
54 }
55
56 animate() {
57 var caller = () => {
58 requestAnimationFrame(caller);
59
60 this.state.renderer = this.renderer;
61 this.state.stage = this.stage;
Gravity 25
62
63 this.state.objects.forEach((object) => {
64 object.animate(this.state);
65 })
66
67 this.renderer.render(this.stage);
68 };
69
70 caller();
71
72 return this;
73 }
74
75 addEventListenerToElement(element) {
76 element.addEventListener("keydown", (event) => {
77 this.state.keys[event.keyCode] = true;
78 });
79
80 element.addEventListener("keyup", (event) => {
81 this.state.keys[event.keyCode] = false;
82 });
83
84 element.addEventListener("mousedown", (event) => {
85 this.state.clicks[event.which] = {
86 "clientX": event.clientX,
87 "clientY": event.clientY
88 };
89 });
90
91 element.addEventListener("mouseup", (event) => {
92 this.state.clicks[event.which] = false;
93 });
94
95 element.addEventListener("mousemove", (event) => {
96 this.state.mouse.clientX = event.clientX;
97 this.state.mouse.clientY = event.clientY;
98 });
99
100 return this;
101 }
102
103 addRendererToElement(element) {
104 element.appendChild(this.renderer.view);
105
106 return this;
Gravity 26
107 }
108
109 addObject(object) {
110 this.state.objects.push(object);
111
112 if (object.sprite) {
113 this.stage.addChild(object.sprite);
114 }
115
116 return this;
117 }
118 }
119
120 export default Game;
..
This is from game.js
Notice the ES6 class getters and setters. They’re useful for filling optional dependencies as needed.We can override renderer and stage if we need to, but they have sensible defaults too.
This is mostly just moving the code that was in main.js to game.js. The only notable difference isthat we no longer require sprites to be added to the stage separately to adding objects to the state.
I’m in two minds about this. On the one hand, it’s much clearer what’s going on if we add themto the stage by hand. On the other, will we be adding sprites (joined to objects) without adding theobjects? I don’t think so.
Perhaps we’ll come back and change that. For now, it makes things a little cleaner. So what doesmain.js look like now?
1 import Game from "./game";
2 import Player from "./player";
3 import Box from "./box";
4
5 var game = new Game();
6
7 game.addObject(
8 new Box(
9 new PIXI.Sprite.fromImage(
10 "box.png"
11 ),
12 new PIXI.Rectangle(
13 window.innerWidth * 1/4, 152, 64, 64
Gravity 27
14 )
15 )
16 );
17
18 game.addObject(
19 new Player(
20 new PIXI.Sprite.fromImage(
21 "player.png"
22 ),
23 new PIXI.Rectangle(
24 window.innerWidth * 2/4, 200, 64, 64
25 )
26 )
27 );
28
29 game.addObject(
30 new Box(
31 new PIXI.Sprite.fromImage(
32 "box.png"
33 ),
34 new PIXI.Rectangle(
35 window.innerWidth * 3/4, 248, 64, 64
36 )
37 )
38 );
39
40 game.addEventListenerToElement(window);
41 game.addRendererToElement(document.body);
42 game.animate();
..
This is from main.js
That looks much better! We have enough control to set starting points for each game object. Butafter the first frame, the animate methods take over. Things like gravity and collisions will start tocontrol how the game progresses. It was always going to be like that, so this file now feels like it too.
We could confine the events and renderer to a smaller set of elements. We could add any numberof objects to the game from here. Everything else game-related is inside the game class. Everythingelse player or box related is inside those classes. It’s neat!
Gravity 28
Adding gravity to the world
One of the things that make platform games fun is a moderate amount of physics are at play. Let’sadd walls and a floor:
1 var game = new Game();
2
3 game.addObject(
4 new Box(
5 null,
6 new PIXI.Rectangle(
7 -10, 0, 10, window.innerHeight
8 )
9 )
10 );
11
12 game.addObject(
13 new Box(
14 null,
15 new PIXI.Rectangle(
16 0, window.innerHeight, window.innerWidth, 10
17 )
18 )
19 );
20
21 game.addObject(
22 new Box(
23 null,
24 new PIXI.Rectangle(
25 window.innerWidth, 0, 10, window.innerHeight
26 )
27 )
28 );
29
30 game.addObject(
31 new Player(
32 new PIXI.Sprite.fromImage(
33 "player.png"
34 ),
35 new PIXI.Rectangle(
36 window.innerWidth * 2/4, 200, 64, 64
37 )
38 )
39 );
Gravity 29
..
This is from main.js
Instead of two red squares, we have three thin rectangles. They collide in the same way. Now, let’ssee about adding that gravity…
1 class Player {
2 constructor(sprite, rectangle) {
3 this.sprite = sprite;
4 this.rectangle = rectangle;
5
6 this.velocityX = 0;
7 this.maximumVelocityX = 12;
8 this.accelerationX = 2;
9 this.frictionX = 0.9;
10
11 this.velocityY = 0;
12 this.maximumVelocityY = 30;
13 this.accelerationY = 10;
14 this.jumpVelocity = -50;
15 }
16
17 animate(state) {
18 if (state.keys[37]) {
19 this.velocityX = Math.max(
20 this.velocityX - this.accelerationX,
21 this.maximumVelocityX * -1
22 );
23 }
24
25 if (state.keys[39]) {
26 this.velocityX = Math.min(
27 this.velocityX + this.accelerationX,
28 this.maximumVelocityX
29 );
30 }
31
32 this.velocityX *= this.frictionX;
33
34 this.velocityY = Math.min(
35 this.velocityY + this.accelerationY,
36 this.maximumVelocityY
37 );
Gravity 30
38
39 state.objects.forEach((object) => {
40 if (object === this) {
41 return;
42 }
43
44 var me = this.rectangle;
45 var you = object.rectangle;
46
47 if (me.x < you.x + you.width &&
48 me.x + me.width > you.x &&
49 me.y < you.y + you.height &&
50 me.y + me.height > you.y) {
51
52 if (this.velocityY > 0 && you.y >= me.y) {
53 this.velocityY = 0;
54 return;
55 }
56
57 if (this.velocityY < 0 && you.y <= me.y) {
58 this.velocityY = this.accelerationY;
59 return;
60 }
61
62 if (this.velocityX < 0 && you.x <= me.x) {
63 this.velocityX = 0;
64 return;
65 }
66
67 if (this.velocityX > 0 && you.x >= me.x) {
68 this.velocityX = 0;
69 return;
70 }
71 }
72 });
73
74 this.rectangle.x += this.velocityX;
75 this.rectangle.y += this.velocityY;
76
77 this.sprite.x = this.rectangle.x;
78 this.sprite.y = this.rectangle.y;
79 }
80 }
81
82 export default Player;
Gravity 31
..
This is from player.js
We begin by creating a set of properties to match the ones we made to track horizontal movement.We don’t need vertical friction, since that level of detail is often omitted from platform games.
We also have to track vertical and horizontal collisions. When the collision is with the player and aplatform/floor then we stop the downward velocity. When it’s against a ceiling, we replace upwardsvelocity with the force of gravity.
Allowing players to jump
Jumping is simply reversing gravity for a short time:
1 animate(state) {
2 if (state.keys[37]) {
3 this.velocityX = Math.max(
4 this.velocityX - this.accelerationX,
5 this.maximumVelocityX * -1
6 );
7 }
8
9 if (state.keys[39]) {
10 this.velocityX = Math.min(
11 this.velocityX + this.accelerationX,
12 this.maximumVelocityX
13 );
14 }
15
16 this.velocityX *= this.frictionX;
17
18 this.velocityY = Math.min(
19 this.velocityY + this.accelerationY,
20 this.maximumVelocityY
21 );
22
23 state.objects.forEach((object) => {
24 if (object === this) {
25 return;
26 }
27
28 var me = this.rectangle;
Gravity 32
29 var you = object.rectangle;
30
31 if (me.x < you.x + you.width &&
32 me.x + me.width > you.x &&
33 me.y < you.y + you.height &&
34 me.y + me.height > you.y) {
35
36 if (this.velocityY > 0 && you.y >= me.y) {
37 this.velocityY = 0;
38 this.grounded = true;
39 this.jumping = false;
40 return;
41 }
42
43 if (this.velocityY < 0 && you.y <= me.y) {
44 this.velocityY = this.accelerationY;
45 return;
46 }
47
48 if (this.velocityX < 0 && you.x <= me.x) {
49 this.velocityX = 0;
50 return;
51 }
52
53 if (this.velocityX > 0 && you.x >= me.x) {
54 this.velocityX = 0;
55 return;
56 }
57 }
58 });
59
60 if (state.keys[32] && !this.jumping && this.grounded) {
61 this.velocityY = this.jumpVelocity;
62 this.jumping = true;
63 this.grounded = false;
64 }
65
66 this.rectangle.x += this.velocityX;
67 this.rectangle.y += this.velocityY;
68
69 this.sprite.x = this.rectangle.x;
70 this.sprite.y = this.rectangle.y;
71 }
Gravity 33
..
This is from player.js
With this, we’ve mapped the space key to jump. We add the keyboard check after the collision checkbecause we only want our player to jump if they are standing on a platform or floor.
Now it’s possible to create levels, out of boxes. To give some of them visible textures, and to jumparound them. Spend some time making a level and jumping around in it!
..
You can see a video of this code at https://vimeo.com/137466509.
LaddersIt would be pretty limiting if players could onlymove upwards by jumping through gaps, or on boxes.Those aren’t the only options though. We still have to learn about elevators, stairs, and ladders. Solet’s build a ladder!
..
You can find this code at https://github.com/assertchris/making-games/tree/ladders
Creating our first ladder
Let’s begin our ladder by copying the Box class:
1 class Ladder {
2 constructor(sprite, rectangle) {
3 this.sprite = sprite;
4 this.rectangle = rectangle;
5 }
6
7 animate(state) {
8 if (this.sprite) {
9 this.sprite.x = this.rectangle.x;
10 this.sprite.y = this.rectangle.y;
11 }
12 }
13 }
14
15 export default Ladder;
..
This is from ladder.js
Ladders 35
1 import Ladder from "./ladder";
2
3 game.addObject(
4 new Ladder(
5 new PIXI.Sprite.fromImage(
6 "ladder.png"
7 ),
8 new PIXI.Rectangle(
9 200, window.innerHeight - 192, 64, 192
10 )
11 )
12 );
13
14 game.addObject(
15 new Box(
16 new PIXI.Sprite.fromImage(
17 "box.png"
18 ),
19 new PIXI.Rectangle(
20 136, window.innerHeight - 191, 64, 64
21 )
22 )
23 );
..
This is from main.js
You’ll need to make a ladder.png image. I chose to use a simple 64 by 192 sprite. Be sure to add thisto the game before adding the player, or the ladder sprite will be in front of the player sprite.
You’ll notice that the player bumps into the ladder, as if it was a box. We’ll need to add a few gettersto our boxes and ladders to that the collision detection can decide whether they still collide witheach other…
Ladders 36
1 if (me.x < you.x + you.width &&
2 me.x + me.width > you.x &&
3 me.y < you.y + you.height &&
4 me.y + me.height > you.y) {
5
6 if (object.collides && this.velocityY > 0 && you.y >= me.y) {
7 this.velocityY = 0;
8 this.grounded = true;
9 this.jumping = false;
10 return;
11 }
12
13 if (object.collides && this.velocityY < 0 && you.y <= me.y) {
14 this.velocityY = this.accelerationY;
15 return;
16 }
17
18 if (object.collides && this.velocityX < 0 && you.x <= me.x) {
19 this.velocityX = 0;
20 return;
21 }
22
23 if (object.collides && this.velocityX > 0 && you.x >= me.x) {
24 this.velocityX = 0;
25 return;
26 }
27 }
..
This is from player.js
We give Box and Ladder a collides property so that Player.animate and ignore collisions withobjects that players shouldn’t collide with. If we were going to allow multiple players in the samegame/level thenwewould also add a collides property to Player. That is unless wewantedmultipleplayers to collide with each other.
..
You can see a video of this code at https://vimeo.com/137673699.
Ladders 37
Allowing players to climb ladders
For players to climb ladders, we have to be able to tell if they’re trying to climb one. We also thenhave to suspend gravity and side motion so they down fall off or slide off:
1 class Player {
2 constructor(sprite, rectangle) {
3 this.sprite = sprite;
4 this.rectangle = rectangle;
5
6 this.velocityX = 0;
7 this.maximumVelocityX = 12;
8 this.accelerationX = 2;
9 this.frictionX = 0.9;
10
11 this.velocityY = 0;
12 this.maximumVelocityY = 20;
13 this.accelerationY = 5;
14 this.jumpVelocity = -40;
15
16 this.climbingSpeed = 10;
17 }
18
19 animate(state) {
20 if (state.keys[37]) {
21 this.velocityX = Math.max(
22 this.velocityX - this.accelerationX,
23 this.maximumVelocityX * -1
24 );
25 }
26
27 if (state.keys[39]) {
28 this.velocityX = Math.min(
29 this.velocityX + this.accelerationX,
30 this.maximumVelocityX
31 );
32 }
33
34 this.velocityX *= this.frictionX;
35
36 this.velocityY = Math.min(
37 this.velocityY + this.accelerationY,
38 this.maximumVelocityY
39 );
40
Ladders 38
41 var onLadder = false;
42
43 state.objects.forEach((object) => {
44 if (object === this) {
45 return;
46 }
47
48 var me = this.rectangle;
49 var you = object.rectangle;
50
51 if (me.x < you.x + you.width &&
52 me.x + me.width > you.x &&
53 me.y < you.y + you.height &&
54 me.y + me.height > you.y) {
55
56 if (object.collides && this.velocityY > 0 && you.y >= me.y) {
57 this.velocityY = 0;
58 this.grounded = true;
59 this.jumping = false;
60 return;
61 }
62
63 if (object.collides && this.velocityY < 0 && you.y <= me.y) {
64 this.velocityY = this.accelerationY;
65 return;
66 }
67
68 if (object.collides && this.velocityX < 0 && you.x <= me.x) {
69 this.velocityX = 0;
70 return;
71 }
72
73 if (object.collides && this.velocityX > 0 && you.x >= me.x) {
74 this.velocityX = 0;
75 return;
76 }
77
78 if (object.constructor.name === "Ladder") {
79 onLadder = true;
80
81 if (state.keys[38] || state.keys[40]) {
82 this.grounded = false;
83 this.jumping = false;
84 this.climbing = true;
85 this.velocityY = 0;
Ladders 39
86 this.velocityX = 0;
87 }
88
89 if (state.keys[38]) {
90 this.rectangle.y -= this.climbingSpeed;
91 }
92
93 if (state.keys[40] && me.y + me.height < you.y + you.height) {
94 this.rectangle.y += this.climbingSpeed;
95 }
96 }
97 }
98 });
99
100 if (!onLadder) {
101 this.climbing = false;
102 }
103
104 if (state.keys[38] && this.grounded) {
105 this.velocityY = this.jumpVelocity;
106 this.jumping = true;
107 this.grounded = false;
108 }
109
110 this.rectangle.x += this.velocityX;
111
112 if (!this.climbing) {
113 this.rectangle.y += this.velocityY;
114 }
115
116 this.sprite.x = this.rectangle.x;
117 this.sprite.y = this.rectangle.y;
118 }
119 }
120
121 export default Player;
..
This is from player.js
We begin by adding a new property, called climbing. This will be true when the player is busyclimbing the ladder. We also create a local onLadder variable, so we can tell if they’re standing stillon a ladder.
Ladders 40
During the usual collision detection, we note whether the object the player is colliding with is aladder or not. If so, and they are pressing the up arrow, we start them climbing. this.climbing isonly set if they are pressing the up arrow, which is why we need that local variable.
We reset player velocity, and the properties related to jumping. We also directly alter the playerrectangle. If the up arrow is being pressed, we move the player up. If down is pressed then we onlymove the player down until the player’s bottom is lower than the ladder’s bottom. This preventsfloor-level ladders from allowing the player to ignore the floor collision.
..
You can see a video of this code at https://vimeo.com/137674792.
Allowing players to stand on ladders
There’s still a problem with our ladders. The moment we reach the top, we fall down again. We’vedisabled collisions for ladders, so we need a way of standing at the top of them so we can jump/moveto the next platform. Let’s move the collision logic into boxes and ladders:
1 class Box {
2 collides(state) {
3 var collides = false;
4
5 state.objects.forEach((object) => {
6 if (object.constructor.name !== "Player") {
7 return;
8 }
9
10 let edges = this.getEdges(this, object);
11
12 if (!(
13 edges.boxTop > edges.playerBottom ||
14 edges.boxRight < edges.playerLeft ||
15 edges.boxBottom < edges.playerTop ||
16 edges.boxLeft > edges.playerRight
17 )) {
18 collides = true;
19 return;
20 }
21 });
22
23 return collides;
24 }
Ladders 41
25
26 getEdges(box, player) {
27 return {
28 "boxLeft" : box.rectangle.x,
29 "boxRight" : box.rectangle.x + box.rectangle.width,
30 "boxTop" : box.rectangle.y,
31 "boxBottom" : box.rectangle.y + box.rectangle.height,
32 "playerLeft" : player.rectangle.x,
33 "playerRight" : player.rectangle.x + player.rectangle.width,
34 "playerTop" : player.rectangle.y,
35 "playerBottom" : player.rectangle.y + player.rectangle.height
36 };
37 }
38
39 collidesInDirection(box, player) {
40 let edges = this.getEdges(box, player);
41
42 let offsetLeft = edges.playerRight - edges.boxLeft;
43 let offsetRight = edges.boxRight - edges.playerLeft;
44 let offsetTop = edges.playerBottom - edges.boxTop;
45 let offsetBottom = edges.boxBottom - edges.playerTop;
46
47 if (Math.min(offsetLeft, offsetRight, offsetTop, offsetBottom) === offsetTop) {
48 return "down";
49 }
50
51 if (Math.min(offsetLeft, offsetRight, offsetTop, offsetBottom) === offsetBottom) {
52 return "up";
53 }
54
55 if (Math.min(offsetLeft, offsetRight, offsetTop, offsetBottom) === offsetLeft) {
56 return "right";
57 }
58
59 if (Math.min(offsetLeft, offsetRight, offsetTop, offsetBottom) === offsetRight) {
60 return "left";
61 }
62
63 return "unknown";
64 }
65
66 constructor(sprite, rectangle) {
67 this.sprite = sprite;
68 this.rectangle = rectangle;
69 }
Ladders 42
70
71 animate(state) {
72 if (this.sprite) {
73 this.sprite.x = this.rectangle.x;
74 this.sprite.y = this.rectangle.y;
75 }
76 }
77 }
78
79 export default Box;
..
This is from box.js
The Box class now calculates whether it is colliding with the player. Once we detect a collision,between a player and box, we can run them through collidesInDirection. We’ll get a neat UTF-8arrow pointing in the direction the player was moving before the collision happened.
While making this change, it occurred to me that the Ladder class isn’t doing anything different tothe box class. The climbing logic happens in the Player class, so the Ladder class may as well extendthe Box class:
1 import Box from "./box";
2
3 class Ladder extends Box {
4
5 }
6
7 export default Ladder;
..
This is from ladder.js
The Player class is looking a lot cleaner, after moving the collision logic out:
Ladders 43
1 animate(state) {
2 if (state.keys[37]) {
3 this.velocityX = Math.max(
4 this.velocityX - this.accelerationX,
5 this.maximumVelocityX * -1
6 );
7 }
8
9 if (state.keys[39]) {
10 this.velocityX = Math.min(
11 this.velocityX + this.accelerationX,
12 this.maximumVelocityX
13 );
14 }
15
16 this.velocityX *= this.frictionX;
17
18 this.velocityY = Math.min(
19 this.velocityY + this.accelerationY,
20 this.maximumVelocityY
21 );
22
23 var onLadder = false;
24
25 state.objects.forEach((object) => {
26 if (object === this) {
27 return;
28 }
29
30 if (object.collides(state)) {
31 let type = object.constructor.name;
32 let direction = object.collidesInDirection(object, this);
33
34 if (type === "Ladder") {
35 onLadder = true;
36
37 let player = this.rectangle;
38 let ladder = object.rectangle;
39
40 if (state.keys[38] || state.keys[40]) {
41 this.grounded = false;
42 this.jumping = false;
43 this.climbing = true;
44 this.velocityY = 0;
45 this.velocityX = 0;
Ladders 44
46 }
47
48 if (state.keys[38]) {
49 let limit = ladder.y - this.rectangle.height + 1;
50
51 this.rectangle.y = Math.max(
52 this.rectangle.y -= this.climbingSpeed,
53 limit
54 );
55
56 if (player.y === limit) {
57 this.grounded = true;
58 this.jumping = false;
59 }
60 }
61
62 if (state.keys[40] && player.y + player.height < ladder.y + ladder.height)\
63 {
64 this.rectangle.y += this.climbingSpeed;
65 }
66
67 return;
68 }
69
70 if (type === "Box") {
71 if (direction === "down") {
72 this.velocityY = 0;
73 this.grounded = true;
74 this.jumping = false;
75 }
76
77 if (direction === "up") {
78 this.velocityY = this.accelerationY;
79 }
80
81 if (direction === "left" && this.velocityX < 0) {
82 this.velocityX = 0;
83 }
84
85 if (direction === "right" && this.velocityX > 0) {
86 this.velocityX = 0;
87 }
88 }
89 }
90 });
Ladders 45
91
92 if (!onLadder) {
93 this.climbing = false;
94 }
95
96 if (state.keys[38] && this.grounded && !this.climbing) {
97 this.velocityY = this.jumpVelocity;
98 this.jumping = true;
99 this.grounded = false;
100 }
101
102 this.rectangle.x += this.velocityX;
103
104 if (!this.climbing) {
105 this.rectangle.y += this.velocityY;
106 }
107
108 this.sprite.x = this.rectangle.x;
109 this.sprite.y = this.rectangle.y;
110 }
..
This is from player.js
The ladder logic is mostly unchanged, except that the player will never move above the highestladder pixel. That’s how we stop the player from falling once they reach the top. This means you’llneed to make your ladders start one pixel above platforms you expect players to move onto. It alsomeans players can’t jump off of ladders.
The box collision logic is much cleaner and simpler now! And the result? Working ladders.
..
You can see a video of this code at https://vimeo.com/137680213.