2D Physics System

Making Pachinko with Physics

The intent for this project was to write a 2D physics system with support to handle rigid body collisions between different 2D objects. The idea was to be able to create a simple Pachinko styled game that could handle bouncing and rotating 2D discs that could collide on a series of obstacles that could be represented as other 2D shapes.

The starting point

At the start of this project, the Prodigy custom C++ engine was only capable of rendering and computing axis-aligned bounding boxes(AABB) and discs. This meant that implementing a physics system would have rectangular objects that would collide but be unable to rotate. It also meant that I would need to create other shapes and to handle objects like capsules, rounded boxes, and other shapes to be able to fully implement Pachinko style physics as desired. So a road map was followed to incorporate the required features and tech to achieve the desired effect.

Project Roadmap

Below was the roadmap followed to implement the required features in the engine in order to implement a functional 2D physics system.

##Project Plan##

#Week 1 and 2#
-	Implement a simple physics zoo supporting AABB and disc2D collisions.
-	Dynamic objects should fall off the screen under the effect of gravity. 
-	Visualize objects by color based on object properties

#Week 3 and 4#
-	Have a basic 2D Physics system with elastic collision response with adjustable restitution and mass. 
-	Generate collision Manifolds as area of object overlap during collision
-	Resolving momentum for inelastic collisions

#Week 5 and 6#
-	Implementing Oriented Bounding Boxes
-	Implementing Capsules
-	Adding mouse support to application

#Week 7 and 8#
-	Implementing rotational forces(torque)
-	Updating intersection code to generate a contact point, as well as be able to apply torque.  

#Week 9 and 10#
-	Adding frictional impulses to the system
-	Adding angular drag forces
-	Adding rotation and linear constraints on rigid bodies
-	Adding nested clocks to slow down or speedup time as needed to observe the system
-	Implementing a debug view on hovering objects in the scene

#Week 11 and 12#
-	Adding collision events using an event system
-	Adding trigger volumes to the system using collider shapes

Using what I already had

Without adding any major new features, I was able to implement a collision zoo that used simple physics logic to handle collision resolution between AABB and disc2D objects. The system used equations from the separating axis theorem to handle collision events between the 2 objects.

I did need to implement a physics system with the existing primitive shapes which required some definition of a rigid body and a collider. To do so, I followed the following architecture.

class Collider2D
{
public:

	virtual void				SetMomentForObject() = 0;
	virtual bool				Contains(Vec2 worldPoint) = 0;

	void						SetCollision(bool inCollision);
	void						SetCollisionEvent(const std::string& eventString);
	void						SetColliderType(eColliderType2D type);
	void						Destroy();

	bool						IsTouching(Collision2D* collision, Collider2D* otherCollider);
	eColliderType2D				GetType();

	void						FireCollisionEvent(EventArgs& args);

public:
	Rigidbody2D*				m_rigidbody = nullptr;
	eColliderType2D				m_colliderType = COLLIDER_UNKOWN;

	bool						m_inCollision = false;
	bool						m_isAlive = true;

	std::string					m_onCollisionEvent = "";
};

//--------------------------------------------------------------------------------
class AABB2Collider: public Collider2D
{
public:
	explicit AABB2Collider(const Vec2& minBounds, const Vec2& maxBounds);
	~AABB2Collider();

	virtual void				SetMomentForObject();
	virtual bool				Contains(Vec2 worldPoint);

	AABB2						GetLocalShape() const;		//Shape relative to rigidbody
	AABB2						GetWorldShape() const;		//Shape in world

	Vec2						GetBoxCenter() const;
public:
	AABB2						m_localShape;
};

//--------------------------------------------------------------------------------
class Disc2DCollider: public Collider2D
{
public:
	explicit Disc2DCollider(const Vec2& centre, float radius);
	~Disc2DCollider();

	virtual void				SetMomentForObject();
	virtual bool				Contains(Vec2 worldPoint);

	Disc2D						GetLocalShape() const;
	Disc2D						GetWorldShape() const;

public:
	Disc2D						m_localShape;
};
//--------------------------------------------------------------------------------
struct PhysicsMaterialT
{
	float restitution = 1.f;
};

//--------------------------------------------------------------------------------
class Rigidbody2D
{
public:
	Rigidbody2D( float mass = 1.0f);
	explicit Rigidbody2D(PhysicsSystem* physicsSystem, eSimulationType simulationType, float mass = 1.0f);
	~Rigidbody2D();

	//Apply a single step of movement
	void									Move(float deltaTime);
	void									ApplyRotation();
	//Apply specific movement
	inline void								MoveBy(Vec2 movement) { m_transform.m_position += movement * Vec2(m_constraints.x, m_constraints.y);}
	
	//Impulses
	void									ApplyImpulses(Vec2 linearImpulse, float angularImpulse);
	void									ApplyImpulseAt(Vec2 linearImpulse, Vec2 pointOfContact);
	
	//Forces and Torques
	inline void								AddForce(Vec2 force) { m_frameForces += force; }
	inline void								AddTorque(float torque) { m_frameTorque += torque; }

	//Render
	void									DebugRender(RenderContext* renderContext, const Rgba& color) const;
	
	//Mutators
	void									SetSimulationMode(eSimulationType simulationType);
	Collider2D*								SetCollider(Collider2D* collider);
	void									SetObject(void* object, Transform2* objectTransform);
	void									SetConstraints(const Vec3& constraints);
	void									SetConstraints(bool x, bool y, bool rotation);
	void									Destroy();

	//Accessors
	Vec2									GetPosition() const;
	eSimulationType							GetSimulationType();
	float									GetLinearDrag();
	float									GetAngularDrag();


public:
	PhysicsSystem*							m_system = nullptr; 			// system this rigidbody belongs to; 
	void*									m_object = nullptr; 			// user (game) pointer for external use
	Transform2*								m_object_transform = nullptr;	// what does this rigidbody affect

	Transform2								m_transform;					// rigidbody transform (mimics the object at start of frame, and used to tell the change to object at end of frame)
	Vec2									m_gravity_scale = Vec2::ONE;	// how much are we affected by gravity
	Vec2									m_velocity = Vec2::ZERO; 
	float 									m_angularVelocity = 0.f;
	float									m_mass;  						// how heavy am I

	Collider2D*								m_collider = nullptr;			// my shape; (could eventually be made a set)
	bool									m_isTrigger = false;
	PhysicsMaterialT						m_material;

	float									m_momentOfInertia = 0.f;
	float									m_rotation = 0.f;

	Vec2									m_frameForces = Vec2::ZERO;
	float									m_frameTorque = 0.f;

	float									m_friction = 1.f;				// Friction along the surface

	float									m_linearDrag = 0.1f;
	float									m_angularDrag = 0.1f;

	Vec3									m_constraints = Vec3(0.f, 1.f, 0.f);
	bool									m_isAlive = true;

private:
	eSimulationType							m_simulationType = TYPE_UNKOWN;

};
struct Collision2D
{
	Collider2D *m_Obj; 
	Collider2D *m_otherObj; 
	Manifold2D m_manifold; // may be referred to as a "contact"

