Vroom Vroom is a simple racing game made for an university subject called Projects III. We were meant to create a videogame engine and develop a 3D videogame on it. The main objective of this project is the creation of our own videogame engine, MotorEngine. In this page I will explain the most interesting part of my contribution to the project. More information about the engine and the game can be found on GitHub!

Input Handling

MotorEngine's Input Manager wraps SDL's functionality to poll events, and lets you create buttons and axes so you can check their value or add a function as a callback. Buttons and axes can be mapped to any SDL Input Event, even more than one. 

To achieve this behaviour, I used a std::unordered_map for the buttons and other for the axis, so that every name is linked with its button or axis. A button is a struct that stores the bool pressed, and the player identificator that pressed it. An axis is a struct that stores the value of the axis between -1 and 1, a bool active, so that  the value returns to zero progressively if it's inactive, and 2 floats for gravity and dead values, which determines the behaviour of the axis. Gravity is the speed in units per second that the axis falls toward neutral when no input is present and dead is how far the user needs to move an analog stick before your application registers the movement.

Then, I used 3 std::unordered_multimap to bind physical inputs with the name of the button or axis, one for the button bindings, and two others for positive and negative axis bindings. A physical input can be from keyboard, mouse or gamepad, specifies which key, button or axis has been interacted with and the value it has, in case its a joystick or mouse motion.

Finally, I also have one last std::unordered_multimap that stores the callback information for every button. The user can add OnButtonPressedEvents that will call the functions they want with custom parameters, filtering player identificators automatically.

Below is the user guide for every InputManager method. I encourage you to check the code here.

Compiling as dll

We were asked to make an engine divided in modules and then export it as a dll. The main project is MotorEngine, and it's the one compiled as a dynamic library. It includes the other modules compiled as static libraries, which are Audio, EntityComponent, Input, Physics, Render and Utils. Every class is marked with a symbol __MOTORENGINE_API that expands to __declspec(dllexport) or __declspec(dllimport) depending on whether the symbol __MOTORENGINE_EXPORT is defined. The idea is to compile the engine with __MOTORENGINE_EXPORT defined, and then when loading it, the symbol won't be defined so the behavior of __MOTORENGINE_API will change automatically. To link every module to MotorEngine, we simply add them as references in VisualStudio.

Our game, VroomVroom, is a separate project that will tell MotorEngine which components it defines, the scene it will load and the input it uses. The game is compiled as a dll, exporting just these functions. MotorEngine will explicitly load the game's dll and call its functions initFactories, init and initInput.

In our main file, we implicitly load the engine's dll and call its methods setup, loop and exit.

Load Lua scenes

Lua is a scripting language we use to create scenes with their entities and components, so we don't need to create new classes, modify the code and recompile it everytime. The creator of the scene won't need to know what the engine does, they'll just write the entities they want inside the "entities" object and specify which components the entity will have and their parameters, like this:

    Car1 = {

        Transform = {

            position = { x=-50, y = 5.5, z = -10 },

            scale = { x = 0.7, y = 0.7, z = 0.7 },

            rotation = {x = 0, y = 0, z = 0}

        },

RigidBody = {

            colShape = 2,

            mvType = 0,

            mass = 1,

            group = 1,

            mask = 63,

            colliderscale = {x = 1, y = .4, z = 1},

            restitution = .5,

            friction = 0,

            isTrigger = false

        },

        vehicleController = {

            acceleration = 0.2,

            rotationspeed = 0.12,

maxspeed = 12,

maxrotationspeed = 3,

            playerNumber = 0

        },

        collider = {

            

        },

        powerupuiwheel = {

            spinspeed = 0.1;

            linkedsprite = "insidecontainer"

        },

        meshrenderer = {

            mesh = "kartone",

            meshName = "RedCar.mesh",

        },

        audiosource = {

            name = "thunderCarOne",

            path = "posibleAumentoDeVel.mp3",

            onstart = false,

            loop = false,

            threed = true,

            groupchannel = "effects"

        },

        audiolistener = {


        }

    }

You can see a full example of a scene here. The engine allows capital letter mistakes by lowering all component names and sets undefined parameters with a default value. The engine shows a window error if Lua detects an error or if the specified type mismatches the parameter.

We can also call Lua functions before and after the engine reads the entities, specifying their name in the awake and start lists. In VroomVroom, for example, use the awake functions to create the walls and checkpoints with a loop.

