Tuesday, January 25, 2011

Let's Code: Pong (part 1)

Lately I've been playing around with writing games including two simple puzzle games based on ideas suggested by my son. Today, I wanted to present my code for a Pong clone. It's not the most exciting game in the world, but it's dead simple to code and a good "Hello World"-style program for starting to write action games.


The first decision that must be made when starting a new coding project is which framework to use. Since my justification for writing games is to help me keep my programming skills active and to continue learning, I wanted to write in an interesting language that I don't know well: Lua. A quick Google search discovered LÖVE, which is an open-source 2D game engine in active development. Since Pong is a 2D game it's not a big deal that our framework isn't 3D. My other little projects simulate a 3D environment with an isometric viewpoint. In the future, if I do a first-person game or something, I'd want to switch frameworks.


I've also decided to use the love.physics module, which is certainly overkill for Pong but will be necessary for physics-based puzzles I'm working toward. It really isn't as hard to use as the LÖVE manual suggests. The module is based on the Box2D library, which has pretty good documentation. Once the simulated world is set up, the physics library can deal with a bunch of details like ball movement, bouncing off walls and paddles, and so on.


love.physics tutorial


love.physics has a fairly basic tutorial that simulates a ball falling to the ground and being rolled around. If you want to follow along, go ahead and grab a copy of LÖVE and try out the tutorial. Below, I'll cover some of the same territory if you'd prefer just to read. Playing around with the tutorial, I noticed that the ball will eventually roll off the edge of the world and disappear. To solve that, I added a two walls and a ceiling to keep physics objects from getting lost. My philosophy is that if you do something twice, you should think about writing a function instead. If you do something more than twice, then you certainly should write a function. Since I needed four walls rather than just the ground, I wrote a make_wall function:


function make_wall(world, x, y, w, h)
local wall = {}
wall.body = love.physics.newBody(world, x, y, 0, 0)
wall.shape = love.physics.newRectangleShape(wall.body, 0, 0, w, h, 0)
wall.shape:setRestitution(1)
return wall
end


Even if you are new to Lua, this code should be fairly easy to read. make_wall needs 5 inputs: world (the physics world), x, y (the coordinates of the center of the wall), and w, h (the width and height of the wall). Lua has a very lightweight object system and this function could be seen as a constructor for a wall object, which is the return value. As PIL points out, "Tables in Lua are not a data structure; they are the data structure." So wall is initialized as an empty table. Objects in the physics module require a body, which represents the object's center of mass, position, attitude, and velocity, and a shape, which defines the space an object occupies and how it interacts with other objects. When we assign values to wall.body and wall.shape, Lua automatically creates variables (members in OOP terms). Walls should be static, so the mass is set to 0, which is the way Box2D represents infinite mass. The shape of the wall is simply a rectangle centered on the center of mass. In order to make balls bounce off the walls, restitution (essentially bounciness) is set to 1.


In love.load(), I'm going to create my walls thusly:


local walls = {}
...
-- Floor
table.insert(walls, make_wall(world,
love.graphics.getWidth()/2,
love.graphics.getHeight()+1,
love.graphics.getWidth(),
2))

-- Ceiling
table.insert(walls, make_wall(world,
love.graphics.getWidth()/2,
-1,
love.graphics.getWidth(),
2))

This inserts a floor two pixels high1 just below the graphics frame and a ceiling just above it. If we weren't using a physics engine, we could just code the ball to reverse vertical motion when it hits the side of the screen. Standard Pong only needs these two walls, so make_wall didn't save us many lines of code. However, it's easy to imagine variations with different wall configurations.


We only need one ball for our basic game, but it's simple and clean to write a make_wall function. And who knows? We might want to add more balls to the game at some point. Initially, I'd set the mass of the ball based on it's size (using Body:setMassFromShapes) but that causes the physics of the game to change when the size of the ball is changed. It seemed easier to just fix the mass to something (somewhat) sensible like the mass of a tennis ball and adjust the force applied until it felt right. It turns out one Newton works pretty well. The radius of the ball is measured in pixels and it can be adjusted without impacting the physics of the game. Some people would probably want these variables to be made constants, but a) there's not much of a performance gain and b) Lua doesn't support making variables constant. Besides, the simplest way to have a variable remain constant is to not alter it's value. The ball shouldn't have any linear damping (more or less the same as drag or fluid resistance) or friction to slow it down. The way Box2D simulates collisions, I don't need to make both the walls and the ball bouncy, but it doesn't hurt to specify the restitution here as well.


local ball
-- Mass of a tennis ball http://hypertextbook.com/facts/2000/ShefiuAzeez.shtml
local ball_mass = 0.057
local ball_force = 1
local ball_radius = 3

function make_ball(world, x, y, r)
local ball = {}
ball.body = love.physics.newBody(world, x, y, ball_mass, 0)
ball.shape = love.physics.newCircleShape(ball.body, 0, 0, r)

ball.body:setLinearDamping(0)
ball.shape:setFriction(0)
ball.shape:setRestitution(1)

return ball
end


The final type of object we need to make for our Pong game is the players' paddles. Classic Pong doesn't really simulate the ball bouncing off a flat paddle, but has the ball bounce off at an angle determined by the location the ball hits the paddle. In other words, the closer to the edge of the paddle, the steeper the angle the ball with come off the paddle (and the more likely your opponent will miss). This makes the game much more interesting since it matters not just whether a player intercepts the ball, but where on the paddle they do so. At some point experimenting with the number of facets would be interesting.


