Zap Engine

May 24, 2026 - Sebastian Himmer
UniversityProjectsEngine Programming
1422 words 7 min read
ZapEditor
8 weeks
👥 7 programmers
💻 Windows, PS5
⚒️ C++, AngelScript

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.

EngineArchitecture

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.

HotReloadingInfo

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

  1. Attempt: Create a temporary srph::Engine, compile scripts into it. If compilation fails, abort and notify.
  2. Serialize: Walk all ScriptComponent entities, serialize their state via the reflection system.
  3. Reinitialize: Shut down the real scripting engine, reinitialize, reload scripts.
  4. 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.

CompilationError

Startup with compilation errors

Editor Tooling

Content Browser

ContentBrowser

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)
  • SelectionAction
  • ComponentChangeAction (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.

UndoRedoPerformance

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.

UnsavedChanges

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.

BUasLogo

This project has been created at BUas