Showing posts with label Pong. Show all posts
Showing posts with label Pong. Show all posts

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.

Tuesday, March 9, 2010

Lego Star Wars: The Complete Saga

Lego Star Wars was the game that prompted me to buy a Wii. I even bought the game before I bought the console.1


The first video game I played was a home version of Pong. I don't remember much about it, but I do recall waiting for the TV to warm up so we could play a few games of Pong before Dukes of Hazard or some such came on. It's a simple game and after a few plays you are ready for something else. One of my friends had an Atari 2600 that got a lot more play from us since it had Breakout, Combat, Indy 500, and especially Space Invaders. I vividly remember spending entire afternoons trying out the various game modes of Space Invaders—the 2600 version had 112 including invisible enemies and moving shields.


Then a series of events pulled me away from home consoles for many years. We moved away, my parents got rid of our old TV and bought a Tandy 1000 SX home computer. So I missed out on the NES, the 16-bit consoles, the PlayStations, and the various other gaming systems that connect to a TV screen. Instead I played tons of PC games from Sopwith to IL-2 Sturmovik. As you might imagine, I also spent tons of time and money upgrading my computer so that I could play the latest PC games. It was sometime after I got married and before our son was born that I got burnt out and gave up on upgrading my PC. A few years later I picked up a Jakks Atari TV game out of nostalgia, but I figured I'd outgrown video games.


Time for a short history of video games digression. Video games exist in three distinct zones. The first is arcade games which reside in public locations such as bars, pizza parlors, movie theaters and, of course arcades. Second is home consoles, which are attached to family TVs in the living room or den. Lastly are computer games that played in the home office or den. As a result of these zones, each type of game has developed it's own distinct traits. Arcade games are bright, inviting, technically advanced, fast-paced, public, and unforgiving because they are designed to eat quarters. On the other end of the spectrum, computer games tend to be darker, complex, technically limited, contemplative, individual, and deep since they reside on the same machine that is used for word processing and spreadsheets. Home consoles sit between the extremes.


Initially, consoles were just cheaper versions of arcade machines that could be experienced in homes. But somewhere around the end of the Atari era and the beginning of the NES era, consoles began to assert a separate style of gameplay that was a little more relaxed than their arcade cousins. Donkey Kong did its best to kill you off in the first minute or two, but Super Mario Bros. gives you a lot more rope to keep playing. Console games could afford to offer a deeper experience without the pressure to cycle through players as there is at the arcade. On the other hand, they were restrained from becoming as complex as PC games since they relied on a public resource (the living room TV). Over time, console games drifted closer to the PC style as more gaming systems were attached to TVs in bedrooms and game rooms. By the time I started looking into consoles again, they were a far cry from the Atari I grew up with.


A few Christmases ago I visited my brother who has an Xbox 360 and a copy of Lego Star Wars, which he fired up between events. It looked fun so I asked to play and he handed me a second controller and I dropped in. We worked together for a while solving puzzles and beating up battle droids. Then he needed to go do something and dropped out for a while. As we played through the story, other family members (including non-gamers) sat around to watch the goofy cut scenes between levels. Gameplay is so accessible almost anyone can start playing (and make progress) moments after picking up the controller. In essence, the game was a lot like the family, living-room, arcade-style games from the Atari and NES eras.


LEGO Star Wars: The Video Game  Various


When I first heard the concept of the game, I couldn't get my head around it. How do you make a game based on both Lego toys and Star Wars? I assumed there would be lots of building with bricks and that didn't seem to fit with the action-oriented movies. And the developers seem to agree since game uses Lego environments mostly for the sake of destruction. Pretty much everything that looks like it's built out of Legos can be destroyed. Besides being fun to smash up the environment, the game scatters studs (Lego currency) everywhere to be collected for buying bonus features and characters later. Building is included for solving puzzles, but it's somewhat abstract as you'll find a pile of bricks, hold a button and your character starts to assemble some useful object. (Often you can turn around and destroy it again, which is therapeutic.)


LEGO Star Wars II: The Original Trilogy Various


As for the movies, I've seen the original trilogy dozens of times and, as good as it is, I've gotten a little numb to the story. As for the prequels, they seem to take themselves too seriously and I actually fell asleep during the most recent one. Lego Star Wars manages to fix both issues at the same time. All the dialog-y bits are presented in pantomime cut-scenes that usually feature some sort of twist. For instance, the dramatic credit scene from Empire gets a gag where Luke's robotic hand jumps off his body and wanders around Thing-like. Then you get to play through all the action scenes which are greatly expanded from the movies. The game manages to capture the feel of the movie action sequence such as firing blasters at Stormtroopers while a droid works to open a blast door. Maybe not groundbreaking, but it feels just right.


LEGO Star Wars II: The Original Trilogy Screenshot


