Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Building a Pong Game (AI generated)

This tutorial will guide you through creating a classic Pong game from scratch using the Gama engine. This is a great project for beginners as it covers all the fundamental concepts of game development in a simple and easy-to-understand way.

Prerequisites

Before you begin, make sure you have the following installed:

  • A C compiler (like GCC)
  • The gama command-line tool

1. Setting Up Your Project

First, let's create a new project. Open your terminal and run:

gama create mypong

This will create a new directory called mypong with a basic project structure.

2. The main.c File

Navigate into your new project directory and open the src/main.c file. This is the entry point for your game. You will see three main functions:

  • init(App *app): Called once when your application starts.
  • create(App *app): Called after the application window has been created.
  • shutdown(App *app): Called when the application is about to close.

Let's start by setting a title for our game window in the init function:

// src/main.c
#include "../assets/gama/gama.h"

void init(App *app) {
    SetAppTitle(app, "Gama Pong");
}

void create(App *app) {}
void shutdown(App *app) {}

3. Setting up the Game Scene

For Pong, we only need one scene. We can set this up directly in our main.c file. We'll define the functions for our scene and then assign them in the create function.

// src/main.c
#include "../assets/gama/gama.h"

// Forward declarations for our scene functions
void gameCreate(Scene *scene);
void gameUpdate(Scene *scene, double theta);
void gameRender(Scene *scene);
void gameKey(Scene *scene, KeyEvent *e);

void init(App *app) {
    SetAppTitle(app, "Gama Pong");
}

void create(App *app) {
    Scene *gameScene = createScene(app);
    gameScene->create = gameCreate;
    gameScene->update = gameUpdate;
    gameScene->render = gameRender;
    gameScene->onkey = gameKey;
    gameScene->background = BLACK;
    showScene(app, gameScene);
}

void shutdown(App *app) {}

// ... implementations of gameCreate, gameUpdate, etc. will go here ...

4. Creating the Game Objects

We need two paddles and a ball. These will be Shape objects. Let's declare them as global variables and initialize them in gameCreate.

// ... after #include
Shape leftPaddle, rightPaddle, ball;

void gameCreate(Scene *scene) {
    // Left Paddle
    createRectangle(&leftPaddle, at(-0.9, 0), at(0.05, 0.3), WHITE);

    // Right Paddle
    createRectangle(&rightPaddle, at(0.9, 0), at(0.05, 0.3), WHITE);

    // Ball
    createCircle(&ball, at(0, 0), 0.025, WHITE);
    setShapeVelocity(&ball, at(0.5, 0.5)); // Give the ball some initial speed
}

A Note on the Coordinate System

Gama uses a 2D Cartesian coordinate system where the center of the screen is (0, 0). The x-axis ranges from -1.0 (left edge) to 1.0 (right edge), and the y-axis ranges from -1.0 (bottom edge) to 1.0 (top edge).

5. Rendering the Game

Now, let's draw our shapes in the gameRender function.

void gameRender(Scene *scene) {
    renderShape(&leftPaddle);
    renderShape(&rightPaddle);
    renderShape(&ball);
}

If you run the game now (gama build -r) which builds your projet and runs it, you should see the paddles and the ball, but nothing is moving yet.

6. Moving the Paddles

We need to handle keyboard input to move the paddles. We'll do this in the gameKey function.

void gameKey(Scene *scene, KeyEvent *e) {
    double paddleSpeed = 1.0;
    if (e->key == KeyW) {
        setShapeVelocity(&leftPaddle, at(0, paddleSpeed));
    }
    if (e->key == KeyS) {
        setShapeVelocity(&leftPaddle, at(0, -paddleSpeed));
    }
    if (e->key == KeyUp) {
        setShapeVelocity(&rightPaddle, at(0, paddleSpeed));
    }
    if (e->key == KeyDown) {
        setShapeVelocity(&rightPaddle, at(0, -paddleSpeed));
    }
}

7. The Game Loop and Frame-Rate Independence

Now we need to update the positions of our objects in gameUpdate. This function is the heart of our game's logic and is called on every frame.

This function receives a theta parameter, which is the time in seconds that has passed since the last frame (also known as delta time). It is crucial to use theta in your movement calculations to ensure your game runs at the same speed on all computers, regardless of their frame rate. This is called frame-rate independence.

void gameUpdate(Scene *scene, double theta) {
    updateShape(&leftPaddle, theta);
    updateShape(&rightPaddle, theta);
    updateShape(&ball, theta);

    // Stop paddles at the window edges
    if (shapeTop(&leftPaddle) > 1.0 || shapeBottom(&leftPaddle) < -1.0) {
        setShapeVelocity(&leftPaddle, at(0, 0));
    }
    if (shapeTop(&rightPaddle) > 1.0 || shapeBottom(&rightPaddle) < -1.0) {
        setShapeVelocity(&rightPaddle, at(0, 0));
    }
}

Run the game now, and you should be able to move the paddles!

