C++ Scripting
High-level ModuCPP authoring and raw native C++ scripts 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/. Use.moducppfor high-level ModuCPP syntax, or.cppfor raw native C++. - 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.
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
- Parse high-level class syntax (
public class X : ModuBehaviour). - Generate equivalent native C++ (
<script>.moducpp.gen.cpp) in script build output. - Run the existing native hook wrapper detection/export flow (
Script_Begin,Script_TickUpdate,Script_OnInspector, etc.). - 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 = ...;andpublic 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 generatedState<T> - Lifecycle methods:
void TickUpdate(MODU_obj, float dt)transpiles to nativevoid TickUpdate(ScriptContext& ctx, float dt) - List action shorthand:
each someList.state(true);transpiles to null-safe iteration over resolved object refs
| 1 | public 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);andAutoSave(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
"Label" as the first argument. If you implement your own Script_OnInspector(MODU_obj), auto inspector generation is skipped.ModuCPP Source + Generated C++ Example
| 1 | #include "ModuCPP" |
| 2 | |
| 3 | public 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 | } |
| 1 | namespace ModuCPPTranspiled_AutoEnable { |
| 2 | struct Config { std::string enableRaw; std::string disableRaw; float interval = 1.0f; }; |
| 3 | struct State { float timer = 0.0f; }; |
| 4 | } |
| 5 | |
| 6 | extern "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 | |
| 12 | void 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
.moducppauthoring 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
| 1 | MODU_SCRIPT(ctx) // installs scoped thread-local context, gives "obj" shorthand |
| 2 | Config<T>() // persisted per-script-instance config data |
| 3 | State<T>() // runtime-only per-script-instance state |
| 4 | BindSetting(...) // bind primitive/string data to inspector/settings persistence |
| 5 | BindArray(...) // bind fixed-size array data |
| 6 | BindArray2D(...) |
Inspector Helpers
| 1 | EditBool(...) // persistent bool inspector widget |
| 2 | EditFloat(...) // persistent float drag widget |
| 3 | EditInt(...) // persistent int drag widget |
| 4 | EditVec3(...) // persistent vec3 drag widget |
| 5 | EditString(...) // persistent string input widget |
| 6 | EditDirectionalClipGrid(...) // directional idle/walk sprite clip picker |
| 7 | EditSoundSet(...) // drag-drop-friendly list of audio clip slots |
Gameplay Helpers
| 1 | // Input |
| 2 | input.WASD() // raw WASD vector |
| 3 | input.WASDNormalized() // normalized WASD |
| 4 | input.sprint() // sprint key state |
| 5 | input.jump() // jump key state |
| 6 | KeyDown(KEY_W) // direct key check (also KEY_SPACE, KEY_ENTER, etc.) |
| 7 | KeyPressed(KEY_W) |
| 8 | |
| 9 | // 2D movement |
| 10 | TryMoveRigidbody2D(...) |
| 11 | moveRigidbody2D(...) |
| 12 | movePosition2D(...) |
| 13 | |
| 14 | // Audio facade |
| 15 | audio.HasSource() |
| 16 | audio.Play() |
| 17 | audio.Stop() |
| 18 | audio.PlayOneShot(clipPath) |
| 19 | |
| 20 | // Sprite facade |
| 21 | sprite.HasClips() |
| 22 | sprite.ClipCount() |
| 23 | sprite.ClipIndex() |
| 24 | sprite.SetClip(index) |
| 25 | sprite.ClipNameAt(index) |
| 26 | |
| 27 | // Logging guards |
| 28 | warnOnce(msg) |
| 29 | warnMissingComponentOnce(msg) |
Note
ModuCPP instead of only ScriptRuntime.h.Shipped Mechanics & Example Scripts
The repo includes a set of reusable gameplay and editor mechanics as .moducpp scripts:
| Script | Description |
|---|---|
AutoEnableAndDisableListsOfObjectsAfterAmountOfTime.moducpp | Timed enable/disable toggling for lists of scene objects via each list.state(...). |
DialogueSystem.moducpp | Localized dialogue playback with typewriter timing, text effects, per-line mouth state toggles, audio cues, and runtime inspector status. |
InteractableObject.moducpp | Proximity/key-driven interactions, one-time-use logic, selection-state object toggles, option lists, and dialogue handoff. |
MainMenuController.moducpp | Keyboard-driven menu cursor/heart movement, configurable orientation, move/select sounds, and per-item enable/disable actions. |
TopDownMovement2D.moducpp | 2D WASD movement, optional Rigidbody2D driving, directional idle/walk clip grids, sprint speed, and randomized footstep audio. |
StandaloneMovementController.moducpp | Reusable grounded 3D locomotion using TickStandaloneMovement, with inspector-driven tuning for movement, look, gravity, and collider. |
FPSDisplay.moducpp | UI text update + optional FPS cap control. |
RigidbodyTest.moducpp | Launch, teleport, and live rigidbody readback examples. |
EditorWindowSample.moducpp | Minimal scripted editor window example. |
AnimationWindow.moducpp | Scripted editor animation tool. |
SampleInspector.moducpp | Manual 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
| 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) |
| 1 | scriptsDir=Assets/Scripts |
| 2 | outDir=Library/CompiledScripts |
| 3 | includeDir=../src |
| 4 | includeDir=../include |
| 5 | cppStandard=c++20 |
| 6 | linux.linkLib=pthread |
| 7 | linux.linkLib=dl |
| 8 | win.linkLib=User32.lib |
| 9 | win.linkLib=Advapi32.lib |
How 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:
| 1 | void 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.
| 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 |
RenderEditorWindow(ScriptContext&) | Manual export | Scripted editor tab — called every frame while open |
ExitRenderEditorWindow(ScriptContext&) | Manual export | Called 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
| Field | Type | Description |
|---|---|---|
engine | Engine* | Engine pointer |
object | SceneObject* | Owning object (may be null) |
script | ScriptComponent* | Owning script component |
Object Lookup
| 1 | FindObjectByName(const std::string&) // first exact name match |
| 2 | FindObjectById(int) |
| 3 | ResolveObjectRef(const std::string&) // resolve serialized object reference |
Object State Helpers
| 1 | IsObjectEnabled() |
| 2 | SetObjectEnabled(bool) |
| 3 | GetLayer() / SetLayer(int) |
| 4 | GetTag() / SetTag(const std::string&) |
| 5 | HasTag(const std::string&) |
| 6 | IsInLayer(int) |
Transform Helpers
| 1 | SetPosition(const glm::vec3&) |
| 2 | SetRotation(const glm::vec3&) // degrees |
| 3 | SetScale(const glm::vec3&) |
| 4 | SetPosition2D(const glm::vec2&) // UI position in pixels |
| 5 | GetPlanarYawPitchVectors(pitchDeg, yawDeg, outForward, outRight) |
| 6 | GetMoveInputWASD(float pitchDeg, float yawDeg) |
| 7 | ApplyMouseLook(pitchDeg, yawDeg, sensitivity, maxDelta, deltaTime, requireMouseButton) |
| 8 | ApplyVelocity(velocity, deltaTime) |
Ground & Standalone Movement
| 1 | ResolveGround(capsuleHalf, probeExtra, groundSnap, verticalVelocity, ...) |
| 2 | BindStandaloneMovementSettings(StandaloneMovementSettings&) |
| 3 | DrawStandaloneMovementInspector(StandaloneMovementSettings&, bool* showDebug) |
| 4 | TickStandaloneMovement(state, settings, deltaTime, debug) |
Rigidbody Helpers (3D)
| 1 | HasRigidbody() |
| 2 | EnsureCapsuleCollider(float height, float radius) |
| 3 | EnsureRigidbody(bool useGravity, bool kinematic) |
| 4 | SetRigidbodyVelocity(const glm::vec3&) |
| 5 | GetRigidbodyVelocity(glm::vec3& out) |
| 6 | AddRigidbodyVelocity(const glm::vec3&) |
| 7 | SetRigidbodyAngularVelocity(const glm::vec3&) |
| 8 | GetRigidbodyAngularVelocity(glm::vec3& out) |
| 9 | AddRigidbodyForce(const glm::vec3&) |
| 10 | AddRigidbodyImpulse(const glm::vec3&) |
| 11 | AddRigidbodyTorque(const glm::vec3&) |
| 12 | AddRigidbodyAngularImpulse(const glm::vec3&) |
| 13 | SetRigidbodyYaw(float yawDegrees) |
| 14 | SetRigidbodyRotation(const glm::vec3& rotDeg) |
| 15 | TeleportRigidbody(const glm::vec3& pos, const glm::vec3& rotDeg) |
| 16 | RaycastClosest(origin, dir, distance, hitPos, hitNormal, hitDistance) |
| 17 | RaycastClosestDetailed(origin, dir, distance, hitPos, hitNormal, hitDistance, |
| 18 | hitObjectId, hitObjectVelocity, hitStaticFriction, hitDynamicFriction) |
Rigidbody2D Helpers
| 1 | HasRigidbody2D() |
| 2 | SetRigidbody2DVelocity(const glm::vec2&) |
| 3 | GetRigidbody2DVelocity(glm::vec2& out) |
Audio Helpers
| 1 | HasAudioSource() |
| 2 | PlayAudio() |
| 3 | StopAudio() |
| 4 | SetAudioLoop(bool) |
| 5 | SetAudioVolume(float) |
| 6 | SetAudioClip(const std::string& path) |
| 7 | PlayAudioOneShot(const std::string& clipPath = "", float volumeScale = 1.0f) |
Animation Helpers
| 1 | HasAnimation() |
| 2 | PlayAnimation(bool restart = true) |
| 3 | StopAnimation(bool resetTime = true) |
| 4 | PauseAnimation(bool pause = true) |
| 5 | ReverseAnimation(bool restartIfStopped = true) |
| 6 | SetAnimationTime(float timeSeconds) |
| 7 | GetAnimationTime() |
| 8 | IsAnimationPlaying() |
| 9 | SetAnimationLoop(bool) |
| 10 | SetAnimationPlaySpeed(float) |
| 11 | SetAnimationPlayOnAwake(bool) |
Settings & Utility
| 1 | GetSetting(key, fallback) / SetSetting(key, value) |
| 2 | GetSettingBool(key, fallback) / SetSettingBool(key, value) |
| 3 | GetSettingFloat(key, fallback) / SetSettingFloat(key, value) |
| 4 | GetSettingVec3(key, fallback) / SetSettingVec3(key, value) |
| 5 | AutoSetting(key, bool|float|int|glm::vec3|std::string|buffer) |
| 6 | SaveAutoSettings() |
| 7 | AddConsoleMessage(text, ConsoleMessageType) |
| 8 | MarkDirty() |
| 9 | SetFPSCap(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&):
| 1 | #include "ScriptRuntime.h" |
| 2 | #include "ThirdParty/imgui/imgui.h" |
| 3 | |
| 4 | static bool autoRotate = false; |
| 5 | |
| 6 | extern "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
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
| 1 | void 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().
| 1 | extern "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.
Sliders as Meters
Set Interactable to false to make a slider read-only.
| 1 | void 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.
| 1 | void 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
| 1 | IsUIButtonPressed() |
| 2 | IsUIInteractable() / SetUIInteractable(bool) |
| 3 | GetUISliderValue() / SetUISliderValue(float) |
| 4 | SetUISliderRange(float min, float max) |
| 5 | SetUILabel(const std::string&) |
| 6 | SetUIColor(const glm::vec4&) |
| 7 | GetUITextScale() / SetUITextScale(float) |
| 8 | SetUISliderStyle(UISliderStyle) |
| 9 | SetUIButtonStyle(UIButtonStyle) |
| 10 | SetUIStylePreset(const std::string&) |
| 11 | RegisterUIStylePreset(name, const ImGuiStyle&, bool replace = false) |
| 12 | 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
| 1 | IEnum_Start(fn) // Start a task |
| 2 | IEnum_Stop(fn) // Stop a task |
| 3 | IEnum_Ensure(fn) // Start if not already running |
Example
| 1 | static bool autoRotate = false; |
| 2 | static glm::vec3 speed = {0, 45, 0}; |
| 3 | |
| 4 | static void RotateTask(ScriptContext& ctx, float dt) { |
| 5 | if (!ctx.object) return; |
| 6 | ctx.SetRotation(ctx.object->rotation + speed * dt); |
| 7 | } |
| 8 | |
| 9 | extern "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
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
| 1 | static bool warned = false; |
| 2 | if (!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 openExitRenderEditorWindow(ScriptContext& ctx)- called once when tab closes
| 1 | #include "ScriptRuntime.h" |
| 2 | #include "ThirdParty/imgui/imgui.h" |
| 3 | |
| 4 | extern "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 | |
| 11 | extern "C" void ExitRenderEditorWindow(ScriptContext& ctx) { |
| 12 | (void)ctx; |
| 13 | } |
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
| 1 | g++ -std=c++20 -fPIC -O0 -g -I../src -I../include -c SampleInspector.cpp -o ../Library/CompiledScripts/SampleInspector.o |
| 2 | g++ -shared ../Library/CompiledScripts/SampleInspector.o -o ../Library/CompiledScripts/SampleInspector.so -ldl -lpthread |
Windows
| 1 | cl /nologo /std:c++20 /EHsc /MD /Zi /Od /I ..\src /I ..\include /c SampleInspector.cpp /Fo ..\Library\CompiledScripts\SampleInspector.obj |
| 2 | link /nologo /DLL ..\Library\CompiledScripts\SampleInspector.obj /OUT:..\Library\CompiledScripts\SampleInspector.dll User32.lib Advapi32.lib |
Troubleshooting
| 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). |
| Changes not saved | Call ctx.MarkDirty() after mutating transforms/settings you want to persist. |
| Editor window not showing | Ensure the hook exists (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 ModuCPP Script
| 1 | #include "ModuCPP" |
| 2 | |
| 3 | public 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
| 1 | #include "ScriptRuntime.h" |
| 2 | |
| 3 | void TickUpdate(ScriptContext& ctx, float /*dt*/) { |
| 4 | if (!ctx.object) return; |
| 5 | } |
Raw C++ Script with Inspector
| 1 | #include "ScriptRuntime.h" |
| 2 | #include "ThirdParty/imgui/imgui.h" |
| 3 | |
| 4 | void TickUpdate(ScriptContext& ctx, float /*dt*/) { |
| 5 | if (!ctx.object) return; |
| 6 | } |
| 7 | |
| 8 | extern "C" void Script_OnInspector(ScriptContext& ctx) { |
| 9 | ImGui::TextDisabled("Hello from inspector!"); |
| 10 | (void)ctx; |
| 11 | } |