Type | Group projects at The Game Assembly. |
Engine | Tonic Engine – Our group's C++ engine built from scratch. |
Timeframe | 7 months (on average 4h/week), from September 2024 |
Intent | Enable the group easily develop games that use collision logic and rigid body physics simulation. |
Preview
Physics gameplay in project 7 at TGA.Table of Contents
- Introduction
- Project 5, Pre Production
- Project 6, Top Down ARPG
- Project 7, First-Person Shooter
- Credits & inspiration
Introduction
As second year students at The Game Assembly, we have the option of building our own game engine in which we develop the 5th, 6th, 7th and 8th group projects.
There is a requirement for a physics library in project 7, but I reasoned that having a physics library integrated already from the start would make sense.
The main reason was that we could use the library as a general collision system as well.
I wanted to work with JoltPhysics in particular because I believed it would give great insight and flexibility to work with an open source solution.
I took on responsibility for implementation. Here's how it went.
Project 5, Pre Production
The goal of the first project was to get our game engine development ready for project 2 where we've actually make a game.
The requirements that I identified were:
- Integrate the JoltPhysics library in our engine.
- Create system that bridges the gap between our engine and JoltPhysics.
- Render colliders.
- Import collider component data from our editor to our engine.
- Create a simple interface for other programmers.
- Enable tying custom logic to collision events.
Make it Work
After getting confirmation through documentation that the library would meet our needs, I went ahead and downloaded the project from git and built it using CMake. I then went through the learning process of configuring the project settings to be able to integrate with our game engine, built the library files needed, and added them to our build.
You simulate the physics of JoltPhyics by calling an update function on the PhysicsSystem class. To get information about what happens is simulation, I made derived classes inheriting from ContactListener to tie in custom collision logic, and
DebugRenderer to render colliders. I also had to implement the collision layer logic.
The default JoltPhysics ragdoll getting crushed by spheres in Tonic Engine
The next step was bridging the transforms in the Jolt with the transforms in our component system. In Tonic, our entities have Entity IDs, while in Jolt the bodies have Body IDs. I made an unordered_map to tie the two together and constructed this method of synchronization:
// New bodies are added to the simulation.
// Transforms that have been changed outside Jolt are updated.
{
auto query = g_World.Query<Collider>();
for (auto c : query)
{
Collider* coll = c.GetComponent<Collider>();
coll->SetActive(coll->GetActiveStateFlag());
coll->PrePhysicsUpdate();
}
}
// The simulation is stepped forward
myPhysicsSystem->Update(aTimeDelta, myCollisionSteps, myTempAllocator, myJobSystem);
// The transforms of dynamic and kinematic rigid bodies that changed in the simulation
// are written back to our component system.
{
query = g_World.Query<Collider, Rigidbody>();
for (auto c : query)
{
c.GetComponent<Collider>()->PostPhysicsUpdate();
}
}
Workflow
Tonic Engine uses Unity as an editor. This means that we build the scenes in Unity, export the scene data to JSON files, and use the files to initialize scenes in Tonic Engine. For the JoltPhysics implementation this means I had to configure how and what data was exported from Unity and provide documentation to the team.
For the other programmers, I wanted to provide an interface to the Jolt that didn't require a deep understanding of the system. Since we use ECS in our Engine, I made a Collider Component to act as an interface for interacting with Jolt. If a programmer wanted to add a collider to an entity they, they can simply add the component and initialize it a shape, motion type, layer and whether it should have a rigid body.
For custom logic, I enabled attaching a std::functinon for contact added, persited and removed.
Project 6, Top Down ARPG
For the project 6 there was a requirement to have some kind of destructibles in the game. We wanted to simulate this with physics.
Destructibles
After discussing with one of our Procedural Artists Fredrik, who'd be doing the graphics for the destructibles, we came up with an approach. We would have the destructible consist of several meshes put together to look whole. Then when activated the individual pieces would start simulating and thereby shattering.
From a technical perspective I designed around a parent entity with a collider that acts as a trigger. Children would be the shattered pieces with inactivated dynamic colliders. The parent would have custom contact added logic that activated the bodies of the children, destroying the image of a whole mesh.
void OnContactAddedDestructible(Collider& aCollider, Collider& aOther)
{
for(auto& child : g_World.GetEntity(aCollider.myEntityID)->GetChildren())
{
child.GetComponent<Collider>()->MarkForActivate(true);
}
aCollider.MarkForActivate(false);
}
While the idea was solid, the synchronization between our component system and Jolt wasn't. I had failed to properly consider scale, childing and the center variable that exists in the Unity collider components. These considerations were added to the import- and simulation steps.
We also realized that we’d would need mesh colliders to properly represent the shattered pieces. Using the mesh collider component in Unity, I collected the vertex and index data during the export and used these to construct mesh colliders in Jolt.
Mesh colliders being activated and sent flying by an enemy attack, project 6 at TGA
Project 7, First-Person Shooter
For project 3, we were heavily inspired by Anger Foot and used it as a reference game.
This put two implementation requirement on the physics- & collision system.
- Enemies should to ragdoll when they die. This is crucial feedback in Anger Foot.
- Collisions impacts need to be measured to determine if they deal damage.
Ragdolls
In a previous individual tools assignment at school, I made export- and import logic for a ragdoll made with Unity's Ragdoll Wizard. Most of the work centered around converting the Unity data (Colliders and CharacterJoints) into RagdollSetting that would be used to create ragdolls in the physics simulation.
Although I had some issues reinterpreting CharacterJoints from Unity to the RagdollSetting in Jolt, I eventually got a ragdoll automatically built in Unity, exported, imported to Tonic, and simulating.
Starting Tonic Engine with a Unity scene containing the automatically built ragdoll. This version had some joint issues that were solved later.
For project 7, the main thing left was using the ragdoll simulation to update the pose of a skeletal mesh. Luckily I had some experience doing this from my rope simulation project.
Some added complexity was that not all bones of the skeletal mesh had a body driving their new transform. To tackle this, I saved the transforms body-less bones in the space of their closest parent bone with a body. This allowed me to keep their relative positions to their parent body as the ragdoll simulates. As I had communicated to our artists that all bones need to be childed to the hip bone, the hip bone could start driving the world position of the entity as the ragdoll activates. The initial position of the bodies in the ragdoll are set from the bones of the animation that was previously playing, along with with lerping between the poses in order for body-less bone transforms to transition smoothly.
Impact
To measure the forces collisions exert on the bodies I had to use the impact normals, relative velocities and masses. Since we’re using rigidbodies, time taken for the collision does not need to be considered. I also used the same system to keep track of whether the player is grounded. Based on the world normal(s) of the contact point(s) on the object(s) in contact with the player.
Credits & inspiration
Graphics for game projects
Karl Elf
Axel Hallström
William Larskog
Ludvig Smeding
Alexander Svanberg
Fredrik Westerlund
Ivar Zetterberg
Tonic Engine implementation built with William Arnberg’s ECS.