Skip to content

01 Pong

The finished Pong game

Make a table tennis game, like Pong: two paddles, one ball, and the first to ten points wins.

“Make it work.” We end up with a working, very basic game. The implementation is deliberately naive. It ignores good architecture and design practices, which keeps things simple and gets us to a working prototype quickly. The rest of the course is about making games like this one better as they grow.

Pong on an old television
The original: Pong (Atari, 1972). Image: pong74ls, derivative work by WikiJunkie, CC BY 3.0, via Wikimedia Commons.

This session is about the game loop. Every game runs the same cycle many times a second. It reads the input, updates the game and draws it. MonoGame runs that loop for us, and we fill it in, first all in Game1, and later with the Update Method pattern. By the end of the session, we will have a game with:

  • An update loop
  • Primitive graphics
  • Collision detection
  • Hardcoded input
  • Crude state management
  • Homemade 8-bit audio

Source code: gar-games/01-pong

  • Building 2D Games, MonoGame’s own tutorial. It builds one game from start to finish, so the sessions link the chapters that match their topics, and the rest is there when you need it.
  • 01: What Is MonoGame?
  • 03: The Game1 File
  • Game Loop (Game Programming Patterns)
  • Update Method (Game Programming Patterns)
  • Optional: 02: Getting Started, for background. Install only the tooling below: our projects build their content with Builder.cs, so you don’t need the MonoGame templates, the VS Code extension or the MGCB Editor.

MonoGame is a framework. It gives us libraries for graphics, input, audio, content loading and math, but it has no editor, scene graph or entity system, as a game engine has. We write those parts ourselves, which makes it a good fit for a course about game architecture.

Tooling used in the course:

  • .NET 10 SDK
  • VS Code with the C# Dev Kit extension
  • Git
  • The course’s games, in one repository on GitHub: gar-games. Each game’s code is split into steps, one project per exercise.

Every game runs a loop: process input, update the simulation, render, repeat. In MonoGame, the Game class runs the loop and calls your Update and Draw overrides.

MonoGame calls Initialize and LoadContent once, then Update and Draw every frame.

Nothing calls Update on your objects for you. Game1 gets one Update and one Draw call per frame, and everything else in your game only runs if you call it from there. For more detail, see What is the Game Loop.

Update changes the state of the world, and Draw only reads that state and renders it. Because they are kept apart, the simulation and the rendering can run at different rates, which is the idea behind a fixed timestep.

Rendering itself uses a double buffer: we draw into a back buffer that isn’t visible, and MonoGame presents it (GraphicsDevice.Present(), called at the end of Draw) so the player never sees a half-drawn frame. See Double Buffer for the pattern.

  • Variable timestep: update once per rendered frame and scale all movement by the elapsed time (deltaTime). Simple, but the simulation behaves slightly differently at different frame rates. A long frame (the window is dragged, the computer stalls) gives a large deltaTime, and a fast object can jump straight through a thin wall in one step.
  • Fixed timestep: update the simulation in fixed-size steps (e.g. 60 per second), running zero, one or several steps per rendered frame. This makes the simulation deterministic and stable, at the cost of some complexity. If one step takes longer than the time it simulates, the game falls further behind every frame; engines limit how many steps they catch up, and MonoGame then runs the game in slow motion. MonoGame uses a fixed timestep by default (IsFixedTimeStep = true).

A row of rendered frames, two of them long. With a variable timestep the two long frames give two big steps. With a fixed timestep every step is the same size, and a long frame runs several of them.

Raw assets (fonts, images, sounds) aren’t loaded directly. When the game builds, the content builder converts them into an optimized format (.xnb files) for the target platform, and at runtime we load them with Content.Load<T>("name") in LoadContent.

flowchart LR
    A["Raw assets<br/>Assets/"] --> B["Builder.cs<br/>rules in C#"] --> C["Content builder<br/>runs on build"] --> D[".xnb files<br/>bin/…/Content"] --> E["ContentManager<br/>Content.Load&lt;T&gt;()"]