	void InvertCollision();
};

typedef std::function<bool(Collision2D* out, Collider2D* a, Collider2D* b)> CollisionCheck2DCallback ;
// 2D arrays are [Y][X] remember
extern CollisionCheck2DCallback COLLISION_LOOKUP_TABLE[][COLLIDER2D_COUNT];

//--------------------------------------------------------------------------------
bool				CheckAABB2ByAABB2(Collision2D* out, Collider2D* a, Collider2D* b );
bool				CheckAABB2ByDisc(Collision2D* out, Collider2D* a, Collider2D* b );
bool				CheckDiscByDisc(Collision2D* out, Collider2D* a, Collider2D* b );
bool				CheckDiscByAABB2(Collision2D* out, Collider2D* a, Collider2D* b );
bool				GetCollisionInfo( Collision2D *out, Collider2D * a, Collider2D *b );

//--------------------------------------------------------------------------------
//Manifold Generation
//--------------------------------------------------------------------------------
bool				GetManifold( Manifold2D *out, AABB2Collider const &obj0, AABB2Collider const &obj1 ); 
bool				GetManifold( Manifold2D *out, AABB2Collider const &obj0, Disc2DCollider const &obj1 ); 
bool				GetManifold( Manifold2D *out, Disc2DCollider const &obj0, Disc2DCollider const &obj1 );
bool				GetManifold( Manifold2D *out, Disc2DCollider const &disc, AABB2Collider const &box );

With these systems in place, I was able to implement the desired frame logic that would become my physics step. The idea was for each frame in the game to call into the physics system update step where all the objects in the scene could be identified using a transform and be manipulated based on a set of forces. The forces were either the forces that affected all objects in the scene such as gravity and forces that were applied on an object by other objects in the scene or externally.

Below is the high-level program flow I followed to achieve the desired collision detection and resolution in the physics system.

void PhysicsSystem::Update( float deltaTime )
{
	CopyTransformsFromObjects(); 

	SetAllCollisionsToFalse();

	RunStep( deltaTime );

	CopyTransformsToObjects();  
}

void PhysicsSystem::RunStep( float deltaTime )
{
	m_frameCount++;

	//First move all rigidbodies based on forces on them
	MoveAllDynamicObjects(deltaTime);

	UpdateAllCollisions();
}

void PhysicsSystem::MoveAllDynamicObjects(float deltaTime)
{
	int numObjects = static_cast<int>(m_rbBucket->m_RbBucket[DYNAMIC_SIMULATION].size());

	for (int objectIndex = 0; objectIndex < numObjects; objectIndex++)
	{
		if(m_rbBucket->m_RbBucket[DYNAMIC_SIMULATION][objectIndex] != nullptr)
		{
			m_rbBucket->m_RbBucket[DYNAMIC_SIMULATION][objectIndex]->Move(deltaTime);
		}

	}
}

void PhysicsSystem::UpdateAllCollisions()
{	
	//Check Static vs Static to mark as collided
	CheckStaticVsStaticCollisions();

	//Dynamic vs Static set 
	ResolveDynamicVsStaticCollisions(true);

	//Dynamic vs Dynamic set
	ResolveDynamicVsDynamicCollisions(true);

	//Dynamic vs static set with no resolution
	ResolveDynamicVsStaticCollisions(false);
}

At the high level, the idea was to copy all the transform information of the objects in the simulation as the first step in the system. Then by moving all the dynamic objects based on the forces acting on them, I was able to accurately measure their overlap after the collision. This allows me to determine how much force needs to be applied as a result of the collision to move the colliding objects apart and how far apart they need to be after the resolution.

Finally, an update collision step allows me to the resolution based on the transform information of all objects in the system that was previously cached off. Below is a GIF of the result achieved by this simple physics system.

Collision Handling

The collision handling was done using separating axis theorem. The elastic collision equations governing the collisions were found here. Using these equations I was able to resolve the collision using the implementation as follows:

bool CheckAABB2ByAABB2(Collision2D* out, Collider2D* a, Collider2D* b)
{
	//Check collision between 2 boxes
	AABB2Collider* boxA = reinterpret_cast<AABB2Collider*>(a);
	AABB2Collider* boxB = reinterpret_cast<AABB2Collider*>(b);

	Manifold2D manifold;
	bool result = GetManifold(&manifold, *boxA, *boxB);

	if(result)
	{
		//Manifold is valid
		out->m_Obj = a;
		out->m_otherObj = b;
		out->m_manifold = manifold;
	}
	else
	{
		//Invalid manifold as no collision
		out->m_Obj = nullptr;
		out->m_otherObj = nullptr;
	}

	return result;
}

//--------------------------------------------------------------------------------
bool CheckAABB2ByDisc(Collision2D* out, Collider2D* a, Collider2D* b)
{
	//box vs disc
	AABB2Collider* boxA = reinterpret_cast<AABB2Collider*>(a);
	Disc2DCollider* discB = reinterpret_cast<Disc2DCollider*>(b);

	Manifold2D manifold;
	bool result = GetManifold(&manifold, *boxA, *discB);

	if(result)
	{
		out->m_manifold = manifold;
		out->m_Obj = a;
		out->m_otherObj = b;
	}
	else
	{
		out->m_Obj = nullptr;
		out->m_otherObj = nullptr;
	}

	return result;
}

//--------------------------------------------------------------------------------
bool CheckDiscByDisc(Collision2D* out, Collider2D* a, Collider2D* b)
{
	//disc vs disc
	Disc2DCollider* discA = reinterpret_cast<Disc2DCollider*>(a);
	Disc2DCollider* discB = reinterpret_cast<Disc2DCollider*>(b);

	Manifold2D manifold;
	bool result = GetManifold(&manifold, *discA, *discB);

	if(result)
	{
		// out here
		out->m_Obj = a;
		out->m_otherObj = b;
		out->m_manifold = manifold;
		return true;
	}
	else
	{
		out->m_otherObj = nullptr;
		out->m_Obj = nullptr;
		return false;
	}
}
bool GetCollisionInfo( Collision2D* out, Collider2D* a, Collider2D* b )
{
	uint aType = a->GetType(); 
	uint bType = b->GetType(); 

	if(aType >= COLLIDER2D_COUNT && bType >= COLLIDER2D_COUNT)
	{
		ERROR_AND_DIE("The Collider type was not part of the COLLISION_LOOKUP_TABLE");
	}

	CollisionCheck2DCallback callBack = COLLISION_LOOKUP_TABLE[aType][bType]; 

	if (callBack == nullptr) 
	{
		return false; // no known collision; 
	}
	else 
	{
		return callBack( out, a, b ); 
	}
}

