From 1ef7d682d12179b31f3ea4d25f8681d4c538eefd Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 10 Apr 2016 18:20:17 -0700 Subject: [PATCH 1/2] Revert "remove gamedev" This reverts commit 56191fee44422e3230bc76b9f54b1168b65b77db. --- gamedev/README.md | 47 ++++ gamedev/part1.md | 428 +++++++++++++++++++++++++++++ gamedev/part2.md | 121 ++++++++ gamedev/part3.md | 194 +++++++++++++ gamedev/part4.md | 150 ++++++++++ gamedev/part5.md | 60 ++++ gamedev/screenshots/1-sprites.png | Bin 0 -> 98409 bytes gamedev/screenshots/2-overlap.png | Bin 0 -> 7326 bytes gamedev/screenshots/3-points.png | Bin 0 -> 1955 bytes gamedev/screenshots/endproduct.gif | Bin 0 -> 198288 bytes gamedev/screenshots/run-button.png | Bin 0 -> 206823 bytes 11 files changed, 1000 insertions(+) create mode 100644 gamedev/README.md create mode 100644 gamedev/part1.md create mode 100644 gamedev/part2.md create mode 100644 gamedev/part3.md create mode 100644 gamedev/part4.md create mode 100644 gamedev/part5.md create mode 100644 gamedev/screenshots/1-sprites.png create mode 100644 gamedev/screenshots/2-overlap.png create mode 100644 gamedev/screenshots/3-points.png create mode 100644 gamedev/screenshots/endproduct.gif create mode 100644 gamedev/screenshots/run-button.png diff --git a/gamedev/README.md b/gamedev/README.md new file mode 100644 index 0000000..44380f6 --- /dev/null +++ b/gamedev/README.md @@ -0,0 +1,47 @@ +# Game Dev + +## What you'll make + + +Playable demo: http://codepen.io/davepvm/full/PNOyrW/ + +## Setup + +We're going to use **Codepen** to make our game. Codepen is an online code editor and community for web development technologies, which Javascript is a part of. + +To be able to save and share your code, go to https://codepen.io/signup, scroll down to **Free Plan**, and sign up. + +Then, come back to this page, and go to http://codepen.io/pen?template=ONJQya&editors=0010 to set up a new project with p5.js and p5.play.js added already. Additionally, there is a small amount of CSS included for you to change the page background colour. + +You likely want to add a "run" button to Codepen so you don't have to reload the page each time or wait for it to automatically rerun your changed code. Click "Settings" in the top-right, go to the Behavior tab and uncheck "auto update preview" to get a run button. + + + +## Vocabulary +- **sprite**: a graphic that we can move around the screen as a single entity +- **canvas**: the region on the screen that we can draw graphics on +- **vector**: same as in math or physics! An angle and a magnitude + +## Instructions +1. Drawing on the screen and Interactivity +2. Gravity and collision detection +3. Different kinds of blocks using Inheritance +4. Adding points and lives +5. Scrolling the camera + +## Next Steps +- Change/animate the character sprite for different states +- Make multiple levels +- Make simple enemies and make the character shoot +- Add powerups +- Make a title screen, menu, and save progress +- Add sound + +## Extend it further +- Change how the game physics works as a core game mechanic. Instead of jumping, maybe the direction of gravity changes? +- Instead of manually making levels, make a game that generates random landscapes for you to explore +- Add a second character controlled by WASD + - Make levels that can only be passed by cooperating. Maybe one character needs to stand on a button to hold a door open for the other? + - Make a game where characters play against each other. Maybe each character has a flag at each end of the screen and the goal is to reach the other flag and bring it back to your own before the other player does? +- Add variable skill levels to your character where you can choose to spend points on upgrading different parts for different abilities +- Come up with an interesting story and non-playable characters to the game who you can talk to or interact with diff --git a/gamedev/part1.md b/gamedev/part1.md new file mode 100644 index 0000000..ac1f6b7 --- /dev/null +++ b/gamedev/part1.md @@ -0,0 +1,428 @@ +# Game Dev +## Part 1: Drawing on the Screen +Back + +### How do game visuals work? + +In games and movies, it looks like the things on the screen on the moving. The movement you see is actually just a bunch of images with small changes played back in rapid succession, and then your brain interprets this as movement. + +A classic example of this is "The Horse In Motion", which was made in 1878 by taking photos of a racehorse at different points in time. Played back, the horse appears to move. + + + +*Frames of The Horse In Motion, from Wikimedia* + + + +*The frames, played back, from Wikimedia* + +In a game, the same thing applies. The movement in a game will be made by the computer drawing slightly different images together fast enough to give the illusion of motion. Unlike in real life, where objects move on its own for us to capture, in a game, the computer needs to figure out where to put everything in the next frame before it can draw it. + +In general, a minimum **frame rate** of 18 frames per second is required for images to appear to be moving to the human brain. Games tend to run at higher rates of 30 or 60 frames per second because reaction time can be central to gameplay, making it important to have more detailed motion. This means that 60 times per second, the computer will run a program to recalculate the new positions of objects, draw an image, and display it on the screen. + +### Javascript crash course + +So we know we're going to have to figure out where to move each object in every update in every frame. Let's see what tools we have at our disposal! + +Our game will be made in Javascript using a graphics library called p5.js. Here are the main parts of Javascript you will be using to make your game: + +#### Variables +Variables **hold values**. You can assign values to them. You reference them by a name you give them. The name can be made of letters, numbers, and underscores (as long as it doesn't start with a number). +```js +// Declaring a variable +var myNumber; + +// Assign a value to a variable +myNumber = 5; + +// Assign a new value to the variable +myNumber = myNumber + 2; +``` + +You can print a variable to the console (Ctrl-Shift-J in Chrome for Windows, Command-Option-J in Chrome for Mac) to help you debug your programs: +```js +console.log("Hey there!"); // Outputs "Hey there!" + +var something = 2; +console.log(something); // Outputs 2 +``` + +There are different default types of variables you can use: +```js +var a = 2.5; // Number +var b = true // Boolean (true or false) +var c = "You have 10 lives left"; // String (a sequence of letters) +var d = [4, 6, 25, 1.5, 5, b, c, "something"]; // Array (a list of other variables) +// Object (a collection of values that each have a unique name) +var d = { + artist: "Eminem", + album: "Slim Shady EP", + title: "The Real Slim Shady", + rating: 11 +}; +var e = new Sprite(); // Object that is an instance of class "Sprite". +// A class is like an object that comes premade with properties and functions +// you can use. We'll talk later about making our own classes. +``` + +To access the first element in an array, you would write `myArray[0]` (the first element is the 0th element.) The second is `myArray[1]`. The last is `myArray[ myArray.length - 1 ]`. + +To access a property in an object by its name (its key), use `myObject["key"]` or `myObject.key`. + +As you can see, each statement is followed by a semicolon. + +#### Conditionals +You can choose to only do something if a condition is met. In javascript, that looks like this: +``` +if (condition) { + // condition is met, do something +} + +// or + +if (condition) { + // condition is met, do something +} else { + // condition is not met, do something else +} + +// or + +if (condition) { + // condition is met, do something +} else if (anotherCondition) { + // first condition is not met but the second is + // (you can chain as many else ifs as you want) +} else { + // none of the above conditions are met + // (you don't need to specify a final else if you don't want to) +} + +// or +while (condition) { + // condition is met, do something. + + // After doing something, it will check the condition again and + // run the code in the brackets again if the condition is still + // true. This will continue until the condition is not met when + // it gets checked. +} +``` + +Condition blocks don't have semicolons after them, only the statements inside and around them do. + +The condition in parentheses is a logical expression, allowing you to perform checks on variables. Here are some examples: +```js +if (x == y) { } // Check if equal +if (x != y) { } // Check if not equal +if (x > y) { } // Check greater than +if (x <= y) { } // Check less than or equal to +if (x) { } // Check "truthiness" (if x is true or is defined and not equal to 0) +if (!x) { } // Check "falsiness" (if x is undefined or 0) +``` + +You can combine multiple conditions using boolean algebra: +```js +if (conditionA && conditionB) { } // If both are true +if (conditionA || conditionB) { } // If either is true + +// A complex example using brackets to show the precedence of expressions +if (!(conditionA || (conditionB && conditionC))) { } +``` + +#### Functions +You can store repetitive code in a function to make it easier to run. When you call an expression with **arguments**, the **parameters** in the function definition will be replaced by the values and variables that were passed in. Functions can optionally **return a value** that you can assign to a variable. +```js +// a and b are parameters +// | +// v +function addNumbers(a, b) { + return a + b; +} + +// 1 and 2 are arguments +// | +// v +var sum = addNumbers(1, 2); // sum is now equal to 3 +``` + +**IMPORTANT NOTE:** when you pass variables into a function, usually the function gets a copy of the variable. However, when you pass an object, array, or function, the function receives **the same** instance, so if you modify it, it won't only be modified inside the function. + +```js +function addOne(num) { + num = num+1; +} +var a = 5; +addOne(a); +// a is still equal to 5 since addOne received a copy of the number + +function addOneToProperty(obj) { + obj.num = obj.num+1; +} +var b = {num: 5}; +addOneToProperty(b); +// watch out! b is now {num: 6} +``` + +You can also pass a function into another function. Some class types have methods that accept functions. For example, arrays have a method `forEach` which accepts a function and runs that function for each item in the array. +```js +var sum = 0; +var items = [1, 2, 3, 4]; +items.forEach(function(item) { + sum = sum + item; +}); +// Sum is now 10 +``` + +#### Debugging practice +To demonstrate how `forEach` works and practice debugging, let's try to fix some broken code. Here's a function that is supposed to take the average of an array of numbers. It is designed to first find the sum of the numbers and then divide by the number of items: +```js +function average(numbers) { + var result = 0; + var numItems = 0; + numbers.forEach(function(n) { + result = result + n; + numItems = numItems + 1; + result = result / numItems; + }); + return result; +} + +var avg = average([1, 2, 3, 4, 5]); +console.log("End result: " + avg); +``` + +To edit this program, go to http://codepen.io/davepvm/pen/mPqQRW?editors=0010 and open the console (Ctrl-shift-J on chrome for Windows, Cmd-option-J on Chrome for Mac). You should see this result: + +``` +VM1284 console_runner-ba402f0….js:1 End result: 1.275 +``` + +Obviously, 1.275 isn't the right answer. Let's add some more logging as we go through so we can see how the program gets executed: +```js +function average(numbers) { + console.log("Starting!"); + var result = 0; + var numItems = 0; + numbers.forEach(function(n) { + console.log("Looking at item: " + n); + result = result + n; + console.log("Result is now " + result); + numItems = numItems + 1; + console.log("Numitems is now " + numItems); + result = result / numItems; + console.log("Result is " + result + " after dividing"); + }); + return result; +} +``` + +After we run this, we can see more logs (at any time, you can press the circle with a line through it to clear the console): +``` +VM1284 console_runner-ba402f0….js:1 Starting! +VM1284 console_runner-ba402f0….js:1 Looking at item: 1 +VM1284 console_runner-ba402f0….js:1 Result is now 1 +VM1284 console_runner-ba402f0….js:1 Numitems is now 1 +VM1284 console_runner-ba402f0….js:1 Result is 1 after dividing +VM1284 console_runner-ba402f0….js:1 Looking at item: 2 +VM1284 console_runner-ba402f0….js:1 Result is now 3 +VM1284 console_runner-ba402f0….js:1 Numitems is now 2 +VM1284 console_runner-ba402f0….js:1 Result is 1.5 after dividing +VM1284 console_runner-ba402f0….js:1 Looking at item: 3 +VM1284 console_runner-ba402f0….js:1 Result is now 4.5 +VM1284 console_runner-ba402f0….js:1 Numitems is now 3 +VM1284 console_runner-ba402f0….js:1 Result is 1.5 after dividing +VM1284 console_runner-ba402f0….js:1 Looking at item: 4 +VM1284 console_runner-ba402f0….js:1 Result is now 5.5 +VM1284 console_runner-ba402f0….js:1 Numitems is now 4 +VM1284 console_runner-ba402f0….js:1 Result is 1.375 after dividing +VM1284 console_runner-ba402f0….js:1 Looking at item: 5 +VM1284 console_runner-ba402f0….js:1 Result is now 6.375 +VM1284 console_runner-ba402f0….js:1 Numitems is now 5 +VM1284 console_runner-ba402f0….js:1 Result is 1.275 after dividing +VM1284 console_runner-ba402f0….js:1 End result: 1.275 +``` + +Now we see the problem, we're dividing by the number of items in every iteration of the loop instead of once at the end. Try moving the division to the end: +```js +function average(numbers) { + console.log("Starting!"); + var result = 0; + var numItems = 0; + numbers.forEach(function(n) { + console.log("Looking at item: " + n); + result = result + n; + console.log("Result is now " + result); + numItems = numItems + 1; + console.log("Numitems is now " + numItems); + }); + result = result / numItems; + console.log("Result is " + result + " after dividing"); + return result; +} +``` + +Here's the new result: +``` +VM1284 console_runner-ba402f0….js:1 Starting! +VM1284 console_runner-ba402f0….js:1 Looking at item: 1 +VM1284 console_runner-ba402f0….js:1 Result is now 1 +VM1284 console_runner-ba402f0….js:1 Numitems is now 1 +VM1284 console_runner-ba402f0….js:1 Looking at item: 2 +VM1284 console_runner-ba402f0….js:1 Result is now 3 +VM1284 console_runner-ba402f0….js:1 Numitems is now 2 +VM1284 console_runner-ba402f0….js:1 Looking at item: 3 +VM1284 console_runner-ba402f0….js:1 Result is now 6 +VM1284 console_runner-ba402f0….js:1 Numitems is now 3 +VM1284 console_runner-ba402f0….js:1 Looking at item: 4 +VM1284 console_runner-ba402f0….js:1 Result is now 10 +VM1284 console_runner-ba402f0….js:1 Numitems is now 4 +VM1284 console_runner-ba402f0….js:1 Looking at item: 5 +VM1284 console_runner-ba402f0….js:1 Result is now 15 +VM1284 console_runner-ba402f0….js:1 Numitems is now 5 +VM1284 console_runner-ba402f0….js:1 Result is 3 after dividing +VM1284 console_runner-ba402f0….js:1 Sum: 3 +``` +There we go, now we get the righ answer! You can take out the `console.log`s out now. + +Now we're ready to start making our game! + +### Setting up our game's visuals + +In your Javascript editor, we will set up our game in the following structure: + +```js +// Declare variables + +function setup() { + // Assign to variables + + // Set up initial state +} + +function draw() { + // Calculate new positions of objects + + // Accept user input (keypresses, etc) + + // Redraw screen +} +``` + +In p5.js, everything happens inside the two functions `setup` and `draw`. p5.js looks specifically for these, so their names can't be changed. `setup()` is called once at the beginning when all of the javascript is loaded and ready to go, and `draw()` is called every frame of the game. + +We want to declare our variables **outside** of the functions so that both functions can "see" them and access them. We only want to assign values to the variables in `setup` though, because we only know for sure that everything has been loaded once `setup` gets run. + +Inside `setup`, we can define the size of the canvas with `createCanvas(width, height)`. A decent starting size might be something like 500 pixels by 400 pixels. (Note: if you have a retina display, one "pixel" might be more than one physical pixel in reality because your browser keeps the size of a "pixel" consistent across devices.) + +So, how do we add some objects into the game? + +Objects in the game are called **Sprites**. Assuming you declared a variable called `myObject`, you might assign a Sprite instance to it like this: +```js +myObject = createSprite(x, y, width, height); +``` + +To specify where on the screen an object will be rendered, we refer to its location in x and y coordinates, where `x == 0` on the left side of the canvas, `x == width` on the right, `y == 0` at the top, and `y == height` at the bottom. For those familiar with Cartesian planes from math class, this isn't quite what you're used to, since the y values get more positive as you go down instead of the getting more negative. The x and y coordinates specified when creating a sprite will be the **center of the sprite.** + +In `draw`, we can call `drawSprites()` to render the sprites we made to the screen. + +Let's start by adding some ground (a long rectangle) and a character to represent the player (a smaller rectangle on top of the ground) as onto the screen. + + + +When you press **Run** in the toolbar on Codepen, you should see your sprites render! + + +You'll notice that every time you hit Run, the colours change. p5.js picks random colours for you if none are specified, so let's specify our own. The canvas can be coloured with `background(red, green, blue)` and sprite colours can be changed with `sprite.shapeColor = color(red, green, blue)`. The parameters `red`, `green`, `blue` are integers from 0 to 255 that specify how much light of each component colour should be mixed to create the overall colour. You might find colorpicker.com useful for finding RGB values to put in these. + +We need to assign a sprite's `shapeColor` only once in `setup`, since it will get rendered every frame with `drawSprites()` in the `draw` function. However, `background(red, green, blue)` simply draws over the screen with a colour, so we will want to run this every frame. + +The order you draw matters! If you want the sprites to appear, you need to draw them after first drawing the background since `background` draws over everything. + + + +### Interactivity and motion + +Next, let's add some interactivity and movement. + +We initialized each sprite with a location, and this location is accessible through `sprite.position.x` and `sprite.position.y`. If we wanted to make the sprite move every frame, we could update the coordinates of the sprite each frame with something like `sprite.position.x += 5;`, but there is a better way. + +The sprite has a **velocity** vector, which we can set **once**, and the sprite will automatically add it to its position each frame. We can set it like `sprite.velocity = createVector(x, y)` or by assigning number values to `sprite.velocity.x` and `sprite.velocity.y`. Try setting an initial velocity for your character and watch it fly offscreen! + + + +You can rerun your code or reload the page (after saving!) to reset the game's state. Let's make this be triggered by a keypress instead of all the time. Inside the `draw` function, we have access to two kinds of key press listeners we can use to change the positions of the objects before we draw them: +- `keyDown(key)`: This will return true if the key is currently down on a given frame. +- `keyWentDown(key)`: This will only return true if the key went down on the current frame, and will be false if the key is down but has been held from a previous frame. + +Let's make our character move when arrow keys are pressed. When a direction key such as `'RIGHT_ARROW'` is down, set the character's velocity, else if another direction is pressed, set a different velocity, else reset the horizontal velocity to zero. When the up arrow first goes down, we can set a negative value for the vertical velocity to simulate jumping. + +In the code describing the order in which we will do our calculations, I put the section for accepting user input after calculating the other positions of objects. It doesn't make a difference yet, but we want to do this anyway because later on when we are doing collision detection, we will want to have logic that depends on positions already being calculated. For example, when the up arrow is pressed, we will only want to let the player jump if they are currently on the ground, which we need to have calculated beforehand. + + + +Now you should be able to move left and right, and fly up in the air when hitting the up key. There's no gravity yet, we're going to do that next! You can rerun the javascript or save and reload the page to reset the game. + +Here is a list of possible values for the `key` parameter in case you want to attach events to different kinds of key presses. + +### What you should have so far +Here's a link to a Codepen project completed up to this step, in case you fall behind: http://codepen.io/davepvm/pen/NNWyrV?&editors=0010 + +Part 2: Gravity and collision detection + +Back diff --git a/gamedev/part2.md b/gamedev/part2.md new file mode 100644 index 0000000..0799627 --- /dev/null +++ b/gamedev/part2.md @@ -0,0 +1,121 @@ +# Game Dev +## Part 2: Gravity and collision detection +Back + +### Creating gravity + +If you remember from physics class, on the earth, gravity accelerates all objects towards the center of the planet at the same rate (approximately 9.81 meters per second squared, depending on your altitude and location.) If you look at the units, this means that every second, the velocity of an object under Earth's gravity will increase by 9.81 meters per second. + +In general, `velocity := acceleration * time` (where `:=` means "is defined as"; this is not valid Javascript.) To simulate this, in our game, we're going to define a gravity vector, and every frame, we are going to add this vector to the player's velocity vector. Our gravity vector shouldn't affect the horizontal movement of the character, so its x component should be 0, and its y component should be positive to cause the player to move downwards. Start with a small y value such as 1 and play around until it feels right. + +Adding to the velocity vector of a sprite can be done with `sprite.velocity.add(anotherVector);`. + +### Checking for collisions + +In the game's current state, there is nothing actually stopping the player from simply falling through the ground. Every frame, after calculating new positions and before accepting user input, we will run a **collision detection check**. Seeing if two sprites are touching is simple. In our game setup, we will make two **sprite groups**, one for objects that have physics and can move around, and one for solid objects that can be collided with. Then in the draw loop, we can use the `overlap` function: + +```js +// In setup: +group1 = new Group(); +group1.add(someSprite); + +// In draw loop: +group1.forEach(function(a) { + group2.forEach(function(b) { + if (a == b) return; // Don't check collisions with self + if (a.overlap(b)) { + // a is touching b + } + }); +}); +``` + +The hard part is that simply knowing that there is an overlap isn't enough. We want to know specifically what parts are overlapping. Here's why: +- There is a difference in our game's logic between touching a wall and touching the ground. In each case, a different component of the player's velocity vector needs to change. +- Because we move the player in frames, in one frame the player might not be touching anything, and in the next frame the character might be *inside* the ground and will need to be moved out. + + + +*Part of collision detection is pushing objects out of solid objects.* + +There are multiple ways to do this, but we are going to see if individual points are touching instead of just the whole rectangle. We'll check a few points on each side of the rectangle using `sprite.overlapPoint(x, y)`: + + + +This isn't perfect, but it's reasonably efficient and works well. I haven't placed as many collision points on the top as on the other sides because it isn't as likely that there will be a collision on the top, and if there is, one point in the middle usually is good enough, so might as well save a few calculations. + +So, to keep track of who's touching whom, before we check collisions, we'll make an object on each sprite to store what sides the player is being touched on, and we will update it inside the collision detection function. + +```js +// Set initial collision state +group1.forEach(function(sprite) { + sprite.collisions = {left: false, right: false, top: false, bottom: false}; +}); + +// Check collisions and update touching state +group1.forEach(function(a) { + group2.forEach(function(b) { + if (a == b) return; // Don't check collisions with self + if (a.overlap(b)) { + // if b is touching a on a's left: + // b.collisions.left = true; + // a.collisions.right = true; + // Also move a out of b so they are no longer overlapping + } + }); +}); + +// Check user interaction, and now we can check if sprite.collisions.bottom +// is true or something like that + +``` + +Now for the hard part, actually checking if there is a collision. To check if there is a collision below the character, this is the basic logic we want to implement: +``` +if (if b is touching a's bottom left OR b is touching a's bottom center OR b is touching a's bottom right) { + move a up so that a's bottom is the same as b's top + + a.collisions.bottom = true; + b.collisions.top = true; +} +if ( ... ) { + ... +} +``` + +Note that I haven't used `else if`s here. This is because there could be a collision on multiple sides at once. It's also important to note that the order of the if statements matters. Let's say the character jumps so that its top is colliding with the bottom of a block. Because the corner is also inside the block, if we check for side-to-side collisions before collisions with the top, the character will be pushed all the way to the side of the block it is colliding with, because that is what we do to resolve collisions on sides. It's another reason why we only have one collision point on the top: if we had multiple, and we checked for collisions on the top before collisions on the side, the character might get pushed down when it really should have been pushed to the side. An alternate approach you can try if you want to extend this further is to check collisions on the middle points first and then the cornets after to avoid this problem. + +Given that the x/y coordinate of a sprite is **in its center**, so to the bottom of a sprite is `sprite.y+sprite.height/2`, here's what this looks like in actual Javascript: +```js +if ( + b.overlapPoint(a.position.x, a.position.y+a.height/2) + || b.overlapPoint(a.position.x-a.width*0.3, a.position.y+a.height/2) + || b.overlapPoint(a.position.x+a.width*0.3, a.position.y+a.height/2) + ) { + a.position.y = b.position.y - b.height/2 - a.height/2; + a.collisions.bottom = true; + b.collisions.top = true; +} +``` + +To test collisions on other sides than just the bottom, you may want to make more ground sprites. As long as you add them all to the `solids` sprite group, the code will still work. + +When you run this now, the character will stop falling when it hits the ground... for a moment. Then it will fall through. This is because the velocity continues to increase even as we push the character out of the block. To fix this, we want to add some additional logic after the collision detection. + +### Responding to collisions + +When colliding with something, we will want the character's velocity to be affected. After we've run through the collision detection loop, we want to run through the following logic: + +- If the character is touching the ground, we want to reset its vertical velocity to the vertical component of gravity + - It might seem logical at first to set the vertical velocity equal to zero. Due to the ordering in which we run our calculations, that would make the character alternate between being on the ground one frame, off the next, on the next, off the next... etc. Simply setting it to the y component of gravity resolves this. +- If the character is touching its head, set the vertical component to zero so that it will begin to fall the next frame +- If the character is touching on either side, set the horizontal components to zero. +- Change the key listener for jumping to make the character jump not only if the up key has been pressed, but also only when the character is on the ground. + +## What you should have so far + +At this point, you should be able to move your character around and stand on platforms! In case you fall behind: http://codepen.io/davepvm/pen/MyWQQe?editors=0010 + +Part 3: Different kinds of blocks using inheritance + +Back diff --git a/gamedev/part3.md b/gamedev/part3.md new file mode 100644 index 0000000..d98b246 --- /dev/null +++ b/gamedev/part3.md @@ -0,0 +1,194 @@ +# Game Dev +## Part 3: Different kinds of blocks using inheritance +Back + +### Intro to classes and objects + +A lot of our code so far has been involving the character sprite and ground sprites directly. However, there's a lot that's similar between them, and there's a lot that will be similar if we decide to create other kinds of blocks too, such as a ground block that bounces you extra high. All these different kinds of objects in the game can be considered parts of general categories of game objects, similar to how a square is a more specific kind of rectangle, which is a more specific kind of polygon, and there are other specific types of polygons such as triangles. + +In programming we refer to these categories as **classes**, and a more specific version of a category is referred to as a **subclass or child class** of the **parent or super class.** + +In Javascript, classes are implemented using **functions and prototypes.** A **class function** is used to initialize an instance of a class, and a **class prototype** defines what methods instances of the class have. A **base class** refers to the top-level category that other classes are all children of at some level. The polygon example might look like this: + +```js +// Polygon will be the base class +var Polygon = function(width, height) { + this.width = width; + this.height = height; +}; +Polygon.prototype.getArea = function() { + return 0; // As an arbitrary Polygon, we don't actually have enough info to know the area +}; + +// Make a Rectangle, which is a child of Polygon +var Rectangle = function(width, height) { + // This code calls the Polygon constructor with parameters `width` and `height` + // but sets it up on `this` instead of making a new object + Polygon.call(this, width, height); +}; +// Make Rectangle a subclass of Polygon by inheriting Polygon's prototype +Rectangle.prototype = Object.create(Polygon.prototype); +Rectangle.prototype.constructor = Rectangle; // This makes `somerect instanceof Rectangle === true` +// Then, redefine the `getArea` method to be Rectangle-specific +Rectangle.prototype.getArea = function() { + return width*height; +} + +// Make a Triangle, which is a child of Polygon +var Triangle = function(width, height) { + Polygon.call(this, width, height); +} +Triangle.prototype = Object.create(Polygon.prototype); +Triangle.prototype.constructor = Triangle; +Triangle.prototype.area = function() { + return width*height/2; +} + +// Make a Square, which is a child of Rectangle +var Square = function(length) { + // Pass `length` into Rectangle's constructor as both width and height + Rectangle.call(this, length, length); +} +Square.prototype = Object.create(Rectangle.prototype); +Square.prototype.constructor = Square; +// We don't need to redefine the area method since the one from Rectangle works for Square too + + + +// Making a new square +var s = new Square(5); +console.log(s instanceof Square); // Logs `true` +console.log(s.getArea()); // Logs `25` +``` + +If you want to learn more about how prototypes work and what all this `.call` stuff is, here's a great artical about it. + +### Refactoring our game to use classes + +When programming, a restructuring of code to make it easier to add new features (or cleaner, easier to use, or different in general) is called a **refactor.** We're going to do one of those now by making classes for our game objects instead of treating each one as its own unique case. All of our game objects were originally made with `createSprite`, which returns an instance of the p5.js `Sprite` class, so it makes sense to use that as our base class. Because `Sprite` is from p5.js, we can only access it with in `setup` or `draw`, so like other game elements, we have to declare it outside both, but define it in `setup`. + +There's a little bit of extra work that `createSprite( ... )` does behind the scenes to make it more useful than `new Sprite( ... )`: it manages depth for you. Internally, it stores a group of all sprites, so when you call `createSprite`, it calls `new Sprite(x+width/2, y+width/2)` to make its coordinate be in the center, adds it to `allSprites`, then sets `sprite.depth = allSprites.maxDepth()+1`. We want to build this in to our base class so that we don't have to worry about depth on our own. + +There are a few methods that we want all game objects to have, but let specific types provide their own definitions for: + +- `recalculate()`: This will be called to let each object recalculate its position each frame (for example, add gravity to its velocity) +- `collideWith(target)`: This will be called in the collision detection loop after we have found a collision and moved the objects apart, allowing us to do something afterwards such as reset velocity +- `interact()`: This will be called every frame after everything else, allowing each object to listen for user interaction such as keyboard events and react to them + +To start you off, this is what the base class might look like: + +```js +var Block; +// ... + +function setup() { + // Start each block with a position and size + Block = function(x, y, w, h) { + Sprite.call(this, x+w/2, y+h/2, w, h); + this.depth = allSprites.maxDepth()+1; // We don't have to define allSprites anywhere, p5.js makes it + allSprites.add(this); + } + Block.prototype = Object.create(Sprite.prototype); // Inherit all of Sprite's methods + // These methods are all empty for now. Subclasses will actually do stuff. + Block.prototype.recalculate = function() { }; + Block.prototype.collideWith = function(target) { }; + Block.prototype.interact = function() { }; + + // ... +} + +``` + +We'll want to make a `PhysicsObject` class which a `Player` class will extend instead of directly making a `Player` class in case we want other things to be able to fall to the ground and have physics. It might have a definition that looks like this: + +```js +PhysicsObject = function(x, y, w, h) { + Block.call(this, x, y, width, height); + this.gravity = createVector(0, 1); // If we make gravity owned by each object instead of global, + // we can edit it per object if we want for some cool effects. + // Up to you. + + solids.add(this); // Add the new object to the solids sprite group + physics.add(this); // Add the new object to the physics sprite group +} +PhysicsObject.prototype = Object.create(Block.prototype); +PhysicsObject.prototype.constructor = PhysicsObject; +PhysicsObject.prototype.recalculate = function() { + this.velocity.add(this.gravity); +}; +PhysicsObject.prototype.collideWith = function(target) { + if (this.collisions.bottom) { + this.velocity.y = this.gravity.y; + } + // ...and the rest of the post-collision code goes here, + // but we can use `this` and `target` +}; +// We don't need to define `interact` because a generic +// PhysicsObject has no interaction with the player +``` + +Now, try making the rest of the classes yourself: +- `Player = function(x, y)` + - It should pass a width and height to the parent constructor automatically + - Set `this.shapeColor` in the constructor + - The only thing it needs to redefine from `PhysicsObject` is the `interact` method to control jumping and horizontal movement +- `Ground = function(x, y, w, h)` + - Set a color here too + - It should add its sprite to the solids sprite group +- `SuperJump = function(x, y, w, h)` + - Set a different color than the default for ground so we can see it's special + - In `collideWith`, change `target.velocity.y` to make it bounce up + - You might need to change `collideWith` for `PhysicsObject` to not reset its y velocity if `target instanceof SuperJump` + +### The new draw loop +If you recall from the first part, this is what our draw loop is composed of: +```js +function draw() { + // Calculate new positions of objects + + // Accept user input (keypresses, etc) + + // Redraw screen +} +``` + +Our old code can now be replaced to something a lot simpler: +```js +function draw() { + allSprites.forEach(function(sprite) { + sprite.recalculate(); + // reset `sprite.touching` here too + }); + + // Do collision detection between physics and solids as usual, + // but you can now call `a.collideWith(b)` and `b.collideWith(a)` + // when there has been a collision to perform the follow-up logic + + // Loop through all sprites again to run their `interact` method + + drawSprites(); +} +``` + +### Defining the level +After the class definitions in `setup`, we need to define the starting locations for objects differently now that we have classes. Since classes automatically add themselves to the necessary sprite groups, all we need to do is this: + +```js +function setup() { + // define classes + + new Player(200, 300); + new Ground(150, 400, 300, 30); + // etc + + // If you want to store a reference to something, you can always use + // theplayer = new Player( ... ); +} +``` + +### What you should have so far +Here's the game up to this point, in case you fall behind: http://codepen.io/davepvm/pen/aNJyXO?editors=0010 + +Part 4: Adding points and lives + +Back diff --git a/gamedev/part4.md b/gamedev/part4.md new file mode 100644 index 0000000..76b94b8 --- /dev/null +++ b/gamedev/part4.md @@ -0,0 +1,150 @@ +# Game Dev +## Part 4: Adding points and lives +Back + +### Resetting the game +Right now, if you fall off the screen, that's it - you have to reload the page in order to continue testing anything. We want the level setup to reset when you fall off the screen. If the setup was in a function, we could just call it again, but right now that all happens in `setup`, which we don't want to call manually because it's a special function to p5.js. Additionally, we use `setup` to define classes. We don't need to redefine classes every time the player dies. To solve this, we can assign a function in `setup` to set up a level and then call it again whenever we need. This includes calling it once immediately after we define it to set up the game initially. + +To reset the game when the player falls offscreen, we need to define `Player.prototype.recalculate` since it needs to do some more work than what `PhysicsObject.prototype.recalculate` already does. In our definition for the `Player`, we still want it to run `PhysicsObject`'s `recalculate` instead of completely defining a new one. We can do this by manually running it as the first line of our new definition with `PhysicsObject.prototype.recalculate.call(this)`. Then we just need to check if `this.position.y` for the character is greater than the screen height, and if so, call `reset()`. + +We also want to get rid of all the existing sprites on the screen since we will be making everything over again in `reset`. An easy way to do this is loop through each element in `allSprites` and call `remove()` on it. This method is built in to p5play.js and will automatically remove the sprite from any groups it may belong to. + +Here's how we want the new setup to look: +```js +// var solids, physics, ... +var reset; + +function setup() { + // define classes + // Player = function ... + Player.prototype.recalculate = function() { + PhysicsObject.prototype.recalculate.call(this); + + if (this.position.y > maxHeight) { // or however else you want to do this check + reset(); + } + }; + + reset = function() { + allSprites.forEach(function(sprite) { + sprite.remove(); + }); // remove things from the screen + + // create the player, ground, etc + }; + reset(); // Call the function to restart the level +} +``` + +While we're at it, in your game, you might have other objects that can fall offscreen. We don't care about them once they're gone, so in the definition of `PhysicsObject.prototype.recalculate`, is would make sense to call the sprite's `remove` method. +``` +if (this.position.y > maxHeight) { + this.remove(); +} +``` + +### Creating lives +Keeping track of lives is a pretty small change now. Declare a variable called `lives` and in `setup` (but not inside `reset`!) set it equal to however many lives you want to start with. Then, in the player's `recalculate` function, before calling `reset`, set `lives` equal to its old value minus one. + +We want to display this information on the screen. There are a few different ways to do this, such as drawing hearts that disappear as you lose lives. Another simpler way is to write text on the screen telling you how many lives you have. (Drawing hearts could be a fun extension to do later!) + +In p5.js, text isn't represented by a sprite, it just draws directly on the screen. This means that we will have to write the number of lives onto the screen after we have finished rendering everything else at the bottom of `draw` so that nothing gets drawn on top of it, covering it up. + +When drawing directly on the screen, you have to set up the "pen" before doing the drawing. In the case of text, we need to set the font size, fill color, and text alignment before calling `text(words, x, y)`. + +A simple lives counter might look like this: +```js +function draw() { + // ...main game logic + drawSprites(); + + textSize(14); + fill(0,0,0); + textAlign(LEFT, TOP); // This makes the x and y function as the top left corner of the text + text("Lives: " + lives, 10, 10); +} +``` + +If you're interested, you can specify a width and height after the x and y of the text and use `CENTER` as a text align parameter. More info on that can be found in the p5.js docs. + +This works, but doesn't limit our lives at all. If you keep falling off, the life counter goes to zero and then into the negatives. To prevent this, let's make a simple Game Over screen. + +Declare a function called `gameOver`, and under our definition for `reset`, we'll define what we want to happen when `gameOver` is called. We'll want to create a button for you to press to start the game over again. p5.js provides us with a `createButton(buttonText)` function to use to make an HTML `