The rules for building assets are ordinary C# in Content/Builder/Builder.cs. For Pong, we need fonts and sounds:

Builder.cs
public class Builder : ContentBuilder
{
public override IContentCollection GetContentCollection()
{
var content = new ContentCollection();
// A .spritefont describes a font (e.g. a .ttf next to it), a size and a set of
// characters. The builder renders those characters into a texture.
content.Include<WildcardRule>("*.spritefont", new FontDescriptionImporter(), new FontDescriptionProcessor
{
PremultiplyAlpha = true,
TextureFormat = TextureProcessorOutputFormat.Compressed,
});
// Short sounds become sound effects, loaded fully into memory.
content.Include<WildcardRule>("*.wav", new WavImporter(), new SoundEffectProcessor
{
Quality = ConversionQuality.Best,
});
return content;
}
}

Each game project imports Content/BuildContent.targets, which runs the builder before compiling. To add an asset, put it in Content/Assets; if no rule matches it, add one. All Pong steps share that folder, and step 3 swaps the plain sans font for the retro font in code. See Content Builder Project.

  • MonoGame’s 2D coordinate system has its origin in the top-left corner, with Y pointing down.

The screen with its origin in the top-left corner, x pointing right and y pointing down. A paddle is placed by its top-left corner, and a ball moving down has a growing y.

  • All 2D drawing goes through a SpriteBatch, between Begin() and End(). Draw doesn’t draw straight away. The sprite batch collects the calls, and End() sends them to the graphics card in as few batches as it can. A batch ends whenever the texture changes, which is why Snake puts its images in one texture. Begin() takes the settings for everything up to End(): the transform matrix, the texture filtering and the blending.
  • Pong doesn’t need image assets, but we still need a texture to draw rectangles. A 1×1 white texture stretched to any Rectangle does the job:
_pixel = new Texture2D(GraphicsDevice, 1, 1);
_pixel.SetData([Color.White]);
_spriteBatch.Draw(_pixel, new Rectangle(x, y, width, height), Color.White);
  • Text is drawn with a SpriteFont asset and SpriteBatch.DrawString(). MeasureString gives the size the text will have, which is what you need to centre it:
Vector2 size = _font.MeasureString("Hello, MonoGame!");
_spriteBatch.Begin();
_spriteBatch.DrawString(_font, "Hello, MonoGame!", new Vector2(640 - size.X / 2, 360 - size.Y / 2), Color.White);
_spriteBatch.End();
  • The window’s size is set on the GraphicsDeviceManager in the constructor, with PreferredBackBufferWidth and PreferredBackBufferHeight.
  • Texture filtering: use SamplerState.PointClamp for crisp pixel art when scaling up.

Point filtering keeps the pixels of scaled-up art sharp; linear filtering blurs them.

See What is a sprite?.

Step Pong2

We render the game at a fixed internal resolution, 320×180, and scale it to the window with a transform matrix passed to SpriteBatch.Begin(). The game logic never needs to know how big the window is.

Pong2 scales by the smaller of the two ratios (window width / 320, window height / 180), so nothing is stretched, and centres the result with GraphicsDevice.Viewport. Black bars fill the rest. It recalculates when Window.ClientSizeChanged fires. The window opens at 1280×720, four times the virtual size. At a whole-number scale, every pixel comes out the same size.

The 320 by 180 game scaled into a wide window, with bars at the sides, and into a tall window, with bars above and below.

Try it (Pong2): drag the window wide, then tall. Where do the bars go? Swap PointClamp for LinearClamp, and look at the edges of the text. Then set the virtual size to 640×360. How big is the text now?

We poll the keyboard every frame:

KeyboardState keyboard = Keyboard.GetState();
if (keyboard.IsKeyDown(Keys.W))
_player1Y -= PaddleSpeed * deltaTime;
if (keyboard.IsKeyDown(Keys.S))
_player1Y += PaddleSpeed * deltaTime;

