Menu Share

Prgramming a pong game in ANSI C

by | Last Updated:
ANSI C

In this tutorial I will teach you about some basics of the C programming language. We will code a simple pong game that you can play with a buddy. There are not a lot of pure C tutorials and games out there. It is an older language and is mostly used for systems and library programming. That shouldn’t stop you though. The standard hasn’t stopped evolving through the years along with the C++ programming standard.

Table of Contents

Before continuing

To completely understand the contents of this article you should know:

Project structure

We need to start from somewher and this would be the project structure. I use CMake but if you’re familiar with other build tools and environments feel free to use them instead and just follow skip to the code part.

To structure the project it would be best to separate the source files in their own folder (clean root folder means easier orientation).

root
├── vcpkg/
│ └── ...for raylib...
├── Pong/
│ ├── src/
│ │ ├── main.h
│ │ └── main.c
│ └──CMakeLists.txt
└── CMakeLists.txt

The root CMakeLists.txt file will contain the basic setup and dependencies management. The one contained in Pong will contain the list of source files and the project executable. This kind of structure allows for defining several executables or libraries like unit testing or other dependencies.

The root cmake file will contain the following code:

cmake_minimum_required(VERSION 3.16)
 # Feel free to change the version to the one you have available
set(CMAKE_TOOLCHAIN_FILE ./vcpkg/scripts/buildsystems/vcpkg.cmake)

project(Pong)

find_package(raylib CONFIG REQUIRED)

add_subdirectory(Pong) # This will include the subdirectory CMakeLists.txt file when we execute cmake from the root of our project

The cmake file in the pong directory will contain the following code initially:

project(Pong LANGUAGES C)

add_executable(Pong
               src/main.h
               src/main.c
               # Add every file that we create as a new line here
               )

# Set the compiler and linker to include Raylib
target_include_directories(Pong PRIVATE ${raylib_INCLUDE_DIRS})
target_link_libraries(Pong PRIVATE ${raylib_LIBRARIES})

We also need to define some code for our main.h and main.c files so that we can successfully compile the program. I will start with a simple main function that we will later extend until the game is complete.

// main.h

#ifndef PONG_MAIN_H
#define PONG_MAIN_H

#include <raylib.h>

#endif

When creating functions later I will intentionally skip on some details like defining function signatures in the header. There are more than a million ways to write code for a pong game. What I want to do is challenge my readers. For you to find a way to compile your own solution using my guidelines instead of giving my ready-made solution. If you want to take a look at the final implementation you can always find the source code for the pong game in the ansi-c branch of my pong repository.

// main.c

#include "main.h"

int main() 
{
    return 0;
}

The program should now compile and run without any output.

Note: If you need help with compiling using CMake you can get a quick reminder in this article about working with cmake.

Creating the window

Next step in our programming adventure is to create and render the window. For this we need to modify our main file structure. Usually in games you would have a main “game loop” which runs until the application is terminated. This loop would update some values and render on the screen. If we take this in consideration we should probably separate the code into small size chunks by creating some functions.

To initialize the window we use the Raylib funciton “InitWindow” by passing it the size and title that we want. It is also a good idea to set a target FPS for our game – its up to you how fast you want the game to update. Drawing is done by the video card (GPU) but we prepare the draw calls in the processor (CPU). We need to say when the frame drawing begins and when it ends by using the funcitons BeginDrawing and EndDrawing. We can also add a background color to our game and this should be the first thing we call every frame by using the ClearBackground function.

// ... Header includes and don't forget to define function signatures ...

int main()
{
    int screenWidth = 800;
    int screenHeight = 450;
    InitWindow(screenWidth, screenHeight, "Pong");
    SetTargetFPS(60);
    
    Loop();
    
    return 0;
}

void Loop()
{
    // The main game loop in raylib is really simple - just loop until window should close!
    while (!WindowShouldClose())
    {
        Update();
        // Draw or Render - whatever suits you most
        Draw();
    }
}

void Update()
{
    // Game rules go here
}

void Draw()
{
    BeginDrawing();
    ClearBackground(BLACK);
    
    // Other draw calls would go here.
    
    EndDrawing();
}

After successful compilation of this code we should be greeted with a window that we can move around. It will have a black background as in the draw method we use BLACK to clear the window. You’re free to choose your own windows sizes and colors this is just what I go for.

Drawing the board and score

The next step will be to draw something on the screen. Raylib makes it really easy to draw to the screen. If you want to see what is available in this library you can visit the raylib cheatsheet. Now lets add a middle line and some score. In the C language primitive data (numbers, characters, etc.) can be combined into chunks called structs. One such struct is Vector2 defined by raylib and represents two numbers for coordinates – x and y. We would need the Vector2 to define the start and end position of the line:

// ...Inside void Draw() function

// Create the structs giving values to x and y
Vector2 from = {.x = (GetScreenWidth() / 2.f), .y = 5};
Vector2 to = {.x = (GetScreenWidth() / 2.f), .y = (GetScreenHeight() - 5.f)};