//--------------------------------------------------------------------------------
bool GetManifold( Manifold2D *out, AABB2Collider const &boxA, AABB2Collider const &boxB )
{
	//Get the intersecting box
	Vec2 min = boxA.GetWorldShape().m_maxBounds.Min(boxB.GetWorldShape().m_maxBounds);
	Vec2 max = boxA.GetWorldShape().m_minBounds.Max(boxB.GetWorldShape().m_minBounds);

	//AABB2 collisionBox = AABB2(max, min);

	if(max < min)
	{
		GenerateManifoldBoxToBox(out, min, max);

		AABB2 boxAShape = boxA.GetWorldShape();
		AABB2 boxBShape = boxB.GetWorldShape();

		if(out->m_normal.y == 0.f)
		{	
			if(((boxAShape.m_maxBounds + boxAShape.m_minBounds)/2).x < ((boxBShape.m_maxBounds + boxBShape.m_minBounds)/2).x)
			{
				//pushing out on x
				out->m_normal *= -1;
			}
		}
		else if(out->m_normal.x == 0.f)
		{
			if(((boxAShape.m_maxBounds + boxAShape.m_minBounds)/2).y < ((boxBShape.m_maxBounds + boxBShape.m_minBounds)/2).y)
			{
				//pushing out on y
				out->m_normal *= -1;
			}
		}

		return true;
	}
	else 
	{
		return false;
	}
}

//--------------------------------------------------------------------------------
bool GetManifold( Manifold2D *out, AABB2Collider const &box, Disc2DCollider const &disc )
{
	Vec2 discCentre = disc.GetWorldShape().GetCentre();
	AABB2 boxShape = box.GetWorldShape();
	Vec2 closestPoint = GetClosestPointOnAABB2( discCentre, boxShape );
	Vec2 boxCenter = boxShape.GetBoxCenter() + boxShape.m_minBounds;

	float distanceSquared = GetDistanceSquared2D(discCentre, closestPoint);
	float radius = disc.GetWorldShape().GetRadius();
	//float distanceBwCenters = GetDistanceSquared2D(discCentre, boxCenter);

	float distance = 0;

	//Check is box inside disc
	if(closestPoint == discCentre)
	{
		//box is inside disc
		distance = GetDistance2D(discCentre, boxCenter);
		Vec2 normal = boxCenter - discCentre;
		normal.Normalize();

		out->m_normal = normal;
		out->m_penetration = distance;
		return true;
	}

	if(distanceSquared < radius * radius)
	{
		//out here
		distance = GetDistance2D(discCentre, closestPoint);
		Vec2 normal = closestPoint - discCentre;
		normal.Normalize();

		out->m_normal = normal;
		out->m_penetration = radius - distance;
		return true;
	}
	else
	{
		return false;
	}
}

//--------------------------------------------------------------------------------
bool GetManifold( Manifold2D *out, Disc2DCollider const &disc, AABB2Collider const &box)
{
	Vec2 discCentre = disc.GetWorldShape().GetCentre();
	AABB2 boxShape = box.GetWorldShape();

	Vec2 closestPoint = GetClosestPointOnAABB2( discCentre, boxShape );

	float distanceSquared = GetDistanceSquared2D(discCentre, closestPoint);
	float radius = disc.GetWorldShape().GetRadius();

	if(closestPoint == discCentre)
	{
		return IsDiscInBox(out, discCentre, boxShape, radius);
	}

	if(distanceSquared < radius * radius)
	{
		//out here
		float distance = GetDistance2D(discCentre, closestPoint);

		Vec2 normal = discCentre - closestPoint;
		normal.Normalize();

		out->m_normal = normal;
		out->m_penetration = radius - distance;
		return true;
	}
	else
	{
		return false;
	}
}

Adding all the new features

Now that I had a basic setup in place, I was able to test collisions in a zoo and make use of the existing object primitives I had support for. To add more features, I could now architect an interface and encapsulate data based on my usage. Below are the various features that were incorporated:

The Oriented Bounding Box (OBB)

Axis aligned bounding boxes allowed me to use them as colliders in a physics system but they have 1 problem. They are axis-aligned. To have box colliders in the scene that could rotate based on impulses acting on the object would require a new type of bounding box. To implement an oriented bounding box(OBB) for this system I created a new class with methods to help access certain properties of the OBB.

The Capsule

With the addition of an oriented bounding box, it became a lot easier to think about the capsule. It had a spine and a radius around this spine which could quite easily be visualized as an OBB of width 0 and some radius ‘r’ and height ‘h’. The resolution of physics would be very similar except that it would now need to account for an OBB thickness. The logic for this calculation was simplified thanks to using Voronoi regions to early out for the simple cases where the contact point was in the disc-shaped regions of the capsule.

Below is my implementation:

class Capsule2D
{
public:
	Capsule2D(); 
	Capsule2D( Vec2 pos );                  // Equivalent to a point; 
	Capsule2D( Vec2 center, float radius ); // Equivalent to a disc; 
	Capsule2D( Vec2 p0, Vec2 p1, float radius ); 
	~Capsule2D();

	// Modification Utility
	void		SetPosition( Vec2 pos );                         // special care here;   Use the center of the line as position, but maintain shape
	void		SetPositions( Vec2 p0, Vec2 p1 ); 
	void		Translate( Vec2 offset ); 

	void		RotateBy( float degrees );                       
				
	// Helpers for describing it; 
	inline const Vec2&		GetStart() const { return m_start; };
	inline const Vec2&		GetEnd() const {return m_end; }; 

	// Collision Utility
	bool		Contains( Vec2 worldPoint ); 
	Vec2		GetClosestPoint( Vec2 worldPoint ); 
	Vec2		GetCenter() const;

	// Useful if you want to use AABB2 as a "broad phase" & "mid phase" check
	// like checking if something fell outside the world
	AABB2		GetBoundingAABB() const; 

public:
	Vec2 m_start					= Vec2::ZERO;
	Vec2 m_end						= Vec2::ZERO; 
	float m_radius					= 0.0f; 
	// Note: defaults basically make a "point" at 0; 
	float m_rotationDegrees			= 0.0f;
};
class OBB2
{
public:
	OBB2();
	~OBB2();
	explicit OBB2( Vec2 center, Vec2 size = Vec2::ZERO, float rotationDegrees = 0.0f ); 
	explicit OBB2( AABB2 const &aabb ); 

	//Modification Utilities
	void Translate( Vec2 offset );
	void MoveTo( Vec2 position );

	void RotateBy( float degrees );
	void SetRotation( float degrees );

	void SetSize( Vec2 newSize );

	//Accessors for OBB Properties
	inline const Vec2& GetRight() const				{ return m_right; }
	inline const Vec2& GetUp() const				{ return m_up; }
	inline const Vec2& GetCenter() const			{ return m_center; }
	inline const Vec2& GetHalfExtents() const				{ return m_halfExtents; }

	//Accessors for the corners themselves
	inline Vec2 GetBottomLeft() const		{ return m_center - m_halfExtents.x * GetRight() - m_halfExtents.y * GetUp(); }
	inline Vec2 GetBottomRight() const		{ return m_center + m_halfExtents.x * GetRight() - m_halfExtents.y * GetUp(); }
	inline Vec2 GetTopLeft() const			{ return m_center - m_halfExtents.x * GetRight() + m_halfExtents.y * GetUp(); }
	inline Vec2 GetTopRight() const			{ return m_center + m_halfExtents.x * GetRight() + m_halfExtents.y * GetUp(); }

