Files
Modularity/docs/Scripting.md

14 KiB
Raw Blame History

title, description
title description
C++ Scripting Hot-compiled native C++ scripts, per-object state, ImGui inspectors, and runtime/editor hooks.

C++ Scripting

Scripts in Modularity are native C++ code compiled into shared libraries and loaded at runtime. They run per scene object and can optionally draw ImGui UI in the inspector and in custom editor windows.

Notes up front:

  • Scripts are not sandboxed. They can crash the editor/game if they dereference bad pointers or do unsafe work.
  • Always null-check ctx.object (objects can be deleted, disabled, or scripts can be detached).

Table of contents

Quickstart

  1. Create a script file under Scripts/ (e.g. Scripts/MyScript.cpp).
  2. Select an object in the scene.
  3. In the Inspector, add/enable a script component and set its path:
    • In the Scripts section, set Path OR click Use Selection after selecting the file in the File Browser.
  4. Compile the script:
    • In the File Browser, right-click the script file and choose Compile Script, or
    • In the Inspectors script component menu, choose Compile.
  5. Implement a tick hook (TickUpdate) and observe behavior in play mode.

Scripts.modu

Each project has a Scripts.modu file (auto-created if missing). It controls compilation.

Common keys:

  • scriptsDir - where script source files live (default: Scripts)
  • outDir - where compiled binaries go (default: Cache/ScriptBin)
  • includeDir=... - add include directories (repeatable)
  • define=... - add preprocessor defines (repeatable)
  • linux.linkLib=... - comma-separated link libs/flags for Linux (e.g. dl,pthread)
  • win.linkLib=... - comma-separated link libs for Windows (e.g. User32,Advapi32)
  • cppStandard - C++ standard (e.g. c++20)

Example:

scriptsDir=Scripts
outDir=Cache/ScriptBin
includeDir=../src
includeDir=../include
cppStandard=c++20
linux.linkLib=dl,pthread
win.linkLib=User32,Advapi32

How compilation works

Modularity compiles scripts into shared libraries and loads them by symbol name.

  • Source lives under Scripts/.
  • Output binaries are written to Cache/ScriptBin/.
  • Binaries are platform-specific:
    • Windows: .dll
    • Linux: .so

Wrapper generation (important)

To reduce boilerplate, Modularity auto-generates a wrapper for these hook names if it detects them in your script:

  • Begin
  • TickUpdate
  • Update
  • Spec
  • TestEditor

That wrapper exports Script_Begin, Script_TickUpdate, etc. This means you can usually write plain functions like:

void TickUpdate(ScriptContext& ctx, float dt) {
    (void)dt;
    if (!ctx.object) return;
}

However:

  • Script_OnInspector is not wrapper-generated. If you want inspector UI, you must export it explicitly with extern "C".
  • Scripted editor windows (RenderEditorWindow, ExitRenderEditorWindow) are also not wrapper-generated; export them explicitly with extern "C".

Lifecycle hooks

All hooks are optional. If a hook is missing, it is simply not called.

Hook list:

  • Script_OnInspector(ScriptContext&) (manual export required)
  • Script_Begin(ScriptContext&, float deltaTime) (wrapper-generated from Begin)
  • Script_TickUpdate(ScriptContext&, float deltaTime) (wrapper-generated from TickUpdate)
  • Script_Update(ScriptContext&, float deltaTime) (wrapper-generated from Update, used only if TickUpdate missing)
  • Script_Spec(ScriptContext&, float deltaTime) (wrapper-generated from Spec)
  • Script_TestEditor(ScriptContext&, float deltaTime) (wrapper-generated from TestEditor)

Runtime notes:

  • Begin runs once per object instance (per script component instance).
  • TickUpdate runs every frame (preferred).
  • Update runs only if TickUpdate is not exported.
  • Spec/TestEditor run every frame only while their global toggles are enabled (main menu -> Scripts).

ScriptContext

ScriptContext is passed into most hooks and provides access to the engine, the owning object, and helper APIs.

Fields:

  • engine (Engine*) - engine pointer
  • object (SceneObject*) - owning object pointer (may be null)
  • script (ScriptComponent*) - owning script component (settings storage)

Object lookup

  • FindObjectByName(const std::string&)
  • FindObjectById(int)

Transform helpers

  • SetPosition(const glm::vec3&)
  • SetRotation(const glm::vec3&) (degrees)
  • SetScale(const glm::vec3&)
  • SetPosition2D(const glm::vec2&) (UI position in pixels)

UI helpers (Buttons/Sliders)

  • IsUIButtonPressed()
  • IsUIInteractable(), SetUIInteractable(bool)
  • GetUISliderValue(), SetUISliderValue(float)
  • SetUISliderRange(float min, float max)
  • SetUILabel(const std::string&), SetUIColor(const glm::vec4&)
  • GetUITextScale(), SetUITextScale(float)
  • SetUISliderStyle(UISliderStyle)
  • SetUIButtonStyle(UIButtonStyle)
  • SetUIStylePreset(const std::string&)
  • RegisterUIStylePreset(const std::string& name, const ImGuiStyle& style, bool replace = false)