// Pass the structs to the DrawLineEx function to draw the line on the screen
DrawLineEx(from, to, 2, LIGHTGRAY);

This should add a nice separator line starting 5 pixels from the middle top of the window and ending just 5 pixels before the bottom middle part.

Next is the score. I like to put the score in the middle. If you would like it somewhere else you can always modify the calculations. It is easy to put text on the right of the separating line as it grows to the right. This is not the same with the left. When the score grows on the left we need to push it to the left. For this we would need to measure the rendered text. First we will use the function TextFormat to make our numbers to strings. Then we will use the MeasureText function that will say how long our text will end up using the default font provided in Raylib (we pass the text and the pixel size). Then we draw the text on the screen.

When drawing with raylib consider that the coordinates (0, 0) are on the top-left part of the screen. They are measured in pixels and X grows positively to the right while Y grows positively downwards. Also element positions are the on the top left so when we specify the coordinates for the text we do so by telling where the first character on the left is located.

const char* scoreLeft = TextFormat("%d", 10);
int scoreSizeLeft = MeasureText(scoreLeft, 20);
DrawText(scoreLeft, (GetScreenWidth() / 2) - 10 - scoreSizeLeft, 10, 20, LIGHTGRAY);
    
const char* scoreRight = TextFormat("%d", 11);
DrawText(scoreRight, (GetScreenWidth() / 2) + 10, 10, 20, LIGHTGRAY);

These small calculations to the positioning will make all the difference on how the end game will look! Now you know how to draw some basic elements like text and lines on the window. For the ball and pads we will use one more methods shown later – DrawRectangle – that accepts position, size and color.

Programming the pads

When I program things I like to separate them into descriptable chunks. Because of this we define our pads with a control scheme, position, size, speed and score. The control scheme would also define an up button and a down button. This is done so that the two pads are controllable by different buttons on the keyboard.

// pad.h
#include <raylib.h>

typedef struct {
    int UpButton;
    int DownButton;
} InputScheme;

typedef struct {
    InputScheme Scheme;
    int Score;
    float Speed;
    
    Vector2 Position;
    Vector2 Size;
} Pad;

I will also define two functions that will take the pads for update and draw. Functions will accept a pointer to the struct so that the data is the same as the calling function and not copied. We will check input inside the update function to see if the player pressed a button last frame. Then we need to see how much the player should move. Speed is a function of time meaning it defines how much in space we would move for a certain amount of time – in our case a second. GetFrameTime would return the time in seconds as a float – meaning if half a second has passed we would get 0.5 as a value. We multiply that with the speed and we move the pad with that many pixels.

// pad.c

void UpdatePad(Pad* pad)
{
    int height = GetScreenHeight();
    
    if (IsKeyDown(pad->Scheme.DownButton))
    {
        // Movement is a function of time
        pad->Position.y += GetFrameTime() * pad->Speed;
        
        // Limit the movement on the screen by force setting the value to the maximum value
        if(pad->Position.y + pad->Size.y / 2 > height) {
            pad->Position.y = height - pad->Size.y / 2;
        }
    }
    
    if (IsKeyDown(pad->Scheme.UpButton))
    {
        pad->Position.y -= GetFrameTime() * pad->Speed;
    
        // Limit the movement on the screen by force setting the value to the uppermost value
        if(pad->Position.y - pad->Size.y / 2 < 0) {
            pad->Position.y = pad->Size.y / 2;
        }
    }
}

void DrawPad(Pad* pad)
{
    DrawRectangle(pad->Position.x, pad->Position.y - (pad->Size.y / 2), pad->Size.x, pad->Size.y, WHITE);
}

Adding the ball

We need to do something similar to the ball but isntead from input it will move on its own every frame. It is useful to add a function to detect if the ball touches one of the pads. The ball can have a constant velocity.

// ball.h

#include <raylib.h>
#include "pad.h"

typedef struct
{
    Vector2 Position;
    Vector2 Velocity;
} Ball;

void UpdateBall(Ball* ball);
void DrawBall(Ball* ball);
bool DetectBallTouchesPad(Ball* ball, Pad* pad);

And the implementation is quite simple – move ball by velocity every frame and draw a rectangle on its position. The detection function would check if the ball position is inside the rectangle of the pad. This is simpler than collision detection between two rectangles but it is all we need. Always be careful to not overcomplicate your games with more code than you need.

#include "ball.h"

void UpdateBall(Ball* ball)
{
    ball->Position.x += GetFrameTime() * ball->Velocity.x;
    ball->Position.y += GetFrameTime() * ball->Velocity.y;
}

void DrawBall(Ball* ball)
{
    DrawRectangle(ball->Position.x - 5, ball->Position.y - 5, 10, 10, WHITE);
}

bool DetectBallTouchesPad(Ball* ball, Pad* pad)
{
    if (ball->Position.x >= pad->Position.x && ball->Position.x <= pad->Position.x + pad->Size.x)
    {
        if (ball->Position.y >= pad->Position.y - (pad->Size.y / 2) &&
            ball->Position.y <= pad->Position.y + (pad->Size.y / 2))
        {
            return true;
        }
    }
    
    return false;
}