	// Collision Utility
	Vec2 ToLocalPoint( Vec2 worldPoint ) const; 
	Vec2 ToWorldPoint( Vec2 localPoint ) const; 

	void GetPlanes(Plane2D* out) const;
	void GetCorners(Vec2* out) const;
	void GetSides(Segment2D* out) const;

	bool Contains( Vec2 worldPoint ) const;
	Vec2 GetClosestPoint( Vec2 worldPoint ) const;

	bool Intersects( OBB2 const &other ) const; 

public:
	Vec2 m_right         = Vec2( 1.0f, 0.0f ); 
	Vec2 m_up            = Vec2( 0.0f, 1.0f );
	Vec2 m_center        = Vec2( 0.0f, 0.0f ); 
	Vec2 m_halfExtents   = Vec2( 0.0f, 0.0f ); 
};
bool CheckOBB2ByOBB2( Collision2D* out, Collider2D* a, Collider2D* b )
{
	//OBB vs OBB
	BoxCollider2D* boxA = reinterpret_cast<BoxCollider2D*>(a);
	BoxCollider2D* boxB = reinterpret_cast<BoxCollider2D*>(b);

	Manifold2D manifold;
	bool result = GetManifold(&manifold, *boxA, *boxB);

	if(result)
	{
		//out here
		out->m_Obj = a;
		out->m_otherObj = b;
		out->m_manifold = manifold;
		return true;
	}
	else
	{
		out->m_Obj = nullptr;
		out->m_otherObj = nullptr;
		return false;
	}
}

//--------------------------------------------------------------------------------
bool CheckCapsuleByCapsule( Collision2D * out, Collider2D * a, Collider2D * b )
{
	//Capsule vs Capsule
	CapsuleCollider2D* capA = reinterpret_cast<CapsuleCollider2D*>(a);
	CapsuleCollider2D* capB = reinterpret_cast<CapsuleCollider2D*>(b);

	Manifold2D manifold;
	bool result = GetManifold(&manifold, *capA, *capB);

	if(result)
	{
		//out here
		out->m_Obj = a;
		out->m_otherObj = b;
		out->m_manifold = manifold;
		return true;
	}
	else
	{
		out->m_Obj = nullptr;
		out->m_otherObj = nullptr;
		return false;
	}
}

//--------------------------------------------------------------------------------
bool CheckCapsuleByOBB2( Collision2D* out, Collider2D* a, Collider2D* b )
{
	//Capsule vs OBB2
	CapsuleCollider2D* capA = reinterpret_cast<CapsuleCollider2D*>(a);
	BoxCollider2D* boxB = reinterpret_cast<BoxCollider2D*>(b);

	Manifold2D manifold;
	bool result = GetManifold(&manifold, *capA, *boxB);

	if(result)
	{
		//out here
		out->m_Obj = a;
		out->m_otherObj = b;
		out->m_manifold = manifold;
		return true;
	}
	else
	{
		out->m_Obj = nullptr;
		out->m_otherObj = nullptr;
		return false;
	}
}

//--------------------------------------------------------------------------------
bool CheckOBB2ByCapsule( Collision2D* out, Collider2D* a, Collider2D* b )
{
	//OBB2 vs Capsule
	BoxCollider2D* boxA = reinterpret_cast<BoxCollider2D*>(a);
	CapsuleCollider2D* capB = reinterpret_cast<CapsuleCollider2D*>(b);

	Manifold2D manifold;
	bool result = GetManifold(&manifold, *boxA, *capB);

	if(result)
	{
		//out here
		out->m_Obj = a;
		out->m_otherObj = b;
		out->m_manifold = manifold;
		return true;
	}
	else
	{
		out->m_Obj = nullptr;
		out->m_otherObj = nullptr;
		return false;
	}
}

