Resolution independent 2D graphics engine
04 Pong
Picture of pong game

This tutorial demonstrates the implementation of a simple Pong game using Quad-ren. It draws on everything discussed in the previous tutorials in addition to demonstrating how custom event types can be created.

#include <quad-ren/quad-ren.h>
#include <math.h>
#include <stdlib.h>
#include <time.h>

The games scene graph consists of 5 scene nodes. At the lowest level is a null node which forms the tree root. Derived from this are 4 nodes, one circle for the ball and 3 quads, two for the bats and one for the field center line.

We will need three custom animators, one to animate the ball, one to animate the bats and one to monitor the balls location and detect when a player has won. The ball animator moves the ball and bounces it off the top and bottom walls of the playing field. The bat animators will change the bats vertical location in response to user input. In addition to this they will also handle bat-ball collision detection and raise a ball hit event if a collision is detected. The final animator is used to monitor the balls location to check if it goes past either of the two bats, and exits the game, displaying the winning player in the terminal. Animators can be used like this because they are called every frame, they are not limited to simply animating the scene.

In order for the bats to be able to detect ball collisions they must know where the ball is at the current point in time. To achieve this a scene graph variable called ball loc is attached to the root node, acting as an in-between between the ball and bat animators. Additionally a qr::key_var_mapper will be attached to the root to map the key states into a scene graph variable called keys_down. This class functions the same as the ship_receiver event receiver which was implemented in the previous tutorial.

To detect and act upon the ball hit event which is raised by the bat animator upon the detection of a collision, a custom event receiver is needed. This will listen for the ball hit event and invert the balls horizontal direction of travel.

To begin, we first implement the ball animator. class ball_animator : public qr::animator
{
protected:

Firstly the on_attach method needs to be defined, the primary purpose of this method is to define the 'ball acceleration' scene graph variable that will be used to store the balls acceleration vector. To make the game more interesting the balls initial trajectory is randomised. First we generate a random angle and convert it into radians.     void on_attach(qr::scene_node *ball)
    {
        float angle = (rand() % 180) + 1;
        angle = angle * (3.142/180);

To prevent the balls starting trajectory being either straight up or straight down, which would result in an unplayable game the balls horizontal velocity is constant. This is randomly inverted to create a random starting direction.         float direction = 0.002;
        if(rand() % 2 == 0)
            direction = direction - (direction * 2);

Finally for the on_attach method, the 'ball acceleration' scene graph variable.         ball->create_var(
            qr::vector2d_f(direction, cos(angle) / 1000),
            "ball acceleration");
    }

Now that the balls acceleration vector has bean created, we can implement the balls animation. The first step is to fetch the balls location, size and the acceleration vector.     void on_animate(qr::scene_node *node, float time_delta)
    {
        qr::vector2d_f ball_accel = node->get_var<qr::vector2d_f>("ball acceleration");
        qr::vector2d_f node_loc    = node->get_location();
        qr::vector2d_f node_size   = node->get_size();

To make the ball bounce of the top and bottom walls of the playing field, we need to know the size of the viewport as this forms the bounds the field. The dimensions of the viewport can be obtained with the renderers get_viewport_size method.         qr::renderer   *l_renderer = node->get_renderer();
        qr::vector2d_f viewp_size   = l_renderer->get_viewport_size();

Now that we have the viewport dimensions, we need to calculate two values which define the minimum and maximum co-ordinate that the ball can be before bouncing. First the top value is calculated by halving the height of the viewport, this alone is not sufficient as it would calculate the collision based on the balls center. In order to fix this problem we must also subtract the radius (half the diameter) of the ball. With the value for the top calculated, the value for the bottom can be created by negating the value for the top.         float view_top = (viewp_size.Y / 2) - (node_size.Y / 2);
        float view_bot = -view_top;

Next we need to compare the balls current location to the bounding values that we just calculated, if the ball is outside of the bounds, the Y component of its acceleration vector is inverted. To invert a value either the negation operator can be used, or the variable can be subtracted from itself times two. Here a value slightly less than two is used so that the ball slows down gradually every time it bounces.         if(node_loc.Y > view_top ||
           node_loc.Y < view_bot)
        {
            ball_accel.Y = ball_accel.Y - (1.97 * ball_accel.Y);

Depending on the speed that the ball hit the side of the playing field at, it may still be out of bounds after its next animation step, which would cause its movement vector to be flipped again, trapping the ball in the side of the playing field. To fix this the ball is moved back to the edge of the field after a collision.             if(node_loc.Y > view_top)
                node_loc = qr::vector2d_f(node_loc.X, view_top);
            if(node_loc.Y < view_bot)
                node_loc = qr::vector2d_f(node_loc.X, view_bot);
        }

Finally for the on animate method we move the ball by adding its current location to the ball acceleration vector. Which is multiplied by the time delta to create framerate independent animation. Additionally the ball acceleration vector and location scene graph variables are updated.         node->set_location(qr::vector2d_f(
            node_loc.X + (ball_accel.X * time_delta),
            node_loc.Y + (ball_accel.Y * time_delta)));

        node->set_var("ball acceleration", ball_accel);
        node->set_var("ball loc", node_loc);
    }

Lastly for the balls animator, the on_detach method is used to delete the balls acceleration vector when the animator is detached from a scene node.     void on_detach(qr::scene_node *node)
    {
        node->delete_var("ball acceleration");
    }
};

