Paddles

There need to be two paddles that behave differently - one is controlled by the player while the other is controlled by the program itself. There is, again, great flexibility on how to achieve this using VUEngine. But we want a clean implementation that doesn’t couple any class to another if it is not absolutely necessary. So, we are going to create two different mutation classes for the paddles: PlayerPaddle and AIPaddle.

# Player Paddle

Delete the previous Paddle.actor file and replace it with a new one called PlayerPaddle.actor under assets/Actor/Paddle/PlayerPaddle/. Then, attach and configure a Sprite as we already did before.

Now, add a Mutator to it and name its mutation class as PlayerPaddle:

Since we deleted the previous PaddleActorSpec, we need to update the PongStageSpec’s PongStageActors array:

[...]

extern ActorSpec PlayerPaddleActorSpec;

[...]

PositionedActorROMSpec PongStageActors[] =
{
    {&DiskActorSpec,                {0, 0, 0}, {0, 0, 0}, {1, 1, 1}, 0, NULL, NULL, NULL, false},
    {&PlayerPaddleActorSpec,        {-180, 0, 0}, {0, 0, 0}, {1, 1, 1}, 0, NULL, NULL, NULL, false},
    {&PlayerPaddleActorSpec,        {180, 0, 0}, {0, 0, 0}, {1, 1, 1}, 0, NULL, NULL, NULL, false},
    {&LowPowerIndicatorActorSpec,   {-192 + 8, 112 - 4, 0}, {0, 0, 0}, {1, 1, 1}, 0, NULL, NULL, NULL, false},

    {NULL, {0, 0, 0}, {0, 0, 0}, {1, 1, 1}, 0, NULL, NULL, NULL, false},
};

Now, we have to create the PlayerPaddle class. Since it is a mutation target, it has to be a mutation class.

To create the PlayerPaddle mutation class, let’s create the folder source/Actors/Paddle/PlayerPaddle and, in it, a header and an implementation file: PlayerPaddle.h and PlayerPaddle.c.

In PlayerPaddle.h, add the following to declare the new class:

#include <Actor.h>

mutation class PlayerPaddle : Actor
{
}

Since it has to be controller by the player, it has to receive the keypad’s inputs to modify its position.

# Processing the user input

The Actors that belong to the Stage can receive messages that propagate through all its children. This allows to decouple the PongState and the Actors, that need to react to the user input, from each other.

First, we’ll create the messages. To do that, right click the config folder and create a new Messages file and add a message called “Keypad Hold Down” to it:

In the PongState’s declaration, override the processUserInput method:

singleton class PongState : GameState
{
    /// @publicsection

    /// Method to GameSaveDataManager the singleton instance
    /// @return AnimationSchemesState singleton
    static PongState getInstance();

    [...]

    /// Process the provided user input.
    /// @param userInput: Struct with the current user input information
    override void processUserInput(const UserInput* userInput);
}

And propagate the appropriate message according to the user input:

#include <Messages.h>

[...]

void PongState::processUserInput(const UserInput* userInput)
{
    if(0 != userInput->holdKey)
    {
        PongState::propagateMessage(this, kMessageKeypadHoldDown);
    }
}

In the PlayerPaddle class, override the Container::handlePropagatedMessage to process the message propagated by the PongState:

#include <Actor.h>

mutation class PlayerPaddle : Actor
{
    /// Default interger message handler for propagateMessage
    /// @param message: Propagated integer message
    /// @return True if the propagation must stop; false if the propagation must reach other containers
    override bool handlePropagatedMessage(int32 message);
}

A minimal implementation of that method should look like this:

[...]

bool PlayerPaddle::handlePropagatedMessage(int32 message)
{
    return false;
}

Notice that the method returns false. This allows the propagation to continue to other Actors.

PlayerPaddle::handlePropagatedMessage is still empty, we haven’t really done anything with the input yet. There are various options here again on how to proceed: directly manipulate the Paddle’s transformation or use physic simulations to give the paddles some weight and inertia. Let’s add a Body to the PlayerPaddle.actor as we did for the Disk.actor:

Finally, we are able to move the paddle. To do so, in the implementation of PlayerPaddle::handlePropagatedMessage, add the following:

[...]

#include <KeypadManager.h>
#include <Messages.h>

[...]

bool PlayerPaddle::handlePropagatedMessage(int32 message)
{
    switch(message)
    {
        case kMessageKeypadHoldDown:
        {
            if(!isDeleted(this->body))
            {
                UserInput userInput = KeypadManager::getUserInput();

                fixed_t forceMagnitude = 0;

                if(0 != (K_LU & userInput.holdKey))
                {
                    forceMagnitude = -__FIXED_MULT(Body::getMass(this->body), Body::getMaximumSpeed(this->body));
                }
                else if(0 != (K_LD & userInput.holdKey))
                {
                    forceMagnitude = __FIXED_MULT(Body::getMass(this->body), Body::getMaximumSpeed(this->body));
                }

                Vector3D force = {0, forceMagnitude, 0};

                PlayerPaddle::applyForce(this, &force, true);
            }

            return true;
        }
    }

    return false;
}