function make_paddle(world, x, y, w, h)
local paddle = {}
paddle.body = love.physics.newBody(world, x, y, 0, 0)

-- Don't use a rectangle for the paddle since the bounces
-- off a flat surface are boring. In stead, we use a flattened wedge:
-- /|
-- | |
-- \|
paddle.shape = love.physics.newPolygonShape(paddle.body, 0, -h/2,
-w/2, -h/6,
-w/2, h/6,
0, h/2,
w/2, h/2,
w/2, -h/2)
paddle.shape:setRestitution(1)
return paddle
end


Now that we can make a paddle, let's build a player object, which has a paddle and a score. The cpu variable will describe the behavior of the player if it is computer controlled. More on that momentarily.


local players = {}

function make_player(world, x, y, l, cpu)
local player = {}

player.score = 0

player.paddle = make_paddle(world, x, y, 2*ball_radius, l)

player.cpu = cpu

return player
end


It's easy to imagine all sorts of AIs for Pong from carefully calculating the intercept location of the ball to moving more or less at random. Most Pong implementations use some variation of what I call the chase AI in which the computer tries to match the location of the ball on the y-axis with some sort of lag. My approach follows Zeno's Paradox of Achilles and the tortoise. In this case, Achilles (the paddle) does occasionally catch the tortoise (the ball) if the delay is set low enough, the paddle is long enough and the ball is moving slow enough along the y-axis. Set delay to 2 for the classic paradox.


-- Basic Pong AI is to have the paddle chase the ball. Use the delay parameter
-- to create an AI that responds more slowly to vertical movement.
function make_chase_ai (delay)
return function (paddle, ball)
local delta_y = ball.body:getY() - paddle.body:getY()
paddle.body:setY(paddle.body:getY() + delta_y/delay)
end
end


If you're familiar with mostly conventional languages (such as BASIC, Java or C++), you might find it odd that make_chase_ai returns a function. In Lua, functions are first-class values which means the can be assigned to variables, passed to other functions and, as seen here, be return values. What I'm doing here is creating a closure over the delay free variable. It's probably easiest to explain by showing how it's used. Here's how I initialize a computer player in love.load():


players[1] = make_player(world,
10,
love.graphics.getHeight()/2,
12*ball_radius,
make_chase_ai(50))

players[1].paddle.body:setAngle(math.pi)

So I pass the physics world to the make_player function, put the paddle 10 pixels in from the left of the screen and centered vertically, set the length of the paddle to 12 ball radii, and provide an AI that chases the ball with a delay of 502. This isn't the best player in the world, but it's surprising how often he catches the ball at the last moment, which makes for some interesting play. Once the function is created, delay is fixed to 50 and it is assigned to the player's cpu variable. In OOP terms, cpu is a virtual method for the player object.


In passing, notice that I rotated the paddle body 180° since make_paddle creates a wedge facing to the left. I could have created the paddle to have facets on both sides or written a mirror_shape function3 to flip the paddle on its vertical axis. But applying the principle of parsimony it seemed simplest to rotate the paddle and be done with it.


Here's how the other player might be initialized if you want to pit an Achilles AI against the more pedestrian AI above:


players[2] = make_player(world,
love.graphics.getWidth()-10,
love.graphics.getHeight()/2,
12*ball_radius,
make_chase_ai(2))

So this player is positioned on the right side of the screen and has a lower delay, but is otherwise the same as player 1. So what benefit is using a closure instead of just making delay a player attribute like paddle length and position? It might help to see how the AI is executed in love.update:

for _, player in ipairs(players) do
if player.cpu then
player.cpu(player.paddle, ball)
end
end

Let's review what happened:

  1. We called make_chase_ai with parameters of 50 and 2 respectively.
  2. We assigned the output, which is a function, to each player's cpu variable.
  3. We checked to see if a player had a true value in cpu.
  4. And if it did, we executed the AI in each time step.

Each step creates one more layer of abstraction, which means we could easily plug a totally different type of AI or no AI at all into a player. For instance, here's an AI that jumps at the last minute:

function make_jump_ai (min, max)
return function (paddle, ball)
local delta_x = math.abs(ball.body:getX() - paddle.body:getX())
if (delta_x < max and delta_x > min ) then
paddle.body:setY(ball.body:getY())
end
end
end

...

players[2] = make_player(world,
love.graphics.getWidth()-10,
love.graphics.getHeight()/2,
12*ball_radius,
make_jump_ai(15, 20))

When the function is called, it takes just the paddle and ball objects and uses a completely different algorithm to move the paddle around. This allows polymorphism, which in turns allows much simpler code in higher level functions such as love.load and love.update.


This has gone on long enough and I've only covered the setup portion of the game. Next time, we'll set the world in motion and interact with it.




1 - I picked 2 pixels because creating a one pixel tall rectangle causes LÖVE (or more accurately Box2D) to crash.


2 - 50 isn't a magic number. It's just what seemed most interesting given a particular configuration of ball size, screen size, ball speed, and paddle length. For a while I was using 100.


3 - I cannot tell a lie. I wrote a mirror_shape function, but decided not to use it since it was more cumbersome than I imagined when I started writing it.

No comments: