C++ Scripting

High-level ModuCPP authoring and raw native C++ scripts 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/. Use .moducpp for high-level ModuCPP syntax, or .cpp for raw native C++.
  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.

ModuCPP (Recommended Authoring Layer)

ModuCPP is the preferred way to write gameplay and editor scripts. It is a compile-time frontend that transpiles high-level class syntax into native C++ before compilation. The runtime is unchanged — ModuCPP generates the same shared library as a raw C++ script.

Transpiler Pipeline

  1. Parse high-level class syntax (public class X : ModuBehaviour).
  2. Generate equivalent native C++ (<script>.moducpp.gen.cpp) in script build output.
  3. Run the existing native hook wrapper detection/export flow (Script_Begin, Script_TickUpdate, Script_OnInspector, etc.).
  4. Compile/link with the same shared-library build/load path used by raw native scripts.

Note

.moducpp files without public class ... : ModuBehaviour are treated as legacy native C++ passthrough, so old script bodies keep compiling while you migrate incrementally.

ModuCPP Syntax Rules

Supported high-level rules in the current transpiler:

  • public class MyScript : ModuBehaviour
  • Public persisted fields: public float/int/bool/vec3/string fieldName = ...; and public List<SceneObj*> fieldName;
  • Optional field metadata: @range(min, max) for numeric fields, @step(value) for drag speed
  • Private runtime-only fields: private <type> fieldName = ...; — stored in generated State<T>
  • Lifecycle methods: void TickUpdate(MODU_obj, float dt) transpiles to native void TickUpdate(ScriptContext& ctx, float dt)
  • List action shorthand: each someList.state(true); transpiles to null-safe iteration over resolved object refs
Example field with metadata
C++
1public float walkSpeed = 4.0f @range(0.0f, 50.0f) @step(0.1f);

Custom Inspector DSL

ModuCPP supports a high-level custom inspector block inside the class. The transpiler converts it to backend ImGui/editor calls:

  • Config binding: Config(Type, var); and AutoSave(var);
  • Layout containers: Tabs, Tab("Name"), Section("Name"), Group("Name"), Foldout("Name")
  • Basic widgets: Toggle, Slider, Number, String, ObjectRef, ObjectList, AudioClip, Enum
  • Specialized widgets: DialogueLines, InteractionOptions, TextEffectFlags, MenuActions, ClipGrid, SoundSet
  • Utility: Header("..."), Separator(), Run(expr), RuntimeDialogueStatus(), RuntimeInteractableStatus(), RuntimeMenuStatus()

Note

All inspector widgets support an optional "Label" as the first argument. If you implement your own Script_OnInspector(MODU_obj), auto inspector generation is skipped.

ModuCPP Source + Generated C++ Example

AutoEnable.moducpp (authoring source)
ModuCPP
1#include "ModuCPP"
2
3public class AutoEnableAndDisableListsOfObjectsAfterAmountOfTime : ModuBehaviour {
4 public List<SceneObj*> enable;
5 public List<SceneObj*> disable;
6 public float interval = 1.0f;
7 private float timer = 0.0f;
8
9 void TickUpdate(MODU_obj, float dt) {
10 timer += dt;
11 if (timer < interval) return;
12
13 each enable.state(true);
14 each disable.state(false);
15
16 timer = 0.0f;
17 }
18}
Generated native C++ (simplified shape)
C++
1namespace ModuCPPTranspiled_AutoEnable {
2struct Config { std::string enableRaw; std::string disableRaw; float interval = 1.0f; };
3struct State { float timer = 0.0f; };
4}
5
6extern "C" void Script_OnInspector(ScriptContext& ctx) {
7 MODU_SCRIPT(ctx);
8 auto& config = ModuCPP::Config<ModuCPPTranspiled_AutoEnable::Config>();
9 // Generated list editors + float editor, then ctx.SaveAutoSettings() on change.
10}
11
12void TickUpdate(ScriptContext& ctx, float dt) {
13 MODU_SCRIPT(ctx);
14 auto& config = ModuCPP::Config<ModuCPPTranspiled_AutoEnable::Config>();
15 auto& state = ModuCPP::State<ModuCPPTranspiled_AutoEnable::State>();
16 state.timer += dt;
17 if (state.timer < config.interval) return;
18
19 for (SceneObject* o : ResolveObjectList(ctx, config.enableRaw)) {
20 if (o) SetResolvedObjectEnabled(ctx, o, true);
21 }
22 for (SceneObject* o : ResolveObjectList(ctx, config.disableRaw)) {
23 if (o) SetResolvedObjectEnabled(ctx, o, false);
24 }
25 state.timer = 0.0f;
26}

Inspector & Persistence Behavior

  • Public fields are generated into Script_OnInspector and bind through BindSetting, persisting through the native auto-settings flow.
  • Private fields are placed in generated State<T> storage and are runtime-only.
  • Default .moducpp authoring does not require writing ImGui calls — inspector widgets are transpiler-generated from public fields and metadata.

ModuCPP Helper Layer (#include "ModuCPP")

The ModuCPP header is not just for the transpiler. It also exposes a thin native helper layer that raw C++ scripts can use directly without using the high-level class syntax.

Core Patterns

C++
1MODU_SCRIPT(ctx) // installs scoped thread-local context, gives "obj" shorthand
2Config<T>() // persisted per-script-instance config data
3State<T>() // runtime-only per-script-instance state
4BindSetting(...) // bind primitive/string data to inspector/settings persistence
5BindArray(...) // bind fixed-size array data
6BindArray2D(...)

Inspector Helpers

C++
1EditBool(...) // persistent bool inspector widget
2EditFloat(...) // persistent float drag widget
3EditInt(...) // persistent int drag widget
4EditVec3(...) // persistent vec3 drag widget
5EditString(...) // persistent string input widget
6EditDirectionalClipGrid(...) // directional idle/walk sprite clip picker
7EditSoundSet(...) // drag-drop-friendly list of audio clip slots

Gameplay Helpers

C++
1// Input
2input.WASD() // raw WASD vector
3input.WASDNormalized() // normalized WASD
4input.sprint() // sprint key state
5input.jump() // jump key state
6KeyDown(KEY_W) // direct key check (also KEY_SPACE, KEY_ENTER, etc.)
7KeyPressed(KEY_W)
8
9// 2D movement
10TryMoveRigidbody2D(...)
11moveRigidbody2D(...)
12movePosition2D(...)
13
14// Audio facade
15audio.HasSource()
16audio.Play()
17audio.Stop()
18audio.PlayOneShot(clipPath)
19
20// Sprite facade
21sprite.HasClips()
22sprite.ClipCount()
23sprite.ClipIndex()
24sprite.SetClip(index)
25sprite.ClipNameAt(index)
26
27// Logging guards
28warnOnce(msg)
29warnMissingComponentOnce(msg)

Note

If you need a custom native script but still want the newer ergonomic helpers, include ModuCPP instead of only ScriptRuntime.h.

Shipped Mechanics & Example Scripts

The repo includes a set of reusable gameplay and editor mechanics as .moducpp scripts:

ScriptDescription
AutoEnableAndDisableListsOfObjectsAfterAmountOfTime.moducppTimed enable/disable toggling for lists of scene objects via each list.state(...).
DialogueSystem.moducppLocalized dialogue playback with typewriter timing, text effects, per-line mouth state toggles, audio cues, and runtime inspector status.
InteractableObject.moducppProximity/key-driven interactions, one-time-use logic, selection-state object toggles, option lists, and dialogue handoff.
MainMenuController.moducppKeyboard-driven menu cursor/heart movement, configurable orientation, move/select sounds, and per-item enable/disable actions.
TopDownMovement2D.moducpp2D WASD movement, optional Rigidbody2D driving, directional idle/walk clip grids, sprint speed, and randomized footstep audio.
StandaloneMovementController.moducppReusable grounded 3D locomotion using TickStandaloneMovement, with inspector-driven tuning for movement, look, gravity, and collider.
FPSDisplay.moducppUI text update + optional FPS cap control.
RigidbodyTest.moducppLaunch, teleport, and live rigidbody readback examples.
EditorWindowSample.moducppMinimal scripted editor window example.
AnimationWindow.moducppScripted editor animation tool.
SampleInspector.moducppManual inspector patterns and Config/State migration from raw native C++.

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.modu
INI
1scriptsDir=Assets/Scripts
2outDir=Library/CompiledScripts
3includeDir=../src
4includeDir=../include
5cppStandard=c++20
6linux.linkLib=pthread
7linux.linkLib=dl
8win.linkLib=User32.lib
9win.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:

C++
1void TickUpdate(ScriptContext& ctx, float dt) {
2 (void)dt;
3 if (!ctx.object) return;
4}

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
RenderEditorWindow(ScriptContext&)Manual exportScripted editor tab — called every frame while open
ExitRenderEditorWindow(ScriptContext&)Manual exportCalled once when editor tab closes

Note

Begin runs once per object instance (per script component instance). TickUpdate runs every frame and is preferred over Update. Spec/TestEditorrun 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

C++
1FindObjectByName(const std::string&) // first exact name match
2FindObjectById(int)
3ResolveObjectRef(const std::string&) // resolve serialized object reference

Object State Helpers

C++
1IsObjectEnabled()
2SetObjectEnabled(bool)
3GetLayer() / SetLayer(int)
4GetTag() / SetTag(const std::string&)
5HasTag(const std::string&)
6IsInLayer(int)

Transform Helpers

C++
1SetPosition(const glm::vec3&)
2SetRotation(const glm::vec3&) // degrees
3SetScale(const glm::vec3&)
4SetPosition2D(const glm::vec2&) // UI position in pixels
5GetPlanarYawPitchVectors(pitchDeg, yawDeg, outForward, outRight)
6GetMoveInputWASD(float pitchDeg, float yawDeg)
7ApplyMouseLook(pitchDeg, yawDeg, sensitivity, maxDelta, deltaTime, requireMouseButton)
8ApplyVelocity(velocity, deltaTime)

Ground & Standalone Movement

C++
1ResolveGround(capsuleHalf, probeExtra, groundSnap, verticalVelocity, ...)
2BindStandaloneMovementSettings(StandaloneMovementSettings&)
3DrawStandaloneMovementInspector(StandaloneMovementSettings&, bool* showDebug)
4TickStandaloneMovement(state, settings, deltaTime, debug)

Rigidbody Helpers (3D)

C++
1HasRigidbody()
2EnsureCapsuleCollider(float height, float radius)
3EnsureRigidbody(bool useGravity, bool kinematic)
4SetRigidbodyVelocity(const glm::vec3&)
5GetRigidbodyVelocity(glm::vec3& out)
6AddRigidbodyVelocity(const glm::vec3&)
7SetRigidbodyAngularVelocity(const glm::vec3&)
8GetRigidbodyAngularVelocity(glm::vec3& out)
9AddRigidbodyForce(const glm::vec3&)
10AddRigidbodyImpulse(const glm::vec3&)
11AddRigidbodyTorque(const glm::vec3&)
12AddRigidbodyAngularImpulse(const glm::vec3&)
13SetRigidbodyYaw(float yawDegrees)
14SetRigidbodyRotation(const glm::vec3& rotDeg)
15TeleportRigidbody(const glm::vec3& pos, const glm::vec3& rotDeg)
16RaycastClosest(origin, dir, distance, hitPos, hitNormal, hitDistance)
17RaycastClosestDetailed(origin, dir, distance, hitPos, hitNormal, hitDistance,
18 hitObjectId, hitObjectVelocity, hitStaticFriction, hitDynamicFriction)

Rigidbody2D Helpers

C++
1HasRigidbody2D()
2SetRigidbody2DVelocity(const glm::vec2&)
3GetRigidbody2DVelocity(glm::vec2& out)

Audio Helpers

C++
1HasAudioSource()
2PlayAudio()
3StopAudio()
4SetAudioLoop(bool)
5SetAudioVolume(float)
6SetAudioClip(const std::string& path)
7PlayAudioOneShot(const std::string& clipPath = "", float volumeScale = 1.0f)

Animation Helpers

C++
1HasAnimation()
2PlayAnimation(bool restart = true)
3StopAnimation(bool resetTime = true)
4PauseAnimation(bool pause = true)
5ReverseAnimation(bool restartIfStopped = true)
6SetAnimationTime(float timeSeconds)
7GetAnimationTime()
8IsAnimationPlaying()
9SetAnimationLoop(bool)
10SetAnimationPlaySpeed(float)
11SetAnimationPlayOnAwake(bool)

Settings & Utility

C++
1GetSetting(key, fallback) / SetSetting(key, value)
2GetSettingBool(key, fallback) / SetSettingBool(key, value)
3GetSettingFloat(key, fallback) / SetSettingFloat(key, value)
4GetSettingVec3(key, fallback) / SetSettingVec3(key, value)
5AutoSetting(key, bool|float|int|glm::vec3|std::string|buffer)
6SaveAutoSettings()
7AddConsoleMessage(text, ConsoleMessageType)
8MarkDirty()
9SetFPSCap(bool enabled, float cap = 120.0f)

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 Example
C++
1#include "ScriptRuntime.h"
2#include "ThirdParty/imgui/imgui.h"
3
4static bool autoRotate = false;
5
6extern "C" void Script_OnInspector(ScriptContext& ctx) {
7 ImGui::Checkbox("Auto Rotate", &autoRotate);
8 ctx.MarkDirty();
9}

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

C++
1void TickUpdate(ScriptContext& ctx, float) {
2 if (!ctx.script) return;
3 ctx.SetSetting("mode", "hard");
4 ctx.MarkDirty();
5}

AutoSetting (Recommended)

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

C++
1extern "C" void Script_OnInspector(ScriptContext& ctx) {
2 static bool enabled = false;
3 ctx.AutoSetting("enabled", enabled);
4 ImGui::Checkbox("Enabled", &enabled);
5 ctx.SaveAutoSettings();
6}

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.

C++
1void TickUpdate(ScriptContext& ctx, float) {
2 if (ctx.IsUIButtonPressed()) {
3 ctx.AddConsoleMessage("Button clicked!");
4 }
5}

Sliders as Meters

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

C++
1void TickUpdate(ScriptContext& ctx, float) {
2 ctx.SetUIInteractable(false);
3 ctx.SetUISliderStyle(UISliderStyle::Fill);
4 ctx.SetUISliderRange(0.0f, 100.0f);
5 ctx.SetUISliderValue(health);
6}

Style Presets

Register custom ImGui style presets in code and select them per UI element in the Inspector.

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

UI Helpers Reference

C++
1IsUIButtonPressed()
2IsUIInteractable() / SetUIInteractable(bool)
3GetUISliderValue() / SetUISliderValue(float)
4SetUISliderRange(float min, float max)
5SetUILabel(const std::string&)
6SetUIColor(const glm::vec4&)
7GetUITextScale() / SetUITextScale(float)
8SetUISliderStyle(UISliderStyle)
9SetUIButtonStyle(UIButtonStyle)
10SetUIStylePreset(const std::string&)
11RegisterUIStylePreset(name, const ImGuiStyle&, bool replace = false)
12SetFPSCap(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

C++
1IEnum_Start(fn) // Start a task
2IEnum_Stop(fn) // Stop a task
3IEnum_Ensure(fn) // Start if not already running

Example

C++
1static bool autoRotate = false;
2static glm::vec3 speed = {0, 45, 0};
3
4static void RotateTask(ScriptContext& ctx, float dt) {
5 if (!ctx.object) return;
6 ctx.SetRotation(ctx.object->rotation + speed * dt);
7}
8
9extern "C" void Script_OnInspector(ScriptContext& ctx) {
10 ImGui::Checkbox("Auto Rotate", &autoRotate);
11 if (autoRotate) IEnum_Ensure(RotateTask);
12 else IEnum_Stop(RotateTask);
13 ctx.MarkDirty();
14}

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

C++
1static bool warned = false;
2if (!warned) {
3 ctx.AddConsoleMessage("[MyScript] Something looks off", ConsoleMessageType::Warning);
4 warned = true;
5}

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 Example
C++
1#include "ScriptRuntime.h"
2#include "ThirdParty/imgui/imgui.h"
3
4extern "C" void RenderEditorWindow(ScriptContext& ctx) {
5 ImGui::TextUnformatted("Hello from script!");
6 if (ImGui::Button("Log")) {
7 ctx.AddConsoleMessage("Editor window clicked");
8 }
9}
10
11extern "C" void ExitRenderEditorWindow(ScriptContext& ctx) {
12 (void)ctx;
13}

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

Bash
1g++ -std=c++20 -fPIC -O0 -g -I../src -I../include -c SampleInspector.cpp -o ../Library/CompiledScripts/SampleInspector.o
2g++ -shared ../Library/CompiledScripts/SampleInspector.o -o ../Library/CompiledScripts/SampleInspector.so -ldl -lpthread

Windows

BAT
1cl /nologo /std:c++20 /EHsc /MD /Zi /Od /I ..\src /I ..\include /c SampleInspector.cpp /Fo ..\Library\CompiledScripts\SampleInspector.obj
2link /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).
Changes not savedCall ctx.MarkDirty() after mutating transforms/settings you want to persist.
Editor window not showingEnsure the hook exists (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 ModuCPP Script

MyScript.moducpp
C++
1#include "ModuCPP"
2
3public class MyScript : ModuBehaviour {
4 public float speed = 1.0f;
5 private float elapsed = 0.0f;
6
7 void TickUpdate(MODU_obj, float dt) {
8 elapsed += dt;
9 }
10}

Minimal Raw C++ Script

MyScript.cpp
C++
1#include "ScriptRuntime.h"
2
3void TickUpdate(ScriptContext& ctx, float /*dt*/) {
4 if (!ctx.object) return;
5}

Raw C++ Script with Inspector

MyScript.cpp
C++
1#include "ScriptRuntime.h"
2#include "ThirdParty/imgui/imgui.h"
3
4void TickUpdate(ScriptContext& ctx, float /*dt*/) {
5 if (!ctx.object) return;
6}
7
8extern "C" void Script_OnInspector(ScriptContext& ctx) {
9 ImGui::TextDisabled("Hello from inspector!");
10 (void)ctx;
11}