8. Collision and Bouncing

Let's make the ball bounce off the walls and the paddles. The bounceShape function is perfect for this. It works by inverting the velocity of a shape on a given axis. For example, bounceShape(&ball, 0, 1.0) will not change the x-velocity but will invert the y-velocity, making the ball bounce vertically.

void gameUpdate(Scene *scene, double theta) {
    // ... (previous code from gameUpdate)

    // Ball bouncing off top and bottom walls
    if (shapeTop(&ball) > 1.0 || shapeBottom(&ball) < -1.0) {
        bounceShape(&ball, 0, 1.0);
    }

    // Ball bouncing off paddles
    if (rectsCollide(getShapePosition(&ball), at(0.05, 0.05), getShapePosition(&leftPaddle), leftPaddle.size) ||
        rectsCollide(getShapePosition(&ball), at(0.05, 0.05), getShapePosition(&rightPaddle), rightPaddle.size)) {
        bounceShape(&ball, 1.0, 0);
    }

    // Scoring and resetting the ball
    if (shapeLeft(&ball) < -1.0 || shapeRight(&ball) > 1.0) {
        setShapePosition(&ball, at(0, 0));
        setShapeVelocity(&ball, at(0.5, 0.5)); // Reset ball speed
    }
}

9 Adding text

For gama to be able to show text to your users, it needs a font. Add the Ubuntu-R.ttf font(which comes with gama) using:

gama font add Ubuntu-R.ttf

This will create the new file in your assets/fonts/Ubuntu-R.ttf your app can now use for rendering text. You can view other available fonts with gama font list.

9. Adding Scoring

Let's add scoring. We'll need a font and two text objects.

// ... after shape declarations
Font *font;
Text *leftScoreText, *rightScoreText;
int leftScore = 0, rightScore = 0;

void gameCreate(Scene *scene) {
    // ... (paddle and ball creation)

    font = loadFont("assets/fonts/Ubuntu-R.ttf"); // the font you just added
    leftScoreText = createTextNulled("0", font, at(-0.1, 0.8));
    rightScoreText = createTextNulled("0", font, at(0.1, 0.8));
}

void gameRender(Scene *scene) {
    // ... (render shapes)
    renderText(leftScoreText);
    renderText(rightScoreText);
}

void updateScore() {
    char scoreStr[10];
    sprintf(scoreStr, "%d", leftScore); // put the score text in scoreStr with printf like formating
    setTextNulled(leftScoreText, scoreStr); // set the text to the new string
    sprintf(scoreStr, "%d", rightScore);
    setTextNulled(rightScoreText, scoreStr);
}

Now we will add logic so that if the ball exits the screen(passes behind a paddl) then it is one point for the other user, and the ball returns to the middle of the scene(at(0, 0))

// In gameUpdate, modify the scoring logic:
// ...
    if (shapeLeft(&ball) < -1.0) { // Right player scores
        rightScore++;
        updateScore();
        setShapePosition(&ball, at(0, 0));
    }
    if (shapeRight(&ball) > 1.0) { // Left player scores
        leftScore++;
        updateScore();
        setShapePosition(&ball, at(0, 0));
    }
// ...

10. Game Over and Restarting

Let's add a simple game over condition. The first player to reach 5 points wins.

// ... after score variables
Text *gameOverText;
int gameOver = 0; // 0 for playing, 1 for game over

void gameCreate(Scene *scene) {
    // ...
    gameOverText = createTextNulled("", font, at(0, 0));
    gameOverText->fontsize = 0.2;
    gameOverText->color = RED;
}

void resetGame() {
    leftScore = 0;
    rightScore = 0;
    updateScore();
    setShapePosition(&ball, at(0, 0));
    setShapeVelocity(&ball, at(0.5, 0.5));
    setTextNulled(gameOverText, "");
    gameOver = 0;
}

// In gameKey, add a restart key:
void gameKey(Scene *scene, KeyEvent *e) {
    // ... (paddle movement)
    if (e->key == KeyR && gameOver) {
        resetGame();
    }
}

// In gameUpdate, check for game over:
void gameUpdate(Scene *scene, double theta) {
    if (gameOver) return; // Don't update the game if it's over

    // ... (rest of the update logic)

    if (leftScore >= 5) {
        setTextNulled(gameOverText, "Left Player Wins!\nPress R to Restart");
        gameOver = 1;
    }
    if (rightScore >= 5) {
        setTextNulled(gameOverText, "Right Player Wins!\nPress R to Restart");
        gameOver = 1;
    }
}

// In gameRender, draw the game over text:
void gameRender(Scene *scene) {
    // ... (render shapes and scores)
    if (gameOver) {
        renderText(gameOverText);
    }
}

11. Conclusion

You now have a complete, working Pong game with a scoring and game over system! This tutorial has covered the basics of creating a game in Gama. From here, you can try to add more features, like a main menu, sound effects, or increasing ball speed over time.