Documentation
Scripting (C++, C, and C#)
Hot-compiled native C++/C scripts plus managed C# scripting, with per-object state, ImGui inspectors, and runtime/editor hooks.
Warning
ctx.object (objects can be deleted, disabled, or scripts can be detached).Quickstart
- Create a script file under
Assets/Scripts/(e.g.Assets/Scripts/Runtime/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. - 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.
Language Options
Language options in the Script component:
| Language | Description |
|---|---|
C++ | Native script with ScriptContext helpers |
C | Native script through ScriptRuntimeCAPI.h + Modu_* hooks |
C# | Managed script loaded from an assembly + type name |
C Scripting (C API Bridge)
You can also write native scripts in plain C (.c). The compiler generates a C++ bridge automatically and links it with your C object file.
- Create
Assets/Scripts/Runtime/MyScript.c(or use Project window -> New -> C Script). - In Inspector -> Script component, set
Languageto C and assign the file. - Compile as usual (right-click file -> Compile Script or script component menu -> Compile).
Minimal C Example
#include "ScriptRuntimeCAPI.h"
void Modu_TickUpdate(ModuScriptContext* ctx, float dt) {
(void)dt;
ModuVec3 pos = Modu_GetPosition(ctx);
pos.x += 0.01f;
Modu_SetPosition(ctx, pos);
}Supported C Hook Names
All hooks are optional:
Modu_BeginModu_TickUpdateModu_UpdateModu_SpecModu_TestEditorModu_OnInspectorModu_RenderEditorWindowModu_ExitRenderEditorWindow
Note
.c), wrapper generation always emits the bridge and maps Modu_* hooks to Script_* exports.scripts.modu
Each project has a scripts.modu file (auto-created if missing). It controls native compilation. Legacy Scripts.modu is still detected for older projects.
Common Keys
| Key | Description |
|---|---|
scriptsDir | Where script source files live (default: Assets/Scripts) |
outDir | Where compiled binaries go (default: Library/CompiledScripts) |
includeDir=... | Add include directories (repeatable) |
define=... | Add preprocessor defines (repeatable) |
linux.linkLib=... | Add one Linux link lib/flag per line (repeatable) |
win.linkLib=... | Add one Windows link lib per line (repeatable) |
cppStandard | C++ standard (e.g. c++20) |
scriptsDir=Assets/Scripts
outDir=Library/CompiledScripts
includeDir=../src
includeDir=../include
cppStandard=c++20
linux.linkLib=pthread
linux.linkLib=dl
win.linkLib=User32.lib
win.linkLib=Advapi32.libHow Compilation Works
Modularity compiles scripts into shared libraries and loads them by symbol name.
- Source lives under
scriptsDir(defaultAssets/Scripts/). - Output binaries are written to
outDir(defaultLibrary/CompiledScripts/). - Binaries are platform-specific:
.dll(Windows),.so(Linux).
Wrapper Generation
To reduce boilerplate, Modularity auto-generates a wrapper for these C++ 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:
void TickUpdate(ScriptContext& ctx, float dt) {
(void)dt;
if (!ctx.object) return;
}Note
.c), wrapper generation always emits the bridge and maps Modu_* hooks to Script_* exports.Lifecycle Hooks
All hooks are optional. If a hook is missing, it is simply not called.
| Hook | Generation | Description |
|---|---|---|
Script_OnInspector(ScriptContext&) | Manual export | Inspector UI drawing |
Script_Begin(ScriptContext&, float) | Auto-generated | Runs once per object instance |
Script_TickUpdate(ScriptContext&, float) | Auto-generated | Runs every frame (preferred) |
Script_Update(ScriptContext&, float) | Auto-generated | Runs only if TickUpdate missing |
Script_Spec(ScriptContext&, float) | Auto-generated | Runs when Spec mode enabled |
Script_TestEditor(ScriptContext&, float) | Auto-generated | Runs when TestEditor enabled |
Note
Begin runs once per object instance (per script component instance). TickUpdate runs every frame and is preferred over Update. Spec/TestEditor run only while their global toggles are enabled (main menu -> Scripts).ScriptContext API
ScriptContext is passed into most hooks and provides access to the engine, the owning object, and helper APIs.
Fields
| Field | Type | Description |
|---|---|---|
engine | Engine* | Engine pointer |
object | SceneObject* | Owning object (may be null) |
script | ScriptComponent* | Owning script component |
Object Lookup
FindObjectByName(const std::string&)
FindObjectById(int)
ResolveObjectRef(const std::string&)Object State Helpers
IsObjectEnabled()
SetObjectEnabled(bool)
GetLayer()
SetLayer(int)
GetTag()
SetTag(const std::string&)
HasTag(const std::string&)
IsInLayer(int)Transform Helpers
SetPosition(const glm::vec3&)
SetRotation(const glm::vec3&) // degrees
SetScale(const glm::vec3&)
SetPosition2D(const glm::vec2&) // UI position in pixels
GetPlanarYawPitchVectors(...)
GetMoveInputWASD(float pitchDeg, float yawDeg)
ApplyMouseLook(...)
ApplyVelocity(...)Ground & Movement Helpers
ResolveGround(...)
BindStandaloneMovementSettings(...)
DrawStandaloneMovementInspector(...)
TickStandaloneMovement(...)Rigidbody Helpers (3D)
HasRigidbody()
EnsureCapsuleCollider(float height, float radius)
EnsureRigidbody(bool useGravity, bool kinematic)
SetRigidbodyVelocity(const glm::vec3&)
GetRigidbodyVelocity(glm::vec3& out)
AddRigidbodyVelocity(const glm::vec3&)
SetRigidbodyAngularVelocity(const glm::vec3&)
GetRigidbodyAngularVelocity(glm::vec3& out)
AddRigidbodyForce(const glm::vec3&)
AddRigidbodyImpulse(const glm::vec3&)
AddRigidbodyTorque(const glm::vec3&)
AddRigidbodyAngularImpulse(const glm::vec3&)
SetRigidbodyYaw(float yawDegrees)
RaycastClosest(...)
RaycastClosestDetailed(...)
SetRigidbodyRotation(const glm::vec3& rotDeg)
TeleportRigidbody(const glm::vec3& pos, const glm::vec3& rotDeg)Rigidbody2D Helpers
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)
GetSettingFloat(key, fallback)
SetSettingFloat(key, value)
GetSettingVec3(key, fallback)
SetSettingVec3(key, value)
AutoSetting(key, bool|float|glm::vec3|buffer)
SaveAutoSettings()
AddConsoleMessage(text, type)
MarkDirty()
SetFPSCap(bool enabled, float cap = 120.0f)C API Quick Reference
Include ScriptRuntimeCAPI.h in .c scripts. The wrapper maps Modu_* calls to the same runtime systems used by C++ scripts.
Object & Transform
Modu_GetObjectId
Modu_IsObjectEnabled
Modu_SetObjectEnabled
Modu_GetPosition / Modu_SetPosition
Modu_GetRotation / Modu_SetRotation
Modu_GetScale / Modu_SetScaleRigidbody & Collision
Modu_SetRigidbodyVelocity
Modu_GetRigidbodyVelocity
Modu_AddRigidbodyForce
Modu_SetRigidbodyRotation
Modu_EnsureCapsuleCollider
Modu_EnsureRigidbodyInput & Movement
Modu_IsSprintDown
Modu_IsJumpDown
Modu_GetMoveInputWASD
Modu_ApplyMouseLook
Modu_RaycastClosestDetailedScript Settings
Modu_GetSettingFloat / Modu_SetSettingFloat
Modu_GetSettingBool / Modu_SetSettingBool
Modu_GetSettingString / Modu_SetSettingStringInspector Helpers
Modu_InspectorText
Modu_InspectorSeparator
Modu_InspectorDragFloat / Modu_InspectorDragFloat2 / Modu_InspectorDragFloat3
Modu_InspectorCheckboxConsole
Modu_AddConsoleMessageImGui 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
extern "C" exports are recommended for clarity, but recent builds can auto-wrap inspector/editor hooks.Warning
ImGui::Text) from TickUpdate or other runtime hooks. Those run before the ImGui frame is active and outside any window, which can crash.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)
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.
Sliders as Meters
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!");
}
}
}UI Helpers Reference
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)
SetFPSCap(bool enabled, float cap = 120.0f)IEnum Tasks
Modularity provides lightweight, opt-in tasks you can start/stop per script component instance. An IEnum task is just a function with signature void(ScriptContext&, float) that is called every frame while it is registered.
Start/Stop Macros
IEnum_Start(fn) // Start a task
IEnum_Stop(fn) // Stop a task
IEnum_Ensure(fn) // Start if not already runningExample
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();
}Note
ScriptComponent instance. 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.
| Type | Usage |
|---|---|
ConsoleMessageType::Info | General information |
ConsoleMessageType::Success | Success messages |
ConsoleMessageType::Warning | Warnings |
ConsoleMessageType::Error | Errors |
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 openExitRenderEditorWindow(ScriptContext& ctx)- called once when tab closes
#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
outDir(defaultLibrary/CompiledScripts/). - 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 ../Library/CompiledScripts/SampleInspector.o
g++ -shared ../Library/CompiledScripts/SampleInspector.o -o ../Library/CompiledScripts/SampleInspector.so -ldl -lpthreadWindows
cl /nologo /std:c++20 /EHsc /MD /Zi /Od /I ..\src /I ..\include /c SampleInspector.cpp /Fo ..\Library\CompiledScripts\SampleInspector.obj
link /nologo /DLL ..\Library\CompiledScripts\SampleInspector.obj /OUT:..\Library\CompiledScripts\SampleInspector.dll User32.lib Advapi32.libTroubleshooting
| Issue | Solution |
|---|---|
| Script not running | Ensure the object is enabled, script component is enabled, script path points to a real file, and compiled binary exists. |
| No inspector UI | Ensure the hook exists (Script_OnInspector for C++, Modu_OnInspector for C). |
| Changes not saved | Call ctx.MarkDirty() after mutating transforms/settings you want to persist. |
| Editor window not showing | Ensure the hook exists (RenderEditorWindow or Modu_RenderEditorWindow) and 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 don't hold references across frames unless you can validate them. |
Templates
Minimal Runtime Script
#include "ScriptRuntime.h"
void TickUpdate(ScriptContext& ctx, float /*dt*/) {
if (!ctx.object) return;
}Minimal Script with Inspector
#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;
}UI Text Update
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
Attach an FPS script (for example Assets/Scripts/Runtime/FPSDisplay.cpp) to a UI Text object to show FPS. The inspector exposes a checkbox to clamp FPS to 120.