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

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).

Quickstart

  1. Create a script file under Assets/Scripts/ (e.g. Assets/Scripts/Runtime/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 Inspector's script component menu, choose Compile.
  5. Implement a tick hook (TickUpdate) and observe behavior in play mode.

Language Options

Language options in the Script component:

LanguageDescription
C++Native script with ScriptContext helpers
CNative 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.

  1. Create Assets/Scripts/Runtime/MyScript.c (or use Project window -> New -> C Script).
  2. In Inspector -> Script component, set Language to C and assign the file.
  3. Compile as usual (right-click file -> Compile Script or script component menu -> Compile).

Minimal C Example

MyScript.ccpp
#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_Begin
  • Modu_TickUpdate
  • Modu_Update
  • Modu_Spec
  • Modu_TestEditor
  • Modu_OnInspector
  • Modu_RenderEditorWindow
  • Modu_ExitRenderEditorWindow

Note

For C scripts (.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

KeyDescription
scriptsDirWhere script source files live (default: Assets/Scripts)
outDirWhere 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)
cppStandardC++ standard (e.g. c++20)
scripts.moduini
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.lib

How Compilation Works

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

  • Source lives under scriptsDir (default Assets/Scripts/).
  • Output binaries are written to outDir (default Library/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:

  • Begin
  • TickUpdate
  • Update
  • Spec
  • TestEditor

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

For C scripts (.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.

HookGenerationDescription
Script_OnInspector(ScriptContext&)Manual exportInspector UI drawing
Script_Begin(ScriptContext&, float)Auto-generatedRuns once per object instance
Script_TickUpdate(ScriptContext&, float)Auto-generatedRuns every frame (preferred)
Script_Update(ScriptContext&, float)Auto-generatedRuns only if TickUpdate missing
Script_Spec(ScriptContext&, float)Auto-generatedRuns when Spec mode enabled
Script_TestEditor(ScriptContext&, float)Auto-generatedRuns 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

FieldTypeDescription
engineEngine*Engine pointer
objectSceneObject*Owning object (may be null)
scriptScriptComponent*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_SetScale

Rigidbody & Collision

Modu_SetRigidbodyVelocity
Modu_GetRigidbodyVelocity
Modu_AddRigidbodyForce
Modu_SetRigidbodyRotation
Modu_EnsureCapsuleCollider
Modu_EnsureRigidbody

Input & Movement

Modu_IsSprintDown
Modu_IsJumpDown
Modu_GetMoveInputWASD
Modu_ApplyMouseLook
Modu_RaycastClosestDetailed

Script Settings

Modu_GetSettingFloat / Modu_SetSettingFloat
Modu_GetSettingBool / Modu_SetSettingBool
Modu_GetSettingString / Modu_SetSettingString

Inspector Helpers

Modu_InspectorText
Modu_InspectorSeparator
Modu_InspectorDragFloat / Modu_InspectorDragFloat2 / Modu_InspectorDragFloat3
Modu_InspectorCheckbox

Console

Modu_AddConsoleMessage

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&):

Inspector UI Examplecpp
#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

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.

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.

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

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 running

Example

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

Tasks are stored per 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.

TypeUsage
ConsoleMessageType::InfoGeneral information
ConsoleMessageType::SuccessSuccess messages
ConsoleMessageType::WarningWarnings
ConsoleMessageType::ErrorErrors

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
Editor Window Examplecpp
#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 outDir (default Library/CompiledScripts/).
  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 ../Library/CompiledScripts/SampleInspector.o
g++ -shared ../Library/CompiledScripts/SampleInspector.o -o ../Library/CompiledScripts/SampleInspector.so -ldl -lpthread

Windows

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.lib

Troubleshooting

IssueSolution
Script not runningEnsure the object is enabled, script component is enabled, script path points to a real file, and compiled binary exists.
No inspector UIEnsure the hook exists (Script_OnInspector for C++, Modu_OnInspector for C).
Changes not savedCall ctx.MarkDirty() after mutating transforms/settings you want to persist.
Editor window not showingEnsure the hook exists (RenderEditorWindow or Modu_RenderEditorWindow) and binary is up to date.
Custom UI style preset not listedEnsure RegisterUIStylePreset(...) ran (e.g. in Begin) before selecting it in the Inspector.
Hard crashAdd null checks, avoid static pointers to scene objects, and don't hold references across frames unless you can validate them.

Templates

Minimal Runtime Script

MyScript.cppcpp
#include "ScriptRuntime.h"

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

Minimal Script with Inspector

MyScript.cppcpp
#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.