401 lines
14 KiB
Markdown
401 lines
14 KiB
Markdown
---
|
||
title: C++ Scripting
|
||
description: 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](#quickstart)
|
||
- [Scripts.modu](#scriptsmodu)
|
||
- [How compilation works](#how-compilation-works)
|
||
- [Lifecycle hooks](#lifecycle-hooks)
|
||
- [ScriptContext](#scriptcontext)
|
||
- [ImGui in scripts](#imgui-in-scripts)
|
||
- [Per-script settings](#per-script-settings)
|
||
- [UI scripting](#ui-scripting)
|
||
- [IEnum tasks](#ienum-tasks)
|
||
- [Logging](#logging)
|
||
- [Scripted editor windows](#scripted-editor-windows)
|
||
- [Manual compile (CLI)](#manual-compile-cli)
|
||
- [Troubleshooting](#troubleshooting)
|
||
- [Templates](#templates)
|
||
|
||
## Quickstart
|
||
1. Create a script file under `Scripts/` (e.g. `Scripts/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.
|
||
|
||
## 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:
|
||
```ini
|
||
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`
|
||
|
||
### Wrapper generation (important)
|
||
To reduce boilerplate, Modularity auto-generates a wrapper for these 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 like:
|
||
```cpp
|
||
void TickUpdate(ScriptContext& ctx, float dt) {
|
||
(void)dt;
|
||
if (!ctx.object) return;
|
||
}
|
||
```
|
||
|
||
However:
|
||
- `Script_OnInspector` is **not** wrapper-generated. If you want inspector UI, you must export it explicitly with `extern "C"`.
|
||
- Scripted editor windows (`RenderEditorWindow`, `ExitRenderEditorWindow`) are also **not** wrapper-generated; export them explicitly with `extern "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 from `Begin`)
|
||
- `Script_TickUpdate(ScriptContext&, float deltaTime)` (wrapper-generated from `TickUpdate`)
|
||
- `Script_Update(ScriptContext&, float deltaTime)` (wrapper-generated from `Update`, used only if TickUpdate missing)
|
||
- `Script_Spec(ScriptContext&, float deltaTime)` (wrapper-generated from `Spec`)
|
||
- `Script_TestEditor(ScriptContext&, float deltaTime)` (wrapper-generated from `TestEditor`)
|
||
|
||
Runtime notes:
|
||
- `Begin` runs once per object instance (per script component instance).
|
||
- `TickUpdate` runs every frame (preferred).
|
||
- `Update` runs only if `TickUpdate` is not exported.
|
||
- `Spec/TestEditor` run 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 pointer
|
||
- `object` (`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&)`:
|
||
```cpp
|
||
#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_OnInspector` must be exported exactly with `extern "C"` (it is not wrapper-generated).
|
||
> Important: 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.
|
||
|
||
### Scripted editor windows (custom tabs)
|
||
See [Scripted editor windows](#scripted-editor-windows).
|
||
|
||
## Per-script settings
|
||
Each `ScriptComponent` owns serialized key/value strings (`ctx.script->settings`). Use them to persist state with the scene.
|
||
|
||
### Direct settings
|
||
```cpp
|
||
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()`.
|
||
```cpp
|
||
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.
|
||
```cpp
|
||
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.
|
||
```cpp
|
||
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.
|
||
```cpp
|
||
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
|
||
```cpp
|
||
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):
|
||
```cpp
|
||
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 `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.
|
||
|
||
Typical types:
|
||
- `ConsoleMessageType::Info`
|
||
- `ConsoleMessageType::Success`
|
||
- `ConsoleMessageType::Warning`
|
||
- `ConsoleMessageType::Error`
|
||
|
||
Warn-once pattern:
|
||
```cpp
|
||
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:
|
||
```cpp
|
||
#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 `Cache/ScriptBin/`.
|
||
2. In the main menu, go to **View -> Scripted Windows** and toggle the entry.
|
||
|
||
## Manual compile (CLI)
|
||
Linux:
|
||
```bash
|
||
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:
|
||
```bat
|
||
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_OnInspector` must be exported with `extern "C"` (no wrapper is generated for it).
|
||
- **Changes not saved**
|
||
- Call `ctx.MarkDirty()` after mutating transforms/settings you want to persist.
|
||
- **Editor window not showing**
|
||
- Ensure `RenderEditorWindow` is exported with `extern "C"` and the 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 (wrapper-based)
|
||
```cpp
|
||
#include "ScriptRuntime.h"
|
||
|
||
void TickUpdate(ScriptContext& ctx, float /*dt*/) {
|
||
if (!ctx.object) return;
|
||
}
|
||
```
|
||
|
||
### Minimal script with inspector (manual export)
|
||
```cpp
|
||
#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:
|
||
```cpp
|
||
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.
|