function createCheckpoints()


    checkpointPositions = {

        { x = -42, y = 0, z = -10 },

        { x = 0, y = 0, z = 0 },

        { x = -80, y = 0, z = 10 },

        { x = -120, y = 0, z = 0 }

    }


    checkpointRotation = {

        180, 90, 180, 90

    }


    for i = 1, 4, 1 do

        checkpoint = {

            Transform = {

                position = checkpointPositions[i],

                scale = { x = 1, y = 1, z = 1 },

                rotation = { x = 0, y = checkpointRotation[i], z = 0 }

            },

            RigidBody = {

                colShape = 1,

                mvType = static,

                mass = 5,

                group = 7,

                mask = 1,

                colliderscale = { x = 1, y = 10, z = 10 },

                restitution = 0.5,

                friction = 0.5,

                isTrigger = true

            },

            collider = {},

            Checkpoint = {

                index = i - 1;

            }

        }


       Entities["checkpoint" .. i-1] = checkpoint

    end

end

The creation of the scene has two phases: first, our LoadManager parses the .lua scene to a meta-map called InfoScene. This map contains the values of the parameters for every component of every entity in the scene. Then, the SceneManager then uses this map to push the entities into the new scene, creating the entities and their components through the factory method pattern.

To let the game define its own components and allow the engine to create them without knowing them, we use the factory method pattern. It consists in inheriting from a ComponentFactory interface, which declares create and destroy methods. Every Component has to define a ComponentFactory.

The create function receives a Parameters map, used to set the values of the component, and returns the created Component. This is the TransformFactory, for example:

Component* FactoryTransform::create(Parameters& params)

{

    Transform* transform = new Transform();

    transform->setPosition(Vector3(Value(params, "position_x", 0.0f),

        Value(params, "position_y", 0.0f), Value(params, "position_z", 0.0f)));

    transform->setRotation(Vector3(Value(params, "rotation_x", 0.0f),

        Value(params, "rotation_y", 0.0f), Value(params, "rotation_z", 0.0f)));

    transform->setScale(Vector3(Value(params, "scale_x", 1.0f),

        Value(params, "scale_y", 1.0f), Value(params, "scale_z", 1.0f)));

    std::string transformParent = Value(params, "parentname", std::string());

    transform->setParentName(transformParent);

    return transform;

}


void me::FactoryTransform::destroy(Component* component)

{

    delete component;

}

We use the value functions to check if the value is defined. If it is, we check if its type is the one we expected and then return it. Otherwise, it returns the default value for that parameter.

float FactoryComponent::Value(Parameters& params, const ParameterName& parameter, float defaultValue)

{

    if (params.count(parameter) > 0) {

        for (char c : params[parameter]) {

            if (!std::isdigit(c) && c != '.' && c != '-') {

                errorManager().throwMotorEngineError("Invalid parameter for " + parameter + " set.", 

                    "Value is not a float.");

                sceneManager().quit();

                return defaultValue;

            }

        }

        return std::stof(params[parameter]);

    }

    else

        return defaultValue;

}

Finally, the game has to export a initFactories function to match the component name expected in the .lua scene with the ComponentFactory.

__VROOMVROOM_API void initFactories()

{

    componentsFactory().addFactoryComponent("camerafollow", new FactoryCameraFollow());

    componentsFactory().addFactoryComponent("vehiclecontroller", new FactoryVehicleController());

    componentsFactory().addFactoryComponent("circuitinfo", new FactoryCirtuitInfo());

    componentsFactory().addFactoryComponent("gamemanager", new FactoryGameManager());

    componentsFactory().addFactoryComponent("checkpoint", new FactoryCheckpoint());

    componentsFactory().addFactoryComponent("uibuttonscene", new FactoryUIButtonScene());

    componentsFactory().addFactoryComponent("uibuttonquit", new FactoryUIButtonQuit());

    componentsFactory().addFactoryComponent("powerupuiwheel", new FactoryPowerUpUIWheel());

    componentsFactory().addFactoryComponent("powerupobject", new FactoryPowerUpObject());

    componentsFactory().addFactoryComponent("oil", new FactoryOil());

    componentsFactory().addFactoryComponent("nerf", new FactoryNerf());

}

Circuit checkpoints

As this is my first racing game, I had to investigate how this genre works. They use a checkpoint system that ensures the player has indeed completed the circuit rightfully. Then, to calculate places, the distance to the next checkpoint is used.