With the balls animator finished, we now need to create the ball hit event which signals that a ball-bat collision has occurred and implement the balls event receiver to react to it. The first thing to do is define an enum to uniquely identify the event. Here the first line of the enum creates a dummy element to set the enumerator and the second line defines the event type identifier. typedef enum {
    MY_EV_START = qr::EV_LAST,
    BALL_HIT_EVENT
} MY_EVENTS;

Now we define the event itself, this class stores the deflection which alows the balls trajectory to be altered depending on the speed at which the bat is moving when it hits the ball. The events constructor is used to set the event type and the deflection value. class ball_hit_event :public qr::event
{
public:
    float deflection;

    ball_hit_event(float deflection)
    {
        this->set_type(BALL_HIT_EVENT);
        this->deflection = deflection;
    }
};

With the event object defined, we move on to the bat animator which allows the bats to be moved with the keyboard and raises a ball hit event if a ball collision is detected. This class has two member variables which store the keys which will move the bat up and down, the values of these are set by the constructor. class bat_animator : public qr::animator
{
    qr::KEY up;
    qr::KEY down;

public:
    bat_animator(qr::KEY up, qr::KEY down)
    {
        this->up   = up;
        this->down = down;
    }

On attach creates a variable to store the bats acceleration, unlike the ball this is a scalar, not a vector as the bat can only move in one direction.     void on_attach(qr::scene_node *node)
    {
        node->create_var(float(0), "bat acceleration");
    }

Next we implement the on_animate method to animate the bat in response to the keys which were registered in the constructor. First we fetch the balls location, the bats location, size and acceleration and the key states array.     void on_animate(qr::scene_node *node, float time_delta)
    {
        qr::vector2d_f ball_loc = node->get_var<qr::vector2d_f>("ball loc");
        qr::vector2d_f bat_loc  = node->get_location();
        qr::vector2d_f bat_size = node->get_size();
        float bat_accel = node->get_var<float>("bat acceleration");

        qr::keyarr keys_down = node->get_var<qr::keyarr>("keys down");

To soften the bats movement, instead of moving the scene node directly like we did in the previous tutorial, we have the keyboard increase an acceleration value which is used to move the bat. Here we check to see if a key is being held down and increase or decrease the bats acceleration.         if(keys_down[this->up])
            bat_accel += 0.001;

        else if(keys_down[this->down])
            bat_accel -= 0.001;

To stop the bat form accelerating indefinitely a negative 'friction' force is applied to it.         bat_accel /= 1.2;

In order to limit the bats location to within the bounds of the playing feild, we get the viewport size, and calculate a minimum and maximum value like in the ball animator.         qr::renderer   *l_renderer = node->get_renderer();
        qr::vector2d_f viewp_size  = l_renderer->get_viewport_size();

        float bat_max = (viewp_size.Y / 2) - (bat_size.Y / 2);
        float bat_min = -bat_max;

Next we check to see if the current location of the bat is outside of the bounding range, if it is the acceleration is set to zero and its location is moved back to the edge of the field.         if(bat_loc.Y > bat_max)
        {
            bat_accel = 0;
            bat_loc.Y = bat_max;
        }

        else if(bat_loc.Y < bat_min)
        {
            bat_accel = 0;
            bat_loc.Y = bat_min;
        }

Now we apply the acceleration to the bats location and update the acceleration scene graph variable.         node->set_location(
            qr::vector2d_f(
                bat_loc.X,
                bat_loc.Y += (bat_accel * time_delta)));

        node->set_var("bat acceleration", bat_accel);

Finally we compare the balls current location against the bat using a simple point-rectangle collision algorithm. if a collision is detected we raise a ball hit event. The bats current acceleration is passed to the event so that it can be used to deflect the balls trajectory.         if(ball_loc.X > bat_loc.X - (bat_size.X * 1.3) &&
           ball_loc.X < bat_loc.X + (bat_size.X * 1.3) &&
           ball_loc.Y < bat_loc.Y + (bat_size.Y * 0.64) &&
           ball_loc.Y > bat_loc.Y - (bat_size.Y * 0.64))
        {
            node->raise_event(new ball_hit_event(bat_accel));
        }
    }

To finish off the bat animator, on_detach is used to delete the bat acceleration scene graph variable.     void on_detach(qr::scene_node *node)
    {
        node->delete_var("bat acceleration");
    }
};

