Our Pong game is almost complete! All we need now is some collisions.
Physics Engine
ChilliSource currently doesn’t supply any form of Physics or Collision detection as part of the engine. A ChilliSource project is written in C++ however, as are most physics engines, meaning it is often simple to integrate one into your project directly. PhysX is now free to use for both commercial and non-commercial projects on Windows, iOS and Android, so it may be a good solution.
While we recommend using a physics engine in any case that requires even moderately complex collision detection or physics, it is sometimes overkill – in which case it can be easier to handle it manually. Our simple Pong game only needs basic collision detection, so let’s roll out own.
State Systems
Up until now, our game logic has all been in components, however a collision system doesn’t relate only to a single entity – rather all entities in the scene. In this case it is much better to build a state system.
A state system is a module, similar to a component, but it applies to the entire state. It also has more strict rules relating to its life cycle: A state system must be created during State::CreateSystems() and will live for the entire life span of the State.
Creating a state system is similar to a component. They inherit from CSCore::StateSystem and are queryable interfaces, meaning they need to to be declared a named type and implement the IsA() method. They also require a static Create() factory method – this is used by CreateSystem() to instantiate the system. Although not required, it is a good practice to declare Create() and the constructor private and expose them to State by making it a friend. This ensures the system can only be created using CreateSystem().
CollisionSystem.h:
#ifndef _PONG_COLLISIONSYSTEM_H_
#define _PONG_COLLISIONSYSTEM_H_
#include
#include
namespace Pong
{
class CollisionSystem final : public CSCore::StateSystem
{
public:
CS_DECLARE_NAMEDTYPE(CollisionSystem);
bool IsA(CSCore::InterfaceIDType in_interfaceId) const override;
private:
friend class CSCore::State;
static CollisionSystemUPtr Create();
CollisionSystem() = default;
void OnUpdate(f32 in_deltaTime) override;
};
}
#endif
CollisionSystem.cpp:
#include
namespace Pong
{
CS_DEFINE_NAMEDTYPE(CollisionSystem)
CollisionSystemUPtr CollisionSystem::Create()
{
return CollisionSystemUPtr(new CollisionSystem());
}
bool CollisionSystem::IsA(CSCore::InterfaceIDType in_interfaceId) const
{
return (CollisionSystem::InterfaceID == in_interfaceId);
}
void CollisionSystem::OnUpdate(f32 in_deltaTime)
{
}
}
Add the new type to ForwardDeclarations.h
CS_FORWARDDECLARE_CLASS(CollisionSystem);
Now we can include CollisionSystem.h in State.cpp and create the system in State::CreateSystems():
CreateSystem();
Collider Component
The overarching collision system should be a state system, but the individual colliders themselves relate only to a single Entity and should be components. We’ll need some way to register the components with the system while they are in the scene so they can be checked for intersections.
First of all forward declare the ColliderComponent in ForwardDeclarations.h
CS_FORWARDDECLARE_CLASS(ColliderComponent);
Now we can add two public methods for registering and de-registering the component to the system. Add the following to the class declaration:
void RegisterCollider(ColliderComponent* in_collider);
void DeregisterCollider(ColliderComponent* in_collider);
Also add the following private member:
std::vector m_colliders;
Include ChilliSource/Core/Container.h in CollisionSystem.cpp and add the following method definitions:
void CollisionSystem::RegisterCollider(ColliderComponent* in_collider)
{
CS_ASSERT(CSCore::VectorUtils::Contains(m_colliders, in_collider) == false, "Collider is already registered.");
m_colliders.push_back(in_collider);
}
void CollisionSystem::DeregisterCollider(ColliderComponent* in_collider)
{
CS_ASSERT(CSCore::VectorUtils::Contains(m_colliders, in_collider) == true, "Collider isn't registered.");
CSCore::VectorUtils::Remove(m_colliders, in_collider);
}
We also need to actually create the Component. Add the following two new files:
ColliderComponent.h
#ifndef _PONG_COLLIDERCOMPONENT_H_
#define _PONG_COLLIDERCOMPONENT_H_
#include
#include
#include
#include
namespace Pong
{
class ColliderComponent final : public CSCore::Component
{
public:
CS_DECLARE_NAMEDTYPE(ColliderComponent);
using Delegate = std::function;
ColliderComponent(CollisionSystem* in_collisionSystem, const CSCore::Vector3& in_size);
bool IsA(CSCore::InterfaceIDType in_interfaceId) const override;
const CSCore::Vector3& GetSize() const;
CSCore::IConnectableEvent& GetCollisionEvent();
private:
friend class CollisionSystem;
void OnAddedToScene() override;
void OnCollisionOccurred(const ColliderComponent* in_other);
void OnRemovedFromScene() override;
CSCore::Vector3 m_size;
CollisionSystem* m_collisionSystem = nullptr;
CSCore::Event m_collisionEvent;
};
}
#endif
ColliderComponent.cpp:
#include
#include
namespace Pong
{
CS_DEFINE_NAMEDTYPE(ColliderComponent);
ColliderComponent::ColliderComponent(CollisionSystem* in_collisionSystem, const CSCore::Vector3& in_size)
: m_collisionSystem(in_collisionSystem), m_size(in_size)
{
CS_ASSERT(in_collisionSystem != nullptr, "Must supply the collision system");
}
bool ColliderComponent::IsA(CSCore::InterfaceIDType in_interfaceId) const
{
return (ColliderComponent::InterfaceID == in_interfaceId);
}
CSCore::IConnectableEvent& ColliderComponent::GetCollisionEvent()
{
return m_collisionEvent;
}
const CSCore::Vector3& ColliderComponent::GetSize() const
{
return m_size;
}
void ColliderComponent::OnAddedToScene()
{
m_collisionSystem->RegisterCollider(this);
}
void ColliderComponent::OnCollisionOccurred(const ColliderComponent* in_other)
{
m_collisionEvent.NotifyConnections(this, in_other);
}
void ColliderComponent::OnRemovedFromScene()
{
m_collisionSystem->DeregisterCollider(this);
}
}
Note that we’ve provided an Event which will be fired whenever OnCollisionOccurred() is called – which is only exposed to the CollisionSystem. This allows any interested system or component to be notified of collisions without the ColliderComponent knowing of them.
Finally, we can add the components to the paddles and the ball. Include ColliderComponent.h in State.cpp and add the following to OnInit():
auto ballColliderComponent = std::make_shared(GetSystem(), ballModel->GetAABB().GetSize());
ballEntity->AddComponent(ballColliderComponent);
auto playerColliderComponent = std::make_shared(GetSystem(), playerModel->GetAABB().GetSize());
playerEntity->AddComponent(playerColliderComponent);
auto opponentColliderComponent = std::make_shared(GetSystem(), opponentModel->GetAABB().GetSize());
opponentEntity->AddComponent(opponentColliderComponent);
Collision Logic
We’re now set up to add our collision logic to the CollisionSystem. Include ChilliSource/Core/Math.h in CollisionSystem.cpp and add the following to OnUpdate():
for (auto colliderA : m_colliders)
{
for (auto colliderB : m_colliders)
{
if (colliderA != colliderB)
{
CSCore::AABB boxA(colliderA->GetEntity()->GetTransform().GetWorldPosition(), colliderA->GetSize());
CSCore::AABB boxB(colliderB->GetEntity()->GetTransform().GetWorldPosition(), colliderB->GetSize());
if (CSCore::ShapeIntersection::Intersects(boxA, boxB) == true)
{
colliderA->OnCollisionOccurred(colliderB);
}
}
}
}
Collision Response
Our collision detection system is now set up, but we still need to do something when a collision occurs. The only object which needs to do anything is the Ball, so let’s add the logic to the BallMovementComponent. This will need to be extended to take in a ColliderComponent in the constructor. It will also need to implement the OnRemovedFromScene() life cycle event.
Add the following public constructor to BallMovementComponent class declaration:
BallMovementComponent(ColliderComponent* in_colliderComponent);
Also add the following private method and members:
void OnRemovedFromScene() override;
ColliderComponent* m_colliderComponent = nullptr;
CSCore::EventConnectionUPtr m_collisionConnection;
These methods should be implemented in BallMovementComponent.cpp:
BallMovementComponent::BallMovementComponent(ColliderComponent* in_colliderComponent)
: m_colliderComponent(in_colliderComponent)
{
CS_ASSERT(m_colliderComponent != nullptr, "Must supply a collider component.");
}
void BallMovementComponent::OnRemovedFromScene()
{
m_collisionConnection.reset();
}
Now it is just a case of listening for the collision event and acting on it. Include ColliderComponent.h and add the following to OnAddedToScene():
m_collisionConnection = m_colliderComponent->GetCollisionEvent().OpenConnection([=](const ColliderComponent* in_this, const ColliderComponent* in_other)
{
auto& transform = GetEntity()->GetTransform();
if ((transform.GetWorldPosition().x > 0.0f && m_velocity.x > 0.0f) || (transform.GetWorldPosition().x < 0.0f && m_velocity.x < 0.0f))
{
m_velocity.x = -m_velocity.x;
}
});
Finally, we need to pass the ball’s ColliderComponent to the BallMovementComponent – make sure you are creating and adding the Collider first.
auto ballColliderComponent = std::make_shared(GetSystem(), ballModel->GetAABB().GetSize());
ballEntity->AddComponent(ballColliderComponent);
auto ballMovementComponent = std::make_shared(ballColliderComponent.get());
ballEntity->AddComponent(ballMovementComponent);
Now if you build this the ball should bounce off the paddles!
It is worth noting that the CSPong sample project has a far more complete example of a basic collision system.