Pong Play here

Step 0: learn C++ and SDL

SDL provides easy access to keyboard, mouse, audio, and graphics hardware, more than enough for a simple pong game.

Good resources

The plan

A simple game has the following structure

    Initialize_Game_Components();

    while (playing) {

        Handle_User_Inputs();
        Update_Game_State();
        Render_Screen();

    }

    Clean_Up();

Specifically for pong, each of the preceding function may do the following

    Initialize_Game_Components() {
        Create objects like the ball and paddles
        Set initial positions of objects
        Initialize scores
        ...
    }
    Handle_User_Inputs() {
        if user presses "up":
            Move paddle up
        if user quits:
            Clean up
    }
    Update_Game_State() {
        for each game object:
            Move it at its velocity
        for each pair of objects:
            Check for collision
        for each collision:
            Handle collision
    }
    Render_Screen() {
        for each object:
            draw it
    }
    Clean_Up() {
        delete objects and free memory
    }

It’s clear that there are three categories components to this game

  • game objects
    • individual items like the ball and the paddles
  • game state
    • a system that manages game information, objects and their interactions
  • main loop
    • the place that uses the game state and game objects, and provides user inputs and other events
    • SDL3 handles this component

The section below explores game objects and the game state further.

Game objects

A simple pong game only has a few components:

  • a ball
  • two paddles
  • two scores

The ball and the two paddles need collision detection and handling, and movement, while the scores only need to update the text they display.

To make boundary detection more extendable, add four ‘walls’, one of each side, and treat them as collidable objects.

Instead of changing the properties of collided objects immediately upon collision detection, it’s best to first detect all collisions, and then handle them in the right order, so that each object has the correct information when deciding how to react to a collision.

In summary, each game obejct should keep track of its own

  • position
  • current velocity
  • identity
  • initial position and velocity (for resetting)

and

  • the next position and velocity after a collision

Further, each object needs to be able to

  • update its own position based on its velocity
  • render itself
  • register a collision with another object
  • handle a registered collision

In addition, there are two text objects that need to

  • render text using a font
  • update text

Game state

The game state, or app state, needs to keep track of all the objects and their interactions, as well as game information like the scores. It also provides the renderer to objects and manipulate them based on user input.

What it needs to do

  • initialize and clean up game objects
  • keep scores
  • start and reset games
  • move paddles based on user input
  • update objects
  • check collisions between objects and make sure the objects handle them
  • render screen

Having a game state object makes implementing the game loop simple. The end product may look something like this

    ...
    as = Appstate();
    ...

    while (playing) {
        as->UpdatePositions();
        as->CheckCollisions();
        as->ProcessCollisions();
        as->Render();
    }

with additional event handling for user input.

Implementation

This repository contains the full implementation.

Classes

There should be a class for game objects and one for the game state. Text objects can be a child of the game object class with additional text capability.

For example, each game object can have the following member functions and variables. The full implementation contains more helper functions.

    void UpdatePos();                   // called every iteration of the main loop
    SDL_Rect *GetCollisionBox();        // for collision detection
    void Render(SDL_Renderer *renderer);// to render itself
    void RegisterCollision(Body *body); // to plan what to do after colliding
    void HandleCollision();             // to enact changes after colliding
    void Reset();                       // to return to the initial state

    GraphicBox mGraphicBox;             // color, position, and collision box
    RigidBody mRigidBody;               // velocity
    Id mId;                             // identity

    SDL_Rect mPostCollisionPos{};       // for collision handling
    int mPostCollisionXvel{};
    int mPostCollisionYvel{};
    bool mCollided{false};

    GraphicBox mInitGraphicBox;         // for resetting
    RigidBody mInitRigidBody;

A text object can have the following additional members

    void setText(std::string text, SDL_Renderer *renderer);

    std::string mText;
    TTF_Font *mFont;
    SDL_Texture *mFontTexture;

A game state object can have the following members

    void startGame(bool ai);

    void incScore(Side side);
    void decScore(Side side);
    void moveBar(Side side, BarDirection dir); // called after input events

    SDL_Window *getWindow();
    SDL_Renderer *getRenderer();

    void UpdatePositions();                    // called in the mainloop
    void CheckCollisions();                    // called in the mainloop
    void ProcessCollisions();                  // called in the mainloop
    void Render();                             // called in the mainloop

    SDL_Window *mWindow;
    SDL_Renderer *mRenderer;

    int mLeftScore;
    int mRightScore;

    TextBody *mLeftScoreBody;                  // game objects
    TextBody *mRightScoreBody;

    Body *mBall;
    Body *mLeftBar;
    Body *mRightBar;

    // invisible bodies
    Body *mTopWall;
    Body *mBottomWall;
    Body *mLeftWall;
    Body *mRightWall;

Notes on implementation

Because of the detailed plan, implementation is quite straightforward. However, there are some areas that could use more explanation.

Position, color, velocity

The following definitions encapsulates this information

        struct GraphicBox {
            SDL_Rect rect; // size and position
            SDL_Color color;
        };

        struct RigidBody { // velocity
            int xvel;
            int yvel;
        };

