Better Physics a little, New UI! And not only that, More simple scripting, Yey!!!!

This commit is contained in:
Anemunt
2026-01-01 00:35:51 -05:00
parent ac1fab021c
commit b5bbbc2937
18 changed files with 2528 additions and 373 deletions

View File

@@ -1,117 +1,400 @@
# Modularity C++ Scripting Quickstart
---
title: C++ Scripting
description: Hot-compiled native C++ scripts, per-object state, ImGui inspectors, and runtime/editor hooks.
---
## Project setup
- Scripts live under `Scripts/` (configurable via `Scripts.modu`).
- The engine generates a wrapper per script when compiling. It exports fixed entry points with `extern "C"` linkage:
- `Script_OnInspector(ScriptContext&)`
- `Script_Begin(ScriptContext&, float deltaTime)`
- `Script_Spec(ScriptContext&, float deltaTime)`
- `Script_TestEditor(ScriptContext&, float deltaTime)`
- `Script_Update(ScriptContext&, float deltaTime)` (fallback if TickUpdate is absent)
- `Script_TickUpdate(ScriptContext&, float deltaTime)`
- Build config file: `Scripts.modu` (auto-created per project). Keys:
- `scriptsDir`, `outDir`, `includeDir=...`, `define=...`, `linux.linkLib`, `win.linkLib`, `cppStandard`.
# 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 Inspectors 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
- **Inspector**: `Script_OnInspector(ScriptContext&)` is called when the script is inspected in the UI.
- **Begin**: `Script_Begin` runs once per object instance before ticking.
- **Spec/Test**: `Script_Spec` and `Script_TestEditor` run every frame when the global “Spec Mode” / “Test Mode” toggles are enabled (Scripts menu).
- **Tick**: `Script_TickUpdate` runs every frame for each script; `Script_Update` is a fallback if TickUpdate is missing.
- All tick-style hooks receive `deltaTime` (seconds) and the `ScriptContext`.
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.
## ScriptContext helpers
Available methods:
- `FindObjectByName`, `FindObjectById`
- `SetPosition`, `SetRotation`, `SetScale`
- `HasRigidbody`
- `SetRigidbodyVelocity`, `GetRigidbodyVelocity`
- `SetRigidbodyAngularVelocity`, `GetRigidbodyAngularVelocity`
- `AddRigidbodyForce`, `AddRigidbodyImpulse`
- `AddRigidbodyTorque`, `AddRigidbodyAngularImpulse`
- `SetRigidbodyRotation`, `TeleportRigidbody`
- `MarkDirty` (flags the project as having unsaved changes)
Fields:
- `engine`: pointer to the Engine
- `object`: pointer to the owning `SceneObject`
- `script`: pointer to the owning `ScriptComponent` (gives access to per-script `settings`)
- `engine` (`Engine*`) - engine pointer
- `object` (`SceneObject*`) - owning object pointer (may be null)
- `script` (`ScriptComponent*`) - owning script component (settings storage)
## Persisting per-script settings
- Each `ScriptComponent` has `settings` (key/value strings) serialized with the scene.
- You can read/write them via `ctx.script->settings` or helper functions in your script.
- After mutating settings or object transforms, call `ctx.MarkDirty()` so Ctrl+S captures changes.
### Object lookup
- `FindObjectByName(const std::string&)`
- `FindObjectById(int)`
## Example pattern (simplified)
### 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
static bool autoRotate = false;
static glm::vec3 speed = {0, 45, 0};
#include "ScriptRuntime.h"
#include "ThirdParty/imgui/imgui.h"
void Script_OnInspector(ScriptContext& ctx) {
static bool autoRotate = false;
extern "C" void Script_OnInspector(ScriptContext& ctx) {
ImGui::Checkbox("Auto Rotate", &autoRotate);
ImGui::DragFloat3("Speed", &speed.x, 1.f, -360.f, 360.f);
ctx.MarkDirty();
}
```
void Script_Begin(ScriptContext& ctx, float) {
ctx.MarkDirty(); // ensure initial state is saved
> 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();
}
```
void Script_TickUpdate(ScriptContext& ctx, float dt) {
if (autoRotate && ctx.object) {
ctx.SetRotation(ctx.object->rotation + speed * dt);
### 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!");
}
}
```
## Runtime behavior
- Scripts tick for all objects every frame, even if not selected.
- Spec/Test toggles are global (main menu → Scripts).
- Compile scripts via the UI “Compile Script” button or run the build command; wrapper generation is automatic.
## Rigidbody helper usage
- `SetRigidbodyAngularVelocity(vec3)` sets angular velocity in radians/sec for dynamic, non-kinematic bodies.
### Sliders as meters (health/ammo)
Set `Interactable` to false to make a slider read-only.
```cpp
ctx.SetRigidbodyAngularVelocity({0.0f, 3.0f, 0.0f});
```
- `GetRigidbodyAngularVelocity(out vec3)` reads current angular velocity into `out`. Returns false if unavailable.
```cpp
glm::vec3 angVel;
if (ctx.GetRigidbodyAngularVelocity(angVel)) {
ctx.AddConsoleMessage("AngVel Y: " + std::to_string(angVel.y));
void TickUpdate(ScriptContext& ctx, float) {
ctx.SetUIInteractable(false);
ctx.SetUISliderStyle(UISliderStyle::Fill);
ctx.SetUISliderRange(0.0f, 100.0f);
ctx.SetUISliderValue(health);
}
```
- `AddRigidbodyForce(vec3)` applies continuous force (mass-aware).
### Style presets
You can register custom ImGui style presets in code and then select them per UI element in the Inspector.
```cpp
ctx.AddRigidbodyForce({0.0f, 0.0f, 25.0f});
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);
}
```
- `AddRigidbodyImpulse(vec3)` applies an instant impulse (mass-aware).
Then select **UI -> Style Preset** on a button or slider.
### Finding other UI objects
```cpp
ctx.AddRigidbodyImpulse({0.0f, 6.5f, 0.0f});
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!");
}
}
}
```
- `AddRigidbodyTorque(vec3)` applies continuous torque.
## 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 its registered.
Start/stop macros:
- `IEnum_Start(fn)` / `IEnum_Stop(fn)` / `IEnum_Ensure(fn)`
Example (toggle rotation without cluttering TickUpdate):
```cpp
ctx.AddRigidbodyTorque({0.0f, 15.0f, 0.0f});
```
- `AddRigidbodyAngularImpulse(vec3)` applies an instant angular impulse.
```cpp
ctx.AddRigidbodyAngularImpulse({0.0f, 4.0f, 0.0f});
```
- `SetRigidbodyRotation(vec3 degrees)` teleports the rigidbody rotation.
```cpp
ctx.SetRigidbodyRotation({0.0f, 90.0f, 0.0f});
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:
- These return false if the object has no enabled rigidbody or is kinematic.
- Use force/torque for continuous input and impulses for bursty actions.
- `SetRigidbodyRotation` is authoritative; use it sparingly during gameplay.
- Tasks are stored per `ScriptComponent` instance.
- Dont 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 example:
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 example:
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 dont 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.