//--------------------------------------------------------------------------------
void Collision2D::InvertCollision()
{
	Collider2D* col = m_Obj;
	m_Obj = m_otherObj;
	m_otherObj = col;
	m_manifold.m_normal *= -1.f;
}
bool GetManifold( Manifold2D *out, BoxCollider2D const &a, BoxCollider2D const &b )
{
	OBB2 boxA = a.GetWorldShape();
	OBB2 boxB = b.GetWorldShape();

	Plane2D planesOfThis[4];    // p0
	Plane2D planesOfOther[4];   // p1

	Segment2D segmentsOfThis[4];
	Segment2D segmentsOfOther[4];

	Vec2 cornersOfThis[4];     // c0
	Vec2 cornersOfOther[4];    // c1

	boxA.GetPlanes( planesOfThis ); 
	boxA.GetCorners( cornersOfThis ); 
	boxA.GetSides (segmentsOfThis );

	boxB.GetPlanes( planesOfOther ); 
	boxB.GetCorners( cornersOfOther ); 
	boxB.GetSides( segmentsOfOther );

	int inFrontOfThis = 0;
	int inFrontOfOther = 0;

	//Data for the manifold generation
	float bestDistToA = 100000; 
	Vec2 bestPointToA;
	
	float bestDistToB = 100000; 
	Vec2 bestPointToB;


	Plane2D bestThisPlane;
	Plane2D bestOtherPlane;

	Segment2D bestSegmentThis;
	Segment2D bestSegmentOther;

	Vec2 worstCornerToPlane[4];
	float worstDistancesToThis[4];
	Vec2 worstCornerToOtherPlane[4];
	float worstDistancesToOther[4];

	for(int planeIndex = 0; planeIndex < 4;planeIndex++)
	{
		Plane2D const &thisPlane = planesOfThis[planeIndex];
		Plane2D const &otherPlane = planesOfOther[planeIndex];

		inFrontOfThis = 0;
		inFrontOfOther = 0;

		for(int cornerIndex = 0; cornerIndex < 4; cornerIndex++)
		{
			Vec2 const &cornerOfThis = cornersOfThis[cornerIndex]; 
			Vec2 const &cornerOfOther = cornersOfOther[cornerIndex];

			float otherFromThis = thisPlane.GetDistance( cornerOfOther ); 
			float thisFromOther = otherPlane.GetDistance( cornerOfThis ); 

			
			if(cornerIndex == 0)
			{
				bestDistToA = otherFromThis;
				bestPointToA = cornerOfOther;
				bestThisPlane = thisPlane;

				bestSegmentThis = segmentsOfThis[planeIndex];
				bestSegmentOther = segmentsOfOther[planeIndex];

				worstCornerToPlane[planeIndex] = cornerOfOther;
				worstDistancesToThis[planeIndex] = otherFromThis;

				bestDistToB = thisFromOther;
				bestPointToB = cornerOfThis;
				bestOtherPlane = otherPlane;
				
				worstCornerToOtherPlane[planeIndex] = cornerOfThis;
				worstDistancesToOther[planeIndex] = thisFromOther;
			}

			//Update the feature point
			if(otherFromThis < bestDistToA)
			{
				bestDistToA = otherFromThis;
				bestPointToA = cornerOfOther;
				bestThisPlane = thisPlane;
				bestSegmentThis = segmentsOfThis[planeIndex];

				worstCornerToPlane[planeIndex] = cornerOfOther;
				worstDistancesToThis[planeIndex] = otherFromThis;
			}

			//Update the feature point for other plane
			if(thisFromOther < bestDistToB)
			{
				bestDistToB = thisFromOther;
				bestPointToB = cornerOfThis;
				bestOtherPlane = otherPlane;
				bestSegmentOther = segmentsOfOther[planeIndex];

				worstCornerToOtherPlane[planeIndex] = cornerOfThis;
				worstDistancesToOther[planeIndex] = thisFromOther;
			}

			inFrontOfThis += (otherFromThis >= 0.0f) ? 1 : 0; 
			inFrontOfOther += (thisFromOther >= 0.0f) ? 1 : 0; 
		}

		//We are not intersecting if there are exactly 4 in front of either plane. Early out bro
		if ((inFrontOfThis == 4) || (inFrontOfOther == 4)) {
			return false;
		}
	}

	float bestCaseThis = 0.f;
	float bestCaseOther = 0.f;
	int bestCaseIndexThis = 0;
	int bestCaseIndexOther = 0;

	Vec2 bestContactThis;
	Vec2 bestContactOther;

	for(int worstCaseIndex = 0; worstCaseIndex < 4; worstCaseIndex++)
	{
		if(worstCaseIndex == 0)
		{
			bestCaseThis = worstDistancesToThis[worstCaseIndex];
			bestCaseOther = worstDistancesToOther[worstCaseIndex];

			bestContactThis = worstCornerToPlane[worstCaseIndex];
			bestContactOther = worstCornerToOtherPlane[worstCaseIndex];
		}

		if(worstDistancesToThis[worstCaseIndex] > bestCaseThis)
		{
			bestCaseThis = worstDistancesToThis[worstCaseIndex];
			bestContactThis = worstCornerToPlane[worstCaseIndex];
			bestCaseIndexThis = worstCaseIndex;
		}

		if(worstDistancesToOther[worstCaseIndex] > bestCaseOther)
		{
			bestCaseOther = worstDistancesToOther[worstCaseIndex];
			bestContactOther = worstCornerToOtherPlane[worstCaseIndex];
			bestCaseIndexOther = worstCaseIndex;
		}

	}
	
	//Check which of the 2 are larger (smaller -ve number)
	if(bestCaseOther > bestCaseThis)
	{
		out->m_penetration = bestCaseOther * -1.f;
		out->m_normal = planesOfOther[bestCaseIndexOther].m_normal;
		out->m_contact = bestContactOther;

		//DEBUG
// 		DebugRenderOptionsT options;
// 		options.relativeCoordinates = true;
// 		options.space = DEBUG_RENDER_SCREEN;
// 		options.beginColor = Rgba::GREEN;
// 		g_debugRenderer->DebugRenderPoint2D(options, bestContactOther, 1.f);
	}
	else
	{
		out->m_penetration = bestCaseThis * -1.f;
		out->m_normal = planesOfThis[bestCaseIndexThis].m_normal * -1.f;
		out->m_contact = bestContactThis;

		//DEBUG
// 		DebugRenderOptionsT options;
// 		options.relativeCoordinates = true;
// 		options.space = DEBUG_RENDER_SCREEN;
// 		options.beginColor = Rgba::BLUE;
// 		g_debugRenderer->DebugRenderPoint2D(options, bestContactThis, 1.f);
	}

	return true; 
}

//--------------------------------------------------------------------------------
bool GetManifold( Manifold2D *out, BoxCollider2D const &a, float aRadius, BoxCollider2D const &b, float bRadius )
{
	OBB2 boxA = a.GetWorldShape();
	OBB2 boxB = b.GetWorldShape();

	if (GetManifold( out, a, b )) {
		out->m_penetration += (aRadius + bRadius); 
		return true;
	}

	Segment2D sidesA[4];
	boxA.GetSides(sidesA); 
	Segment2D sidesB[4];
	boxB.GetSides(sidesB); 
	
	Vec2 cornersA[4];
	boxA.GetCorners(cornersA); 
	Vec2 cornersB[4];
	boxB.GetCorners(cornersB); 

	float bestMatchToA = 10000000; 
	float bestMatchToB = 10000000; 

	Vec2 bestA;
	Vec2 bestB;

	for(int sideIndex = 0; sideIndex < 4; sideIndex++)
	{
		Segment2D const &sideA = sidesA[sideIndex];
		Segment2D const &sideB = sidesB[sideIndex];

		for(int cornerIndex = 0; cornerIndex < 4; cornerIndex++)
		{
			Vec2 const &cornerA = cornersA[cornerIndex];
			Vec2 const &cornerB = cornersB[cornerIndex];

			Vec2 closestA = sideA.GetClosestPoint(cornerB); 
			Vec2 closestB = sideB.GetClosestPoint(cornerA); 

			// distances are...
			// closestA to cornerB
			// closestB to cornerA
			float lengthSquared = (closestA - cornerB).GetLengthSquared() ;
			if (lengthSquared < bestMatchToA) {
				bestA = closestA; 
				bestB = cornerB; 
				bestMatchToA = lengthSquared;
			}

			lengthSquared = (closestB - cornerA).GetLengthSquared() ;
			if (lengthSquared < bestMatchToB) {
				bestA = cornerA; 
				bestB = closestB; 
				bestMatchToB = lengthSquared; 
			}
		}
	}


	// two closets points are bestA, bestB; 
	// normal is directoin between them;
	// penetration is sum of radius - distance; 

	float distance = GetDistance2D(bestA, bestB);
	if(distance > (aRadius + bRadius))
	{
		return false;
	}
	else
	{
		out->m_normal = bestB - bestA;
		out->m_penetration = aRadius + bRadius - distance;
		return true;
	}
}

//--------------------------------------------------------------------------------
bool GetManifold( Manifold2D *out, CapsuleCollider2D const &a, CapsuleCollider2D const &b )
{
	//Call the GetManifold with 2 BoxColliders and radius
	return GetManifold(out, a.GetWorldShape(), a.GetCapsuleRadius(), b.GetWorldShape(), b.GetCapsuleRadius());
}

