Learning to Love(2d) Game Development 0 — Building Pong with CSCI E-23a

This is a walkthrough of my code for Assignment 0 for the excellent CSCI E-23a course released by Harvard in Spring 2018. Extensive lecture notes created by @AbhirathMahipal can be found here.

Prerequisites

  • Basic programming (variables, functions, and objects)
  • Love2d installed
  • It’s helpful if you’ve watched the lectures. I’m not great at explaining things, which is one reasons I’m writing this.

I’ve always loved video games and recently started the CSCI E-23a game development course which is available free online. I’m a Javascript developer and this course uses the Lua framework Love2d, so it’s been easier to focus on concepts.

After completing the lecture, I tried recreating Pong using Colton Ogden’s (Lecturer) structure to get the concepts to stick. Most of the code is similar to what was created in the lecture, but I organized some things differently. You can find the repo here. The stucture is simple:

/sounds
Ball.lua
class.lua — Lib used for creating classes in Lua
font.ttf
main.lua
Paddle.lua
push.lua — Lib used to simplify rendering

Main.lua — The entry point

Love2d looks for a file called main.lua when you start up a game. This is where you’ll stitch together all of your game logic and override the Love2d game loop methods. The first thing we need to do is import the class and push libraries and declare some variables. We also need to bring in our Paddle and Ball classes.

By using the require keyword, we can reference push, Class, Paddle, and Ball as variables later on. Lua looks for these files within the directory. We define a virtual height and width along with actual height and width because we want to emulate a lower resolution. These, along with PADDLE_SPEED, are global variables, while scoreNeededToWin is only available within main.lua. The rest of main.lua implements the love2d game loop.

What is a “Game Loop”?
After a game is loaded, it basically has three steps to do over and over: Collect any user input, Respond to input by updating game state, and then Rendering the screen based on this state. Love2d provides an interface that developers implement which it loops infinitely when a program is run. The basic structure looks like this:

love.load() is called once on start up, and then the remainder of the functions are called each loop. You’ll notice that love.update() takes in a parameter called dt. This is “delta time” or the time since the last loop iteration began. This is used to scale update functions so that the game runs at the same speed regardless of how powerful a user’s computer is. A general (and much better) explanation of game loops was referenced in the lecture and can be found here.

First, I override love.load() to initialize the game and load assets (helpers are in different location in actual file):

The order of the functions in load can be executed in any order, and their purpose is clear from their names. There are a couple love functions used to load fonts and sounds: love.graphics.newFont( filename ) and love.audio.newSource( filename, type ).

Player paddles and the ball are also created here. I’ll go into detail for each class later, but for now all you need to know is that both constructors take in x and y coordinates. The Paddle class takes in an additional configuration table to define control keys and whether the paddle is a “computer” player or not.

Finally, there are a few global variables defined here: “gameState” in load() and player scores in loadPlayers. I’m not a big fan of global variables, much less defining them deep within function calls. It’d be much better to have these managed by a state object, but I didn’t sweat too much over this since it’s a small project with one developer.

With all assets and basic game state initialized, the next step is to implement love.update(dt):

handleKeyPress() is straightforward since it delegates capturing keypresses to the Paddle class’s move function. It takes in the virtual height of the screen to constrain the paddles vertical movement to the screen and the speed of the paddle. I’m also passing the reference to the ball instance as a quick and dirty solution to the “AI Update” assignment. It’s dumb and I’ll explain it later.

updatePlayers(dt) checks the gameState to see if either player has won, resetting the scores if someone did win. It then delegates paddle updates to each player instance’s update(dt). I’ll come back to those as well.

updateBalls(dt) is the most interesting. I guess there could be some mixing of concerns here, but each of the sub-functions changes ball state or reacts to ball state. At least that was my logic for putting them together like this:

  • checkGameState() either serves the ball or resets it based on certain states. serveBall() sets the ball.dx and ball.dy to random numbers and ball.isMoving to true. There’s an example of how to do a ternary operator in Lua: math.random(-1, 1) < 0) and -1 or 1 generates a number between -1 and 1 and tests whether it is less than 0. If it is, a -1 is returned. Otherwise, a 1. This randomly makes ball.dx negative or positive to make the ball go left or right, respectively. The ball seems to go right more often than not, so I think this can be done better.
  • checkScreenEdgeCollision() is concerned with whether the ball has hit the edges of the screen. If it hits the top or bottom, a sound is played and ball.dy is changed. If it hits the left or right, the player on the opposite side’s score is incremented and gameState set to denote which player scored. The changed gameState is then picked up by checkGameState on the next iteration and the ball is reset.
  • checkPaddleCollision() looks for paddle collisions. It defers this check to the ball object’s collides( paddle ) function, which it stores in 2 global variables. If a collision occurs, it reverses and increases the ball.dx. It also makes sure that the ball’s x position is outside of the paddle. See changeBallHorizontalSpeed(). ball.dy is set to a random speed but continues in the same vertical direction.

