Skip to content

Making a ping pong game

Twarit Waikar edited this page Oct 13, 2019 · 17 revisions

Rubeus: Making a ping-pong game

Created by SDSLabs with ❤️

In this tutorial, we will be learning how to start with using Rubeus to make a simple ping-pong game. You will see that the game code is already provided inside the Game/ folder. Before starting, we prefer you make a backup of this folder elsewhere and delete all the contents inside Game/.

Goals

  1. Setting up Rubeus
  2. Setting up Project Manager
  3. Using Project Manager to start a Rubeus project
  4. Creating a ping-pong game
  5. Testing your ping-pong game
  6. Packaging and distributing your project

1. Setting up Rubeus

  1. Follow setup instructions to have a working copy of Rubeus on your machine. It may take a while to build all dependencies but hang tight! It will too, pass.
  2. You may test the project by building and running in your development environment. You will get some errors saying that the compiler cannot find some user_init.h file. This is normal. You will have to set up a Rubeus project first to test run your game. This is where Project Manager comes into play.

2. Setting up Project Manager

Windows- Simply execute ProjectManager.exe supplied in the release

Linux

In Windows the binary could be present in Rubeus/RubeusCore/ProjectManager/Debug depending upon the build mode set in VS, in case you decide to build your own

You will see something like this on running the executable:

ERROR: Couldn't load Photo

Enter the Rubeus root directory and hit OK:

ERROR: Couldn't load Photo

  1. Nice! Project Manager now knows where to generate the Rubeus project files

3. Using Project Manager to start a Rubeus project

Note- Make sure to name your project something other then the sample project ping_pong or delete the sample project by right-clicking on it before proceeding further-

ERROR: Couldn't load Photo

Follow the steps as shown in proceeding screenshots:

ERROR: Couldn't load Photo

ERROR: Couldn't load Photo

On clicking Create:

ERROR: Couldn't load Photo

On clicking ping_pong:

ERROR: Couldn't load Photo

Now decide on a level name. If you expect to create the main menu for your game, you can just name your level as "main_menu". But for now, we are going to jump right into the main game. Let's call it play_level. now enter play_level in the textbox and click Create New Level

ERROR: Couldn't load Photo

ERROR: Couldn't load Photo

  1. A ping-pong game needs two important types of objects, i.e. a ball and two paddles. Let us create ball and paddle game objects in our game:

ERROR: Couldn't load Photo

ERROR: Couldn't load Photo

ERROR: Couldn't load Photo

Click on USE THIS PROJECT to run the relevant cmake

Nice! Now all of our objects are in place. We are done with Project Manager for now.

4. Creating a ping-pong game

  1. Now that all of our objects and levels are in place, let us have a look at the file structure inside /RubeusCore/Game
Game/
└── ping_pong
    ├── engine_files
    │   ├── level.play_level.cpp
    │   ├── level.play_level.h
    │   ├── object.ball.cpp
    │   ├── object.ball.h
    │   ├── object.paddle.cpp
    │   └── object.paddle.h
    ├── user_init.cpp
    └── user_init.h
  1. Open up the Rubeus directory/solution in your text-editor/IDE. We recommend Visual Studio on Windows and Visual Studio Code on Linux-like distributions.

  2. Open user_init.cpp. This is the file where you will be initialising all the game objects and levels that your game requires. You may also define your own functions here for future use. You may also remove the comments inside user_init.cpp for now.

  3. Open user_init.h. This is the file which will contain all the declarations for your classes that extend the Rubeus game object classes, levels etc. You can also add your own helper function declarations here. Make Rubeus aware of your levels and objects by adding the following snippet to your user_init.h file:

// Levels
#include "engine_files/level.play_level.h"

// Objects
#include "engine_files/object.ball.h"
#include "engine_files/object.paddle.h"

Good job! We are done with user_init.h for now. Let's move on to user_init.cpp

  1. Create a string variable named startupLevel which tells Rubeus which level to load at startup (obviously).
std::string startupLevel = "play_level";

Rubeus puts the responsibility of instantiating the levels and game objects entirely onto the user so as to give full control on with what properties objects and levels are created.

Let's instantiate our play_level:

Lplay_level * playLevel = new Lplay_level("play_level");

You will see what we have done is that we have created a pointer to a heap-allocated object. We don't need to delete this object later because Rubeus will do that for us. Yay!

Let us also create our paddles:

Opaddle * leftPaddle = new Opaddle();