To implement this behaviour, I created a Checkpoint component to manage their identifiers and the total number of checkpoints. A checkpoint entity also has a box trigger that the players use to mark their progress through the lap.

        if (checkpoint->getIndex() == (mCheckpointIndex + 1) % checkpoint->GetNumCheckpoints()) {

            //Next checkpoint

            mCheckpointIndex++;


            if (mCheckpointIndex == checkpoint->GetNumCheckpoints()) {

                //Add lap

                mCheckpointIndex = 0;

                mLap++;


                if(mLap != mCircuitInfo->getLaps())

                    mLapsText->setText("Lap " + std::to_string(mLap + 1) + "/" 

                        + std::to_string(mCircuitInfo->getLaps()));


                if (mLap == mCircuitInfo->getLaps()) {

                    //Finish race

                    mFinishTime = mCircuitInfo->getFinishTime();

                    mChrono->setText(mFinishTime);

                    mControllable = false;

                    mFinishAudio->play();


                    Entity* finishEntity = mEntity->getScene()

                        ->findEntity("finish" + std::to_string(mPlayerNumber)).get();

                    UIText* finishUIText = finishEntity->getComponent<UIText>("uitext");

                    finishUIText->setActive(true);

                }

             }

          }

The circuit is the one that updates the places of the players. Every frame it sets them all to firsts, and then compares them in all possible pairs. In the comparison, the kart that is behind will have their place incremented. This is stupid considering VroomVroom is a 2 player maximum game, however, I wanted to generalize it for the possibility of having more than 2 karts in the race.

void CircuitInfo::calculatePlaces()

{

//Reset all vehicles to first place and then push them back one by one

for (VehicleController* vehicle : mVehicles)

vehicle->setPlace(1);


for (int i = 0; i < mVehicles.size(); i++) 

for (int j = i + 1; j < mVehicles.size(); j++) 

//First check their lap

if (mVehicles[i]->getLap() < mVehicles[j]->getLap())

//i is behind

mVehicles[i]->setPlace(mVehicles[i]->getPlace() + 1);

else if (mVehicles[i]->getLap() > mVehicles[j]->getLap())

//j is behind

mVehicles[j]->setPlace(mVehicles[j]->getPlace() + 1);


//Then check their checkpoint

else if (mVehicles[i]->getChekpointIndex() < mVehicles[j]->getChekpointIndex())

//i is behind

mVehicles[i]->setPlace(mVehicles[i]->getPlace() + 1);

else if (mVehicles[i]->getChekpointIndex() > mVehicles[j]->getChekpointIndex())

//j is behind

mVehicles[j]->setPlace(mVehicles[j]->getPlace() + 1);


//Lastly check their remaining distance to the next checkpoint

else {

Vector3 nextCheckpointPosition =

mCheckpoints[(mVehicles[i]->getChekpointIndex() + 1) % Checkpoint::GetNumCheckpoints()]

->getEntity()->getComponent<Transform>("transform")->getPosition();


Vector3 iVehiclePosition = mVehicles[i]->getEntity()

->getComponent<Transform>("transform")->getPosition();

Vector3 jVehiclePosition = mVehicles[j]->getEntity()

->getComponent<Transform>("transform")->getPosition();


if (nextCheckpointPosition.distance(iVehiclePosition)

> nextCheckpointPosition.distance(jVehiclePosition))

//i is behind

mVehicles[i]->setPlace(mVehicles[i]->getPlace() + 1);

else

//j is behind

mVehicles[j]->setPlace(mVehicles[j]->getPlace() + 1);

}


//Update UI place

for (VehicleController* vehicle : mVehicles) {

EntityName placeUIName;

switch (vehicle->getPlayerNumber()) {

case PLAYERNUMBER_1:

placeUIName = "place1";

break;

case PLAYERNUMBER_2:

placeUIName = "place2";

break;

default:

continue;

}


UISpriteRenderer* placeUISprite = getEntity()->getScene()->findEntity(placeUIName).get()

->getComponent<UISpriteRenderer>("uispriterenderer");


switch (vehicle->getPlace()) {

case 1:

placeUISprite->setSpriteMaterial("first");

break;

case 2:

placeUISprite->setSpriteMaterial("second");

break;

default:

break;

}

}

}

Driving the kart