//--------------------------------------------------------------------------------
bool GetManifold( Manifold2D *out, OBB2 const &a, float aRadius, OBB2 const &b, float bRadius )
{
	if (GetManifold( out, a, b )) 
	{
		out->m_penetration += (aRadius + bRadius); 
		return true;
	}

	Segment2D sidesA[4];
	a.GetSides(sidesA); 
	Segment2D sidesB[4];
	b.GetSides(sidesB); 

	Vec2 cornersA[4];
	a.GetCorners(cornersA); 
	Vec2 cornersB[4];
	b.GetCorners(cornersB); 

	float bestMatch = 10000000; 
	// float bestMatchToB = 10000000; 

	Vec2 bestA;
	Vec2 bestB;

	for(int sideIndex = 0; sideIndex < 4; sideIndex++)
	{
		Segment2D const &sideA = sidesA[sideIndex];
		Segment2D const &sideB = sidesB[sideIndex];

		for(int cornerIndex = 0; cornerIndex < 4; cornerIndex++)
		{
			Vec2 const &cornerA = cornersA[cornerIndex];
			Vec2 const &cornerB = cornersB[cornerIndex];

			Vec2 closestA = sideA.GetClosestPoint(cornerB); 
			Vec2 closestB = sideB.GetClosestPoint(cornerA); 

			// distances are...
			// closestA to cornerB
			// closestB to cornerA
			float lengthSquared = (closestA - cornerB).GetLengthSquared() ;
			if (lengthSquared < bestMatch) {
				bestA = closestA; 
				bestB = cornerB; 
				bestMatch = lengthSquared;
			}

			lengthSquared = (closestB - cornerA).GetLengthSquared() ;
			if (lengthSquared < bestMatch) {
				bestA = cornerA; 
				bestB = closestB; 
				bestMatch = lengthSquared; 
			}
		}
	}


	// two closets points are bestA, bestB; 
	// normal is directoin between them;
	// penetration is sum of radius - distance; 

	float distance = GetDistance2D(bestA, bestB);
	if(distance > (aRadius + bRadius))
	{
		return false;
	}
	else
	{
		Vec2 disp = bestB - bestA;
		if(disp.GetLengthSquared() < 0.000001)
		{
			out->m_normal = Vec2(0.f, 1.f);
		}
		else
		{
			out->m_normal = disp.GetNormalized() * -1.f;
		}
		out->m_penetration = (aRadius + bRadius) - distance;
		out->m_contact = bestA + aRadius * -1.f * out->m_normal;

// 		DebugRenderOptionsT options;
// 		options.relativeCoordinates = true;
// 		options.space = DEBUG_RENDER_SCREEN;
// 		options.beginColor = Rgba::GREEN;
// 		g_debugRenderer->DebugRenderPoint2D(options, out->m_contact, 1.f);

		return true;
	}
}

//--------------------------------------------------------------------------------
bool GetManifold( Manifold2D *out, BoxCollider2D const &a, CapsuleCollider2D const &b )
{
	return GetManifold(out, a.GetWorldShape(), 0.f, b.GetWorldShape(), b.GetCapsuleRadius());
}

//--------------------------------------------------------------------------------
bool GetManifold( Manifold2D *out, CapsuleCollider2D const &a, BoxCollider2D const &b )
{
	return GetManifold(out, a.GetWorldShape(), a.GetCapsuleRadius(), b.GetWorldShape(), 0.f);
}
//------------------------------------------------------------------------------------------------------------------------------
class BoxCollider2D: public Collider2D
{
public:
	explicit BoxCollider2D( Vec2 center, Vec2 size = Vec2::ZERO, float rotationDegrees = 0.0f );
	~BoxCollider2D();

	virtual void				SetMomentForObject();
	virtual bool				Contains(Vec2 worldPoint);

	OBB2						GetLocalShape() const;
	OBB2						GetWorldShape() const;

public:
	OBB2						m_localShape;
};

//------------------------------------------------------------------------------------------------------------------------------
class CapsuleCollider2D: public Collider2D
{
public:
	explicit CapsuleCollider2D ( Vec2 start, Vec2 end, float radius );
	~CapsuleCollider2D();

	virtual void				SetMomentForObject();
	virtual bool				Contains(Vec2 worldPoint);

	OBB2						GetLocalShape() const;
	OBB2						GetWorldShape() const;

	const Capsule2D&			GetReferenceShape() const;

	float						GetCapsuleRadius() const;

public:
	Capsule2D					m_referenceCapsule;

	OBB2						m_localShape;
	float						m_radius;
};

Mouse Controls and Debug

Adding mouse control would make the application easier to use by allowing the user to drag the mouse to draw objects of the desired size. It would also allow the user to perform actions such as clicking to hold an object and hovering to view information about the object. Adding these features made debugging easier and allowed for better user experience.

To make life a little easier when verifying contact positions being generated for the OBB and Capsules, I needed a simple debug interface that can allow me to identify the points. I added a draw call to show the user the contact points on the screen and change color based on time passed since the collision. While debugging, things ended up looking like this:

Below is the implementation I followed to use the mouse cursor:

class GameCursor
{
public:
	GameCursor();
	~GameCursor();

	void			StartUp();

	void			Update(float deltaTime);
	void			Render() const; 

	void			HandleKeyPressed( unsigned char keyCode );
	void			HandleKeyReleased( unsigned char keyCode );

	void			SetCursorPosition(const Vec2& position);

	const Vec2&     GetCursorPositon() const;

private:
	Vec2			m_cursorPosition = Vec2::ZERO;
	Rgba			m_cursorColor = Rgba::WHITE;
	float			m_cursorThickness = 0.25f;
	float			m_cursorRingRadius = 2.0f;

	//Movement Data
	Vec2			m_movementVector = Vec2::ZERO;
	float			m_cursorSpeed = 0.25f;
};
GameCursor::GameCursor()
{
	StartUp();
}

GameCursor::~GameCursor()
{

}

void GameCursor::StartUp()
{
	m_cursorPosition = Vec2(WORLD_CENTER_X, WORLD_CENTER_Y);
}

void GameCursor::Update( float deltaTime )
{
	UNUSED(deltaTime);

	//New code to implement cursor at mouse position;
	IntVec2 intVecPos = g_windowContext->GetClientMousePosition();
	IntVec2 clientBounds = g_windowContext->GetClientBounds();

	Vec2 worldPos = Game::GetClientToWorldPosition2D(intVecPos, clientBounds);

	m_cursorPosition = worldPos;
}

void GameCursor::Render() const
{
	std::vector<Vertex_PCU> ringVerts;
	AddVertsForRing2D(ringVerts, m_cursorPosition, m_cursorRingRadius, m_cursorThickness, m_cursorColor);

	std::vector<Vertex_PCU>	lineVerts;
	Vec2 vertLineOffset = Vec2(0.f, m_cursorRingRadius);
	Vec2 horLineOffset = Vec2(m_cursorRingRadius, 0.f);

	AddVertsForLine2D(lineVerts, m_cursorPosition - vertLineOffset - Vec2(0.f, 0.5f), m_cursorPosition + vertLineOffset + Vec2(0.f, 0.5f), m_cursorThickness, m_cursorColor);
	AddVertsForLine2D(lineVerts, m_cursorPosition - horLineOffset - Vec2(0.5f, 0.f), m_cursorPosition + horLineOffset + Vec2(0.5f, 0.f), m_cursorThickness, m_cursorColor);

	//g_renderContext->BindTexture(nullptr);
	g_renderContext->BindTextureViewWithSampler(0U, nullptr);
	g_renderContext->DrawVertexArray(lineVerts);
	g_renderContext->DrawVertexArray(ringVerts);
}

