3D Pong: The Player

In the last tutorial we added the ball to Pong, so our game is beginning to take shape! Let’s add some game play: the player paddle. This will cover mouse and touch input, the event system and conversion from screen coordinates to world space.

Rendering the Player

The player is rendered in the same manner as the arena and the ball. Build PaddleLeft.csmodel from the 3D Pong Asset Pack using the asset pipeline and then add the following to State::OnInit():

auto playerModel = resourcePool->LoadResource(CSCore::StorageLocation::k_package, "Models/PaddleLeft.csmodel");
        
CSRendering::StaticMeshComponentSPtr playerModelComponent = renderComponentFactory->CreateStaticMeshComponent(playerModel, modelsMaterial);
        
CSCore::EntitySPtr playerEntity = CSCore::Entity::Create();
playerEntity->AddComponent(playerModelComponent);
        
GetScene()->Add(playerEntity);

We’ll also want to place it on the left hand side of the arena:

playerEntity->GetTransform().SetPosition(CSCore::Vector3(-50.0f, 0.0f, 0.0f));

The Player Component

For player movement we’ll need a new PlayerMovementComponent. This is created the same way as the BallMovementComponent in the previous tutorial, however we’re going to need a couple of additional things. First of all we’ll need to implement the OnRemovedFromScene() life cycle event method. Secondly we’re going to need the CameraComponent, so we’ll pass it into the constructor.

PlayerMovementComponent.h:

#ifndef _PONG_PLAYERMOVEMENTCOMPONENT_H_
#define _PONG_PLAYERMOVEMENTCOMPONENT_H_

#include 
#include 

namespace Pong
{
    class PlayerMovementComponent final : public CSCore::Component
    {
    public:
        CS_DECLARE_NAMEDTYPE(PlayerMovementComponent);
        PlayerMovementComponent(CSRendering::CameraComponent* in_cameraComponent);
        bool IsA(CSCore::InterfaceIDType in_interfaceId) const override;
        
    private:
        void OnAddedToScene() override;
        void OnUpdate(f32 in_deltaTime) override;
        void OnRemovedFromScene() override;
        
        CSRendering::CameraComponent* m_cameraComponent = nullptr;
    };
}

#endif

PlayerMovementComponent.cpp:

#include 

namespace Pong
{
    CS_DEFINE_NAMEDTYPE(PlayerMovementComponent);
    
    PlayerMovementComponent::PlayerMovementComponent(CSRendering::CameraComponent* in_cameraComponent)
        : m_cameraComponent(in_cameraComponent)
    {
        CS_ASSERT(m_cameraComponent != nullptr, "Must supply a camera component");
    }
    
    bool PlayerMovementComponent::IsA(CSCore::InterfaceIDType in_interfaceId) const
    {
        return (PlayerMovementComponent::InterfaceID == in_interfaceId);
    }
    
    void PlayerMovementComponent::OnAddedToScene()
    {
    }
    
    void PlayerMovementComponent::OnUpdate(f32 in_deltaTime)
    {
    }
    
    void PlayerMovementComponent::OnRemovedFromScene()
    {
    }
}

Remember to add our new type to ForwardDeclarations.h:

CS_FORWARDDECLARE_CLASS(PlayerMovementComponent);

Finally, include PlayerMovementComponent.h in State.cpp and add the following to State::OnInit():

auto playerMovementComponent = std::make_shared(cameraComponent.get());
playerEntity->AddComponent(playerMovementComponent);

Mouse & Touch Input

Now that we’ve got our component set up, we can add some player interaction. Mouse and Touch input is handled by the PointerSystem. This can be acquired from CSCore::Application.

Include ChilliSource/Core/Base.h and ChilliSource/Input/Pointer.h in PlayerMovementComponent.cpp and add the following to OnAddedToScene():

auto pointerSystem = CSCore::Application::Get()->GetSystem();

PointerSystem exposes mouse and touch input using the ChilliSource Event API. This allows listeners to open a connection with an event and perform some logic whenever the event occurs. When registering a connection, a handle to the connection is returned; once the handle goes out of scope the connection is lost. This means that the handle must be retained for as long as the connection with the event is needed.

Add the following private members to PlayerMovementComponent:

CSCore::EventConnectionUPtr m_pointerDownConnection;
CSCore::EventConnectionUPtr m_pointerMovedConnection;
CSCore::EventConnectionUPtr m_pointerUpConnection;