Still with me? I just took a break to fix some food and play with our cat. The lesson is that this shouldn’t be one post. I won’t be offended if you don’t finish this in one sitting. Ready? Ok, let’s talk about love.draw().

Here’s the gist:

As you saw in the basic game loop gist, all of our render logic is between push:apply(‘start’) and push:apply(‘end’). This lets push know we’re ready to start drawing things. I’m going to skim over the functions here since it’s pretty obvious what’s happening based on function names. Each screen is drawn based on gameState and each has its own function. The players and ball are always drawn, no matter the state.

A few points about love functions used:

Ball and Player rendering are deferred to their respective render() functions. You’ll also notice some mixing of concerns as I’m making changes to state in a couple of places. It would be better to have these in the update() method because that’s the most obvious place for state changes, but I got a little lazy here.

And that’s main.lua in an extremely large nutshell. A nutshell for giants. Ball.lua and Paddle.lua will be easier.

Ball.lua
This is relatively simple. The Ball class has the following methods:

  • init() — This is the constructor
  • collides() — Checks if the ball has collided with the incoming paddle. We’ll talk about collision detection in a bit
  • reset() — sets the ball’s current x and y positions to the starting point given to init()
  • update() — Remember this was called in main.lua by love.update(). If ball.isMoving is true, the ball’s x and y are updated by ball.dx and ball.dy, both scaled by dt (delta time)
  • render() — Likewise, this was called by love.draw(). It simply calls love.graphics.rectangle()

Here’s the gist:

Pretty straight-forward. A couple key points: “Ball = Class{}” uses the class.lua library to create the class and init() contructs each instance.

Collision Detection Detour
I promised I’d come back to Ball:collides( paddle ) and here we are. Collides uses simple AABB, or “Axis-Aligned Bounding Box”, collision detection. Well, wtf is that?

Let’s take “Bounding Boxes” first. Detecting collisions can be intensive if objects are odd shapes or have lots of edges. Checking if two rectangles collide is much simpler so developers will define a box around complex shapes. Take this plane for instance:

Source: Here — On the left, a single box is defined around the plane. On the right, multiple boxes are defined to increase accuracy.

Anything that comes within the red box is defined as a collision. As you can see, it’s a tradeoff between accuracy and performance. That’s why in the right image there are more boxes. Using that model, you would need to run 4 checks vs the one using the model on the left.

So that’s great, everything is surrounded with boxes. What about the “Axis Aligned” part? That’s much simpler. Love2d and other graphical engines use coordinate systems, in this case with an x and y axis. It’s much easier to rule out a collision if both boxes are aligned to the same axis.

And how to we do that? I’m glad you asked. There are four questions to ask and if you answer “yes” to any then no collision occurred:

  1. Is the left edge of rectangle 1 to the right of rectangle 2’s right edge?
  2. Is the left edge of rectangle 2 to the right of rectangle 1’s right edge?
  3. Is the top edge of rectangle 1 below the bottom edge of rectangle 2?
  4. Is the top edge of rectangle 2 below the bottom edge of rectangle 1?

Otherwise, a collision occurred. Now you can see why Pong is such a great first game to recreate. Everything is a box and has at most one bounding box. Go ahead and take a look at the collides function and you’ll see how those questions are translated to Lua.

Take a look at MDN’s explanation here if you’re confused. There’s a sweet playground where you can actually move rectangles around and see things happen. The explanation that I got the airplane diagram from is also great, but keep in mind that the origin is the bottom-left corner even though the concept is the same. If you want to go off the deep-end, checkout Gamasutra’s explanation here. Now, let’s talk about the Paddles.

Paddle.lua
Here’s the gist:

Pretty similar to Ball without the collision detection, right? I don’t think anything besides Paddle:move() needs to be mentioned after you’ve read the code.

The first thing Paddle:move() has to look at is if a key is pressed down. Remember the movement keys are passed in when the paddle is constructed, so we check for those keys and move up or down depending on which is pressed. To do this, we use love.keyboard.isDown() which returns a boolean true if a key is down.

As part of assignment 0, a simple AI should follow the ball and try to hit it back. I did this by checking if a paddle is a computer and then the current position of the ball. If the ball is a certain amount above or below the paddle’s y, the paddle moves towards it.

This is dumb and doesn’t work very well. Most implementations I’ve seen simulate an invisible ball to predict where the ball is going and has the computer paddle move to that position. You can make this more or less accurate based on the current score or a difficulty level. I plan to implement this once I have some more time.

Lastly, we want our paddles constrained to the bottom and top edges of the screen, so that’s the final check and why I pass in the VIRTUAL_HEIGHT as screenHeight within main.lua.

Conclusion
I’ve tried several times to pick up game development ever since I brought home Doom 1 and Quake 1, and I’ve never stuck with it. I’ve enjoyed this course so far and am trying to find time to complete it. I’ve finished the Flappy Bird session and I think I’ll do that post more as a tutorial series rather than one long article skimming over my code.

Please let me know if you have any questions or found anything confusing or incorrect. I’ll update the article and try to improve in future posts.

Web Developer, Runner, Human Fellow