After you finish a level in Story Mode, you unlock the option to play again in Free Play mode. In addition to using different characters, you can also find all sorts of hidden objects the second time around since different character classes are required to open up certain areas. There's just so much content and attention to detail it's hard to take it all in. Even in the overworld (Mos Eisley Cantina) you can easily amuse yourself getting into brawls, breaking up furniture, solving mini puzzles, and so on. By the way, get the Complete Saga version that includes levels from the first two games plus a few little extras.


By nature these games are cooperative. If you play alone, the computer takes over the other character, but it's not as fun. Either the computer will basically solve puzzles for you or will refuse to do their part causing you to switch from one character to another in a frantic attempt to do everything. After you've seen how to get through a section, it doesn't hurt to replay it on your own, but don't start off that way. Two player mode has it's own problems. Since there's only one camera, players can't wander where they please. It's not uncommon for one person to press ahead while the other wants to hang around and find secrets. Until one or the other caves in, this results in a frustrating fight for control of the camera that leaves both players stuck at the edge of the screen.


The other serious issue comes from the platforming elements. I don't know what it is about 3D platforming, but it's hard to judge jumps and the camera loves to move at the exact moment you need to pick a direction. Lego Star Wars exacerbates the problems by making the edges of the platforms mushy so you tend to slip to your doom when you get too close to a bottomless pit. If that weren't bad enough, you always respawn in the exact same spot and if you don't take action right away you'll fall in again and again. A stupid trick to play with a "friend" is to push them over a cliff and not move so they fall over and over. The computer loves to do that. (For an example of how to do this the right way, look at New Super Mario Bros. Wii.) Thankfully, death doesn't cost anything but studs, but this is a completely avoidable problem.


Another problem, especially for younger/less-experienced gamers, comes from the complex and layered nature of the levels. Often there will be a little hint that something is hidden behind a wall or a puzzle to be solved, but these are sometimes premature in Story Mode—they require characters that are unavailable. Even for me, it was sometimes hard to figure out what needed to be done to get through some levels. Particularly frustrating are the vehicle levels which seem to go on and on without giving any indication of how to proceed. In addition, they are the least cooperative sections of the game and even encourage competition.


And cooperation really makes this game special. It's a game that my son asks to play with me and then I get to be his hero by fighting off the bad guys and he gets to be the hero by finding the key to some puzzle or the direction to take next. And then my wife comes along and we switch to Wii bowling for a while.


Entertainment value: All 6 Star Wars DVDs edited by Steven Spielberg and a pile of Star Wars Lego sets.





1 - The reason I bought it before a Wii was that I wanted to get a copy of "The Complete Saga" for my brother who only had Episodes I-III. Unfortunately, I didn't know that XBox game boxes are green and Wii boxes are white. So I returned the game and bought it again about a year later.

Thursday, November 19, 2009

Bit.Trip: Beat (Demo)

Nintendo recently released the first demos on their WiiWare platform, which gave me several games to try out on my non-existent games budget. All demos take several minutes to download, limit the features of the game, prevent saves and boot you to the Wii Shop Channel on completion. Unlike other reviews, these are from the perspective of how effective the demo is at capturing sales in my opinion.


First up is Bit.Trip: Beat, which is Pong meets side-scrolling shmup meets rhythm game. Tilting the Wiimote positions your paddle/ship/beat-collector in order to catch balls/enemies/beats that approach from the left side of the screen. Successfully bouncing the beats back from whence they came increases your score and failing to do so brings you closer to demise. Doing well is also rewarded with musical beats that add to the background music while misses make a little whiff sound. Stringing together longer sequences opens more complex background images and music while misses cause you to drop into a mode that closely resembles the graphics and sounds of Pong itself. Meanwhile the controller rumble slightly shakes in time to the beat.


Bit.Trip: Beat Screenshot


Prior interest: high


I've been looking for a simple, old-school, action game to play when I have a few moments to fill at odd times. The Bit.Trip series seems like it fits the bill perfectly. I've seen videos of people playing in the groove that look simply amazing. Tilt control may be my favorite feature of the Wii. I hate having to dig through my collection to find disks to play a quick game. Plus I don't like spending a lot of money.


Odds of purchase: low


Overall, the demo is amazing and generous. Too generous. I died before getting to the end of the first song/level and was going on five minutes. Videos of the entire first level, which I believe is available in it's entirety, last nearly 15 minutes. That's pretty much plenty for me. I don't see myself playing this game often and seriously enough to need to play the other two songs anytime soon and certainly not at the cost of $3 each.


The Bit.Trip games seem ideal for demos since they turn on the quality of the experience. There are bound to be people who balk at spending money on a game that is widely seen as short and quirky, but who might be pushed over the edge by a good, immersive demo such as this one. In fact, despite my initial reluctance to pull the trigger this time around having a significant portion of the game available every time I turn on my system just might make the difference when I finally finish the first level.

Update:


Well I played a few more times, got better and discovered the demo ends after 7 minutes or so. Which slightly increases my odds of buying Bit.Trip: Beat. Slightly.