Description
Zap is a custom C++ game engine built during the second year at BUas by a team of 7 programmers.
My responsibilities covered the full scripting layer (AngelScript integration via my Seraph library, hot-reloading, reflection, serialization), editor tooling (content browser, undo/redo, spline tooling), Perforce integration, CI/CD, and crash reporting.
Galaxy Dog - demo project
Zap Editor
Engine Architecture
Zap is split into three build targets: zap-core (engine library), zap-editor (editor executable), and zap-player (shipping runtime). The core uses EnTT for ECS, Jolt for physics, FMOD for audio, and GLM for math. The editor is built entirely in ImGui.
Engine Architecture
AngelScript is the scripting language, integrated through Seraph - a wrapper library I built during the
previous block. All C++ APIs exposed to scripts go through a central ScriptGlue layer that handles type registration,
binding, and cross-platform compatibility.
Scripting
AngelScript scripting is the backbone of all gameplay code in Zap. Scripts implement an IComponent interface that
exposes lifecycle methods. Properties marked with [Property] appear in the editor
inspector and are serialized into scene files. A [Reflect] attribute on data classes enables recursive serialization
down to primitive types.
[Reflect] class BoostSettings {
[Property] float boostMaxDuration = 3.0f;
[Property] float boostSpeed = 40.0f;
}
class PlayerController : IComponent {
Entity self;
void SetSelf(Entity _self) { self = _self; }
[Property] float moveSpeed = 10.0f;
[Property] BoostSettings boostSettings;
void Start() { }
void Update(float deltaTime) { }
}Hot-reloading allows script changes to apply in under 100ms without restarting the engine. The system serializes all script state, validates the new code in a temporary engine instance, then reinitializes and deserializes if compilation succeeds.
Hot-reload pipeline
Script hot-reloading in action
I also implemented ZapEvent<T>, a generic event dispatcher usable from AngelScript. It was used all over the game code
to simplify communication between systems and decouple gameplay logic.
ZapEvent<string> OnMarkerHit;
OnMarkerHit.AddListener(ZapEvent<string>::CallbackType(OnSplineMarkerHit));
OnMarkerHit.Invoke("marker_1");Technical Details: Scripting
Templated GetComponent function
Instead of registering per-type accessors (GetTransform(), GetPhysicsBody()), I implemented a single
GetComponent<T>() using AngelScript's generic calling convention and entt::meta for runtime type resolution:
void GetComponent(asIScriptGeneric* gen)
{
auto& scene = Engine.ECS().Scene();
auto* engine = Engine.Scripting().GetNativeEngine();
auto id = gen->GetFunction()->GetSubTypeId();
ScriptEntity* entity = static_cast<ScriptEntity*>(gen->GetObject());
asITypeInfo* typeInfo = engine->GetTypeInfoById(id);
const entt::id_type hash = entt::hashed_string::value(typeInfo->GetName());
const entt::meta_type type = entt::resolve(hash);
void* component = scene.GetComponentTypeVoidPtr(entity->id, type);
gen->SetReturnObject(component);
}This converts a string typename from AngelScript into a C++ entt::meta_type, then retrieves the raw component pointer.
Without runtime reflection system this would not be possible.
This saved us from a lot of boilerplate code and made the scripting API much cleaner and more flexible.
ZapEvent Details
Registering a template type with a funcdef callback in AngelScript required registering
ZapEvent<T>::CallbackType(const T&in if_handle_then_const arg) as a nested funcdef. I have not used funcdefs in
AngelScript before, so this was a lot of trial and error.
Internally, each callback stores either a free function or a delegate (object pointer + type info + function pointer).
struct Callback
{
asIScriptFunction* func = nullptr;
void* callbackObject = nullptr;
asITypeInfo* callbackObjectType = nullptr;
uint32_t id;
};Reflection and Serialization
The [Property] attribute marks fields for inspector visibility and serialization. The [Reflect] attribute on a class
tells the serializer to recursively break it down into primitives rather than requiring a C++ serialization case per
type.
Serialized output:
{
"fields": [
{
"name": "data",
"value": [
{
"name": "a",
"value": 0
},
{
"name": "b",
"value": 0
}
]
}
],
"type": "MyComponent"
}Hot-Reload Pipeline
- Attempt: Create a temporary
srph::Engine, compile scripts into it. If compilation fails, abort and notify. - Serialize: Walk all
ScriptComponententities, serialize their state via the reflection system. - Reinitialize: Shut down the real scripting engine, reinitialize, reload scripts.
- Deserialize: Restore serialized state onto the new script instances.
One issue that I found: if the engine starts with compilation errors, hot-reloading cannot recover. I did try a few approaches to allow hot-reloading to fix compile errors on startup, but it was too complex to implement in the available time. The editor simply shows an error message and prompts the user to fix the code and hot-reload.
Startup with compilation errors
Editor Tooling
Content Browser
Content browser with custom icons and previews
The content browser provides a visual representation of all indexed resources in the project. I designed custom icons in Figma for each resource type and render texture previews directly for image assets. The layout uses a card-style design built in ImGui.
It allows for directory navigation with breadcrumbs, drag-and-drop importing, and double-click actions - scripts open in VSCode with the cursor at the correct file, scenes load into the editor.
Scripts in the content browser
Undo/Redo
The undo/redo system uses the command pattern. Every tracked operation is an IAction subclass that captures
before/after state and can reverse or reapply itself.
I implemented multiple actions:
EntityAction(create/delete)SelectionActionComponentChangeAction(serializes component state before and after)ParentChangeAction
These were all the actions needed to cover every editor operation, from shortcuts to drag-and-drop.
The main challenge was ImGui's continuous input elements (drag floats, sliders) which fire every frame during
interaction. I use ImGui::IsItemActivated() and ImGui::IsItemDeactivatedAfterEdit() to capture only the start and
end state:
void Inspector::TrackEdit(const std::string& message)
{
if (ImGui::IsItemActivated() && !m_pendingAction)
StartPendingAction();
if (ImGui::IsItemDeactivatedAfterEdit() && m_pendingAction)
{
m_pendingAction->Commit(message);
m_pendingAction.reset();
}
}All editor operations (shortcuts, menus, drag-and-drop) go through a centralized editor_actions.cpp so every code
path is tracked.
The undo/redo system is very performant, since actions only serialize the relevant component or entity rather than the entire scene. This allows for near-instant undos even in large scenes with hundreds of entities.
Undo/redo performance
The undo system also drives unsaved change detection. Any committed action (except selection) marks the current scene dirty, triggering a save/discard popup when switching scenes or closing the editor.
Unsaved changes popup
Perforce Integration
Perforce integration in the editor
I built a VersionControl class that wraps the p4 CLI to provide checkout, add, submit, and status queries directly
from the editor. The alternative was the official P4 API, but it was too complex to integrate in the available time.
The integration hooks into multiple editor workflows: right-click actions in the content browser, automatic
mark for add on resource creation, and warnings when opening scenes checked out by others or saving out-of-date files.
Spline Editor
Our demo game relied on splines for player movement and event timing. I implemented editor tooling for spline creation.
Spline gizmos and markers
The editor allows selecting and moving segment start/end points and control points. Spline markers (named points at set distances along the spline) can be placed in the editor and listened to from scripts:
array<string>@ markers = spline.EvaluateMarkers(lastDistance, currentDistance);
for (uint i = 0; i < markers.length(); i++) {
Log::Info(format("Hit {}", markers[i]));
}CI/CD
When we moved into the production phase for our game, I set up a CI/CD pipeline that allowed us to automatically build and distribute editor and player builds on to the rest of the team. Engine's development continued on GitHub, and I created a workflow that packaged the editor and player and uploaded them to Perforce.
This saved a lot of manual work for the team and assured that we don't make mistakes in the packaging process. Whenever a new build was needed, I just had to merge the latest code and the workflow would take care of the rest.
Closing remarks
I am very grateful for the opportunity to work on this project with other amazing people. It was a huge learning experience and a big insight into how complicated and time-consuming game engine development can be, especially when building tools and pipelines on top of the core engine.
This project has been created at BUas