Notice that the method returns either true or false. If a handlePropagatedMessage returns true, the propagation of the message is halted, preventing other Actors from reacting to it. Since only one of the paddles must be controlled by the player, the method halts the propagation of the kMessageKeypadHoldDown message, but allows other messages to continue to be propagated by returning false.

If everything went right, once the game is built and run, both paddles will move when the user presses UP or DOWN on the left directional pad.

However, we don’t want the player to control both paddles, so we’ll need some AI-controlled opponent.

# AI Paddle

Repeat the steps above to create an AIPaddle.actor file in its respective folder, assets/Actor/Paddle/AIPaddle/. Then, attach and configure a Sprite, a Body and a Mutator as we already did for the PlayerPaddle.actor, and create in a similar manner an AIPaddle mutation class.

Don’t forget to update the PongStageSpec’s PongStageActors array:

[...]

extern ActorSpec AIPaddleActorSpec;

[...]

PositionedActorROMSpec PongStageActors[] =
{
    {&DiskActorSpec,                {0, 0, 0}, {0, 0, 0}, {1, 1, 1}, 0, NULL, NULL, NULL, false},
    {&PlayerPaddleActorSpec,        {-180, 0, 0}, {0, 0, 0}, {1, 1, 1}, 0, NULL, NULL, NULL, false},
    {&AIPaddleActorSpec,            {180, 0, 0}, {0, 0, 0}, {1, 1, 1}, 0, NULL, NULL, NULL, false},
    {&LowPowerIndicatorActorSpec,   {-192 + 8, 112 - 4, 0}, {0, 0, 0}, {1, 1, 1}, 0, NULL, NULL, NULL, false},

    {NULL, {0, 0, 0}, {0, 0, 0}, {1, 1, 1}, 0, NULL, NULL, NULL, false},
};

Since this paddle doesn’t react to the user inputs but to the Disk instance, its logic will be a little bit different from that of the PlayerPaddle.

The Container class has an update method that is called on all the instances inside a Stage every game cycle. So, override it to implement the AI’s logic in it:

#include <Actor.h>

[...]

mutation class AIPaddle : Actor
{
    /// Update this instance's logic.
    override void update();
}

The implementation will be very simple: retrieve a reference to the Disk instance through the Stage by calling AIPaddle::getRelativeByName, check the disk’s position over the Y axis and apply a force to the AIPaddle to catch up with it:

#include <Body.h>

#include "AIPaddle.h"

[...]

mutation class AIPaddle;

void AIPaddle::update()
{
    Actor disk = Actor::safeCast(AIPaddle::getRelativeByName(this, "Disk"));

    if(!isDeleted(disk))
    {
        fixed_t forceMagnitude = 0;

        fixed_t diskYPosition = Actor::getPosition(disk)->y;

        if(__PIXELS_TO_METERS(2) < __ABS(diskYPosition - this->transformation.position.y))
        {
            if(diskYPosition < this->transformation.position.y)
            {
                forceMagnitude = -__FIXED_MULT(Body::getMass(this->body), Body::getMaximumSpeed(this->body));
            }
            else
            {
                forceMagnitude = __FIXED_MULT(Body::getMass(this->body), Body::getMaximumSpeed(this->body));
            }

            Vector3D force = {0, forceMagnitude, 0};

            AIPaddle::applyForce(this, &force, true);
        }
    }
}

Since the Disk Actor has not been named yet, AIPaddle::getRelativeByName will find nothing. Let’s head back to the PongStageSpec.c file and assign a name to DiskActorSpec to change that.

PositionedActorROMSpec PongStageActors[] =
{
    {&DiskActorSpec,                {0, 0, 0},    {0, 0, 0}, {1, 1, 1}, 0, "Disk", NULL, NULL, false},
    {&PlayerPaddleActorSpec,        {-180, 0, 0}, {0, 0, 0}, {1, 1, 1}, 0, NULL, NULL, NULL, false},
    {&AIPaddleActorSpec,            {180, 0, 0},  {0, 0, 0}, {1, 1, 1}, 0, NULL, NULL, NULL, false},
    {&LowPowerIndicatorActorSpec,   {-192 + 8, 112 - 4, 0}, {0, 0, 0}, {1, 1, 1}, 0, NULL, NULL, NULL, false},

    {NULL, {0, 0, 0}, {0, 0, 0}, {1, 1, 1}, 0, NULL, NULL, NULL, false},
};

When running the game, both paddles will now work as you’d expect.

Blimey! But you will notice that the paddles can not yet interact with the Disk. We will need to add some Colliders .