Rigidbody helpers (3D)

  • HasRigidbody()
  • SetRigidbodyVelocity(const glm::vec3&), GetRigidbodyVelocity(glm::vec3& out)
  • SetRigidbodyAngularVelocity(const glm::vec3&), GetRigidbodyAngularVelocity(glm::vec3& out)
  • AddRigidbodyForce(const glm::vec3&), AddRigidbodyImpulse(const glm::vec3&)
  • AddRigidbodyTorque(const glm::vec3&), AddRigidbodyAngularImpulse(const glm::vec3&)
  • SetRigidbodyRotation(const glm::vec3& rotDeg)
  • TeleportRigidbody(const glm::vec3& pos, const glm::vec3& rotDeg)

Rigidbody2D helpers (UI/canvas only)

  • HasRigidbody2D()
  • SetRigidbody2DVelocity(const glm::vec2&), GetRigidbody2DVelocity(glm::vec2& out)

Audio helpers

  • HasAudioSource()
  • PlayAudio(), StopAudio()
  • SetAudioLoop(bool)
  • SetAudioVolume(float)
  • SetAudioClip(const std::string& path)

Settings + utility

  • GetSetting(key, fallback), SetSetting(key, value)
  • GetSettingBool(key, fallback), SetSettingBool(key, value)
  • GetSettingVec3(key, fallback), SetSettingVec3(key, value)
  • AutoSetting(key, bool|glm::vec3|buffer), SaveAutoSettings()
  • AddConsoleMessage(text, type)
  • MarkDirty()

ImGui in scripts

Modularity uses Dear ImGui for editor UI. Scripts can draw ImGui in two places:

Inspector UI (per object)

Export Script_OnInspector(ScriptContext&):

#include "ScriptRuntime.h"
#include "ThirdParty/imgui/imgui.h"

static bool autoRotate = false;

extern "C" void Script_OnInspector(ScriptContext& ctx) {
    ImGui::Checkbox("Auto Rotate", &autoRotate);
    ctx.MarkDirty();
}

Tip: Script_OnInspector must be exported exactly with extern "C" (it is not wrapper-generated). Important: Do not call ImGui functions (e.g., ImGui::Text) from TickUpdate or other runtime hooks. Those run before the ImGui frame is active and outside any window, which can crash.

Scripted editor windows (custom tabs)

See Scripted editor windows.

Per-script settings

Each ScriptComponent owns serialized key/value strings (ctx.script->settings). Use them to persist state with the scene.

Direct settings

void TickUpdate(ScriptContext& ctx, float) {
    if (!ctx.script) return;
    ctx.SetSetting("mode", "hard");
    ctx.MarkDirty();
}

AutoSetting binds a variable to a key and loads/saves automatically when you call SaveAutoSettings().

extern "C" void Script_OnInspector(ScriptContext& ctx) {
    static bool enabled = false;
    ctx.AutoSetting("enabled", enabled);
    ImGui::Checkbox("Enabled", &enabled);
    ctx.SaveAutoSettings();
}

UI scripting

UI elements are scene objects (Create -> 2D/UI). They render in the Game Viewport overlay.

Button clicks

IsUIButtonPressed() is true only on the frame the click happens.

void TickUpdate(ScriptContext& ctx, float) {
    if (ctx.IsUIButtonPressed()) {
        ctx.AddConsoleMessage("Button clicked!");
    }
}

Sliders as meters (health/ammo)

Set Interactable to false to make a slider read-only.

void TickUpdate(ScriptContext& ctx, float) {
    ctx.SetUIInteractable(false);
    ctx.SetUISliderStyle(UISliderStyle::Fill);
    ctx.SetUISliderRange(0.0f, 100.0f);
    ctx.SetUISliderValue(health);
}

Style presets

You can register custom ImGui style presets in code and then select them per UI element in the Inspector.

void Begin(ScriptContext& ctx, float) {
    ImGuiStyle style = ImGui::GetStyle();
    style.Colors[ImGuiCol_Button] = ImVec4(0.20f, 0.50f, 0.90f, 1.00f);
    style.Colors[ImGuiCol_ButtonHovered] = ImVec4(0.25f, 0.60f, 1.00f, 1.00f);
    ctx.RegisterUIStylePreset("Ocean", style, true);
}

Then select UI -> Style Preset on a button or slider.

Finding other UI objects

void TickUpdate(ScriptContext& ctx, float) {
    if (SceneObject* other = ctx.FindObjectByName("UI Button 3")) {
        if (other->type == ObjectType::UIButton && other->ui.buttonPressed) {
            ctx.AddConsoleMessage("Other button clicked!");
        }
    }
}

IEnum tasks

Modularity provides lightweight, opt-in “tasks” you can start/stop per script component instance.