Since the position and size of the object is perfectly described by the position and size of its appearance on screen, GraphicBox doubles as graphics and collision box. These structs aren’t necessary since they carry little information, and their members can simply be member variables of the object.

Registering collision

Since there is no child classes for different types of game objects, registering what to do after a collision involves a lot more code on testing the identity of the two colliding objects and choosing the correct behavior. In pseudo-code, it looks like:

        if (this is a ball) {
            if (the other is a paddle) {
                reverse velocity in the x-axis;
            }
            if (the other is a top or bottom wall) {
                reverse velocity in the y-axis;
            }
            ...
        } else if (this is a paddle) {
            if (the other is a top or bottom wall) {
                stop going into the wall;
            }
            ...
        }

Game object initialization

The constructor of the game state object decides the dimensions, looks, initial positions, and velocity of game objects. A lot of these values involve scaling screen dimensions with magic numbers obtained through testing.

There are better ways to initialize them, such as defining clear constants, or having different children classes for each object type that initialize themselves.

Score objects require a call to the function that increases or decreases the score once to render for the first time, which is why the startGame function sets the scores to -1 and increments them immediately. Also, for score keeping, likely one function would be enough although currently there are two, one for incrementing and one for decrementing.

Detecting collisions

The game state object only has to check collisions for objects that could collide. For example, the top wall and the bottom wall never move or tap each other. In the same vein, only some objects need to register collisions. For example, the ball needs to update itself after colliding with anything, but the paddles don’t need to update after colliding with the ball.

Game loop

To remove platform-specific details, and make compiling to WebAssembly easier, it’s best to use main callbacks in SDL3.

SDL_AppInit contains mostly boilerplate code since the game state’s own constructor does the heavy-lifting.

SDL_AppIterate contains calls to various functions in the game state object

    SdlPong::AppState *as = static_cast<SdlPong::AppState *>(appstate);

    as->UpdatePositions();
    as->CheckCollisions();
    as->ProcessCollisions();
    as->Render();

SDL_AppEvent handles events like keyboard inputs, meaning it calls the moveBar and startGame functions of the game state object.

Result

Demo

Repository

Appendix: compiling SDL3 projects using emscripten

As of writing this post, emscripten doesn’t have a port for SDL3, which is still under rapid development. So you have to build SDL from source using emscripten following this guide. In short, inside the repository,

    mkdir build
    cd build
    emcmake cmake ..
    emmake make

To use SDL3_ttf, you have to do the same for SDL_ttf, but with some additional steps.

First, you may have to build freetype with emscripten to use it when building SDL3_ttf.

Second, before building SDL3_ttf, you may have to check the declaration of SDL_CreateHashTable within SDL3_ttf. As of this commit, there is a mismatch between this and the version in the main SDL repository. Without changing anything, comiling your project may result in this warning:

        wasm-ld: warning: function signature mismatch: SDL_CreateHashTable
        >>> defined as (i32, i32, i32, i32, i32, i32) -> i32 in (build dir)/SDL_ttf/build/libSDL3_ttf.a(SDL_ttf.c.o)
        >>> defined as (i32, i32, i32, i32, i32, i32, i32) -> i32 in (build dir)/SDL/build/libSDL3.a(SDL_hashtable.c.o)

and the script would have a runtime error.

To fix it, change the declaration of SDL_CreateHashTable to match that in SDL3, by adding an argument bool threadsafe, and change all invocations of that function within SDL_ttf to use an additional argument, which can simply be false.

Then, use emscripten to compile SDL3_ttf

    mkdir build
    cd build
    emcmake cmake .. -DSDL3_DIR=$EMSCRIPTEN_BUILD_DIR/SDL/build -DBUILD_SHARED_LIBS=OFF -DFREETYPE_LIBRARY=$EMSCRIPTEN_BUILD_DIR/freetype/build/libfreetype.a -DFREETYPE_INCLUDE_DIRS=$EMSCRIPTEN_BUILD_DIR/freetype/include
    emmake make

$EMSCRIPTEN_BUILD_DIR is where you built SDL3 and freetype with emscripten.

Finally, to build the SDL pong project1, make sure to include and link the newly built packages and libraries,

    mkdir build
    cd build
    em++ -o index.html -I$EMSCRIPTEN_BUILD_DIR/freetype/include/ -I$EMSCRIPTEN_BUILD_DIR/SDL/include -I$EMSCRIPTEN_BUILD_DIR/SDL_ttf/include $EMSCRIPTEN_BUILD_DIR/freetype/build/libfreetype.a $EMSCRIPTEN_BUILD_DIR/SDL_ttf/build/libSDL3_ttf.a $EMSCRIPTEN_BUILD_DIR/SDL/build/libSDL3.a ../game.cpp ../sdl_pong.cpp --embed-file ../slkscr.ttf@/font/slkscr.ttf -std=c++20
  1. the build command assume the default font path in sdl_pong.cpp has changed to /font/slkscr.ttf