The kart control was difficult to achieve, as using bullet physics made it tremble. We tried to change drag atributes for both the kart and the circuit and constraint the rotation, but it kept trembling and sometimes start to fly when hitting a wall. We finally decided to give the physics away and make its movement manually. Now the kart is locked to the ground plane.

The acceleration is limited to a maximum velocity, and the acceleration of the player is greater than the natural damping. The steering is dependent of our speed to avoid rotating in place. Everything uses parameters set in Lua, for easy changing.

void VehicleController::applyPush(const double& dt, bool accelerate, bool decelerate)

{

    Vector3 vForward = mTransform->forward().normalize();

    Vector3 lastVelocity;


    bool movingBackwards = isMovingBackwards();


    if (movingBackwards)

        lastVelocity =  vForward * -(mRigidBody->getVelocity().magnitude() * mLinearDamping);

    else

        lastVelocity =  vForward * (mRigidBody->getVelocity().magnitude() * mLinearDamping);


    Vector3 newVelocity = lastVelocity;

    float velocity;


    if (accelerate) {

        if (movingBackwards)

            velocity = -mRigidBody->getVelocity().magnitude() + (mAcceleration * mAccelerationBoost);

        else

            velocity = mRigidBody->getVelocity().magnitude() + mAcceleration;


        clamp(velocity, -mActualMaxSpeed, mActualMaxSpeed);

        newVelocity = vForward * velocity;

    }

    else if (decelerate) {

        if (movingBackwards)

            velocity = -mRigidBody->getVelocity().magnitude() - mAcceleration;

        else

            velocity = mRigidBody->getVelocity().magnitude() - (mAcceleration * mAccelerationBoost);



        clamp(velocity, -mActualMaxSpeed, mActualMaxSpeed);

        newVelocity = vForward * velocity;

    }


    mRigidBody->setVelocity(newVelocity);

}


void VehicleController::applyRotation(const double& dt, float deltaX)

{

    // Limit movement

    float rotationMultiplier = mRigidBody->getVelocity().magnitude() / mActualMaxSpeed * mSpeedBasedRotationMultiplier;

    clamp(rotationMultiplier, 0, 1);


    Vector3 angVel = mRigidBody->getAngularVelocity();

    float lastAngularVelocity = 0;


    if (angVel.y < 0)

        lastAngularVelocity = -mRigidBody->getAngularVelocity().magnitude();

    else if (angVel.y > 0)

        lastAngularVelocity = mRigidBody->getAngularVelocity().magnitude();


    Vector3 newAngularVelocity = Vector3::Up() * (lastAngularVelocity * mAngularDamping);

    float rotationVelocity;


    if (isMovingBackwards())

        deltaX = -deltaX;


    if (deltaX > 0) { // Derecha

        if (angVel.y < 0)

            rotationVelocity = lastAngularVelocity - (mRotationSpeed * deltaX);

        else

            rotationVelocity = lastAngularVelocity - (mRotationSpeed * mSteeringBoost * deltaX);


        // Limit angular velocity

        clamp(rotationVelocity, -mMaxAngularSpeed, mMaxAngularSpeed);


        newAngularVelocity = Vector3::Up() * rotationVelocity * rotationMultiplier;

    }

    else if (deltaX < 0) { // Izquierda

        if (angVel.y < 0)

            rotationVelocity = lastAngularVelocity - (mRotationSpeed * mSteeringBoost * deltaX);

        else 

            rotationVelocity = lastAngularVelocity - (mRotationSpeed * deltaX);


        // Limit angular velocity

        clamp(rotationVelocity, -mMaxAngularSpeed, mMaxAngularSpeed);


        newAngularVelocity = Vector3::Up() * rotationVelocity * rotationMultiplier;

    }


    mRigidBody->setAngularVelocity(newAngularVelocity);

}

Power-ups

Power-ups are the only entities that are created at runtime in VroomVroom. When a player hits a power up box, it gives them a random power up of the three we have: thunder, oil and nerf. The thunder gives the kart a velocity boost beyond the kart's maximum velocity. The oil barrel makes an oil puddle right behind the kart, and the next kart that drives on it will have its maximum velocity reduced until it leaves the puddle, and then the puddle disappears. The nerf bullet flies straight until hitting a kart or the wall. If it hits a kart, it will stop and spin for a while.

I made the nerf. The nerfs are stored below the circuit off camera, and when they are used they teleport right in front of the player that uses it. They are faster than the karts, so they can't hit the user and can reach the next kart.