CrackEngine is a custom C++ game engine developed over six months by a small TGA student team, built in parallel with several student game projects. It features a full rendering pipeline built on DirectX 11, integrated physics, flexible gameplay systems, and editor support. The engine was iteratively improved project by project, enabling rapid prototyping, reliable reuse of core systems, and efficient iteration on gameplay features.
6 Months
3-6
C++
The GameObject–Component System is the core of CrackEngine’s architecture, designed and implemented entirely by me. Inspired by Unity's component model, it allows gameplay elements to be composed flexibly: each scene consists of game objects, which can have multiple components and nested child objects. This modular system enables rapid iteration, reusable behaviors, and visual manipulation of objects directly in the editor. Components encapsulate functionality such as rendering, physics, input handling, and AI, and can be reused across different objects and projects.
This snippet demonstrates the core of the GameObject–Component System in CrackEngine. Components can be added to a GameObject dynamically using templates, and the system supports querying both single and multiple components of a given type. This design allows gameplay elements to be composed flexibly, enabling modular behaviors such as rendering, physics, AI, and input handling. The template and dynamic_cast usage ensures type safety while keeping the system generic and reusable across projects.
1 // Adds a component of type T to this GameObject2 template<typename T = Component>3 void AddComponent(T* aComponent)4 {5 myComponents.push_back(aComponent);6 aComponent->SetOwner(this);7 }89 // Retrieves the first component of type T10 template<typename T = Component>11 T* GetComponent()12 {13 for (auto& c : myComponents)14 {15 T* val = dynamic_cast<T*>(c);16 if (val == nullptr) continue;17 return val;18 }19 return nullptr;20 }2122 // Retrieves all components of type T23 template<typename T = Component>24 std::vector<T*> GetComponents()25 {26 std::vector<T*> components;27 for (auto& c : myComponents)28 {29 T* val = dynamic_cast<T*>(c);30 if (val == nullptr) continue;31 components.push_back(val);32 }33 return components;34 }
I built the editor backend for CrackEngine to support efficient development and iteration across all projects using the engine. It allows inspection and modification of GameObjects, component composition, transforms, hierarchies, and runtime states. This editor was central to the development workflow, enabling rapid iteration on enemies, player behavior, and interactive objects. The system was designed to integrate tightly with engine math, component management, and scene hierarchy.
The editor supports robust drag-and-drop workflows, both internally and from the operating system. Assets such as models and textures can be dragged directly from Windows Explorer into the editor, where they are imported and instantiated using the engine’s asset and component systems. The hierarchy view fully supports parent–child relationships. GameObjects can be reparented via drag-and-drop, with local and world transforms recalculated automatically to preserve spatial consistency. This behavior is driven by the engine’s transform and math systems, ensuring correct position, rotation, and scale propagation across complex hierarchies rather than relying on editor-specific overrides.
Components can be added, inspected, and modified directly in the editor. The system supports dynamic templates, allowing developers to attach multiple component types to GameObjects, query them individually or in groups, and edit properties at runtime. This made testing and iterating on game mechanics extremely efficient.
The editor integrates with the engine's save/load system to persist scenes, object transforms, and component properties. All data is serialized to JSON, including hierarchical relationships, component types, and values, allowing scenes to be restored exactly as they were edited in the editor.
I implemented the PhysX foundation, scene management, and simulation loop for CrackEngine. This includes fixed-timestep simulation, robust collision filtering, and routing collision events to gameplay components. The system was designed to remain stable under fluctuating frame rates while giving gameplay systems precise control over physics interactions.
The physics simulation runs with a fixed timestep, capped at 60 updates per second, while still using accumulated frame delta time. This ensures consistent physics behavior even when the game experiences frame drops or stalls. The system also supports skipping a simulation frame when needed for special gameplay events, such as respawning or scene transitions.
The PhysX scene is stepped using an accumulator-based fixed timestep. After simulation, collision data is processed and forwarded to gameplay components unless explicitly skipped for special cases.
1 void PhysXManager::Update(float deltaTime)2 {3 if (!myPxScene)4 return;56 myAccumulator += deltaTime;78 if (myAccumulator < myTimeStep)9 return;1011 myAccumulator -= myTimeStep;1213 myPxScene->simulate(myTimeStep);14 myPxScene->fetchResults(true);1516 // Can skip giving contact data (used for respawn)17 if (skipFrame)18 {19 std::queue<PairData> empty;20 std::swap(myPairData, empty);21 skipFrame = false;22 return;23 }2425 afterContact();26 }
Collision handling is performed after the physics simulation step. Contact pairs collected during simulation are filtered using PhysX filter data to determine which interactions should generate gameplay responses. This allows precise control over collision behavior while keeping gameplay systems decoupled from the physics layer. Filtered collisions are then routed to the appropriate components attached to the involved GameObjects, allowing different component types to respond to physics events without hard dependencies or tight coupling between systems.
1 void PhysXManager::afterContact()2 {3 while (!myPairData.empty())4 {5 const physx::PxContactPair* pairs = myPairData.front().myPairs;6 physx::PxU32 nbPairs = myPairData.front().nbPairs;78 for (int i = 0; i < (int)nbPairs; i++)9 {10 physx::PxFilterData filterData0 = pairs[i].shapes[0]->getSimulationFilterData();11 physx::PxFilterData filterData1 = pairs[i].shapes[1]->getSimulationFilterData();1213 if (filterData0.word0 & filterData1.word1)14 {15 static_cast<PhysicsComponent*>(pairs[i].shapes[1]->userData)16 ->CallCollisionFunction(pairs[i].shapes[0]);17 }1819 if (filterData1.word0 & filterData0.word1)20 {21 static_cast<PhysicsComponent*>(pairs[i].shapes[0]->userData)22 ->CallCollisionFunction(pairs[i].shapes[1]);23 }24 }25 myPairData.pop();26 }27 }
To support development and verification of the physics system, the engine streams live simulation data to the PhysX Visual Debugger in debug builds. This provided deep visibility into the physics scene, making it possible to inspect simulation state, collision filtering, and actor behavior directly within PhysX’s tooling. In addition to simulation and collision handling, the engine exposes filtered raycasts, sweeps, and overlap queries at the engine level. These are thin wrappers around PhysX functionality and use the same filtering setup as the simulation, ensuring consistent and predictable behavior across all physics interactions.
I implemented a global, static logging system that allows any part of the codebase to call `Log(std::string)` with minimal overhead. Log entries can be filtered by type, and in release builds all calls are replaced by empty functions, ensuring zero performance impact.
The engine includes a 3D LineDrawer to visualize debug information such as lines and bounding boxes. All rendering, including vertex buffers and shaders, was implemented from scratch. Lines are drawn at the end of the frame so they appear above other objects. To further improve this system these lines could quite easily be batched but I never did this as it seemed a waste of time for our small engine and it only being used for debugging purposes
1 void LineDrawer::Render(DebugLine& aLine, ID3D11DeviceContext* someContext)2 {3 UpdateVertexes(aLine);4 unsigned int stride = sizeof(SimpleVertex);5 unsigned int offset = 0;6 someContext->IASetVertexBuffers(0, 1, myVertexBuffer.GetAddressOf(), &stride, &offset);78 someContext->Draw(2, 0);9 }1011 void LineDrawer::DrawAllLines()12 {13 #ifdef _DEBUG14 if (myActive == false)15 return;1617 auto* instance = GraphicsEngine::GetInstance();18 auto* context = instance->GetContext();19 context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_LINELIST);20 context->IASetInputLayout(myInputLayout.Get());2122 context->VSSetShader(myVertexShader.Get(), nullptr, 0);23 context->PSSetShader(myPixelShader.Get(), nullptr, 0);2425 for (int i = 0; i < myLines.size(); ++i)26 {27 Render(myLines[i], context);28 }29 myLines.clear();30 #endif31 }
The video shows the LineDrawer streaming debug lines live from the engine in a debug build.
The engine includes a lightweight global event system based on the observer pattern. Any system can subscribe to specific message types and receive events without direct dependencies on the sender. Observers automatically register on creation and unregister on destruction, keeping lifetime management simple and avoiding dangling references. Any number of observers can listen to the same event type, allowing systems to react independently to the same input or state change. A typical use case is input handling. When a key is pressed, a message with Type: InputKeyPressed is sent. Any system currently interested in input receives the event and can inspect the payload to determine whether it should react, for example by checking which key was pressed. Messages carry both a type and a generic payload using std::any. This allows different systems to interpret the same event differently without coupling the event system to specific gameplay logic. The system could be extended with state-based filtering to only dispatch events to observers active in a given game state. Since the event system was used across both engine and gameplay code, this extra complexity was not necessary for the scope of the projects built with it.
1 struct Message2 {3 std::any myData;4 Type myType;56 Message(Type aType, std::any aData = nullptr)7 : myType(aType), myData(aData) {}8 };910 EventSystem::EventSystem()11 {12 for (size_t i = 0; i < (int)Message::Type::Count; i++)13 {14 myObservers.push_back(std::vector<Observer*>());15 }16 }1718 void EventSystem::SendMsg(const Message& aMessage)19 {20 for (int i = 0; i < myObservers[(int)aMessage.myType].size(); i++)21 {22 myObservers[(int)aMessage.myType][i]->Receive(aMessage);23 }24 }2526 void EventSystem::Subscribe(Observer* anObserver, const Message::Type aMessageType)27 {28 myObservers[(int)aMessageType].push_back(anObserver);29 }