MonoGame has no built-in “was this key just pressed?”. We emulate it by comparing with last frame’s state:

bool enterPressed = keyboard.IsKeyDown(Keys.Enter) && !_previousKeyboard.IsKeyDown(Keys.Enter);
_previousKeyboard = keyboard;

deltaTime is the time since the last update, in seconds:

float deltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds;

Multiplying movement by deltaTime makes speed independent of frame rate. A paddle moving at 200 units per second moves 200 units per second, whether the game runs at 30 or 144 FPS. Velocity (a direction and a speed, e.g. a Vector2) lets the ball keep moving without input.

One second of movement at 30 and at 120 frames per second. Scaled by delta time, both cover 200 pixels. With a fixed step per frame, 120 frames per second goes four times as far.

Our first version puts everything in Game1. As soon as we have a paddle and a ball, we move their data and behaviour into Paddle and Ball classes, each with its own Update and Draw methods. Game1 then just calls them. This is the Update Method pattern: each object knows how to simulate one frame of its own behaviour.

Encapsulation: the data of a thing and the code that uses it move into a class of their own.

It has costs too. Objects are updated one after another, so the order matters. An object updated early sees the others as they were last frame, and one updated late sees them already moved. Objects that interact, like the ball and the paddles, need a reference to each other, or a place like Game1 that checks them together. Every object is updated every frame even when it has nothing to do. And adding or removing objects while looping over them needs care (Angry Birds shows how).

The Update Method pattern: the game calls Update on every object, and each object moves itself one frame on.

Axis-Aligned Bounding Boxes are rectangles that never rotate. Two of them overlap if they overlap on both axes:

public static bool AABB(Rectangle a, Rectangle b)
{
return a.Left < b.Right &&
a.Right > b.Left &&
a.Top < b.Bottom &&
a.Bottom > b.Top;
}

(MonoGame provides the same check as Rectangle.Intersects(), but it’s worth writing yourself once.)

Two pairs of rectangles with their shadows on the x and y axes. The first pair overlaps on both axes and collides. The second overlaps on x only and misses.

Detecting the hit is half of it. The ball also has to respond. Flip its horizontal velocity, and move it back to the paddle’s edge, or it is still inside the paddle on the next frame and bounces again. In Pong7, where the ball hits the paddle sets its new angle. The middle sends it straight back and the ends send it off steeply, so the player can aim:

float offset = ballCentreY - paddleCentreY;
float newVelocityY = offset / (paddle.Height / 2f) * 110;

A paddle with the ball hitting it in five places. From the middle the ball goes straight back, and the nearer the end it hits, the steeper it leaves.

Steps Pong7 → Pong10

Pong has distinct phases: start, serve, play and done. Pong8 keeps the score and shows it in a bigger font, Pong9 adds a serve mode (the player who was scored on serves next), and Pong10 a done mode when a player reaches 10 points. The modes are a simple string field and if-checks in Update and Draw. That is fine for four states, but every new state means another if in each method. In Flappy Bird we replace it with a state machine.

stateDiagram-v2
    [*] --> start
    start --> serve : Enter
    serve --> play : Enter
    play --> serve : a point
    play --> done : 10 points
    done --> serve : Enter

Try it (Pong10): make a game end at 3 points. Then add a paused mode that P toggles during play. Which methods did you have to change?

Step Pong11

SoundEffect is for short clips (hits, jumps, clicks). They are loaded fully into memory and several can play at once. Longer music uses Song, as in Flappy Bird. Pong11 plays a sound for a paddle hit, a wall hit and a point.

Make your own 8-bit sounds with bfxr.net.

Try it (Pong11): Play also takes a volume, a pitch and a pan. Raise the paddle sound’s pitch as the ball speeds up.

Step Pong11

Pong11 is the finished game. Game1 still runs everything. It holds the two paddles and the ball, the mode string, the scores and the sounds, and it checks the ball against each paddle. Paddle and Ball only move and draw themselves.