Opaddle * rightPaddle = new Opaddle();

We do not need to delete these game objects later.

At this moment you will also see that we haven't provided any arguments to the constructors. Let's do that together now! We recommend having the RGameObject() documentation open as you go through this section of the tutorial. Open this in a new tab and see if you can come up with the initialisations.

Once you are done with it, go on to initialise the rightPaddle and the leftPaddle objects. Use the Tab key at the documentation to search for class names like ABoxCollider, which you will be needing to provide a collider to your paddle.

Quick primer on colliders: Colliders are specified areas on the screen which if collide a.k.a. overlap with another collider will trigger the onHit() function of the game object that they are a part of. It is a way of emulating real-life collisions in a virtual environment.

You will also need to look up the Vector3D struct inside the RML namespace.

You will most likely end up with a bunch of initialisations that look like this:

Opaddle * leftPaddle = new Opaddle("left_paddle", "play_level", 0.0f, 0.0f, 0.5f, 3.0f,
				 "Assets/debug.png", true,
				 Rubeus::Awerere::EColliderType::BOX,
		           	 new Rubeus::Awerere::ABoxCollider(RML::Vector3D(0.0f, 0.0f, 1.0f),
	      		         RML::Vector3D(0.5f, 3.0f, 1.0f)),
				 true);

Opaddle * rightPaddle = new Opaddle("right_paddle", "play_level", 15.5f, 0.0f, 0.5f, 3.0f,
				  "Assets/debug.png", true,
				  Rubeus::Awerere::EColliderType::BOX,
				  new Rubeus::Awerere::ABoxCollider(RML::Vector3D(15.5f, 0.0f, 1.0f),															 
                                                                    RML::Vector3D(16.0f, 3.0f, 1.0f)),
				  true);

Oball * Ball = new Oball("ball", "play_level", 8.0f, 4.5f, 0.5f, 0.5f,
		       "Assets/debug.png", true,
	       	       Rubeus::Awerere::EColliderType::BOX,
		       new Rubeus::Awerere::ABoxCollider(RML::Vector3D(8.0f, 4.5f, 1.0f),
							 RML::Vector3D(8.5f, 5.0f, 1.0f)),
		       true);

Even if you didn't manage to get a similar result, just copy and paste and use our definitions for now.

Now take a deep breath and build your project. (Press F5 on Visual Studio or press the launch button on the Debug tab in Visual Studio Code, or use the build function in your text editor/IDE for Windows OR Run make then run the RubeusCore executable present in Rubeus/RubeusCore/ for Linux)

You will see that a window loads up with this at first. But with time all your objects fall to the ground. This is because all objects have gravity enabled by default. Let's not do that for now. We will achieve this in the object files. Our job is done inside user_init.cpp.

  1. Open Game/ping_pong/engine_files/object.ball.cpp. You will see mainly these functions already present there:
#include "object.ball.h"               // This is a necessary header file inclusion so that this file is able to access the Oball class

void Oball::begin()
{
    // This function is called only once before the level is started
}

void Oball::onHit(RGameObject * hammer, RGameObject * nail, const Rubeus::Awerere::ACollideData & collisionData)
{
    // This function is only called if the object has physics enabled 
    // and only when the collider of this object collides with another collider.
    // This function will remain empty for now.
}

void Oball::tick()
{
    // This function is called once every frame.
}

Our objective is to turn the gravity to 0 and also add a bit of momentum to the ball before the game starts.

This means we need to add stuff to the begin() function. Now we need to know that every physics-enabled object has a physics object inside it which in turn also has the collider object. The collider object also has information about what physical constants affect our object's location.

Our first task is to gain access to the collider through the physics object of the ball inside the begin() function.

Next, we need to find the APhysicsMaterial object inside the collider. After which we need to find the gravity constant and turn its vertical component to 0.0f.

See if you can figure how to do this with the help of the documentation here.

Your Oball::begin() function should look like this:

void Oball::begin()
{
    this->m_PhysicsObject->m_Collider->m_PhysicsMaterial.m_Gravity.y = 0.0f;
}

Let's run and test this quickly. You will see that the ball stays on screen but the paddles still drop down, which is normal because we haven't disabled the gravity for the paddles.

Open object.paddle.cpp and make sure the Opaddle::begin() function looks like this:

void Opaddle::begin()
{
    this->m_PhysicsObject->m_Collider->m_PhysicsMaterial.m_Gravity.y = 0.0f;
}

