14 KiB
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
- Scripts.modu
- How compilation works
- Lifecycle hooks
- ScriptContext
- ImGui in scripts
- Per-script settings
- UI scripting
- IEnum tasks
- Logging
- Scripted editor windows
- Manual compile (CLI)
- Troubleshooting
- Templates
Quickstart
- Create a script file under
Scripts/(e.g.Scripts/MyScript.cpp). - Select an object in the scene.
- In the Inspector, add/enable a script component and set its path:
- In the Scripts section, set
PathOR click Use Selection after selecting the file in the File Browser.
- In the Scripts section, set
- Compile the script:
- In the File Browser, right-click the script file and choose Compile Script, or
- In the Inspector’s script component menu, choose Compile.
- 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
- Windows:
Wrapper generation (important)
To reduce boilerplate, Modularity auto-generates a wrapper for these hook names if it detects them in your script:
BeginTickUpdateUpdateSpecTestEditor
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_OnInspectoris not wrapper-generated. If you want inspector UI, you must export it explicitly withextern "C".- Scripted editor windows (
RenderEditorWindow,ExitRenderEditorWindow) are also not wrapper-generated; export them explicitly withextern "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 fromBegin)Script_TickUpdate(ScriptContext&, float deltaTime)(wrapper-generated fromTickUpdate)Script_Update(ScriptContext&, float deltaTime)(wrapper-generated fromUpdate, used only if TickUpdate missing)Script_Spec(ScriptContext&, float deltaTime)(wrapper-generated fromSpec)Script_TestEditor(ScriptContext&, float deltaTime)(wrapper-generated fromTestEditor)
Runtime notes:
Beginruns once per object instance (per script component instance).TickUpdateruns every frame (preferred).Updateruns only ifTickUpdateis not exported.Spec/TestEditorrun 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 pointerobject(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_OnInspectormust be exported exactly withextern "C"(it is not wrapper-generated). Important: Do not call ImGui functions (e.g.,ImGui::Text) fromTickUpdateor other runtime hooks. Those run before the ImGui frame is active and outside any window, which can crash.
Scripted editor windows (custom tabs)
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 (recommended for inspector UI)
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 it’s 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
ScriptComponentinstance. - Don’t 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::InfoConsoleMessageType::SuccessConsoleMessageType::WarningConsoleMessageType::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:
- Compile the script so the binary is updated under
Cache/ScriptBin/. - 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_OnInspectormust be exported withextern "C"(no wrapper is generated for it).
- Changes not saved
- Call
ctx.MarkDirty()after mutating transforms/settings you want to persist.
- Call
- Editor window not showing
- Ensure
RenderEditorWindowis exported withextern "C"and the binary is up to date.
- Ensure
- Custom UI style preset not listed
- Ensure
RegisterUIStylePreset(...)ran (e.g. inBegin) before selecting it in the Inspector.
- Ensure
- Hard crash
- Add null checks, avoid static pointers to scene objects, and don’t 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.