Mixing it all together

In the previous two sections we defined some data types and update functions for them but we have not yet mixed them into our game loop. Our game loop needs to also hold on to these values so that it can pass them around the update functions so we could also create a simple struct called GameData to hold the values:

// ...in main.h
typedef struct {
    Pad player1;
    Pad player2;
    Ball ball;
} GameData;

// ... we also need to modify our functions to accept it as an argument
void Loop(GameData* data);
void Update(GameData* data);
void Draw(GameData* data);

// ... and in main
int main()
{
    // ...window initialization

    int height = GetScreenHeight();
    int width = GetScreenWidth();

    GameData data;
    
    Vector2 ballPosition = {.x = width / 2, .y = height / 2};
    Vector2 ballVelocity = {.x = 100, .y = 50};
    data.ball.Position = ballPosition;
    data.ball.Velocity = ballVelocity;
    
    Vector2 padSize = {.x = 10, .y = 100};
    
    InputScheme player1Input = {.UpButton = KEY_W, .DownButton = KEY_S};
    Vector2 player1Position = {.x = (20), .y = (height / 2.f)};
    data.player1.Position = player1Position;
    data.player1.Size = padSize;
    data.player1.Speed = 220;
    data.player1.Score = 0;
    data.player1.Scheme = player1Input;
    
    InputScheme player2Input = {.UpButton = KEY_UP, .DownButton = KEY_DOWN};
    Vector2 player2Position = {.x = (width - 20.f - padSize.x), .y = (height / 2.f)};
    data.player2.Position = player2Position;
    data.player2.Size = padSize;
    data.player2.Speed = 220;
    data.player2.Score = 0;
    data.player2.Scheme = player2Input;

    // ... loop

    return 0;
}

This should be enough to initialize our for both players. I give you some direct reference values but you can vary them until you like how the game plays out. We also need to add code for updating the pads and ball and drawing them on the screen:

// ... in update function
UpdatePad(&data->player1);
UpdatePad(&data->player2);
UpdateBall(&data->ball);

// ... in draw function
DrawBall(&data->ball);
DrawPad(&data->player1);
DrawPad(&data->player2);

// ... and also modify the score text TextFormat("%d", data->player1.Score) and TextFormat("%d", data->player2.Score)

There is only one last part missing before the game is completed for playing. This is the rules of the game. If the ball exits the screen on left or right it should bring a point to one of the players. The ball should also reset ot the center. If the ball hits a pad its velocity should get reflected on X. Also if it hits the screen on top or bottom it should be reflected on Y.

// ... You can create a separate function or put inside update - your decision

if (DetectBallTouchesPad(&data->ball, &data->player1) || DetectBallTouchesPad(&data->ball, &data->player2))
{
    data->ball.Velocity.x *= -1;
}

if (data->ball.Position.y > height || data->ball.Position.y < 0)
{
    data->ball.Velocity.y *= -1;
}

if (data->ball.Position.x > width)
{
    data->player1.Score += 1;
    Vector2 ballPosition = {.x = width / 2, .y = height / 2};
    data->ball.Position = ballPosition;
    data->ball.Velocity = INITIAL_BALL_VELOCITY;
}

if (data->ball.Position.x < 0)
{
    data->player2.Score += 1;
    Vector2 ballPosition = {.x = width / 2, .y = height / 2};
    data->ball.Position = ballPosition;
    data->ball.Velocity = INITIAL_BALL_VELOCITY;
}

Conclusion

I hope you learned something valuable in this quick tutorial. I intentionally don’t cover every part of the code here. That way you can solve some of the problems on your own. This will be the most valuable lesson I can teach you. You can always refer to the source code. If you need more game ideas check my article about a few of those for beginners.

Read next: How to port my game for the web

Leave a comment

Comments:

Jens |
Hi, thanks for this nice tutorial. I am a beginner and trying to catch up but announced as ANSI C the pong game does not work out as expected. the example code on github is clearly cpp, not ye goode olde C code. I setup the whole thing in C on codeblocks IDE with raylib properly setup and linked to the shared libs. The whole thing compiles after several tiny changes (most including the header files again or defining variables), but leaves me with an eventually blank window. :-) in the beginning, I wanted to learn how to handle concurrent events by event handlers and event loops but when I realised this all is managed by the raylib it was to late for me to return. I was already too deep in the rabbit hole. Now I would like to finish this little game at least before turning to something else. Probably, I am missing something essentially completely (also from the tutorial. Do you mind having a look? link to github
Hristo Stamenov |
Hi, Jens, In my github sxample there is a branch with the ansi C version of the game. I will write you additionaly when I take a look at your code.
Jens |
@Hristo thanks, I figured it out. it didn't let me alone. Added some stuff and at least I can study the code now. Thanks, I learned a lot from this here. Wished to see more such beginner tuts - especially in clean and simple c code. Well, for all interested in figuring this tutorial out you can check out my git.