Now we can open connections to each of the events in OnAddedToScene(), using a lambda for each callback:

m_pointerDownConnection = pointerSystem->GetPointerDownEvent().OpenConnection([=](const CSInput::Pointer& in_pointer, f64 in_timestamp, CSInput::Pointer::InputType in_inputType)
{
    //Pointer down logic
});
m_pointerMovedConnection = pointerSystem->GetPointerMovedEvent().OpenConnection([=](const CSInput::Pointer& in_pointer, f64 in_timestamp)
{
    //Pointer moved logic
});
m_pointerUpConnection = pointerSystem->GetPointerUpEvent().OpenConnection([=](const CSInput::Pointer& in_pointer, f64 in_timestamp, CSInput::Pointer::InputType in_inputType)
{
    //Pointer up logic
});

We also need to ensure we clean up the connections in OnRemovedFromScene():

m_pointerDownConnection.reset();
m_pointerMovedConnection.reset();
m_pointerUpConnection.reset();

Screen Coordinates to World Space

The coordinates returned by GetPosition() in Pointer are in screen space. This isn’t very useful when working in 3D – we want world space coordinates. Conversion to world space is the process of “un-projecting” from screen space, which is achieved using the Unproject() method in CameraComponent. This returns a ray in world space.

This still isn’t quite what we want – we want a point in world space. We can, however, use the intersection of the ray on a plane. Our game takes place in the XY plane so let’s use that. The intersection can be calculated by considering just the z part of the equation of a ray:

intersection.z = origin.z + t * direction.z

We know intersection.z = 0.0  which means:

t = -origin.z / direction.z.

This can then be plugged back into the full equation of a ray:

intersection = origin + (-origin.z / direction.z) * direction

Add a new private method to our component class declaration:

CSCore::Vector3 ConvertToWorldSpace(const CSCore::Vector2& in_screenPos);

Then include ChilliSource/Rendering/Camera.h in PlayerMovementComponent.cpp and implement the following:

CSCore::Vector3 PlayerMovementComponent::ConvertToWorldSpace(const CSCore::Vector2& in_screenPos)
{
    CSCore::Ray ray = m_cameraComponent->Unproject(in_screenPos);
    return ray.vOrigin + (-ray.vOrigin.z / ray.vDirection.z) * ray.vDirection;
}

Moving the Player

All that’s left now is to add the movement logic. We want the paddle to follow the pointer if the left mouse button is down or a touch is pressed to the screen. Include ChilliSource/Input/Pointer.h in PlayerMovementComponent.h and add the following private members in to the class declaration:

bool m_isMoving = false;
CSInput::Pointer::Id m_pointerId = 0;
CSCore::Vector2 m_targetPosition;

Add the following to the pointer down callback:

//check the pointer type is either left mouse button or a touch on a touch screen
if (m_isMoving == false && in_inputType == CSInput::Pointer::GetDefaultInputType())
{
    m_isMoving = true;
    m_pointerId = in_pointer.GetId();
    m_targetY = ConvertToWorldSpace(in_pointer.GetPosition()).y;
}

Add this to the pointer moved callback:

if (m_isMoving == true && in_pointer.GetId() == m_pointerId)
{
    m_targetY = ConvertToWorldSpace(in_pointer.GetPosition()).y;
}

And add this to the pointer up callback:

if (m_isMoving == true && in_pointer.GetId() == m_pointerId && in_inputType == CSInput::Pointer::GetDefaultInputType())
{
    m_isMoving = false;
}

Now that we’ve got the required touch information, we can move the player paddle based on it. Add the following to OnUpdate():

if (m_isMoving == true)
{
    auto& transform = GetEntity()->GetTransform();
    
    f32 distance = m_targetY - transform.GetWorldPosition().y;
    transform.MoveBy(0.0f, distance * 0.2f, 0.0f);
    
    const f32 k_limits = 30.0f;
    if (transform.GetWorldPosition().y > k_limits)
    {
        transform.SetPosition(transform.GetWorldPosition().x, k_limits, transform.GetWorldPosition().z);
    }
    if (transform.GetWorldPosition().y < -k_limits)
    {
        transform.SetPosition(transform.GetWorldPosition().x, -k_limits, transform.GetWorldPosition().z);
    }
}

If you build this now you should be able to move your player up and down the screen!

Pong-Player

Next: The Opponent