Next we need to define an event receiver for the ball to detect the ball_hit_event and invert the horizontal component of the balls acceleration vector. As we do not need to define any scene graph variables within this event receiver we skip directly to implementing the on_event method. class ball_receiver : public qr::event_receiver
{
    void on_event(qr::scene_node *node, qr::event *n_event)
    {

Now we check the events type, if it is a ball_hit_event we downcast the event object. After that the balls location and acceleration are retrieved.         if(n_event->get_type() == BALL_HIT_EVENT)
        {
            ball_hit_event *h_event = (ball_hit_event*) n_event;

            qr::vector2d_f node_loc    = node->get_location();
            qr::vector2d_f ball_accel =
                node->get_var<qr::vector2d_f>("ball acceleration");

To make the ball bounce off the bat we negate the horizontal component of its acceleration vector. The balls location is also moved out of the bat to stop it getting trapped inside the bat.             ball_accel.X = -ball_accel.X;

            if(node_loc.X > 4.52)
                node_loc.X = 4.519;
            if(node_loc.X < -4.52)
                node_loc.X = -4.519;

Here the bats acceleration which was stored in the event is applied to the Y component of the balls acceleration vector, causing its trajectory to be altered depending on the bats speed when the collision occurred.             float deflection = h_event->deflection;
            ball_accel.Y += deflection / 2;

Lastly the acceleration vector and ball location are updated. The on_attach and on_detach methods also need to be overridden to allow the class to be instanced, though we don't use them for anything.             node->set_var("ball acceleration", ball_accel);
            node->set_var("ball loc", node_loc);
            node->set_location(node_loc);
        }
    }

    void on_attach(qr::scene_node *ball){}
    void on_detach(qr::scene_node *ball){}
};

The last class we need to implement is an animator to detect when the ball has gone past one of the bats and one of the players has one the game. This class just compares the balls current location against the horizontal co-ordinates 4.7 and -4.7, which are around the center lines of the two bats. If the balls location is outside of this range then we print the player that one to the terminal and raise a quit event to exit the application. class win_detector : public qr::animator
{
    void on_animate(qr::scene_node *node, float time_delta)
    {
        qr::vector2d_f ball_loc =
            node->get_var<qr::vector2d_f>("ball loc");

        if(ball_loc.X < -4.7)
        {
            std::cout<<"Right player won\n";
            node->raise_event(new qr::quit_event());
        }
       
        if(ball_loc.X > 4.7)
        {
            std::cout<<"Left player won\n";
            node->raise_event(new qr::quit_event());
        }

    }

    void on_attach(qr::scene_node *node) {}
    void on_detach(qr::scene_node *node) {}
};

With all of the classes implemented, we just have to set everything up. First we do the usual renderer setup and seed the random number generator with the current time so that it generates a different starting trajectory each time the program is ran.. int main()
{
    qr::renderer *renderer = new qr::renderer(qr::vector2d_i(16, 9),
        10.0 ,false ,qr::vector2d_i(0, 0), false, 0);

    qr::scene_manager *scene_man = renderer ->
        get_scene_manager();

    renderer -> set_window_title("QR Pong - Quad-Ren");

    srand(time(NULL));

We set the background to a solid dark gray.     qr::sprite *bg = new qr::sprite(scene_man, 1);
    bg->create_solid_frame(10,10,10,255,1);
    bg->convert_data();
    renderer->set_bg_sprite(bg);

Now we actually start to build the scene graph, we begin by creating the scenes root node and creating the ball loc scene graph variable on it. We also attach an instance of the qr::key_var_mapper class to map the key states array into a scene graph variable.     qr::scene_node *root = new qr::null_node(scene_man);
    root->create_var(qr::vector2d_f(), "ball loc");

    qr::event_receiver *var_mapper = new qr::key_var_mapper("keys down");
    root->set_event_receiver(var_mapper);

Next we create a quad for the center line and attach our win detector and an instance of qr::quit on keypress to it.     qr::scene_node *center = new qr::quad(root);
    center->set_size(qr::vector2d_f(0.02, 7));

    qr::animator *detector = new win_detector();
    center->set_animator(detector);

    qr::event_receiver *quit = new qr::quit_on_keypress(qr::KEY_ESCAPE);
    center->set_event_receiver(quit);

Now we create a circle scene node for the ball and attach its animator and event receiver.     qr::scene_node *ball = new qr::circle(root);
    ball->set_size(qr::vector2d_f(0.3,0.3));

    qr::animator *ball_anim = new ball_animator();
    ball->set_animator(ball_anim);

    qr::event_receiver *ball_res = new ball_receiver();
    ball->set_event_receiver(ball_res);

Lastly for the setup, we create the two bats and attach there animators.     qr::scene_node *bat1 = new qr::quad(root);
    bat1->set_location(qr::vector2d_f(4.8,0));
    bat1->set_size(qr::vector2d_f(0.2,1));

    qr::scene_node *bat2 = new qr::quad(root);
    bat2->set_location(qr::vector2d_f(-4.8,0));
    bat2->set_size(qr::vector2d_f(0.2,1));

    qr::animator *bat_anim1 = new bat_animator(qr::KEY_K, qr::KEY_J);
    qr::animator *bat_anim2 = new bat_animator(qr::KEY_D, qr::KEY_S);

    bat1->set_animator(bat_anim1);
    bat2->set_animator(bat_anim2);

Finally we run the applications main loop, when this returns we clean up the resources that were allocated.     renderer -> main();

    renderer -> drop();
    delete quit;
    delete detector;
    delete var_mapper;
    delete ball_anim;
    delete ball_res;
    delete bat_anim1;
    delete bat_anim2;
}