Important: In this version, an IEnum task is just a function with signature void(ScriptContext&, float) that is called every frame while its registered.

Start/stop macros:

  • IEnum_Start(fn) / IEnum_Stop(fn) / IEnum_Ensure(fn)

Example (toggle rotation without cluttering TickUpdate):

static bool autoRotate = false;
static glm::vec3 speed = {0, 45, 0};

static void RotateTask(ScriptContext& ctx, float dt) {
    if (!ctx.object) return;
    ctx.SetRotation(ctx.object->rotation + speed * dt);
}

extern "C" void Script_OnInspector(ScriptContext& ctx) {
    ImGui::Checkbox("Auto Rotate", &autoRotate);
    if (autoRotate) IEnum_Ensure(RotateTask);
    else            IEnum_Stop(RotateTask);
    ctx.MarkDirty();
}

Notes:

  • Tasks are stored per ScriptComponent instance.
  • Dont spam logs every frame inside a task; use “warn once” patterns.

Logging

Use ctx.AddConsoleMessage(text, type) to write to the editor console.

Typical types:

  • ConsoleMessageType::Info
  • ConsoleMessageType::Success
  • ConsoleMessageType::Warning
  • ConsoleMessageType::Error

Warn-once pattern:

static bool warned = false;
if (!warned) {
    ctx.AddConsoleMessage("[MyScript] Something looks off", ConsoleMessageType::Warning);
    warned = true;
}

Scripted editor windows

Scripts can expose ImGui-powered editor tabs by exporting:

  • RenderEditorWindow(ScriptContext& ctx) (called every frame while tab is open)
  • ExitRenderEditorWindow(ScriptContext& ctx) (called once when tab closes)

Example:

#include "ScriptRuntime.h"
#include "ThirdParty/imgui/imgui.h"

extern "C" void RenderEditorWindow(ScriptContext& ctx) {
    ImGui::TextUnformatted("Hello from script!");
    if (ImGui::Button("Log")) {
        ctx.AddConsoleMessage("Editor window clicked");
    }
}

extern "C" void ExitRenderEditorWindow(ScriptContext& ctx) {
    (void)ctx;
}

How to open:

  1. Compile the script so the binary is updated under Cache/ScriptBin/.
  2. In the main menu, go to View -> Scripted Windows and toggle the entry.

Manual compile (CLI)

Linux:

g++ -std=c++20 -fPIC -O0 -g -I../src -I../include -c SampleInspector.cpp -o ../Cache/ScriptBin/SampleInspector.o
g++ -shared ../Cache/ScriptBin/SampleInspector.o -o ../Cache/ScriptBin/SampleInspector.so -ldl -lpthread

Windows:

cl /nologo /std:c++20 /EHsc /MD /Zi /Od /I ..\\src /I ..\\include /c SampleInspector.cpp /Fo ..\\Cache\\ScriptBin\\SampleInspector.obj
link /nologo /DLL ..\\Cache\\ScriptBin\\SampleInspector.obj /OUT:..\\Cache\\ScriptBin\\SampleInspector.dll User32.lib Advapi32.lib

Troubleshooting

  • Script not running
    • Ensure the object is enabled and the script component is enabled.
    • Ensure the script path points to a real file and the compiled binary exists.
  • No inspector UI
    • Script_OnInspector must be exported with extern "C" (no wrapper is generated for it).
  • Changes not saved
    • Call ctx.MarkDirty() after mutating transforms/settings you want to persist.
  • Editor window not showing
    • Ensure RenderEditorWindow is exported with extern "C" and the binary is up to date.
  • Custom UI style preset not listed
    • Ensure RegisterUIStylePreset(...) ran (e.g. in Begin) before selecting it in the Inspector.
  • Hard crash
    • Add null checks, avoid static pointers to scene objects, and dont hold references across frames unless you can validate them.

Templates

Minimal runtime script (wrapper-based)

#include "ScriptRuntime.h"

void TickUpdate(ScriptContext& ctx, float /*dt*/) {
    if (!ctx.object) return;
}

Minimal script with inspector (manual export)

#include "ScriptRuntime.h"
#include "ThirdParty/imgui/imgui.h"

void TickUpdate(ScriptContext& ctx, float /*dt*/) {
    if (!ctx.object) return;
}

extern "C" void Script_OnInspector(ScriptContext& ctx) {
    ImGui::TextDisabled("Hello from inspector!");
    (void)ctx;
}

Text

Use UI Text objects for on-screen text. Update their label and size from scripts:

void TickUpdate(ScriptContext& ctx, float) {
    if (SceneObject* text = ctx.FindObjectByName("UI Text 2")) {
        if (text->type == ObjectType::UIText) {
            text->ui.label = "Speed: 12.4";
            text->ui.textScale = 1.4f;
            ctx.MarkDirty();
        }
    }
}

FPS display example

Attach Scripts/FPSDisplay.cpp to a UI Text object to show FPS. The inspector exposes a checkbox to clamp FPS to 120.