classDiagram
    Game <|-- Game1
    Game1 --> Paddle : two
    Game1 --> Ball
    Ball ..> Paddle : collides with

The diagram is small because Game1 is big. Every later session moves something out of it.

Concern Answer
What runs every frame The game loop, which calls Update and then Draw
The same speed at any frame rate Movement multiplied by delta time
Who moves each object The object itself, in its own Update (Update Method)
Any window size A virtual resolution, scaled with a matrix
Whether two things touch AABB
Which phase the game is in A string field, which Flappy Bird replaces with a state machine

Build Pong step by step:

  1. An empty game: clone gar-games, open 01-pong/Pong0 (an empty MonoGame game set up with the content builder) and run it from the 01-pong folder with dotnet run --project Pong0. Do the exercises in Pong0; step PongN is a solution to exercise N, to compare with when you’re done.
  2. Text on screen: draw “Hello, MonoGame!” centred on a black 1280×720 window. The font is already in Content/Assets: load it with Content.Load<SpriteFont>("sans"), and centre the text with MeasureString.
  3. Virtual resolution: bring your game up to Pong2. Copy the virtual size constants, the matrix field and UpdateScreenScaleMatrix, call it from Initialize and when Window.ClientSizeChanged fires, and pass the matrix and SamplerState.PointClamp to Begin. Centre the text in 320×180.
  4. Rectangles from a pixel: draw two paddles (5×20) and a ball (4×4), stretched from one white pixel. Switch to the retro font ("font", also in Content/Assets) and the background colour (40, 45, 52).
  5. Input and delta time: W/S and Up/Down move the paddles, at the same speed whatever the frame rate. To check it, set TargetElapsedTime = TimeSpan.FromSeconds(1 / 30.0) in the constructor. Does a paddle still cross the screen in the same time? And without the multiplication by delta time?
  6. A moving ball: launch the ball in a random direction on Enter. A string field remembers the game’s mode ("start" or "play"), and the start mode shows “Press Enter to launch the ball”. Keep the paddles on screen (Math.Clamp).
  7. The Update Method: refactor into Paddle and Ball classes using the Update Method pattern. The game must behave as it did before. In which order does Game1 update the three objects, and would another order change anything?
  8. Collisions and the bounce: bounce off paddles and walls with AABB. Where the ball hits the paddle sets the new angle: straight back from the middle, steeply from the ends. Speed up the ball by 10% on each paddle hit.

Going further (optional): carry on from your own game with what Pong8 to Pong11 show: scoring in a bigger font ("font-big"), a serve mode, a winner at 10 points, and sound effects for paddle hits, wall hits and points (in Content/Assets/sounds, or made with bfxr).

  • Which games would you like to make? Pick two or three candidates for your course project.
  • For each, what updates every frame, and what is drawn? That’s its game loop.
  • Could you build a naive prototype of the core idea first, like this Pong, before worrying about architecture?
What is the difference between a game engine and a game framework?

An engine is a complete environment: editor, scene and object model, and a loop that calls your scripts. A framework like MonoGame is a set of libraries. You write the game’s structure yourself, including the loop’s contents.

Why do we multiply movement by deltaTime?

So that movement per second is the same no matter how many frames are rendered per second.

Why is a string field a fragile way to store game state?

Typos aren’t caught by the compiler, every Update/Draw needs an if-chain over all states, and adding a state means editing all of those chains.

What does the Update Method pattern cost?

Objects are updated one after another, so the order matters. An object updated early sees the others as they were last frame. Objects that interact need a reference to each other or a place that checks them together, and every object is updated every frame, even when it has nothing to do.

How do a fixed and a variable timestep differ?

A variable timestep updates once per rendered frame and scales movement by the elapsed time, so the simulation depends a little on the frame rate. A fixed timestep always advances the simulation by the same amount, running zero, one or several steps per frame, which keeps it stable and repeatable.

Related exam questions: 1, 4, 6, 7.