void GameCursor::HandleKeyPressed( unsigned char keyCode )
{
	switch( keyCode )
	{
		case UP_ARROW:
		m_movementVector.y += 1.f;
		break;
		case DOWN_ARROW:
		m_movementVector.y -= 1.f;
		break;
		case RIGHT_ARROW:
		m_movementVector.x += 1.f;
		break;
		case LEFT_ARROW:
		m_movementVector.x -= 1.f;
		break;
	}
}

void GameCursor::HandleKeyReleased( unsigned char keyCode )
{
	switch( keyCode )
	{
		case UP_ARROW:
		m_movementVector.y = 0.f;
		break;
		case DOWN_ARROW:
		m_movementVector.y = 0.f;
		break;
		case RIGHT_ARROW:
		m_movementVector.x = 0.f;
		break;
		case LEFT_ARROW:
		m_movementVector.x = 0.f;
		break;
	}
}

void GameCursor::SetCursorPosition( const Vec2& position )
{
	m_cursorPosition = position;
}

const Vec2& GameCursor::GetCursorPositon() const
{
	return m_cursorPosition;
}

Saving and loading scene information

The first game feature I added to the application was being able to create a scene with a bunch of physics objects and be able to save and load these scenes. This would allow me to make scenes as desired and save them using the console in the game engine. Below is what the process looked like:

void Game::SaveToFile(const std::string& filePath)
{
	//Save all scene objects to the file
	tinyxml2::XMLDocument saveDoc;

	tinyxml2::XMLNode* rootNode = saveDoc.NewElement("SavedGeometry");
	saveDoc.InsertFirstChild(rootNode);

	int numObjects = (int)m_allGeometry.size();
	for (int index = 0; index < numObjects; index++)
	{
		//Save all the object properties using XML
		XMLElement* geometry = saveDoc.NewElement("GeometryData");
		XMLElement* rbElem = saveDoc.NewElement("RigidBody");
		geometry->InsertEndChild(rbElem);

		//Rigidbody data
		rbElem->SetAttribute("SimType", m_allGeometry[index]->m_rigidbody->GetSimulationType());
		rbElem->SetAttribute("Shape", m_allGeometry[index]->m_collider->m_colliderType);
		rbElem->SetAttribute("Mass", m_allGeometry[index]->m_rigidbody->m_mass);
		rbElem->SetAttribute("Friction", m_allGeometry[index]->m_rigidbody->m_friction);
		rbElem->SetAttribute("AngularDrag", m_allGeometry[index]->m_rigidbody->m_angularDrag);
		rbElem->SetAttribute("LinearDrag", m_allGeometry[index]->m_rigidbody->m_linearDrag);
		rbElem->SetAttribute("Freedom", m_allGeometry[index]->m_rigidbody->m_constraints.GetAsString().c_str());
		rbElem->SetAttribute("Moment", m_allGeometry[index]->m_rigidbody->m_momentOfInertia);
		rbElem->SetAttribute("Restitution", m_allGeometry[index]->m_rigidbody->m_material.restitution);

		XMLElement* colElem = saveDoc.NewElement("Collider");
		geometry->InsertEndChild(colElem);

		//Collider data
		eColliderType2D type = m_allGeometry[index]->m_collider->GetType();
		Collider2D* collider = m_allGeometry[index]->m_collider;
		
		switch (type)
		{
			case COLLIDER_BOX:
			{
				BoxCollider2D* boxCollider = reinterpret_cast<BoxCollider2D*>(collider);
				colElem->SetAttribute("Center", boxCollider->GetWorldShape().GetCenter().GetAsString().c_str());
				colElem->SetAttribute("Size", (Vec2(boxCollider->GetWorldShape().GetHalfExtents()) * 2.f).GetAsString().c_str());
				colElem->SetAttribute("Rotation", boxCollider->m_rigidbody->m_rotation);
			}
			break;
			case COLLIDER_CAPSULE:
			{
				CapsuleCollider2D* col = reinterpret_cast<CapsuleCollider2D*>(collider);
				colElem->SetAttribute("Start", col->GetReferenceShape().m_start.GetAsString().c_str());
				colElem->SetAttribute("End", col->GetReferenceShape().m_end.GetAsString().c_str());
				colElem->SetAttribute("Radius", col->GetCapsuleRadius());
			}
			break;
		}

		//Transform stuff
		XMLElement* tranformElem = saveDoc.NewElement("Transform");
		geometry->InsertEndChild(tranformElem);

		tranformElem->SetAttribute("Position", m_allGeometry[index]->m_transform.m_position.GetAsString().c_str());
		tranformElem->SetAttribute("Rotation", m_allGeometry[index]->m_transform.m_rotation);
		tranformElem->SetAttribute("Scale", m_allGeometry[index]->m_transform.m_scale.GetAsString().c_str());
		
		rootNode->InsertEndChild(geometry);
	}
	
	//Save to the file
	tinyxml2::XMLError eResult = saveDoc.SaveFile(filePath.c_str());

	if (eResult != tinyxml2::XML_SUCCESS)
	{
		printf("Error: %i\n", eResult);
		ASSERT_RECOVERABLE(true, Stringf("Error: %i\n", eResult));
	}
}
void Game::LoadFromFile(const std::string& filePath)
{
	//Delete all existing objects
	for (int index = 0; index < (int)m_allGeometry.size(); index++)
	{
		delete m_allGeometry[index];
		m_allGeometry[index] = nullptr;
	}
	 
	m_allGeometry.erase(m_allGeometry.begin(), m_allGeometry.end());

	//Open the xml file and parse it
	tinyxml2::XMLDocument saveDoc;
	saveDoc.LoadFile(filePath.c_str());

	if (saveDoc.ErrorID() != tinyxml2::XML_SUCCESS)
	{
		printf("\n >> Error loading XML file from %s ", filePath);
		printf("\n >> Error ID : %i ", saveDoc.ErrorID());
		printf("\n >> Error line number is : %i", saveDoc.ErrorLineNum());
		printf("\n >> Error name : %s", saveDoc.ErrorName());
		
		ERROR_AND_DIE(">> Error loading Save Game XML file ");
		return;
	}
	else
	{
		//Load from the file and spawn required objects
		XMLElement* rootElement = saveDoc.RootElement();

		XMLElement* geometry = rootElement->FirstChildElement();

		while(geometry != nullptr)
		{
			//Read RB data first
			XMLElement* elem = geometry->FirstChildElement("RigidBody");

			int type = ParseXmlAttribute(*elem, "SimType", 0);
			int shape = ParseXmlAttribute(*elem, "Shape", 0);
			float mass = ParseXmlAttribute(*elem, "Mass", 0.1f);
			float friction = ParseXmlAttribute(*elem, "Friction", 0.f);
			float angularDrag = ParseXmlAttribute(*elem, "AngularDrag", 0.f);
			float linearDrag = ParseXmlAttribute(*elem, "LinearDrag", 0.f);
			Vec3 freedom = ParseXmlAttribute(*elem, "Freedom", Vec3::ONE);
			float moment = ParseXmlAttribute(*elem, "Moment", INFINITY);
			float restitution = ParseXmlAttribute(*elem, "Restitution", 1.f);

			//Read Collider data 
			elem = elem->NextSiblingElement("Collider");
			Geometry* entity = nullptr;

			switch (shape)
			{
			case COLLIDER_BOX:
			{
				Vec2 center = ParseXmlAttribute(*elem, "Center", Vec2::ZERO);
				Vec2 size = ParseXmlAttribute(*elem, "Size", Vec2::ZERO);
				float rotation = ParseXmlAttribute(*elem, "Rotation", 0.f);

				if (type == STATIC_SIMULATION)
				{
					if (size.x == 80.f)
					{
						entity = new Geometry(*g_physicsSystem, STATIC_SIMULATION, BOX_GEOMETRY, center, rotation, size.y, Vec2::ZERO, true);
					}
					else
					{
						entity = new Geometry(*g_physicsSystem, STATIC_SIMULATION, BOX_GEOMETRY, center, rotation, size.y);
					}
					entity->m_rigidbody->m_mass = mass;
					entity->m_rigidbody->SetConstraints(false, false, false);
				}
				else
				{
					entity = new Geometry(*g_physicsSystem, DYNAMIC_SIMULATION, BOX_GEOMETRY, center, rotation, size.y);
					entity->m_rigidbody->m_mass = mass;
					entity->m_rigidbody->SetConstraints(freedom);
				}
				entity->m_rigidbody->m_material.restitution = restitution;
				entity->m_rigidbody->m_friction = friction;
				entity->m_rigidbody->m_angularDrag = angularDrag;
				entity->m_rigidbody->m_linearDrag = linearDrag;
				entity->m_rigidbody->m_momentOfInertia = moment;
			}
			break;
			case COLLIDER_CAPSULE:
			{
				Vec2 start = ParseXmlAttribute(*elem, "Start", Vec2::ZERO);
				Vec2 end = ParseXmlAttribute(*elem, "End", Vec2::ZERO);
				float radius = ParseXmlAttribute(*elem, "Radius", 0.f);
				UNUSED(radius);

				Vec2 disp = start - end;
				Vec2 norm = disp.GetNormalized();
				float length = disp.GetLength();

				Vec2 center = end + length * norm * 0.5f;
				float rotationDegrees = disp.GetAngleDegrees() + 90.f;

				if (type == STATIC_SIMULATION)
				{
					entity = new Geometry(*g_physicsSystem, STATIC_SIMULATION, CAPSULE_GEOMETRY, start, rotationDegrees, 0.f, end);
					entity->m_rigidbody->m_mass = INFINITY;
					entity->m_rigidbody->SetConstraints(false, false, false);
				}
				else
				{
					entity = new Geometry(*g_physicsSystem, DYNAMIC_SIMULATION, CAPSULE_GEOMETRY, start, rotationDegrees, 0.f, end);
					entity->m_rigidbody->m_mass = mass;
					entity->m_rigidbody->SetConstraints(freedom);
				}
				entity->m_rigidbody->m_friction = friction;
				entity->m_rigidbody->m_angularDrag = angularDrag;
				entity->m_rigidbody->m_linearDrag = linearDrag;
				entity->m_rigidbody->m_momentOfInertia = moment;
				entity->m_rigidbody->m_material.restitution = restitution;
			}
			break;
			default:
			{
				ERROR_AND_DIE("The rigidbody shape in XML file is unknown");
			}
			break;
			}

			//Read Collider data 
			elem = elem->NextSiblingElement("Transform");

			Vec2 position = ParseXmlAttribute(*elem, "Position", Vec2::ZERO);
			float rotation = ParseXmlAttribute(*elem, "Rotation", 0.f);
			Vec2 scale = ParseXmlAttribute(*elem, "Scale", Vec2::ZERO);

			entity->m_transform.m_position = position;
			entity->m_transform.m_rotation = rotation;
			entity->m_transform.m_scale = scale;

			m_allGeometry.push_back(entity);

			//Proceed to next sibling
			geometry = geometry->NextSiblingElement();
		}
	}
}