Now do a test run and the objects should now stay in their place.

Let us also set up the controls for the game. Pressing the UP keys should add a vertically upward velocity to the paddles and vice-versa for pressing the DOWN key.

Input Manager is present inside the Engine object pointer inside the Rubeus namespace. You will need to gain access to it and register a keybinding for each key.

Add the keybindings inside the paddle::begin() functions likewise:

void Opaddle::begin()
{
    this->m_PhysicsObject->m_Collider->m_PhysicsMaterial.m_Gravity.y = 0.0f;

    auto * inputManager = Rubeus::Engine->getCurrentLevelInputManager();

    inputManager->addKeyToKeyBinding("Ascend", Rubeus::EKeyboardKeys::__UP);
    inputManager->addKeyToKeyBinding("Descend", Rubeus::EKeyboardKeys::__DOWN);
}

Next task is querying for these keys inside the Opaddle::tick() functions to check if the keys are pressed and add to the momentum accordingly.

void Opaddle::tick()
{
	if (Rubeus::Engine->getCurrentLevelInputManager()->isKeyBindingPressed("Ascend"))
	{
		this->m_PhysicsObject->m_Collider->m_Momentum.y = +3.0f;
	}
	else if (Rubeus::Engine->getCurrentLevelInputManager()->isKeyBindingPressed("Descend"))
	{
		this->m_PhysicsObject->m_Collider->m_Momentum.y = -3.0f;
	}
	else
	{
		this->m_PhysicsObject->m_Collider->m_Momentum.y = 0.0f;
	}

	this->m_PhysicsObject->m_Collider->m_Momentum.x = 0.0f;   // You need to lock the x axis movement for this game to function as expected
}

Try running the game now. You might be shocked to notice that your utterly simple code is not working. The paddles will not respond to the key presses yet.

We need to realise that tick() functions are very costly in terms of performance, but only when you do costly operations in them. This is why the tick() functions of all objects are turned OFF by default. You will need to turn the tick functions ON manually inside the Opaddle::begin() function.

In addition to this, it is important to know that Awerere, which is the physics engine Rubeus uses, regards masses that are greater than 1,000,000 kg as immovable objects. Since our paddles, in fact, should not be movable by the physics engine, we also need to set the mass accordingly. Now your paddle::begin() function will look like this:

void Opaddle::begin()
{
	this->m_PhysicsObject->m_Collider->m_PhysicsMaterial.m_Gravity.y = 0.0f;

	auto * inputManager = Rubeus::Engine->getCurrentLevelInputManager();

	inputManager->addKeyToKeyBinding("Ascend", Rubeus::EKeyboardKeys::__UP);
	inputManager->addKeyToKeyBinding("Descend", Rubeus::EKeyboardKeys::__DOWN);

        this->m_PhysicsObject->m_Collider->m_PhysicsMaterial.m_Mass = 1000000.0f;  // <--- This line is a new addition
	this->m_ThisTicks = true;                                                  // <--- This line is a new addition
}

Let us try running the game. You can now control the paddles! Give yourself a big pat on the back

We are now finishing the game, but we can see there is not much we can do right now if the ball isn't moving.

Open object.ball.cpp and add a bit of momentum to the ball before the game starts with this:

void Oball::begin()
{
	this->m_PhysicsObject->m_Collider->m_PhysicsMaterial.m_Gravity.y = 0.0f;
	this->m_PhysicsObject->m_Collider->m_Momentum.x = 1.0f;       // <--This line is a new addition
}

5. Testing your ping-pong game

Hit build (or F5 on Visual Studio / make and run on Linux) and see your first Rubeus game in action! Now you have the time to play it a bit yourself and then we can move on to distributing the game to your friends!

6. Packaging and distributing your project

Now it is the time to package your project. Make an output/ directory in your file system (it doesn't matter where it is).

Follow the steps given below to package your game:

  1. Copy all the files from the build directory (This is user-dependent. For VS users, it is all the files inside RubeusCore/Debug, for VSCode users it may be a single binary inside RubeusCore/ itself) to the output/ directory.
  2. Copy the folders RubeusCore/Assets and RubeusCore/Shaders to output/
  3. Rename RubeusCore.exe (for Linux is it simply RubeusCore) to ping_pong.exe (for Linux, it may just be ping_pong. Linux doesn't seem to mind the extensions too often.)
  4. Share your output/ folder with your friends and have fun!