Implementing triggers

Adding triggers to the game was as simple as using the existing rigid bodies but simplifying them to only detect collision but provide no resolution steps. This was a very simple feature to add but provided a lot of added usage from the system. By creating event callbacks that can be bound to the collision events, the triggers could be used in the system for a variety of gameplay behaviors.

Below is the implementation I followed to use triggers:

class Trigger2D
{
public:
	Trigger2D(PhysicsSystem* physicsSystem, eSimulationType simType);
	~Trigger2D();

	//Update
	void									Update(uint frameNumber);
	void									UpdateTouchesArray(Rigidbody2D* rb, uint frameNumber);

	//Render
	void									DebugRender(RenderContext* renderContext, const Rgba& color) const;

	//Mutators
	void									SetSimulationMode(eSimulationType simulationType);
	void									SetCollider(Collider2D* collider);
	void									SetTransform(const Transform2& transform);
	void									SetOnEnterEvent(const std::string& enterEventString);
	void									SetOnExitEvent(const std::string& exitEventString);

	//Accessors
	Vec2									GetPosition() const;
	eSimulationType							GetSimulationType();
	
public:
	PhysicsSystem*							m_system = nullptr; 			// system this trigger belongs to; 
	
	Transform2								m_transform;					// trigger transform
	Collider2D*								m_collider = nullptr;			// my shape; (could eventually be made a set)

	std::string								m_onEnterEvent = "";
	std::string								m_onExitEvent = "";

private:
	eSimulationType							m_simulationType = TYPE_UNKOWN;
	std::vector<TriggerTouch2D*>			m_touches;
};
class TriggerTouch2D
{
public:
	explicit TriggerTouch2D(Collider2D* collider, uint entryFrame);
	~TriggerTouch2D();

	inline void				SetCurrentFrame(uint frameNumber) { m_currentFrame = frameNumber; }

	inline Collider2D*		GetCollider() { return m_collider; }
	inline uint				GetCurrentFrame() { return m_currentFrame; }
	inline uint				GetEntryFrame() { return m_entryFrame; }

	void					Destroy();

private:
	Collider2D*		m_collider;
	uint			m_entryFrame;
	uint			m_currentFrame;
};

The results

Finally using all these features, we are able to create a complete 2D physics system that utilizes colliders to simulate rigid bodies and triggers.

It ends up looking something like this:

Retrospectives

Things that went well 🙂

  • Architecture for the project was laid out in a way adding features would be very easy.
  • Setting up mouse controls improved the UX greatly
  • The design choice to separate rigid body and trigger as opposed to repurposing a rigid body to add trigger behavior.
  • Using pillboxes that can be handled as either an OBB rigid body or capsule-shaped rigid body.

Things that went wrong 🙁

  • Lacked the time to create a game out of this.
  • Saving and loading objects could have been done easier if binary file storage was used. That way it could have been as simple as loading an array of rigid bodies rather than parsing their information via XML.

Things I would do differently if I had a second attempt

  • Definitely would perform scene save and load using binary files to simplify that process.
  • I would like to incorporate simple 2D joint systems like support for a spring joint and fixed joint.