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

44
Scripts/FPSDisplay.cpp Normal file
View File

@@ -0,0 +1,44 @@
#include "ScriptRuntime.h"
#include "SceneObject.h"
#include "ThirdParty/imgui/imgui.h"
#include <string>
#include <algorithm>
namespace {
bool clampTo120 = false;
float smoothFps = 0.0f;
float smoothing = 0.15f;
}
extern "C" void Script_OnInspector(ScriptContext& ctx) {
ctx.AutoSetting("ClampFPS120", clampTo120);
if (ctx.script) {
std::string saved = ctx.GetSetting("FpsSmoothing", "");
if (!saved.empty()) {
try { smoothing = std::stof(saved); } catch (...) {}
}
}
bool changed = false;
ImGui::TextUnformatted("FPS Display");
ImGui::Separator();
changed |= ImGui::Checkbox("Clamp FPS to 120", &clampTo120);
changed |= ImGui::DragFloat("Smoothing", &smoothing, 0.01f, 0.0f, 1.0f, "%.2f");
if (changed) {
ctx.SetFPSCap(clampTo120, 120.0f);
ctx.SetSetting("FpsSmoothing", std::to_string(smoothing));
ctx.SaveAutoSettings();
}
ImGui::TextDisabled("Attach to a UI Text object.");
}
void TickUpdate(ScriptContext& ctx, float deltaTime) {
if (!ctx.object || ctx.object->type != ObjectType::UIText) return;
float fps = (deltaTime > 1e-6f) ? (1.0f / deltaTime) : 0.0f;
float k = std::clamp(smoothing, 0.0f, 1.0f);
if (smoothFps <= 0.0f) smoothFps = fps;
smoothFps = smoothFps + (fps - smoothFps) * k;
ctx.object->ui.label = "FPS: " + std::to_string(static_cast<int>(smoothFps + 0.5f));
}

View File

@@ -36,15 +36,10 @@ extern "C" void Script_OnInspector(ScriptContext& ctx) {
ImGui::TextUnformatted("RigidbodyTest"); ImGui::TextUnformatted("RigidbodyTest");
ImGui::Separator(); ImGui::Separator();
bool changed = false; ImGui::Checkbox("Launch on Begin", &autoLaunch);
changed |= ImGui::Checkbox("Launch on Begin", &autoLaunch); ImGui::Checkbox("Show Velocity Readback", &showVelocity);
changed |= ImGui::Checkbox("Show Velocity Readback", &showVelocity); ImGui::DragFloat3("Launch Velocity", &launchVelocity.x, 0.25f, -50.0f, 50.0f);
changed |= ImGui::DragFloat3("Launch Velocity", &launchVelocity.x, 0.25f, -50.0f, 50.0f); ImGui::DragFloat3("Teleport Offset", &teleportOffset.x, 0.1f, -10.0f, 10.0f);
changed |= ImGui::DragFloat3("Teleport Offset", &teleportOffset.x, 0.1f, -10.0f, 10.0f);
if (changed) {
ctx.SaveAutoSettings();
}
if (ImGui::Button("Launch Now")) { if (ImGui::Button("Launch Now")) {
Launch(ctx); Launch(ctx);
@@ -76,4 +71,4 @@ void Begin(ScriptContext& ctx, float /*deltaTime*/) {
if (autoLaunch) { if (autoLaunch) {
Launch(ctx); Launch(ctx);
} }
} }

View File

@@ -3,13 +3,11 @@
#include "ThirdParty/imgui/imgui.h" #include "ThirdParty/imgui/imgui.h"
namespace { namespace {
// Script state (persisted by AutoSetting binder)
bool autoRotate = false; bool autoRotate = false;
glm::vec3 spinSpeed = glm::vec3(0.0f, 45.0f, 0.0f); // deg/sec glm::vec3 spinSpeed = glm::vec3(0.0f, 45.0f, 0.0f); // deg/sec
glm::vec3 offset = glm::vec3(0.0f, 1.0f, 0.0f); glm::vec3 offset = glm::vec3(0.0f, 1.0f, 0.0f);
char targetName[128] = "MyTarget"; char targetName[128] = "MyTarget";
// Runtime behavior
static void ApplyAutoRotate(ScriptContext& ctx, float deltaTime) { static void ApplyAutoRotate(ScriptContext& ctx, float deltaTime) {
if (!autoRotate || !ctx.object) return; if (!autoRotate || !ctx.object) return;
ctx.SetRotation(ctx.object->rotation + spinSpeed * deltaTime); ctx.SetRotation(ctx.object->rotation + spinSpeed * deltaTime);
@@ -17,7 +15,6 @@ namespace {
} }
extern "C" void Script_OnInspector(ScriptContext& ctx) { extern "C" void Script_OnInspector(ScriptContext& ctx) {
// Auto settings (loaded once, saved only when changed)
ctx.AutoSetting("autoRotate", autoRotate); ctx.AutoSetting("autoRotate", autoRotate);
ctx.AutoSetting("spinSpeed", spinSpeed); ctx.AutoSetting("spinSpeed", spinSpeed);
ctx.AutoSetting("offset", offset); ctx.AutoSetting("offset", offset);
@@ -26,15 +23,10 @@ extern "C" void Script_OnInspector(ScriptContext& ctx) {
ImGui::TextUnformatted("SampleInspector"); ImGui::TextUnformatted("SampleInspector");
ImGui::Separator(); ImGui::Separator();
bool changed = false; ImGui::Checkbox("Auto Rotate", &autoRotate);
changed |= ImGui::Checkbox("Auto Rotate", &autoRotate); ImGui::DragFloat3("Spin Speed (deg/s)", &spinSpeed.x, 1.0f, -360.0f, 360.0f);
changed |= ImGui::DragFloat3("Spin Speed (deg/s)", &spinSpeed.x, 1.0f, -360.0f, 360.0f); ImGui::DragFloat3("Offset", &offset.x, 0.1f);
changed |= ImGui::DragFloat3("Offset", &offset.x, 0.1f); ImGui::InputText("Target Name", targetName, sizeof(targetName));
changed |= ImGui::InputText("Target Name", targetName, sizeof(targetName));
if (changed) {
ctx.SaveAutoSettings();
}
if (ctx.object) { if (ctx.object) {
ImGui::TextDisabled("Attached to: %s (id=%d)", ctx.object->name.c_str(), ctx.object->id); ImGui::TextDisabled("Attached to: %s (id=%d)", ctx.object->name.c_str(), ctx.object->id);
@@ -44,15 +36,12 @@ extern "C" void Script_OnInspector(ScriptContext& ctx) {
} }
} }
if (ImGui::Button("Nudge Target")) { if (ImGui::Button("Nudge Target")) {
if (SceneObject* target = ctx.FindObjectByName(targetName)) { if (SceneObject* target = ctx.ResolveObjectRef(targetName)) {
target->position += offset; target->position += offset;
} }
} }
} }
void Begin(ScriptContext& ctx, float /*deltaTime*/) {
}
void Spec(ScriptContext& ctx, float deltaTime) { void Spec(ScriptContext& ctx, float deltaTime) {
ApplyAutoRotate(ctx, deltaTime); ApplyAutoRotate(ctx, deltaTime);
} }

View File

@@ -8,101 +8,41 @@
#include "ScriptRuntime.h" #include "ScriptRuntime.h"
#include "SceneObject.h" #include "SceneObject.h"
#include "ThirdParty/imgui/imgui.h" #include "ThirdParty/imgui/imgui.h"
#include <string>
#include <algorithm>
#include <sstream>
namespace { namespace {
bool autoRotate = false; bool autoRotate = false;
glm::vec3 spinSpeed = glm::vec3(0.0f, 45.0f, 0.0f); glm::vec3 spinSpeed = glm::vec3(0.0f, 45.0f, 0.0f);
glm::vec3 offset = glm::vec3(0.0f, 1.0f, 0.0f); glm::vec3 offset = glm::vec3(0.0f, 1.0f, 0.0f);
char targetName[128] = "MyTarget"; char targetName[128] = "MyTarget";
int settingsLoadedForId = -1;
ScriptComponent* settingsLoadedForScript = nullptr;
void setSetting(ScriptContext& ctx, const std::string& key, const std::string& value) { void bindSettings(ScriptContext& ctx) {
if (!ctx.script) return; ctx.AutoSetting("autoRotate", autoRotate);
auto it = std::find_if(ctx.script->settings.begin(), ctx.script->settings.end(), ctx.AutoSetting("spinSpeed", spinSpeed);
[&](const ScriptSetting& s) { return s.key == key; }); ctx.AutoSetting("offset", offset);
if (it != ctx.script->settings.end()) { ctx.AutoSetting("targetName", targetName, sizeof(targetName));
it->value = value;
} else {
ctx.script->settings.push_back({key, value});
}
}
std::string getSetting(const ScriptContext& ctx, const std::string& key, const std::string& fallback = "") {
if (!ctx.script) return fallback;
auto it = std::find_if(ctx.script->settings.begin(), ctx.script->settings.end(),
[&](const ScriptSetting& s) { return s.key == key; });
return (it != ctx.script->settings.end()) ? it->value : fallback;
}
void loadSettings(ScriptContext& ctx) {
if (!ctx.script || !ctx.object) return;
if (settingsLoadedForId == ctx.object->id && settingsLoadedForScript == ctx.script) return;Segmentation fault (core dumped)
settingsLoadedForId = ctx.object->id;
settingsLoadedForScript = ctx.script;
auto parseBool = [](const std::string& v, bool def) {
if (v == "1" || v == "true") return true;
if (v == "0" || v == "false") return false;
return def;
};
auto parseVec3 = [](const std::string& v, const glm::vec3& def) {
glm::vec3 out = def;
std::stringstream ss(v);
std::string part;
for (int i = 0; i < 3 && std::getline(ss, part, ','); ++i) {
try { out[i] = std::stof(part); } catch (...) {}
}
return out;
};
autoRotate = parseBool(getSetting(ctx, "autoRotate", autoRotate ? "1" : "0"), autoRotate);
spinSpeed = parseVec3(getSetting(ctx, "spinSpeed", ""), spinSpeed);
offset = parseVec3(getSetting(ctx, "offset", ""), offset);
std::string tgt = getSetting(ctx, "targetName", targetName);
if (!tgt.empty()) {
std::snprintf(targetName, sizeof(targetName), "%s", tgt.c_str());
}
}
void persistSettings(ScriptContext& ctx) {
setSetting(ctx, "autoRotate", autoRotate ? "1" : "0");
setSetting(ctx, "spinSpeed",
std::to_string(spinSpeed.x) + "," + std::to_string(spinSpeed.y) + "," + std::to_string(spinSpeed.z));
setSetting(ctx, "offset",
std::to_string(offset.x) + "," + std::to_string(offset.y) + "," + std::to_string(offset.z));
setSetting(ctx, "targetName", targetName);
ctx.MarkDirty();
} }
void applyAutoRotate(ScriptContext& ctx, float deltaTime) { void applyAutoRotate(ScriptContext& ctx, float deltaTime) {
if (!autoRotate || !ctx.object) return; if (!autoRotate || !ctx.object) return;
if (ctx.HasRigidbody() && !ctx.object->rigidbody.isKinematic) {
if (ctx.SetRigidbodyAngularVelocity(glm::radians(spinSpeed))) {
return;
}
}
ctx.SetRotation(ctx.object->rotation + spinSpeed * deltaTime); ctx.SetRotation(ctx.object->rotation + spinSpeed * deltaTime);
} }
} // namespace } // namespace
extern "C" void Script_OnInspector(ScriptContext& ctx) { extern "C" void Script_OnInspector(ScriptContext& ctx) {
loadSettings(ctx); bindSettings(ctx);
ImGui::TextUnformatted("SampleInspector"); ImGui::TextUnformatted("SampleInspector");
ImGui::Separator(); ImGui::Separator();
if (ImGui::Checkbox("Auto Rotate", &autoRotate)) { ImGui::Checkbox("Auto Rotate", &autoRotate);
persistSettings(ctx); ImGui::DragFloat3("Spin Speed (deg/s)", &spinSpeed.x, 1.0f, -360.0f, 360.0f);
} ImGui::DragFloat3("Offset", &offset.x, 0.1f);
if (ImGui::DragFloat3("Spin Speed (deg/s)", &spinSpeed.x, 1.0f, -360.0f, 360.0f)) {
persistSettings(ctx);
}
if (ImGui::DragFloat3("Offset", &offset.x, 0.1f)) {
persistSettings(ctx);
}
ImGui::InputText("Target Name", targetName, sizeof(targetName)); ImGui::InputText("Target Name", targetName, sizeof(targetName));
persistSettings(ctx);
if (ctx.object) { if (ctx.object) {
ImGui::TextDisabled("Attached to: %s (id=%d)", ctx.object->name.c_str(), ctx.object->id); ImGui::TextDisabled("Attached to: %s (id=%d)", ctx.object->name.c_str(), ctx.object->id);
@@ -113,7 +53,7 @@ extern "C" void Script_OnInspector(ScriptContext& ctx) {
} }
if (ImGui::Button("Nudge Target")) { if (ImGui::Button("Nudge Target")) {
if (SceneObject* target = ctx.FindObjectByName(targetName)) { if (SceneObject* target = ctx.ResolveObjectRef(targetName)) {
target->position += offset; target->position += offset;
} }
} }
@@ -122,7 +62,7 @@ extern "C" void Script_OnInspector(ScriptContext& ctx) {
// New lifecycle hooks supported by the compiler wrapper. These are optional stubs demonstrating usage. // New lifecycle hooks supported by the compiler wrapper. These are optional stubs demonstrating usage.
void Begin(ScriptContext& ctx, float /*deltaTime*/) { void Begin(ScriptContext& ctx, float /*deltaTime*/) {
// Initialize per-script state here. // Initialize per-script state here.
loadSettings(ctx); bindSettings(ctx);
} }
void Spec(ScriptContext& ctx, float deltaTime) { void Spec(ScriptContext& ctx, float deltaTime) {

View File

@@ -8,6 +8,8 @@ struct ControllerState {
float pitch = 0.0f; float pitch = 0.0f;
float yaw = 0.0f; float yaw = 0.0f;
float verticalVelocity = 0.0f; float verticalVelocity = 0.0f;
glm::vec3 debugVelocity = glm::vec3(0.0f);
bool debugGrounded = false;
bool initialized = false; bool initialized = false;
}; };
@@ -38,23 +40,6 @@ void bindSettings(ScriptContext& ctx) {
ctx.AutoSetting("enforceRigidbody", enforceRigidbody); ctx.AutoSetting("enforceRigidbody", enforceRigidbody);
ctx.AutoSetting("showDebug", showDebug); ctx.AutoSetting("showDebug", showDebug);
} }
void ensureComponents(ScriptContext& ctx, float height, float radius) {
if (!ctx.object) return;
if (enforceCollider) {
ctx.object->hasCollider = true;
ctx.object->collider.enabled = true;
ctx.object->collider.type = ColliderType::Capsule;
ctx.object->collider.convex = true;
ctx.object->collider.boxSize = glm::vec3(radius * 2.0f, height, radius * 2.0f);
}
if (enforceRigidbody) {
ctx.object->hasRigidbody = true;
ctx.object->rigidbody.enabled = true;
ctx.object->rigidbody.useGravity = true;
ctx.object->rigidbody.isKinematic = false;
}
}
} // namespace } // namespace
extern "C" void Script_OnInspector(ScriptContext& ctx) { extern "C" void Script_OnInspector(ScriptContext& ctx) {
@@ -63,19 +48,24 @@ extern "C" void Script_OnInspector(ScriptContext& ctx) {
ImGui::TextUnformatted("Standalone Movement Controller"); ImGui::TextUnformatted("Standalone Movement Controller");
ImGui::Separator(); ImGui::Separator();
bool changed = false; ImGui::DragFloat3("Walk/Run/Jump", &moveTuning.x, 0.05f, 0.0f, 25.0f, "%.2f");
changed |= ImGui::DragFloat3("Walk/Run/Jump", &moveTuning.x, 0.05f, 0.0f, 25.0f, "%.2f"); ImGui::DragFloat2("Look Sens/Clamp", &lookTuning.x, 0.01f, 0.0f, 500.0f, "%.2f");
changed |= ImGui::DragFloat2("Look Sens/Clamp", &lookTuning.x, 0.01f, 0.0f, 500.0f, "%.2f"); ImGui::DragFloat3("Height/Radius/Snap", &capsuleTuning.x, 0.02f, 0.0f, 5.0f, "%.2f");
changed |= ImGui::DragFloat3("Height/Radius/Snap", &capsuleTuning.x, 0.02f, 0.0f, 5.0f, "%.2f"); ImGui::DragFloat3("Gravity/Probe/MaxFall", &gravityTuning.x, 0.05f, -50.0f, 50.0f, "%.2f");
changed |= ImGui::DragFloat3("Gravity/Probe/MaxFall", &gravityTuning.x, 0.05f, -50.0f, 50.0f, "%.2f"); ImGui::Checkbox("Enable Mouse Look", &enableMouseLook);
changed |= ImGui::Checkbox("Enable Mouse Look", &enableMouseLook); ImGui::Checkbox("Hold RMB to Look", &requireMouseButton);
changed |= ImGui::Checkbox("Hold RMB to Look", &requireMouseButton); ImGui::Checkbox("Force Collider", &enforceCollider);
changed |= ImGui::Checkbox("Force Collider", &enforceCollider); ImGui::Checkbox("Force Rigidbody", &enforceRigidbody);
changed |= ImGui::Checkbox("Force Rigidbody", &enforceRigidbody); ImGui::Checkbox("Show Debug", &showDebug);
changed |= ImGui::Checkbox("Show Debug", &showDebug);
if (changed) { if (showDebug && ctx.object) {
ctx.SaveAutoSettings(); auto it = g_states.find(ctx.object->id);
if (it != g_states.end()) {
const ControllerState& state = it->second;
ImGui::Separator();
ImGui::Text("Move (%.2f, %.2f, %.2f)", state.debugVelocity.x, state.debugVelocity.y, state.debugVelocity.z);
ImGui::Text("Grounded: %s", state.debugGrounded ? "yes" : "no");
}
} }
} }
@@ -89,14 +79,16 @@ void Begin(ScriptContext& ctx, float /*deltaTime*/) {
state.verticalVelocity = 0.0f; state.verticalVelocity = 0.0f;
state.initialized = true; state.initialized = true;
} }
ensureComponents(ctx, capsuleTuning.x, capsuleTuning.y); if (enforceCollider) ctx.EnsureCapsuleCollider(capsuleTuning.x, capsuleTuning.y);
if (enforceRigidbody) ctx.EnsureRigidbody(true, false);
} }
void TickUpdate(ScriptContext& ctx, float deltaTime) { void TickUpdate(ScriptContext& ctx, float deltaTime) {
if (!ctx.object) return; if (!ctx.object) return;
ControllerState& state = getState(ctx.object->id); ControllerState& state = getState(ctx.object->id);
ensureComponents(ctx, capsuleTuning.x, capsuleTuning.y); if (enforceCollider) ctx.EnsureCapsuleCollider(capsuleTuning.x, capsuleTuning.y);
if (enforceRigidbody) ctx.EnsureRigidbody(true, false);
const float walkSpeed = moveTuning.x; const float walkSpeed = moveTuning.x;
const float runSpeed = moveTuning.y; const float runSpeed = moveTuning.y;
@@ -125,17 +117,9 @@ void TickUpdate(ScriptContext& ctx, float deltaTime) {
} }
} }
glm::quat q = glm::quat(glm::radians(glm::vec3(state.pitch, state.yaw, 0.0f))); glm::vec3 planarForward(0.0f);
glm::vec3 forward = glm::normalize(q * glm::vec3(0.0f, 0.0f, -1.0f)); glm::vec3 planarRight(0.0f);
glm::vec3 right = glm::normalize(q * glm::vec3(1.0f, 0.0f, 0.0f)); ctx.GetPlanarYawPitchVectors(state.pitch, state.yaw, planarForward, planarRight);
glm::vec3 planarForward = glm::normalize(glm::vec3(forward.x, 0.0f, forward.z));
glm::vec3 planarRight = glm::normalize(glm::vec3(right.x, 0.0f, right.z));
if (!std::isfinite(planarForward.x) || glm::length(planarForward) < 1e-3f) {
planarForward = glm::vec3(0.0f, 0.0f, -1.0f);
}
if (!std::isfinite(planarRight.x) || glm::length(planarRight) < 1e-3f) {
planarRight = glm::vec3(1.0f, 0.0f, 0.0f);
}
glm::vec3 move(0.0f); glm::vec3 move(0.0f);
if (ImGui::IsKeyDown(ImGuiKey_W)) move += planarForward; if (ImGui::IsKeyDown(ImGuiKey_W)) move += planarForward;
@@ -195,8 +179,8 @@ void TickUpdate(ScriptContext& ctx, float deltaTime) {
} }
if (showDebug) { if (showDebug) {
ImGui::Text("Move (%.2f, %.2f, %.2f)", velocity.x, velocity.y, velocity.z); state.debugVelocity = velocity;
ImGui::Text("Grounded: %s", grounded ? "yes" : "no"); state.debugGrounded = grounded;
} }
} }

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 # C++ Scripting
- Scripts live under `Scripts/` (configurable via `Scripts.modu`). 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.
- The engine generates a wrapper per script when compiling. It exports fixed entry points with `extern "C"` linkage:
- `Script_OnInspector(ScriptContext&)` > Notes up front:
- `Script_Begin(ScriptContext&, float deltaTime)` > - Scripts are not sandboxed. They can crash the editor/game if they dereference bad pointers or do unsafe work.
- `Script_Spec(ScriptContext&, float deltaTime)` > - Always null-check `ctx.object` (objects can be deleted, disabled, or scripts can be detached).
- `Script_TestEditor(ScriptContext&, float deltaTime)`
- `Script_Update(ScriptContext&, float deltaTime)` (fallback if TickUpdate is absent) ## Table of contents
- `Script_TickUpdate(ScriptContext&, float deltaTime)` - [Quickstart](#quickstart)
- Build config file: `Scripts.modu` (auto-created per project). Keys: - [Scripts.modu](#scriptsmodu)
- `scriptsDir`, `outDir`, `includeDir=...`, `define=...`, `linux.linkLib`, `win.linkLib`, `cppStandard`. - [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 ## Lifecycle hooks
- **Inspector**: `Script_OnInspector(ScriptContext&)` is called when the script is inspected in the UI. All hooks are optional. If a hook is missing, it is simply not called.
- **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). Hook list:
- **Tick**: `Script_TickUpdate` runs every frame for each script; `Script_Update` is a fallback if TickUpdate is missing. - `Script_OnInspector(ScriptContext&)` (manual export required)
- All tick-style hooks receive `deltaTime` (seconds) and the `ScriptContext`. - `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: Fields:
- `engine`: pointer to the Engine - `engine` (`Engine*`) - engine pointer
- `object`: pointer to the owning `SceneObject` - `object` (`SceneObject*`) - owning object pointer (may be null)
- `script`: pointer to the owning `ScriptComponent` (gives access to per-script `settings`) - `script` (`ScriptComponent*`) - owning script component (settings storage)
## Persisting per-script settings ### Object lookup
- Each `ScriptComponent` has `settings` (key/value strings) serialized with the scene. - `FindObjectByName(const std::string&)`
- You can read/write them via `ctx.script->settings` or helper functions in your script. - `FindObjectById(int)`
- After mutating settings or object transforms, call `ctx.MarkDirty()` so Ctrl+S captures changes.
## 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 ```cpp
static bool autoRotate = false; #include "ScriptRuntime.h"
static glm::vec3 speed = {0, 45, 0}; #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::Checkbox("Auto Rotate", &autoRotate);
ImGui::DragFloat3("Speed", &speed.x, 1.f, -360.f, 360.f);
ctx.MarkDirty(); ctx.MarkDirty();
} }
```
void Script_Begin(ScriptContext& ctx, float) { > Tip: `Script_OnInspector` must be exported exactly with `extern "C"` (it is not wrapper-generated).
ctx.MarkDirty(); // ensure initial state is saved > 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) { ### AutoSetting (recommended for inspector UI)
if (autoRotate && ctx.object) { `AutoSetting` binds a variable to a key and loads/saves automatically when you call `SaveAutoSettings()`.
ctx.SetRotation(ctx.object->rotation + speed * dt); ```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 ### Sliders as meters (health/ammo)
- Scripts tick for all objects every frame, even if not selected. Set `Interactable` to false to make a slider read-only.
- 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.
```cpp ```cpp
ctx.SetRigidbodyAngularVelocity({0.0f, 3.0f, 0.0f}); void TickUpdate(ScriptContext& ctx, float) {
``` ctx.SetUIInteractable(false);
- `GetRigidbodyAngularVelocity(out vec3)` reads current angular velocity into `out`. Returns false if unavailable. ctx.SetUISliderStyle(UISliderStyle::Fill);
```cpp ctx.SetUISliderRange(0.0f, 100.0f);
glm::vec3 angVel; ctx.SetUISliderValue(health);
if (ctx.GetRigidbodyAngularVelocity(angVel)) {
ctx.AddConsoleMessage("AngVel Y: " + std::to_string(angVel.y));
} }
``` ```
- `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 ```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 ```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 ```cpp
ctx.AddRigidbodyTorque({0.0f, 15.0f, 0.0f}); static bool autoRotate = false;
``` static glm::vec3 speed = {0, 45, 0};
- `AddRigidbodyAngularImpulse(vec3)` applies an instant angular impulse.
```cpp static void RotateTask(ScriptContext& ctx, float dt) {
ctx.AddRigidbodyAngularImpulse({0.0f, 4.0f, 0.0f}); if (!ctx.object) return;
``` ctx.SetRotation(ctx.object->rotation + speed * dt);
- `SetRigidbodyRotation(vec3 degrees)` teleports the rigidbody rotation. }
```cpp
ctx.SetRigidbodyRotation({0.0f, 90.0f, 0.0f}); extern "C" void Script_OnInspector(ScriptContext& ctx) {
ImGui::Checkbox("Auto Rotate", &autoRotate);
if (autoRotate) IEnum_Ensure(RotateTask);
else IEnum_Stop(RotateTask);
ctx.MarkDirty();
}
``` ```
Notes: Notes:
- These return false if the object has no enabled rigidbody or is kinematic. - Tasks are stored per `ScriptComponent` instance.
- Use force/torque for continuous input and impulses for bursty actions. - Dont spam logs every frame inside a task; use “warn once” patterns.
- `SetRigidbodyRotation` is authoritative; use it sparingly during gameplay.
## 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) ## Manual compile (CLI)
Linux example: Linux:
```bash ```bash
g++ -std=c++20 -fPIC -O0 -g -I../src -I../include -c SampleInspector.cpp -o ../Cache/ScriptBin/SampleInspector.o 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 g++ -shared ../Cache/ScriptBin/SampleInspector.o -o ../Cache/ScriptBin/SampleInspector.so -ldl -lpthread
``` ```
Windows example:
Windows:
```bat ```bat
cl /nologo /std:c++20 /EHsc /MD /Zi /Od /I ..\\src /I ..\\include /c SampleInspector.cpp /Fo ..\\Cache\\ScriptBin\\SampleInspector.obj 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 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.

View File

@@ -188,7 +188,7 @@ void Engine::renderLauncher() {
ImGui::SetWindowFontScale(1.4f); ImGui::SetWindowFontScale(1.4f);
ImGui::TextColored(ImVec4(0.95f, 0.96f, 0.98f, 1.0f), "Modularity"); ImGui::TextColored(ImVec4(0.95f, 0.96f, 0.98f, 1.0f), "Modularity");
ImGui::SetWindowFontScale(1.0f); ImGui::SetWindowFontScale(1.0f);
ImGui::TextColored(ImVec4(0.70f, 0.73f, 0.80f, 1.0f), "Modularity | Debug Build V0.7.0"); ImGui::TextColored(ImVec4(0.70f, 0.73f, 0.80f, 1.0f), "Modularity | Beta V1.0");
ImGui::EndChild(); ImGui::EndChild();
@@ -349,13 +349,10 @@ void Engine::renderLauncher() {
ImGui::Spacing(); ImGui::Spacing();
} }
} }
ImGui::Spacing(); ImGui::Spacing();
ImGui::Separator(); ImGui::Separator();
ImGui::Spacing(); ImGui::Spacing();
ImGui::TextDisabled("Modularity Engine - Beta V1.0");
ImGui::TextDisabled("Modularity Engine - Version 0.6.8");
ImGui::EndChild(); ImGui::EndChild();
} }

View File

@@ -29,10 +29,17 @@ namespace {
case ObjectType::AreaLight: return IM_COL32(255, 200, 90, 220); case ObjectType::AreaLight: return IM_COL32(255, 200, 90, 220);
case ObjectType::PostFXNode: return IM_COL32(200, 140, 230, 220); case ObjectType::PostFXNode: return IM_COL32(200, 140, 230, 220);
case ObjectType::OBJMesh: case ObjectType::OBJMesh:
case ObjectType::Model: return IM_COL32(120, 200, 150, 220); case ObjectType::Model:
case ObjectType::Sprite: return IM_COL32(120, 200, 150, 220);
case ObjectType::Mirror: return IM_COL32(180, 200, 210, 220); case ObjectType::Mirror: return IM_COL32(180, 200, 210, 220);
case ObjectType::Plane: return IM_COL32(170, 180, 190, 220); case ObjectType::Plane: return IM_COL32(170, 180, 190, 220);
case ObjectType::Torus: return IM_COL32(155, 215, 180, 220); case ObjectType::Torus: return IM_COL32(155, 215, 180, 220);
case ObjectType::Canvas: return IM_COL32(120, 180, 255, 220);
case ObjectType::UIImage:
case ObjectType::UISlider:
case ObjectType::UIButton:
case ObjectType::UIText:
case ObjectType::Sprite2D: return IM_COL32(160, 210, 255, 220);
default: return IM_COL32(140, 190, 235, 220); default: return IM_COL32(140, 190, 235, 220);
} }
} }
@@ -194,6 +201,25 @@ void Engine::renderHierarchyPanel() {
ImGuiPopupFlags_MouseButtonRight | ImGuiPopupFlags_MouseButtonRight |
ImGuiPopupFlags_NoOpenOverItems)) ImGuiPopupFlags_NoOpenOverItems))
{ {
auto createUIWithCanvas = [&](ObjectType type, const std::string& baseName) {
int canvasId = -1;
for (const auto& obj : sceneObjects) {
if (obj.type == ObjectType::Canvas) {
canvasId = obj.id;
break;
}
}
if (canvasId < 0) {
addObject(ObjectType::Canvas, "Canvas");
if (!sceneObjects.empty()) {
canvasId = sceneObjects.back().id;
}
}
addObject(type, baseName);
if (!sceneObjects.empty() && canvasId >= 0) {
setParent(sceneObjects.back().id, canvasId);
}
};
if (ImGui::BeginMenu("Create")) if (ImGui::BeginMenu("Create"))
{ {
// ── Primitives ───────────────────────────── // ── Primitives ─────────────────────────────
@@ -204,10 +230,23 @@ void Engine::renderHierarchyPanel() {
if (ImGui::MenuItem("Capsule")) addObject(ObjectType::Capsule, "Capsule"); if (ImGui::MenuItem("Capsule")) addObject(ObjectType::Capsule, "Capsule");
if (ImGui::MenuItem("Plane")) addObject(ObjectType::Plane, "Plane"); if (ImGui::MenuItem("Plane")) addObject(ObjectType::Plane, "Plane");
if (ImGui::MenuItem("Torus")) addObject(ObjectType::Torus, "Torus"); if (ImGui::MenuItem("Torus")) addObject(ObjectType::Torus, "Torus");
if (ImGui::MenuItem("Sprite (Quad)")) addObject(ObjectType::Sprite, "Sprite");
if (ImGui::MenuItem("Mirror")) addObject(ObjectType::Mirror, "Mirror"); if (ImGui::MenuItem("Mirror")) addObject(ObjectType::Mirror, "Mirror");
ImGui::EndMenu(); ImGui::EndMenu();
} }
if (ImGui::BeginMenu("RMesh"))
{
if (ImGui::BeginMenu("Primitives"))
{
if (ImGui::MenuItem("Cube")) createRMeshPrimitive("Cube");
if (ImGui::MenuItem("Sphere")) createRMeshPrimitive("Sphere");
if (ImGui::MenuItem("Plane")) createRMeshPrimitive("Plane");
ImGui::EndMenu();
}
ImGui::EndMenu();
}
// ── Lights ──────────────────────────────── // ── Lights ────────────────────────────────
if (ImGui::BeginMenu("Lights")) if (ImGui::BeginMenu("Lights"))
{ {
@@ -224,6 +263,16 @@ void Engine::renderHierarchyPanel() {
if (ImGui::MenuItem("Post FX Node")) addObject(ObjectType::PostFXNode, "Post FX"); if (ImGui::MenuItem("Post FX Node")) addObject(ObjectType::PostFXNode, "Post FX");
ImGui::EndMenu(); ImGui::EndMenu();
} }
if (ImGui::BeginMenu("2D/UI"))
{
if (ImGui::MenuItem("Canvas")) addObject(ObjectType::Canvas, "Canvas");
if (ImGui::MenuItem("UI Image")) createUIWithCanvas(ObjectType::UIImage, "UI Image");
if (ImGui::MenuItem("UI Slider")) createUIWithCanvas(ObjectType::UISlider, "UI Slider");
if (ImGui::MenuItem("UI Button")) createUIWithCanvas(ObjectType::UIButton, "UI Button");
if (ImGui::MenuItem("UI Text")) createUIWithCanvas(ObjectType::UIText, "UI Text");
if (ImGui::MenuItem("Sprite2D")) createUIWithCanvas(ObjectType::Sprite2D, "Sprite2D");
ImGui::EndMenu();
}
if (ImGui::MenuItem("Camera")) addObject(ObjectType::Camera, "Camera"); if (ImGui::MenuItem("Camera")) addObject(ObjectType::Camera, "Camera");
ImGui::EndMenu(); ImGui::EndMenu();
@@ -341,6 +390,20 @@ void Engine::renderObjectNode(SceneObject& obj, const std::string& filter,
deleteSelected(); deleteSelected();
} }
ImGui::Separator(); ImGui::Separator();
if (obj.type == ObjectType::Canvas && ImGui::BeginMenu("Create UI Child")) {
auto createChild = [&](ObjectType type, const std::string& baseName) {
addObject(type, baseName);
if (!sceneObjects.empty()) {
setParent(sceneObjects.back().id, obj.id);
}
};
if (ImGui::MenuItem("UI Image")) createChild(ObjectType::UIImage, "UI Image");
if (ImGui::MenuItem("UI Slider")) createChild(ObjectType::UISlider, "UI Slider");
if (ImGui::MenuItem("UI Button")) createChild(ObjectType::UIButton, "UI Button");
if (ImGui::MenuItem("UI Text")) createChild(ObjectType::UIText, "UI Text");
if (ImGui::MenuItem("Sprite2D")) createChild(ObjectType::Sprite2D, "Sprite2D");
ImGui::EndMenu();
}
if (ImGui::MenuItem("Clear Parent") && obj.parentId != -1) { if (ImGui::MenuItem("Clear Parent") && obj.parentId != -1) {
setParent(obj.id, -1); setParent(obj.id, -1);
} }
@@ -901,6 +964,14 @@ void Engine::renderInspectorPanel() {
SceneObject& obj = *it; SceneObject& obj = *it;
ImGui::PushID(obj.id); // Scope per-object widgets to avoid ID collisions ImGui::PushID(obj.id); // Scope per-object widgets to avoid ID collisions
auto isUIObjectType = [](ObjectType type) {
return type == ObjectType::Canvas ||
type == ObjectType::UIImage ||
type == ObjectType::UISlider ||
type == ObjectType::UIButton ||
type == ObjectType::UIText ||
type == ObjectType::Sprite2D;
};
if (selectedObjectIds.size() > 1) { if (selectedObjectIds.size() > 1) {
ImGui::Text("Multiple objects selected: %zu", selectedObjectIds.size()); ImGui::Text("Multiple objects selected: %zu", selectedObjectIds.size());
@@ -931,6 +1002,13 @@ void Engine::renderInspectorPanel() {
case ObjectType::Capsule: typeLabel = "Capsule"; break; case ObjectType::Capsule: typeLabel = "Capsule"; break;
case ObjectType::OBJMesh: typeLabel = "OBJ Mesh"; break; case ObjectType::OBJMesh: typeLabel = "OBJ Mesh"; break;
case ObjectType::Model: typeLabel = "Model"; break; case ObjectType::Model: typeLabel = "Model"; break;
case ObjectType::Sprite: typeLabel = "Sprite"; break;
case ObjectType::Sprite2D: typeLabel = "Sprite2D"; break;
case ObjectType::Canvas: typeLabel = "Canvas"; break;
case ObjectType::UIImage: typeLabel = "UI Image"; break;
case ObjectType::UISlider: typeLabel = "UI Slider"; break;
case ObjectType::UIButton: typeLabel = "UI Button"; break;
case ObjectType::UIText: typeLabel = "UI Text"; break;
case ObjectType::Camera: typeLabel = "Camera"; break; case ObjectType::Camera: typeLabel = "Camera"; break;
case ObjectType::DirectionalLight: typeLabel = "Directional Light"; break; case ObjectType::DirectionalLight: typeLabel = "Directional Light"; break;
case ObjectType::PointLight: typeLabel = "Point Light"; break; case ObjectType::PointLight: typeLabel = "Point Light"; break;
@@ -986,6 +1064,9 @@ void Engine::renderInspectorPanel() {
if (obj.type == ObjectType::PostFXNode) { if (obj.type == ObjectType::PostFXNode) {
ImGui::TextDisabled("Transform is ignored for post-processing nodes."); ImGui::TextDisabled("Transform is ignored for post-processing nodes.");
} }
if (isUIObjectType(obj.type)) {
ImGui::TextDisabled("UI objects use the UI section for positioning.");
}
ImGui::Text("Position"); ImGui::Text("Position");
ImGui::PushItemWidth(-1); ImGui::PushItemWidth(-1);
@@ -1032,6 +1113,138 @@ void Engine::renderInspectorPanel() {
ImGui::PopStyleColor(); ImGui::PopStyleColor();
if (isUIObjectType(obj.type)) {
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.25f, 0.45f, 0.65f, 1.0f));
if (ImGui::CollapsingHeader("UI", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::PushID("UI");
ImGui::Indent(10.0f);
const char* anchors[] = { "Center", "Top Left", "Top Right", "Bottom Left", "Bottom Right" };
int anchor = static_cast<int>(obj.ui.anchor);
if (ImGui::Combo("Anchor", &anchor, anchors, IM_ARRAYSIZE(anchors))) {
obj.ui.anchor = static_cast<UIAnchor>(anchor);
projectManager.currentProject.hasUnsavedChanges = true;
}
if (ImGui::DragFloat2("Position (px)", &obj.ui.position.x, 1.0f)) {
projectManager.currentProject.hasUnsavedChanges = true;
}
glm::vec2 minSize(8.0f, 8.0f);
if (ImGui::DragFloat2("Size (px)", &obj.ui.size.x, 1.0f, minSize.x, 4096.0f)) {
obj.ui.size.x = std::max(minSize.x, obj.ui.size.x);
obj.ui.size.y = std::max(minSize.y, obj.ui.size.y);
projectManager.currentProject.hasUnsavedChanges = true;
}
if (obj.type == ObjectType::UIButton || obj.type == ObjectType::UISlider) {
if (ImGui::Checkbox("Interactable", &obj.ui.interactable)) {
projectManager.currentProject.hasUnsavedChanges = true;
}
const auto& presets = getUIStylePresets();
if (!presets.empty()) {
int presetIndex = findUIStylePreset(obj.ui.stylePreset);
if (presetIndex < 0) presetIndex = 0;
const char* currentPreset = presets[presetIndex].name.c_str();
if (ImGui::BeginCombo("Style Preset", currentPreset)) {
for (int i = 0; i < (int)presets.size(); ++i) {
bool selected = (i == presetIndex);
if (ImGui::Selectable(presets[i].name.c_str(), selected)) {
obj.ui.stylePreset = presets[i].name;
projectManager.currentProject.hasUnsavedChanges = true;
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
}
}
if (obj.type == ObjectType::UIButton || obj.type == ObjectType::UISlider || obj.type == ObjectType::UIImage || obj.type == ObjectType::UIText || obj.type == ObjectType::Sprite2D) {
char labelBuf[128] = {};
std::snprintf(labelBuf, sizeof(labelBuf), "%s", obj.ui.label.c_str());
if (ImGui::InputText(obj.type == ObjectType::UIText ? "Text" : "Label", labelBuf, sizeof(labelBuf))) {
obj.ui.label = labelBuf;
projectManager.currentProject.hasUnsavedChanges = true;
}
}
if (obj.type == ObjectType::UIText) {
if (ImGui::DragFloat("Text Size", &obj.ui.textScale, 0.05f, 0.1f, 10.0f, "%.2f")) {
obj.ui.textScale = std::max(0.1f, obj.ui.textScale);
projectManager.currentProject.hasUnsavedChanges = true;
}
}
if (obj.type == ObjectType::UIImage || obj.type == ObjectType::Sprite2D) {
ImGui::TextUnformatted("Texture");
ImGui::SetNextItemWidth(-160);
char texBuf[512] = {};
std::snprintf(texBuf, sizeof(texBuf), "%s", obj.albedoTexturePath.c_str());
if (ImGui::InputText("##UITexture", texBuf, sizeof(texBuf))) {
obj.albedoTexturePath = texBuf;
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::SameLine();
if (ImGui::SmallButton("Clear##UITexture")) {
obj.albedoTexturePath.clear();
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::SameLine();
bool canUseTex = !fileBrowser.selectedFile.empty() && fs::exists(fileBrowser.selectedFile) &&
fileBrowser.isTextureFile(fs::directory_entry(fileBrowser.selectedFile));
ImGui::BeginDisabled(!canUseTex);
if (ImGui::SmallButton("Use Selection##UITexture")) {
obj.albedoTexturePath = fileBrowser.selectedFile.string();
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::EndDisabled();
}
if (obj.type == ObjectType::UISlider) {
const char* sliderStyles[] = { "ImGui", "Fill", "Circle" };
int sliderStyle = static_cast<int>(obj.ui.sliderStyle);
if (ImGui::Combo("Style", &sliderStyle, sliderStyles, IM_ARRAYSIZE(sliderStyles))) {
obj.ui.sliderStyle = static_cast<UISliderStyle>(sliderStyle);
projectManager.currentProject.hasUnsavedChanges = true;
}
if (ImGui::DragFloat("Min", &obj.ui.sliderMin, 0.1f)) {
projectManager.currentProject.hasUnsavedChanges = true;
}
if (ImGui::DragFloat("Max", &obj.ui.sliderMax, 0.1f)) {
projectManager.currentProject.hasUnsavedChanges = true;
}
if (obj.ui.sliderMax < obj.ui.sliderMin) {
std::swap(obj.ui.sliderMin, obj.ui.sliderMax);
}
if (ImGui::SliderFloat("Value", &obj.ui.sliderValue, obj.ui.sliderMin, obj.ui.sliderMax)) {
projectManager.currentProject.hasUnsavedChanges = true;
}
}
ImVec4 uiColor(obj.ui.color.r, obj.ui.color.g, obj.ui.color.b, obj.ui.color.a);
if (ImGui::ColorEdit4("Tint", &uiColor.x)) {
obj.ui.color = glm::vec4(uiColor.x, uiColor.y, uiColor.z, uiColor.w);
projectManager.currentProject.hasUnsavedChanges = true;
}
if (obj.type == ObjectType::UIButton) {
const char* buttonStyles[] = { "ImGui", "Outline" };
int buttonStyle = static_cast<int>(obj.ui.buttonStyle);
if (ImGui::Combo("Style", &buttonStyle, buttonStyles, IM_ARRAYSIZE(buttonStyles))) {
obj.ui.buttonStyle = static_cast<UIButtonStyle>(buttonStyle);
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::TextDisabled("Last Pressed: %s", obj.ui.buttonPressed ? "yes" : "no");
}
ImGui::Unindent(10.0f);
ImGui::PopID();
}
ImGui::PopStyleColor();
}
if (obj.hasCollider) { if (obj.hasCollider) {
ImGui::Spacing(); ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.5f, 0.35f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.5f, 0.35f, 1.0f));
@@ -1080,7 +1293,7 @@ void Engine::renderInspectorPanel() {
} }
ImGui::TextDisabled("Capsule aligned to Y axis."); ImGui::TextDisabled("Capsule aligned to Y axis.");
} else { } else {
if (ImGui::Checkbox("Use Convex Hull (required for Rigidbody)", &obj.collider.convex)) { if (ImGui::Checkbox("Use Convex Hull (required for Rigidbody3D)", &obj.collider.convex)) {
changed = true; changed = true;
} }
ImGui::TextDisabled("Uses mesh from the object (OBJ/Model). Non-convex is static-only."); ImGui::TextDisabled("Uses mesh from the object (OBJ/Model). Non-convex is static-only.");
@@ -1164,7 +1377,7 @@ void Engine::renderInspectorPanel() {
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.45f, 0.45f, 0.25f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.45f, 0.45f, 0.25f, 1.0f));
bool removeRigidbody = false; bool removeRigidbody = false;
bool changed = false; bool changed = false;
auto header = drawComponentHeader("Rigidbody", "Rigidbody", &obj.rigidbody.enabled, true, [&]() { auto header = drawComponentHeader("Rigidbody3D", "Rigidbody3D", &obj.rigidbody.enabled, true, [&]() {
if (ImGui::MenuItem("Remove")) { if (ImGui::MenuItem("Remove")) {
removeRigidbody = true; removeRigidbody = true;
} }
@@ -1173,9 +1386,12 @@ void Engine::renderInspectorPanel() {
changed = true; changed = true;
} }
if (header.open) { if (header.open) {
ImGui::PushID("Rigidbody"); ImGui::PushID("Rigidbody3D");
ImGui::Indent(10.0f); ImGui::Indent(10.0f);
ImGui::TextDisabled("Collider required for physics."); ImGui::TextDisabled("Collider required for physics.");
if (isUIObjectType(obj.type)) {
ImGui::TextDisabled("Rigidbody3D is for 3D objects (use Rigidbody2D for UI/canvas).");
}
if (ImGui::DragFloat("Mass", &obj.rigidbody.mass, 0.05f, 0.01f, 1000.0f, "%.2f")) { if (ImGui::DragFloat("Mass", &obj.rigidbody.mass, 0.05f, 0.01f, 1000.0f, "%.2f")) {
obj.rigidbody.mass = std::max(0.01f, obj.rigidbody.mass); obj.rigidbody.mass = std::max(0.01f, obj.rigidbody.mass);
@@ -1218,6 +1434,52 @@ void Engine::renderInspectorPanel() {
ImGui::PopStyleColor(); ImGui::PopStyleColor();
} }
if (obj.hasRigidbody2D) {
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.55f, 0.45f, 1.0f));
bool removeRigidbody2D = false;
bool changed = false;
auto header = drawComponentHeader("Rigidbody2D", "Rigidbody2D", &obj.rigidbody2D.enabled, true, [&]() {
if (ImGui::MenuItem("Remove")) {
removeRigidbody2D = true;
}
});
if (header.enabledChanged) {
changed = true;
}
if (header.open) {
ImGui::PushID("Rigidbody2D");
ImGui::Indent(10.0f);
if (!isUIObjectType(obj.type)) {
ImGui::TextDisabled("Rigidbody2D is for UI/canvas objects only.");
}
if (ImGui::Checkbox("Use Gravity", &obj.rigidbody2D.useGravity)) {
changed = true;
}
if (ImGui::DragFloat("Gravity Scale", &obj.rigidbody2D.gravityScale, 0.05f, 0.0f, 10.0f, "%.2f")) {
obj.rigidbody2D.gravityScale = std::max(0.0f, obj.rigidbody2D.gravityScale);
changed = true;
}
if (ImGui::DragFloat("Linear Damping", &obj.rigidbody2D.linearDamping, 0.01f, 0.0f, 10.0f)) {
obj.rigidbody2D.linearDamping = std::clamp(obj.rigidbody2D.linearDamping, 0.0f, 10.0f);
changed = true;
}
if (ImGui::DragFloat2("Velocity", &obj.rigidbody2D.velocity.x, 0.1f)) {
changed = true;
}
ImGui::Unindent(10.0f);
ImGui::PopID();
}
if (removeRigidbody2D) {
obj.hasRigidbody2D = false;
changed = true;
}
if (changed) {
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::PopStyleColor();
}
if (obj.hasAudioSource) { if (obj.hasAudioSource) {
ImGui::Spacing(); ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.55f, 0.4f, 0.3f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.55f, 0.4f, 0.3f, 1.0f));
@@ -1481,7 +1743,7 @@ void Engine::renderInspectorPanel() {
} }
// Material section (skip for pure light objects) // Material section (skip for pure light objects)
if (obj.type != ObjectType::DirectionalLight && obj.type != ObjectType::PointLight && obj.type != ObjectType::SpotLight && obj.type != ObjectType::AreaLight && obj.type != ObjectType::Camera && obj.type != ObjectType::PostFXNode) { if (obj.type != ObjectType::DirectionalLight && obj.type != ObjectType::PointLight && obj.type != ObjectType::SpotLight && obj.type != ObjectType::AreaLight && obj.type != ObjectType::Camera && obj.type != ObjectType::PostFXNode && obj.type != ObjectType::Canvas && obj.type != ObjectType::UIImage && obj.type != ObjectType::UISlider && obj.type != ObjectType::UIButton && obj.type != ObjectType::UIText && obj.type != ObjectType::Sprite2D) {
ImGui::Spacing(); ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.35f, 0.55f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.35f, 0.55f, 1.0f));
@@ -1951,6 +2213,7 @@ void Engine::renderInspectorPanel() {
ImGui::PushID(inspectorId.c_str()); ImGui::PushID(inspectorId.c_str());
inspector(ctx); inspector(ctx);
ImGui::PopID(); ImGui::PopID();
ctx.SaveAutoSettings();
} else if (!scriptRuntime.getLastError().empty()) { } else if (!scriptRuntime.getLastError().empty()) {
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.6f, 1.0f), "Inspector load failed"); ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.6f, 1.0f), "Inspector load failed");
ImGui::TextWrapped("%s", scriptRuntime.getLastError().c_str()); ImGui::TextWrapped("%s", scriptRuntime.getLastError().c_str());
@@ -2052,11 +2315,21 @@ void Engine::renderInspectorPanel() {
ImGui::OpenPopup("AddComponentPopup"); ImGui::OpenPopup("AddComponentPopup");
} }
if (ImGui::BeginPopup("AddComponentPopup")) { if (ImGui::BeginPopup("AddComponentPopup")) {
if (!obj.hasRigidbody && ImGui::MenuItem("Rigidbody")) { bool isUIType = isUIObjectType(obj.type);
ImGui::BeginDisabled(isUIType);
if (!obj.hasRigidbody && ImGui::MenuItem("Rigidbody3D")) {
obj.hasRigidbody = true; obj.hasRigidbody = true;
obj.rigidbody = RigidbodyComponent{}; obj.rigidbody = RigidbodyComponent{};
componentChanged = true; componentChanged = true;
} }
ImGui::EndDisabled();
ImGui::BeginDisabled(!isUIType);
if (!obj.hasRigidbody2D && ImGui::MenuItem("Rigidbody2D")) {
obj.hasRigidbody2D = true;
obj.rigidbody2D = Rigidbody2DComponent{};
componentChanged = true;
}
ImGui::EndDisabled();
if (!obj.hasPlayerController && ImGui::MenuItem("Player Controller")) { if (!obj.hasPlayerController && ImGui::MenuItem("Player Controller")) {
obj.hasPlayerController = true; obj.hasPlayerController = true;
obj.playerController = PlayerControllerComponent{}; obj.playerController = PlayerControllerComponent{};

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@
#include <functional> #include <functional>
#include <unordered_set> #include <unordered_set>
#include <unordered_map> #include <unordered_map>
#include "ThirdParty/glm/gtc/constants.hpp"
#pragma region Material File IO Helpers #pragma region Material File IO Helpers
namespace { namespace {
@@ -78,6 +79,122 @@ bool writeMaterialFile(const MaterialFileData& data, const std::string& path) {
f << "fragmentShader=" << data.fragmentShader << "\n"; f << "fragmentShader=" << data.fragmentShader << "\n";
return true; return true;
} }
RawMeshAsset buildCubeRMesh() {
RawMeshAsset mesh;
mesh.positions.reserve(24);
mesh.normals.reserve(24);
mesh.uvs.reserve(24);
mesh.faces.reserve(12);
struct Face {
glm::vec3 n;
glm::vec3 v[4];
};
const float h = 0.5f;
Face faces[] = {
{ glm::vec3(0, 0, 1), { {-h,-h, h}, { h,-h, h}, { h, h, h}, {-h, h, h} } }, // +Z
{ glm::vec3(0, 0,-1), { { h,-h,-h}, {-h,-h,-h}, {-h, h,-h}, { h, h,-h} } }, // -Z
{ glm::vec3(1, 0, 0), { { h,-h, h}, { h,-h,-h}, { h, h,-h}, { h, h, h} } }, // +X
{ glm::vec3(-1,0, 0), { {-h,-h,-h}, {-h,-h, h}, {-h, h, h}, {-h, h,-h} } }, // -X
{ glm::vec3(0, 1, 0), { {-h, h, h}, { h, h, h}, { h, h,-h}, {-h, h,-h} } }, // +Y
{ glm::vec3(0,-1, 0), { {-h,-h,-h}, { h,-h,-h}, { h,-h, h}, {-h,-h, h} } }, // -Y
};
glm::vec2 uvs[4] = {
glm::vec2(0, 0),
glm::vec2(1, 0),
glm::vec2(1, 1),
glm::vec2(0, 1),
};
for (const auto& f : faces) {
uint32_t base = static_cast<uint32_t>(mesh.positions.size());
for (int i = 0; i < 4; ++i) {
mesh.positions.push_back(f.v[i]);
mesh.normals.push_back(f.n);
mesh.uvs.push_back(uvs[i]);
}
mesh.faces.push_back(glm::u32vec3(base, base + 1, base + 2));
mesh.faces.push_back(glm::u32vec3(base, base + 2, base + 3));
}
mesh.boundsMin = glm::vec3(-h);
mesh.boundsMax = glm::vec3(h);
mesh.hasNormals = true;
mesh.hasUVs = true;
return mesh;
}
RawMeshAsset buildPlaneRMesh() {
RawMeshAsset mesh;
mesh.positions = {
glm::vec3(-0.5f, 0.0f, 0.5f),
glm::vec3( 0.5f, 0.0f, 0.5f),
glm::vec3( 0.5f, 0.0f, -0.5f),
glm::vec3(-0.5f, 0.0f, -0.5f),
};
mesh.normals = {
glm::vec3(0, 1, 0),
glm::vec3(0, 1, 0),
glm::vec3(0, 1, 0),
glm::vec3(0, 1, 0),
};
mesh.uvs = {
glm::vec2(0, 0),
glm::vec2(1, 0),
glm::vec2(1, 1),
glm::vec2(0, 1),
};
mesh.faces = {
glm::u32vec3(0, 1, 2),
glm::u32vec3(0, 2, 3),
};
mesh.boundsMin = glm::vec3(-0.5f, 0.0f, -0.5f);
mesh.boundsMax = glm::vec3(0.5f, 0.0f, 0.5f);
mesh.hasNormals = true;
mesh.hasUVs = true;
return mesh;
}
RawMeshAsset buildSphereRMesh(int slices = 24, int stacks = 16) {
RawMeshAsset mesh;
const float radius = 0.5f;
for (int i = 0; i <= stacks; ++i) {
float v = static_cast<float>(i) / static_cast<float>(stacks);
float phi = v * glm::pi<float>();
float y = std::cos(phi);
float r = std::sin(phi);
for (int j = 0; j <= slices; ++j) {
float u = static_cast<float>(j) / static_cast<float>(slices);
float theta = u * glm::two_pi<float>();
float x = r * std::cos(theta);
float z = r * std::sin(theta);
glm::vec3 pos = glm::vec3(x, y, z) * radius;
mesh.positions.push_back(pos);
mesh.normals.push_back(glm::normalize(glm::vec3(x, y, z)));
mesh.uvs.push_back(glm::vec2(u, 1.0f - v));
}
}
for (int i = 0; i < stacks; ++i) {
for (int j = 0; j < slices; ++j) {
uint32_t i0 = i * (slices + 1) + j;
uint32_t i1 = i0 + 1;
uint32_t i2 = i0 + (slices + 1);
uint32_t i3 = i2 + 1;
mesh.faces.push_back(glm::u32vec3(i0, i2, i1));
mesh.faces.push_back(glm::u32vec3(i1, i2, i3));
}
}
mesh.boundsMin = glm::vec3(-radius);
mesh.boundsMax = glm::vec3(radius);
mesh.hasNormals = true;
mesh.hasUVs = true;
return mesh;
}
} // namespace } // namespace
#pragma endregion #pragma endregion
@@ -310,6 +427,7 @@ void Engine::run() {
std::cerr << "[DEBUG] Entering main loop, showLauncher=" << showLauncher << std::endl; std::cerr << "[DEBUG] Entering main loop, showLauncher=" << showLauncher << std::endl;
while (!glfwWindowShouldClose(editorWindow)) { while (!glfwWindowShouldClose(editorWindow)) {
double frameStart = glfwGetTime();
if (glfwGetWindowAttrib(editorWindow, GLFW_ICONIFIED)) { if (glfwGetWindowAttrib(editorWindow, GLFW_ICONIFIED)) {
ImGui_ImplGlfw_Sleep(10); ImGui_ImplGlfw_Sleep(10);
continue; continue;
@@ -362,6 +480,11 @@ void Engine::run() {
updatePlayerController(deltaTime); updatePlayerController(deltaTime);
} }
bool simulate2D = (isPlaying && !isPaused) || (!isPlaying && specMode) || (!isPlaying && testMode);
if (simulate2D) {
updateRigidbody2D(deltaTime);
}
updateHierarchyWorldTransforms(); updateHierarchyWorldTransforms();
bool simulatePhysics = physics.isReady() && ((isPlaying && !isPaused) || (!isPlaying && specMode)); bool simulatePhysics = physics.isReady() && ((isPlaying && !isPaused) || (!isPlaying && specMode));
@@ -465,6 +588,16 @@ void Engine::run() {
} }
glfwSwapBuffers(editorWindow); glfwSwapBuffers(editorWindow);
if (fpsCapEnabled && fpsCap > 1.0f) {
double target = 1.0 / fpsCap;
double frameEnd = glfwGetTime();
double elapsed = frameEnd - frameStart;
if (elapsed < target) {
int sleepMs = static_cast<int>((target - elapsed) * 1000.0);
if (sleepMs > 0) ImGui_ImplGlfw_Sleep(sleepMs);
}
}
if (firstFrame) { if (firstFrame) {
std::cerr << "[DEBUG] First frame complete!" << std::endl; std::cerr << "[DEBUG] First frame complete!" << std::endl;
@@ -575,6 +708,45 @@ void Engine::convertModelToRawMesh(const std::string& filepath) {
addConsoleMessage("Raw mesh export failed: " + error, ConsoleMessageType::Error); addConsoleMessage("Raw mesh export failed: " + error, ConsoleMessageType::Error);
} }
} }
void Engine::createRMeshPrimitive(const std::string& primitiveName) {
if (!projectManager.currentProject.isLoaded) {
addConsoleMessage("Load a project before creating RMesh primitives", ConsoleMessageType::Warning);
return;
}
fs::path root = projectManager.currentProject.assetsPath / "Models" / "RMeshes" / "Primitives";
std::error_code ec;
fs::create_directories(root, ec);
if (ec) {
addConsoleMessage("Failed to create RMesh folder: " + root.string(), ConsoleMessageType::Error);
return;
}
RawMeshAsset asset;
if (primitiveName == "Cube") {
asset = buildCubeRMesh();
} else if (primitiveName == "Sphere") {
asset = buildSphereRMesh();
} else if (primitiveName == "Plane") {
asset = buildPlaneRMesh();
} else {
addConsoleMessage("Unknown RMesh primitive: " + primitiveName, ConsoleMessageType::Warning);
return;
}
fs::path filePath = root / (primitiveName + ".rmesh");
if (!fs::exists(filePath)) {
std::string error;
if (!getModelLoader().saveRawMesh(asset, filePath.string(), error)) {
addConsoleMessage("Failed to save RMesh primitive: " + error, ConsoleMessageType::Error);
return;
}
fileBrowser.needsRefresh = true;
}
importModelToScene(filePath.string(), primitiveName);
}
#pragma endregion #pragma endregion
#pragma region Mesh Editing #pragma region Mesh Editing
@@ -600,6 +772,7 @@ bool Engine::ensureMeshEditTarget(SceneObject* obj) {
} }
meshEditLoaded = true; meshEditLoaded = true;
meshEditPath = obj->meshPath; meshEditPath = obj->meshPath;
meshEditDirty = false;
meshEditSelectedVertices.clear(); meshEditSelectedVertices.clear();
meshEditSelectedEdges.clear(); meshEditSelectedEdges.clear();
meshEditSelectedFaces.clear(); meshEditSelectedFaces.clear();
@@ -617,6 +790,23 @@ bool Engine::syncMeshEditToGPU(SceneObject* obj) {
projectManager.currentProject.hasUnsavedChanges = true; projectManager.currentProject.hasUnsavedChanges = true;
return true; return true;
} }
bool Engine::saveMeshEditAsset(std::string& error) {
if (!meshEditLoaded) {
error = "No mesh loaded for editing";
return false;
}
if (meshEditPath.empty()) {
error = "Mesh edit path is empty";
return false;
}
if (!getModelLoader().saveRawMesh(meshEditAsset, meshEditPath, error)) {
return false;
}
meshEditDirty = false;
fileBrowser.needsRefresh = true;
return true;
}
#pragma endregion #pragma endregion
#pragma region Material IO #pragma region Material IO
@@ -942,6 +1132,33 @@ void Engine::updatePlayerController(float delta) {
} }
syncLocalTransform(*player); syncLocalTransform(*player);
} }
void Engine::updateRigidbody2D(float delta) {
if (delta <= 0.0f) return;
const float gravityPx = -980.0f;
auto isUIType = [](ObjectType type) {
return type == ObjectType::Canvas ||
type == ObjectType::UIImage ||
type == ObjectType::UISlider ||
type == ObjectType::UIButton ||
type == ObjectType::UIText ||
type == ObjectType::Sprite2D;
};
for (auto& obj : sceneObjects) {
if (!obj.enabled || !obj.hasRigidbody2D || !obj.rigidbody2D.enabled) continue;
if (!isUIType(obj.type)) continue;
glm::vec2 vel = obj.rigidbody2D.velocity;
if (obj.rigidbody2D.useGravity) {
vel.y += gravityPx * obj.rigidbody2D.gravityScale * delta;
}
float damping = std::max(0.0f, obj.rigidbody2D.linearDamping);
if (damping > 0.0f) {
vel -= vel * std::min(1.0f, damping * delta);
}
obj.ui.position += vel * delta;
obj.rigidbody2D.velocity = vel;
}
}
#pragma endregion #pragma endregion
#pragma region Transform Hierarchy #pragma region Transform Hierarchy
@@ -1071,6 +1288,13 @@ void Engine::updateHierarchyWorldTransforms() {
worldPos = obj.position; worldPos = obj.position;
worldRot = QuatFromEulerXYZ(obj.rotation); worldRot = QuatFromEulerXYZ(obj.rotation);
worldScale = obj.scale; worldScale = obj.scale;
} else if (obj.parentId == -1) {
obj.position = obj.localPosition;
obj.rotation = NormalizeEulerDegrees(obj.localRotation);
obj.scale = obj.localScale;
worldPos = obj.position;
worldRot = QuatFromEulerXYZ(obj.rotation);
worldScale = obj.scale;
} else { } else {
glm::quat localRot = QuatFromEulerXYZ(obj.localRotation); glm::quat localRot = QuatFromEulerXYZ(obj.localRotation);
worldRot = parentRot * localRot; worldRot = parentRot * localRot;
@@ -1331,6 +1555,27 @@ void Engine::addObject(ObjectType type, const std::string& baseName) {
sceneObjects.back().scale = glm::vec3(2.0f, 2.0f, 0.05f); sceneObjects.back().scale = glm::vec3(2.0f, 2.0f, 0.05f);
} else if (type == ObjectType::Plane) { } else if (type == ObjectType::Plane) {
sceneObjects.back().scale = glm::vec3(2.0f, 2.0f, 0.05f); sceneObjects.back().scale = glm::vec3(2.0f, 2.0f, 0.05f);
} else if (type == ObjectType::Sprite) {
sceneObjects.back().scale = glm::vec3(1.0f, 1.0f, 0.05f);
sceneObjects.back().material.ambientStrength = 1.0f;
} else if (type == ObjectType::Canvas) {
sceneObjects.back().ui.label = "Canvas";
sceneObjects.back().ui.size = glm::vec2(600.0f, 400.0f);
} else if (type == ObjectType::UIImage) {
sceneObjects.back().ui.label = "Image";
sceneObjects.back().ui.size = glm::vec2(200.0f, 200.0f);
} else if (type == ObjectType::UISlider) {
sceneObjects.back().ui.label = "Slider";
sceneObjects.back().ui.size = glm::vec2(240.0f, 32.0f);
} else if (type == ObjectType::UIButton) {
sceneObjects.back().ui.label = "Button";
sceneObjects.back().ui.size = glm::vec2(160.0f, 40.0f);
} else if (type == ObjectType::UIText) {
sceneObjects.back().ui.label = "Text";
sceneObjects.back().ui.size = glm::vec2(240.0f, 32.0f);
} else if (type == ObjectType::Sprite2D) {
sceneObjects.back().ui.label = "Sprite2D";
sceneObjects.back().ui.size = glm::vec2(128.0f, 128.0f);
} }
sceneObjects.back().localPosition = sceneObjects.back().position; sceneObjects.back().localPosition = sceneObjects.back().position;
sceneObjects.back().localRotation = NormalizeEulerDegrees(sceneObjects.back().rotation); sceneObjects.back().localRotation = NormalizeEulerDegrees(sceneObjects.back().rotation);
@@ -1369,6 +1614,8 @@ void Engine::duplicateSelected() {
newObj.postFx = it->postFx; newObj.postFx = it->postFx;
newObj.hasRigidbody = it->hasRigidbody; newObj.hasRigidbody = it->hasRigidbody;
newObj.rigidbody = it->rigidbody; newObj.rigidbody = it->rigidbody;
newObj.hasRigidbody2D = it->hasRigidbody2D;
newObj.rigidbody2D = it->rigidbody2D;
newObj.hasCollider = it->hasCollider; newObj.hasCollider = it->hasCollider;
newObj.collider = it->collider; newObj.collider = it->collider;
newObj.hasPlayerController = it->hasPlayerController; newObj.hasPlayerController = it->hasPlayerController;
@@ -1379,6 +1626,7 @@ void Engine::duplicateSelected() {
newObj.localInitialized = true; newObj.localInitialized = true;
newObj.hasAudioSource = it->hasAudioSource; newObj.hasAudioSource = it->hasAudioSource;
newObj.audioSource = it->audioSource; newObj.audioSource = it->audioSource;
newObj.ui = it->ui;
sceneObjects.push_back(newObj); sceneObjects.push_back(newObj);
setPrimarySelection(id); setPrimarySelection(id);
@@ -1565,6 +1813,13 @@ bool Engine::raycastClosestFromScript(const glm::vec3& origin, const glm::vec3&
} }
void Engine::syncLocalTransform(SceneObject& obj) { void Engine::syncLocalTransform(SceneObject& obj) {
if (obj.parentId == -1) {
obj.localPosition = obj.position;
obj.localRotation = NormalizeEulerDegrees(obj.rotation);
obj.localScale = obj.scale;
obj.localInitialized = true;
return;
}
glm::vec3 parentPos(0.0f); glm::vec3 parentPos(0.0f);
glm::quat parentRot(1.0f, 0.0f, 0.0f, 0.0f); glm::quat parentRot(1.0f, 0.0f, 0.0f, 0.0f);
glm::vec3 parentScale(1.0f); glm::vec3 parentScale(1.0f);
@@ -1578,6 +1833,11 @@ void Engine::syncLocalTransform(SceneObject& obj) {
updateLocalFromWorld(obj, parentPos, parentRot, parentScale); updateLocalFromWorld(obj, parentPos, parentRot, parentScale);
} }
void Engine::setFrameRateCapFromScript(bool enabled, float cap) {
fpsCapEnabled = enabled;
fpsCap = std::max(1.0f, cap);
}
bool Engine::playAudioFromScript(int id) { bool Engine::playAudioFromScript(int id) {
SceneObject* obj = findObjectById(id); SceneObject* obj = findObjectById(id);
if (!obj || !obj->hasAudioSource) return false; if (!obj || !obj->hasAudioSource) return false;
@@ -1820,6 +2080,7 @@ void Engine::setupImGui() {
style.WindowRounding = 0.0f; style.WindowRounding = 0.0f;
style.Colors[ImGuiCol_WindowBg].w = 1.0f; style.Colors[ImGuiCol_WindowBg].w = 1.0f;
} }
initUIStylePresets();
std::cerr << "[DEBUG] setupImGui: initializing ImGui GLFW backend..." << std::endl; std::cerr << "[DEBUG] setupImGui: initializing ImGui GLFW backend..." << std::endl;
ImGui_ImplGlfw_InitForOpenGL(editorWindow, true); ImGui_ImplGlfw_InitForOpenGL(editorWindow, true);
@@ -1832,3 +2093,62 @@ void Engine::setupImGui() {
std::cerr << "[DEBUG] setupImGui: complete!" << std::endl; std::cerr << "[DEBUG] setupImGui: complete!" << std::endl;
} }
#pragma endregion #pragma endregion
void Engine::initUIStylePresets() {
uiStylePresets.clear();
uiStylePresets.shrink_to_fit();
UIStylePreset current;
current.name = "Default";
current.style = ImGui::GetStyle();
current.builtin = true;
uiStylePresets.push_back(current);
UIStylePreset editor;
editor.name = "Editor Style";
editor.style = ImGui::GetStyle();
editor.builtin = true;
uiStylePresets.push_back(editor);
UIStylePreset imguiDefault;
imguiDefault.name = "ImGui Default";
imguiDefault.style = ImGui::GetStyle();
ImGui::StyleColorsDark(&imguiDefault.style);
imguiDefault.builtin = true;
uiStylePresets.push_back(imguiDefault);
uiStylePresetIndex = 0;
}
int Engine::findUIStylePreset(const std::string& name) const {
for (size_t i = 0; i < uiStylePresets.size(); ++i) {
if (uiStylePresets[i].name == name) return static_cast<int>(i);
}
return -1;
}
const Engine::UIStylePreset* Engine::getUIStylePreset(const std::string& name) const {
int idx = findUIStylePreset(name);
if (idx < 0) return nullptr;
return &uiStylePresets[idx];
}
void Engine::registerUIStylePreset(const std::string& name, const ImGuiStyle& style, bool replace) {
if (name.empty()) return;
int idx = findUIStylePreset(name);
if (idx >= 0) {
if (replace) {
uiStylePresets[idx].style = style;
}
return;
}
UIStylePreset preset;
preset.name = name;
preset.style = style;
preset.builtin = false;
uiStylePresets.push_back(preset);
}
void Engine::registerUIStylePresetFromScript(const std::string& name, const ImGuiStyle& style, bool replace) {
registerUIStylePreset(name, style, replace);
}

View File

@@ -121,6 +121,8 @@ private:
char meshBuilderFaceInput[128] = ""; char meshBuilderFaceInput[128] = "";
bool meshEditMode = false; bool meshEditMode = false;
bool meshEditLoaded = false; bool meshEditLoaded = false;
bool meshEditDirty = false;
bool meshEditExtrudeMode = false;
std::string meshEditPath; std::string meshEditPath;
RawMeshAsset meshEditAsset; RawMeshAsset meshEditAsset;
std::vector<int> meshEditSelectedVertices; std::vector<int> meshEditSelectedVertices;
@@ -139,6 +141,15 @@ private:
bool specMode = false; bool specMode = false;
bool testMode = false; bool testMode = false;
bool collisionWireframe = false; bool collisionWireframe = false;
bool fpsCapEnabled = false;
float fpsCap = 120.0f;
struct UIStylePreset {
std::string name;
ImGuiStyle style;
bool builtin = false;
};
std::vector<UIStylePreset> uiStylePresets;
int uiStylePresetIndex = 0;
// Private methods // Private methods
SceneObject* getSelectedObject(); SceneObject* getSelectedObject();
glm::vec3 getSelectionCenterWorld(bool worldSpace) const; glm::vec3 getSelectionCenterWorld(bool worldSpace) const;
@@ -154,8 +165,10 @@ private:
void importOBJToScene(const std::string& filepath, const std::string& objectName); void importOBJToScene(const std::string& filepath, const std::string& objectName);
void importModelToScene(const std::string& filepath, const std::string& objectName); // Assimp import void importModelToScene(const std::string& filepath, const std::string& objectName); // Assimp import
void convertModelToRawMesh(const std::string& filepath); void convertModelToRawMesh(const std::string& filepath);
void createRMeshPrimitive(const std::string& primitiveName);
bool ensureMeshEditTarget(SceneObject* obj); bool ensureMeshEditTarget(SceneObject* obj);
bool syncMeshEditToGPU(SceneObject* obj); bool syncMeshEditToGPU(SceneObject* obj);
bool saveMeshEditAsset(std::string& error);
void handleKeyboardShortcuts(); void handleKeyboardShortcuts();
void OpenProjectPath(const std::string& path); void OpenProjectPath(const std::string& path);
@@ -184,6 +197,11 @@ private:
void compileScriptFile(const fs::path& scriptPath); void compileScriptFile(const fs::path& scriptPath);
void updateScripts(float delta); void updateScripts(float delta);
void updatePlayerController(float delta); void updatePlayerController(float delta);
void updateRigidbody2D(float delta);
void initUIStylePresets();
int findUIStylePreset(const std::string& name) const;
const UIStylePreset* getUIStylePreset(const std::string& name) const;
void registerUIStylePreset(const std::string& name, const ImGuiStyle& style, bool replace);
void renderFileBrowserToolbar(); void renderFileBrowserToolbar();
void renderFileBrowserBreadcrumb(); void renderFileBrowserBreadcrumb();
@@ -265,4 +283,7 @@ public:
bool setAudioVolumeFromScript(int id, float volume); bool setAudioVolumeFromScript(int id, float volume);
bool setAudioClipFromScript(int id, const std::string& path); bool setAudioClipFromScript(int id, const std::string& path);
void syncLocalTransform(SceneObject& obj); void syncLocalTransform(SceneObject& obj);
const std::vector<UIStylePreset>& getUIStylePresets() const { return uiStylePresets; }
void registerUIStylePresetFromScript(const std::string& name, const ImGuiStyle& style, bool replace = false);
void setFrameRateCapFromScript(bool enabled, float cap);
}; };

View File

@@ -16,8 +16,12 @@ PxVec3 ToPxVec3(const glm::vec3& v) {
} }
PxQuat ToPxQuat(const glm::vec3& eulerDeg) { PxQuat ToPxQuat(const glm::vec3& eulerDeg) {
glm::vec3 radians = glm::radians(eulerDeg); glm::vec3 r = glm::radians(eulerDeg);
glm::quat q = glm::quat(radians); glm::mat4 m(1.0f);
m = glm::rotate(m, r.x, glm::vec3(1.0f, 0.0f, 0.0f));
m = glm::rotate(m, r.y, glm::vec3(0.0f, 1.0f, 0.0f));
m = glm::rotate(m, r.z, glm::vec3(0.0f, 0.0f, 1.0f));
glm::quat q = glm::quat_cast(glm::mat3(m));
return PxQuat(q.x, q.y, q.z, q.w); return PxQuat(q.x, q.y, q.z, q.w);
} }
@@ -25,9 +29,20 @@ glm::vec3 ToGlmVec3(const PxVec3& v) {
return glm::vec3(v.x, v.y, v.z); return glm::vec3(v.x, v.y, v.z);
} }
glm::vec3 ExtractEulerXYZ(const glm::mat3& m) {
float T1 = std::atan2(m[2][1], m[2][2]);
float C2 = std::sqrt(m[0][0] * m[0][0] + m[1][0] * m[1][0]);
float T2 = std::atan2(-m[2][0], C2);
float S1 = std::sin(T1);
float C1 = std::cos(T1);
float T3 = std::atan2(S1 * m[0][2] - C1 * m[0][1], C1 * m[1][1] - S1 * m[1][2]);
return glm::vec3(-T1, -T2, -T3);
}
glm::vec3 ToGlmEulerDeg(const PxQuat& q) { glm::vec3 ToGlmEulerDeg(const PxQuat& q) {
glm::quat gq(q.w, q.x, q.y, q.z); glm::quat gq(q.w, q.x, q.y, q.z);
return glm::degrees(glm::eulerAngles(gq)); glm::mat3 m = glm::mat3_cast(gq);
return glm::degrees(ExtractEulerXYZ(m));
} }
} // namespace } // namespace
@@ -234,6 +249,13 @@ bool PhysicsSystem::attachPrimitiveShape(PxRigidActor* actor, const SceneObject&
tuneShape(shape, std::min({halfExtents.x, halfExtents.y, halfExtents.z}) * 2.0f, isDynamic); tuneShape(shape, std::min({halfExtents.x, halfExtents.y, halfExtents.z}) * 2.0f, isDynamic);
break; break;
} }
case ObjectType::Sprite: {
glm::vec3 halfExtents = glm::max(obj.scale * 0.5f, glm::vec3(0.01f));
halfExtents.z = std::max(halfExtents.z, 0.01f);
shape = mPhysics->createShape(PxBoxGeometry(ToPxVec3(halfExtents)), *mDefaultMaterial, true);
tuneShape(shape, std::min({halfExtents.x, halfExtents.y, halfExtents.z}) * 2.0f, isDynamic);
break;
}
case ObjectType::Torus: { case ObjectType::Torus: {
float radius = std::max({obj.scale.x, obj.scale.y, obj.scale.z}) * 0.5f; float radius = std::max({obj.scale.x, obj.scale.y, obj.scale.z}) * 0.5f;
radius = std::max(radius, 0.01f); radius = std::max(radius, 0.01f);

View File

@@ -299,7 +299,7 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
if (!file.is_open()) return false; if (!file.is_open()) return false;
file << "# Scene File\n"; file << "# Scene File\n";
file << "version=10\n"; file << "version=11\n";
file << "nextId=" << nextId << "\n"; file << "nextId=" << nextId << "\n";
file << "objectCount=" << objects.size() << "\n"; file << "objectCount=" << objects.size() << "\n";
file << "\n"; file << "\n";
@@ -328,6 +328,14 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
file << "rbLockRotY=" << (obj.rigidbody.lockRotationY ? 1 : 0) << "\n"; file << "rbLockRotY=" << (obj.rigidbody.lockRotationY ? 1 : 0) << "\n";
file << "rbLockRotZ=" << (obj.rigidbody.lockRotationZ ? 1 : 0) << "\n"; file << "rbLockRotZ=" << (obj.rigidbody.lockRotationZ ? 1 : 0) << "\n";
} }
file << "hasRigidbody2D=" << (obj.hasRigidbody2D ? 1 : 0) << "\n";
if (obj.hasRigidbody2D) {
file << "rb2dEnabled=" << (obj.rigidbody2D.enabled ? 1 : 0) << "\n";
file << "rb2dUseGravity=" << (obj.rigidbody2D.useGravity ? 1 : 0) << "\n";
file << "rb2dGravityScale=" << obj.rigidbody2D.gravityScale << "\n";
file << "rb2dLinearDamping=" << obj.rigidbody2D.linearDamping << "\n";
file << "rb2dVelocity=" << obj.rigidbody2D.velocity.x << "," << obj.rigidbody2D.velocity.y << "\n";
}
file << "hasCollider=" << (obj.hasCollider ? 1 : 0) << "\n"; file << "hasCollider=" << (obj.hasCollider ? 1 : 0) << "\n";
if (obj.hasCollider) { if (obj.hasCollider) {
file << "colliderEnabled=" << (obj.collider.enabled ? 1 : 0) << "\n"; file << "colliderEnabled=" << (obj.collider.enabled ? 1 : 0) << "\n";
@@ -394,6 +402,19 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
file << "cameraNear=" << obj.camera.nearClip << "\n"; file << "cameraNear=" << obj.camera.nearClip << "\n";
file << "cameraFar=" << obj.camera.farClip << "\n"; file << "cameraFar=" << obj.camera.farClip << "\n";
file << "cameraPostFX=" << (obj.camera.applyPostFX ? 1 : 0) << "\n"; file << "cameraPostFX=" << (obj.camera.applyPostFX ? 1 : 0) << "\n";
file << "uiAnchor=" << static_cast<int>(obj.ui.anchor) << "\n";
file << "uiPosition=" << obj.ui.position.x << "," << obj.ui.position.y << "\n";
file << "uiSize=" << obj.ui.size.x << "," << obj.ui.size.y << "\n";
file << "uiSliderValue=" << obj.ui.sliderValue << "\n";
file << "uiSliderMin=" << obj.ui.sliderMin << "\n";
file << "uiSliderMax=" << obj.ui.sliderMax << "\n";
file << "uiLabel=" << obj.ui.label << "\n";
file << "uiColor=" << obj.ui.color.r << "," << obj.ui.color.g << "," << obj.ui.color.b << "," << obj.ui.color.a << "\n";
file << "uiInteractable=" << (obj.ui.interactable ? 1 : 0) << "\n";
file << "uiSliderStyle=" << static_cast<int>(obj.ui.sliderStyle) << "\n";
file << "uiButtonStyle=" << static_cast<int>(obj.ui.buttonStyle) << "\n";
file << "uiStylePreset=" << obj.ui.stylePreset << "\n";
file << "uiTextScale=" << obj.ui.textScale << "\n";
if (obj.type == ObjectType::PostFXNode) { if (obj.type == ObjectType::PostFXNode) {
file << "postEnabled=" << (obj.postFx.enabled ? 1 : 0) << "\n"; file << "postEnabled=" << (obj.postFx.enabled ? 1 : 0) << "\n";
file << "postBloomEnabled=" << (obj.postFx.bloomEnabled ? 1 : 0) << "\n"; file << "postBloomEnabled=" << (obj.postFx.bloomEnabled ? 1 : 0) << "\n";
@@ -556,6 +577,20 @@ bool SceneSerializer::loadScene(const fs::path& filePath,
currentObj->rigidbody.lockRotationY = std::stoi(value) != 0; currentObj->rigidbody.lockRotationY = std::stoi(value) != 0;
} else if (key == "rbLockRotZ") { } else if (key == "rbLockRotZ") {
currentObj->rigidbody.lockRotationZ = std::stoi(value) != 0; currentObj->rigidbody.lockRotationZ = std::stoi(value) != 0;
} else if (key == "hasRigidbody2D") {
currentObj->hasRigidbody2D = std::stoi(value) != 0;
} else if (key == "rb2dEnabled") {
currentObj->rigidbody2D.enabled = std::stoi(value) != 0;
} else if (key == "rb2dUseGravity") {
currentObj->rigidbody2D.useGravity = std::stoi(value) != 0;
} else if (key == "rb2dGravityScale") {
currentObj->rigidbody2D.gravityScale = std::stof(value);
} else if (key == "rb2dLinearDamping") {
currentObj->rigidbody2D.linearDamping = std::stof(value);
} else if (key == "rb2dVelocity") {
sscanf(value.c_str(), "%f,%f",
&currentObj->rigidbody2D.velocity.x,
&currentObj->rigidbody2D.velocity.y);
} else if (key == "hasCollider") { } else if (key == "hasCollider") {
currentObj->hasCollider = std::stoi(value) != 0; currentObj->hasCollider = std::stoi(value) != 0;
} else if (key == "colliderEnabled") { } else if (key == "colliderEnabled") {
@@ -701,6 +736,40 @@ bool SceneSerializer::loadScene(const fs::path& filePath,
currentObj->camera.farClip = std::stof(value); currentObj->camera.farClip = std::stof(value);
} else if (key == "cameraPostFX") { } else if (key == "cameraPostFX") {
currentObj->camera.applyPostFX = (std::stoi(value) != 0); currentObj->camera.applyPostFX = (std::stoi(value) != 0);
} else if (key == "uiAnchor") {
currentObj->ui.anchor = static_cast<UIAnchor>(std::stoi(value));
} else if (key == "uiPosition") {
sscanf(value.c_str(), "%f,%f",
&currentObj->ui.position.x,
&currentObj->ui.position.y);
} else if (key == "uiSize") {
sscanf(value.c_str(), "%f,%f",
&currentObj->ui.size.x,
&currentObj->ui.size.y);
} else if (key == "uiSliderValue") {
currentObj->ui.sliderValue = std::stof(value);
} else if (key == "uiSliderMin") {
currentObj->ui.sliderMin = std::stof(value);
} else if (key == "uiSliderMax") {
currentObj->ui.sliderMax = std::stof(value);
} else if (key == "uiLabel") {
currentObj->ui.label = value;
} else if (key == "uiColor") {
sscanf(value.c_str(), "%f,%f,%f,%f",
&currentObj->ui.color.r,
&currentObj->ui.color.g,
&currentObj->ui.color.b,
&currentObj->ui.color.a);
} else if (key == "uiInteractable") {
currentObj->ui.interactable = (std::stoi(value) != 0);
} else if (key == "uiSliderStyle") {
currentObj->ui.sliderStyle = static_cast<UISliderStyle>(std::stoi(value));
} else if (key == "uiButtonStyle") {
currentObj->ui.buttonStyle = static_cast<UIButtonStyle>(std::stoi(value));
} else if (key == "uiStylePreset") {
currentObj->ui.stylePreset = value;
} else if (key == "uiTextScale") {
currentObj->ui.textScale = std::stof(value);
} else if (key == "postEnabled") { } else if (key == "postEnabled") {
currentObj->postFx.enabled = (std::stoi(value) != 0); currentObj->postFx.enabled = (std::stoi(value) != 0);
} else if (key == "postBloomEnabled") { } else if (key == "postBloomEnabled") {

View File

@@ -993,7 +993,7 @@ void Renderer::renderObject(const SceneObject& obj) {
shader->setFloat("specularStrength", obj.material.specularStrength); shader->setFloat("specularStrength", obj.material.specularStrength);
shader->setFloat("shininess", obj.material.shininess); shader->setFloat("shininess", obj.material.shininess);
shader->setFloat("mixAmount", obj.material.textureMix); shader->setFloat("mixAmount", obj.material.textureMix);
shader->setBool("unlit", obj.type == ObjectType::Mirror); shader->setBool("unlit", obj.type == ObjectType::Mirror || obj.type == ObjectType::Sprite);
Texture* baseTex = texture1; Texture* baseTex = texture1;
if (!obj.albedoTexturePath.empty()) { if (!obj.albedoTexturePath.empty()) {
@@ -1046,6 +1046,9 @@ void Renderer::renderObject(const SceneObject& obj) {
case ObjectType::Mirror: case ObjectType::Mirror:
if (planeMesh) planeMesh->draw(); if (planeMesh) planeMesh->draw();
break; break;
case ObjectType::Sprite:
if (planeMesh) planeMesh->draw();
break;
case ObjectType::Torus: case ObjectType::Torus:
if (torusMesh) torusMesh->draw(); if (torusMesh) torusMesh->draw();
break; break;
@@ -1078,6 +1081,14 @@ void Renderer::renderObject(const SceneObject& obj) {
break; break;
case ObjectType::PostFXNode: case ObjectType::PostFXNode:
break; break;
case ObjectType::Sprite2D:
case ObjectType::Canvas:
case ObjectType::UIImage:
case ObjectType::UISlider:
case ObjectType::UIButton:
case ObjectType::UIText:
// UI types are rendered via ImGui, not here.
break;
} }
} }
@@ -1196,7 +1207,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
if (!obj.enabled) continue; if (!obj.enabled) continue;
if (!drawMirrorObjects && obj.type == ObjectType::Mirror) continue; if (!drawMirrorObjects && obj.type == ObjectType::Mirror) continue;
// Skip light gizmo-only types and camera helpers // Skip light gizmo-only types and camera helpers
if (obj.type == ObjectType::PointLight || obj.type == ObjectType::SpotLight || obj.type == ObjectType::AreaLight || obj.type == ObjectType::Camera || obj.type == ObjectType::PostFXNode) { if (obj.type == ObjectType::PointLight || obj.type == ObjectType::SpotLight || obj.type == ObjectType::AreaLight || obj.type == ObjectType::Camera || obj.type == ObjectType::PostFXNode || obj.type == ObjectType::Canvas || obj.type == ObjectType::UIImage || obj.type == ObjectType::UISlider || obj.type == ObjectType::UIButton || obj.type == ObjectType::UIText || obj.type == ObjectType::Sprite2D) {
continue; continue;
} }
@@ -1283,6 +1294,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
else if (obj.type == ObjectType::Capsule) meshToDraw = capsuleMesh; else if (obj.type == ObjectType::Capsule) meshToDraw = capsuleMesh;
else if (obj.type == ObjectType::Plane) meshToDraw = planeMesh; else if (obj.type == ObjectType::Plane) meshToDraw = planeMesh;
else if (obj.type == ObjectType::Mirror) meshToDraw = planeMesh; else if (obj.type == ObjectType::Mirror) meshToDraw = planeMesh;
else if (obj.type == ObjectType::Sprite) meshToDraw = planeMesh;
else if (obj.type == ObjectType::Torus) meshToDraw = torusMesh; else if (obj.type == ObjectType::Torus) meshToDraw = torusMesh;
else if (obj.type == ObjectType::OBJMesh && obj.meshId != -1) { else if (obj.type == ObjectType::OBJMesh && obj.meshId != -1) {
meshToDraw = g_objLoader.getMesh(obj.meshId); meshToDraw = g_objLoader.getMesh(obj.meshId);
@@ -1461,7 +1473,7 @@ unsigned int Renderer::applyPostProcessing(const std::vector<SceneObject>& scene
return target.texture; return target.texture;
} }
void Renderer::renderScene(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int /*selectedId*/, float fovDeg, float nearPlane, float farPlane, bool drawColliders) { void Renderer::renderScene(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int selectedId, float fovDeg, float nearPlane, float farPlane, bool drawColliders) {
updateMirrorTargets(camera, sceneObjects, currentWidth, currentHeight, fovDeg, nearPlane, farPlane); updateMirrorTargets(camera, sceneObjects, currentWidth, currentHeight, fovDeg, nearPlane, farPlane);
renderSceneInternal(camera, sceneObjects, currentWidth, currentHeight, true, fovDeg, nearPlane, farPlane, true); renderSceneInternal(camera, sceneObjects, currentWidth, currentHeight, true, fovDeg, nearPlane, farPlane, true);
if (drawColliders) { if (drawColliders) {
@@ -1470,6 +1482,7 @@ void Renderer::renderScene(const Camera& camera, const std::vector<SceneObject>&
renderCollisionOverlay(camera, sceneObjects, currentWidth, currentHeight, fovDeg, nearPlane, farPlane); renderCollisionOverlay(camera, sceneObjects, currentWidth, currentHeight, fovDeg, nearPlane, farPlane);
glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindFramebuffer(GL_FRAMEBUFFER, 0);
} }
renderSelectionOutline(camera, sceneObjects, selectedId, fovDeg, nearPlane, farPlane);
unsigned int result = applyPostProcessing(sceneObjects, viewportTexture, currentWidth, currentHeight, true); unsigned int result = applyPostProcessing(sceneObjects, viewportTexture, currentWidth, currentHeight, true);
displayTexture = result ? result : viewportTexture; displayTexture = result ? result : viewportTexture;
} }
@@ -1498,7 +1511,11 @@ void Renderer::renderCollisionOverlay(const Camera& camera, const std::vector<Sc
GLint prevPoly[2] = { GL_FILL, GL_FILL }; GLint prevPoly[2] = { GL_FILL, GL_FILL };
glGetIntegerv(GL_POLYGON_MODE, prevPoly); glGetIntegerv(GL_POLYGON_MODE, prevPoly);
GLboolean depthTest = glIsEnabled(GL_DEPTH_TEST); GLboolean depthTest = glIsEnabled(GL_DEPTH_TEST);
GLboolean depthMask = GL_TRUE;
glGetBooleanv(GL_DEPTH_WRITEMASK, &depthMask);
GLboolean cullFace = glIsEnabled(GL_CULL_FACE); GLboolean cullFace = glIsEnabled(GL_CULL_FACE);
GLint prevCullMode = GL_BACK;
glGetIntegerv(GL_CULL_FACE_MODE, &prevCullMode);
GLboolean polyOffsetLine = glIsEnabled(GL_POLYGON_OFFSET_LINE); GLboolean polyOffsetLine = glIsEnabled(GL_POLYGON_OFFSET_LINE);
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
@@ -1572,6 +1589,9 @@ void Renderer::renderCollisionOverlay(const Camera& camera, const std::vector<Sc
case ObjectType::Plane: case ObjectType::Plane:
meshToDraw = planeMesh; meshToDraw = planeMesh;
break; break;
case ObjectType::Sprite:
meshToDraw = planeMesh;
break;
case ObjectType::Torus: case ObjectType::Torus:
meshToDraw = sphereMesh; meshToDraw = sphereMesh;
break; break;
@@ -1605,6 +1625,153 @@ void Renderer::renderCollisionOverlay(const Camera& camera, const std::vector<Sc
glPolygonMode(GL_FRONT_AND_BACK, prevPoly[0]); glPolygonMode(GL_FRONT_AND_BACK, prevPoly[0]);
} }
void Renderer::renderSelectionOutline(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int selectedId, float fovDeg, float nearPlane, float farPlane) {
if (!defaultShader || selectedId < 0 || currentWidth <= 0 || currentHeight <= 0) return;
const SceneObject* selectedObj = nullptr;
for (const auto& obj : sceneObjects) {
if (obj.id == selectedId) {
selectedObj = &obj;
break;
}
}
if (!selectedObj || !selectedObj->enabled) return;
if (selectedObj->type == ObjectType::PointLight ||
selectedObj->type == ObjectType::SpotLight ||
selectedObj->type == ObjectType::AreaLight ||
selectedObj->type == ObjectType::Camera ||
selectedObj->type == ObjectType::PostFXNode ||
selectedObj->type == ObjectType::Canvas ||
selectedObj->type == ObjectType::UIImage ||
selectedObj->type == ObjectType::UISlider ||
selectedObj->type == ObjectType::UIButton ||
selectedObj->type == ObjectType::UIText ||
selectedObj->type == ObjectType::Sprite2D) {
return;
}
Mesh* meshToDraw = nullptr;
if (selectedObj->type == ObjectType::Cube) meshToDraw = cubeMesh;
else if (selectedObj->type == ObjectType::Sphere) meshToDraw = sphereMesh;
else if (selectedObj->type == ObjectType::Capsule) meshToDraw = capsuleMesh;
else if (selectedObj->type == ObjectType::Plane) meshToDraw = planeMesh;
else if (selectedObj->type == ObjectType::Mirror) meshToDraw = planeMesh;
else if (selectedObj->type == ObjectType::Sprite) meshToDraw = planeMesh;
else if (selectedObj->type == ObjectType::Torus) meshToDraw = torusMesh;
else if (selectedObj->type == ObjectType::OBJMesh && selectedObj->meshId != -1) {
meshToDraw = g_objLoader.getMesh(selectedObj->meshId);
} else if (selectedObj->type == ObjectType::Model && selectedObj->meshId != -1) {
meshToDraw = getModelLoader().getMesh(selectedObj->meshId);
}
if (!meshToDraw) return;
GLint prevPoly[2] = { GL_FILL, GL_FILL };
glGetIntegerv(GL_POLYGON_MODE, prevPoly);
GLboolean depthTest = glIsEnabled(GL_DEPTH_TEST);
GLboolean depthMask = GL_TRUE;
glGetBooleanv(GL_DEPTH_WRITEMASK, &depthMask);
GLboolean cullFace = glIsEnabled(GL_CULL_FACE);
GLint prevCullMode = GL_BACK;
glGetIntegerv(GL_CULL_FACE_MODE, &prevCullMode);
GLboolean stencilTest = glIsEnabled(GL_STENCIL_TEST);
GLint prevStencilFunc = GL_ALWAYS;
GLint prevStencilRef = 0;
GLint prevStencilValueMask = 0xFF;
GLint prevStencilFail = GL_KEEP;
GLint prevStencilZFail = GL_KEEP;
GLint prevStencilZPass = GL_KEEP;
GLint prevStencilWriteMask = 0xFF;
glGetIntegerv(GL_STENCIL_FUNC, &prevStencilFunc);
glGetIntegerv(GL_STENCIL_REF, &prevStencilRef);
glGetIntegerv(GL_STENCIL_VALUE_MASK, &prevStencilValueMask);
glGetIntegerv(GL_STENCIL_FAIL, &prevStencilFail);
glGetIntegerv(GL_STENCIL_PASS_DEPTH_FAIL, &prevStencilZFail);
glGetIntegerv(GL_STENCIL_PASS_DEPTH_PASS, &prevStencilZPass);
glGetIntegerv(GL_STENCIL_WRITEMASK, &prevStencilWriteMask);
GLboolean prevColorMask[4] = { GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE };
glGetBooleanv(GL_COLOR_WRITEMASK, prevColorMask);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glViewport(0, 0, currentWidth, currentHeight);
glClearStencil(0);
glClear(GL_STENCIL_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
glDepthMask(GL_FALSE);
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
Shader* active = defaultShader;
active->use();
active->setMat4("view", camera.getViewMatrix());
active->setMat4("projection", glm::perspective(glm::radians(fovDeg), (float)currentWidth / (float)currentHeight, nearPlane, farPlane));
active->setVec3("viewPos", camera.position);
active->setBool("unlit", true);
active->setBool("hasOverlay", false);
active->setBool("hasNormalMap", false);
active->setInt("lightCount", 0);
active->setFloat("mixAmount", 0.0f);
active->setVec3("materialColor", glm::vec3(1.0f, 0.5f, 0.1f));
active->setFloat("ambientStrength", 1.0f);
active->setFloat("specularStrength", 0.0f);
active->setFloat("shininess", 1.0f);
active->setInt("texture1", 0);
active->setInt("overlayTex", 1);
active->setInt("normalMap", 2);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, debugWhiteTexture ? debugWhiteTexture : (texture1 ? texture1->GetID() : 0));
glm::mat4 baseModel = glm::mat4(1.0f);
baseModel = glm::translate(baseModel, selectedObj->position);
baseModel = glm::rotate(baseModel, glm::radians(selectedObj->rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
baseModel = glm::rotate(baseModel, glm::radians(selectedObj->rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
baseModel = glm::rotate(baseModel, glm::radians(selectedObj->rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
baseModel = glm::scale(baseModel, selectedObj->scale);
// Mark the object in the stencil buffer.
glEnable(GL_STENCIL_TEST);
glStencilMask(0xFF);
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
if (cullFace) {
glEnable(GL_CULL_FACE);
glCullFace(prevCullMode);
} else {
glDisable(GL_CULL_FACE);
}
active->setMat4("model", baseModel);
meshToDraw->draw();
// Draw the scaled outline where stencil is not marked.
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00);
glEnable(GL_CULL_FACE);
glCullFace(GL_FRONT);
const float outlineScale = 1.03f;
glm::mat4 outlineModel = glm::scale(baseModel, glm::vec3(outlineScale));
active->setMat4("model", outlineModel);
meshToDraw->draw();
if (!cullFace) {
glDisable(GL_CULL_FACE);
} else {
glCullFace(prevCullMode);
}
glDepthMask(depthMask);
if (depthTest) glEnable(GL_DEPTH_TEST);
else glDisable(GL_DEPTH_TEST);
glPolygonMode(GL_FRONT_AND_BACK, prevPoly[0]);
glColorMask(prevColorMask[0], prevColorMask[1], prevColorMask[2], prevColorMask[3]);
glStencilFunc(prevStencilFunc, prevStencilRef, prevStencilValueMask);
glStencilOp(prevStencilFail, prevStencilZFail, prevStencilZPass);
glStencilMask(prevStencilWriteMask);
if (!stencilTest) glDisable(GL_STENCIL_TEST);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
void Renderer::endRender() { void Renderer::endRender() {
glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindFramebuffer(GL_FRAMEBUFFER, 0);
} }

View File

@@ -147,6 +147,7 @@ public:
void renderSkybox(const glm::mat4& view, const glm::mat4& proj); void renderSkybox(const glm::mat4& view, const glm::mat4& proj);
void renderObject(const SceneObject& obj); void renderObject(const SceneObject& obj);
void renderScene(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int selectedId = -1, float fovDeg = FOV, float nearPlane = NEAR_PLANE, float farPlane = FAR_PLANE, bool drawColliders = false); void renderScene(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int selectedId = -1, float fovDeg = FOV, float nearPlane = NEAR_PLANE, float farPlane = FAR_PLANE, bool drawColliders = false);
void renderSelectionOutline(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int selectedId, float fovDeg, float nearPlane, float farPlane);
unsigned int renderScenePreview(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane, bool applyPostFX = false); unsigned int renderScenePreview(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane, bool applyPostFX = false);
void renderCollisionOverlay(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane); void renderCollisionOverlay(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane);
void endRender(); void endRender();

View File

@@ -3,20 +3,27 @@
#include "Common.h" #include "Common.h"
enum class ObjectType { enum class ObjectType {
Cube, Cube = 0,
Sphere, Sphere = 1,
Capsule, Capsule = 2,
OBJMesh, OBJMesh = 3,
Model, // New type for Assimp-loaded models (FBX, GLTF, etc.) Model = 4, // New type for Assimp-loaded models (FBX, GLTF, etc.)
DirectionalLight, DirectionalLight = 5,
PointLight, PointLight = 6,
SpotLight, SpotLight = 7,
AreaLight, AreaLight = 8,
Camera, Camera = 9,
PostFXNode, PostFXNode = 10,
Mirror, Mirror = 11,
Plane, Plane = 12,
Torus Torus = 13,
Sprite = 14, // 3D quad sprite (lit/unlit with material)
Sprite2D = 15, // Screen-space sprite
Canvas = 16, // UI canvas root
UIImage = 17,
UISlider = 18,
UIButton = 19,
UIText = 20
}; };
struct MaterialProperties { struct MaterialProperties {
@@ -34,6 +41,25 @@ enum class LightType {
Area = 3 Area = 3
}; };
enum class UIAnchor {
Center = 0,
TopLeft = 1,
TopRight = 2,
BottomLeft = 3,
BottomRight = 4
};
enum class UISliderStyle {
ImGui = 0,
Fill = 1,
Circle = 2
};
enum class UIButtonStyle {
ImGui = 0,
Outline = 1
};
struct LightComponent { struct LightComponent {
LightType type = LightType::Point; LightType type = LightType::Point;
glm::vec3 color = glm::vec3(1.0f); glm::vec3 color = glm::vec3(1.0f);
@@ -142,6 +168,31 @@ struct PlayerControllerComponent {
float yaw = 0.0f; float yaw = 0.0f;
}; };
struct UIElementComponent {
UIAnchor anchor = UIAnchor::Center;
glm::vec2 position = glm::vec2(0.0f); // offset in pixels from anchor
glm::vec2 size = glm::vec2(160.0f, 40.0f);
float sliderValue = 0.5f;
float sliderMin = 0.0f;
float sliderMax = 1.0f;
std::string label = "UI Element";
bool buttonPressed = false;
glm::vec4 color = glm::vec4(1.0f);
bool interactable = true;
UISliderStyle sliderStyle = UISliderStyle::ImGui;
UIButtonStyle buttonStyle = UIButtonStyle::ImGui;
std::string stylePreset = "Default";
float textScale = 1.0f;
};
struct Rigidbody2DComponent {
bool enabled = true;
bool useGravity = false;
float gravityScale = 1.0f;
float linearDamping = 0.0f;
glm::vec2 velocity = glm::vec2(0.0f);
};
struct AudioSourceComponent { struct AudioSourceComponent {
bool enabled = true; bool enabled = true;
std::string clipPath; std::string clipPath;
@@ -188,12 +239,15 @@ public:
std::vector<std::string> additionalMaterialPaths; std::vector<std::string> additionalMaterialPaths;
bool hasRigidbody = false; bool hasRigidbody = false;
RigidbodyComponent rigidbody; RigidbodyComponent rigidbody;
bool hasRigidbody2D = false;
Rigidbody2DComponent rigidbody2D;
bool hasCollider = false; bool hasCollider = false;
ColliderComponent collider; ColliderComponent collider;
bool hasPlayerController = false; bool hasPlayerController = false;
PlayerControllerComponent playerController; PlayerControllerComponent playerController;
bool hasAudioSource = false; bool hasAudioSource = false;
AudioSourceComponent audioSource; AudioSourceComponent audioSource;
UIElementComponent ui;
SceneObject(const std::string& name, ObjectType type, int id) SceneObject(const std::string& name, ObjectType type, int id)
: name(name), : name(name),

View File

@@ -2,6 +2,8 @@
#include "Engine.h" #include "Engine.h"
#include "SceneObject.h" #include "SceneObject.h"
#include <algorithm> #include <algorithm>
#include <cmath>
#include <cctype>
#include <iterator> #include <iterator>
#include <unordered_map> #include <unordered_map>
@@ -27,6 +29,27 @@ std::string makeScriptInstanceKey(const ScriptContext& ctx) {
} }
return key; return key;
} }
std::string trimString(const std::string& input) {
size_t start = 0;
while (start < input.size() && std::isspace(static_cast<unsigned char>(input[start]))) {
++start;
}
size_t end = input.size();
while (end > start && std::isspace(static_cast<unsigned char>(input[end - 1]))) {
--end;
}
return input.substr(start, end - start);
}
bool isUIObjectType(ObjectType type) {
return type == ObjectType::Canvas ||
type == ObjectType::UIImage ||
type == ObjectType::UISlider ||
type == ObjectType::UIButton ||
type == ObjectType::UIText ||
type == ObjectType::Sprite2D;
}
} }
SceneObject* ScriptContext::FindObjectByName(const std::string& name) { SceneObject* ScriptContext::FindObjectByName(const std::string& name) {
@@ -39,6 +62,31 @@ SceneObject* ScriptContext::FindObjectById(int id) {
return engine->findObjectById(id); return engine->findObjectById(id);
} }
SceneObject* ScriptContext::ResolveObjectRef(const std::string& ref) {
if (ref.empty()) return nullptr;
std::string trimmed = trimString(ref);
if (trimmed == "ObjectSelf") return object;
const std::string namePrefix = "Object.";
const std::string idPrefix = "Object.ID-";
if (trimmed.rfind(idPrefix, 0) == 0) {
std::string idStr = trimmed.substr(idPrefix.size());
if (idStr.empty()) return nullptr;
try {
int id = std::stoi(idStr);
return FindObjectById(id);
} catch (...) {
return nullptr;
}
}
if (trimmed.rfind(namePrefix, 0) == 0) {
std::string name = trimmed.substr(namePrefix.size());
if (name.empty()) return nullptr;
return FindObjectByName(name);
}
return nullptr;
}
bool ScriptContext::IsObjectEnabled() const { bool ScriptContext::IsObjectEnabled() const {
return object ? object->enabled : false; return object ? object->enabled : false;
} }
@@ -97,6 +145,12 @@ void ScriptContext::SetPosition(const glm::vec3& pos) {
} }
} }
void ScriptContext::SetPosition2D(const glm::vec2& pos) {
if (!object) return;
object->ui.position = pos;
MarkDirty();
}
void ScriptContext::SetRotation(const glm::vec3& rot) { void ScriptContext::SetRotation(const glm::vec3& rot) {
if (object) { if (object) {
object->rotation = NormalizeEulerDegrees(rot); object->rotation = NormalizeEulerDegrees(rot);
@@ -126,10 +180,204 @@ void ScriptContext::SetScale(const glm::vec3& scl) {
} }
} }
void ScriptContext::GetPlanarYawPitchVectors(float pitchDeg, float yawDeg,
glm::vec3& outForward, glm::vec3& outRight) const {
glm::quat q = glm::quat(glm::radians(glm::vec3(pitchDeg, yawDeg, 0.0f)));
glm::vec3 forward = glm::normalize(q * glm::vec3(0.0f, 0.0f, -1.0f));
glm::vec3 right = glm::normalize(q * glm::vec3(1.0f, 0.0f, 0.0f));
outForward = glm::normalize(glm::vec3(forward.x, 0.0f, forward.z));
outRight = glm::normalize(glm::vec3(right.x, 0.0f, right.z));
if (!std::isfinite(outForward.x) || glm::length(outForward) < 1e-3f) {
outForward = glm::vec3(0.0f, 0.0f, -1.0f);
}
if (!std::isfinite(outRight.x) || glm::length(outRight) < 1e-3f) {
outRight = glm::vec3(1.0f, 0.0f, 0.0f);
}
}
bool ScriptContext::IsUIButtonPressed() const {
return object && object->type == ObjectType::UIButton && object->ui.buttonPressed;
}
bool ScriptContext::IsUIInteractable() const {
return object ? object->ui.interactable : false;
}
void ScriptContext::SetUIInteractable(bool interactable) {
if (!object) return;
if (object->ui.interactable != interactable) {
object->ui.interactable = interactable;
MarkDirty();
}
}
float ScriptContext::GetUISliderValue() const {
if (!object || object->type != ObjectType::UISlider) return 0.0f;
return object->ui.sliderValue;
}
void ScriptContext::SetUISliderValue(float value) {
if (!object || object->type != ObjectType::UISlider) return;
float clamped = std::clamp(value, object->ui.sliderMin, object->ui.sliderMax);
if (object->ui.sliderValue != clamped) {
object->ui.sliderValue = clamped;
MarkDirty();
}
}
void ScriptContext::SetUISliderRange(float minValue, float maxValue) {
if (!object || object->type != ObjectType::UISlider) return;
if (maxValue < minValue) std::swap(minValue, maxValue);
object->ui.sliderMin = minValue;
object->ui.sliderMax = maxValue;
object->ui.sliderValue = std::clamp(object->ui.sliderValue, minValue, maxValue);
MarkDirty();
}
void ScriptContext::SetUILabel(const std::string& label) {
if (!object) return;
if (object->ui.label != label) {
object->ui.label = label;
MarkDirty();
}
}
void ScriptContext::SetUIColor(const glm::vec4& color) {
if (!object) return;
if (object->ui.color != color) {
object->ui.color = color;
MarkDirty();
}
}
float ScriptContext::GetUITextScale() const {
if (!object || object->type != ObjectType::UIText) return 1.0f;
return object->ui.textScale;
}
void ScriptContext::SetUITextScale(float scale) {
if (!object || object->type != ObjectType::UIText) return;
float clamped = std::max(0.1f, scale);
if (object->ui.textScale != clamped) {
object->ui.textScale = clamped;
MarkDirty();
}
}
void ScriptContext::SetUISliderStyle(UISliderStyle style) {
if (!object || object->type != ObjectType::UISlider) return;
if (object->ui.sliderStyle != style) {
object->ui.sliderStyle = style;
MarkDirty();
}
}
void ScriptContext::SetUIButtonStyle(UIButtonStyle style) {
if (!object || object->type != ObjectType::UIButton) return;
if (object->ui.buttonStyle != style) {
object->ui.buttonStyle = style;
MarkDirty();
}
}
void ScriptContext::SetUIStylePreset(const std::string& name) {
if (!object || name.empty()) return;
if (object->ui.stylePreset != name) {
object->ui.stylePreset = name;
MarkDirty();
}
}
void ScriptContext::RegisterUIStylePreset(const std::string& name, const ImGuiStyle& style, bool replace) {
if (engine) {
engine->registerUIStylePresetFromScript(name, style, replace);
}
}
void ScriptContext::SetFPSCap(bool enabled, float cap) {
if (engine) {
engine->setFrameRateCapFromScript(enabled, cap);
}
}
bool ScriptContext::HasRigidbody() const { bool ScriptContext::HasRigidbody() const {
return object && object->hasRigidbody && object->rigidbody.enabled; return object && object->hasRigidbody && object->rigidbody.enabled;
} }
bool ScriptContext::HasRigidbody2D() const {
return object && isUIObjectType(object->type) && object->hasRigidbody2D && object->rigidbody2D.enabled;
}
bool ScriptContext::EnsureCapsuleCollider(float height, float radius) {
if (!object) return false;
bool changed = false;
if (!object->hasCollider) {
object->hasCollider = true;
changed = true;
}
ColliderComponent& col = object->collider;
if (!col.enabled) {
col.enabled = true;
changed = true;
}
if (col.type != ColliderType::Capsule) {
col.type = ColliderType::Capsule;
changed = true;
}
if (!col.convex) {
col.convex = true;
changed = true;
}
glm::vec3 size(radius * 2.0f, height, radius * 2.0f);
if (col.boxSize != size) {
col.boxSize = size;
changed = true;
}
if (changed) {
MarkDirty();
}
return true;
}
bool ScriptContext::EnsureRigidbody(bool useGravity, bool kinematic) {
if (!object) return false;
bool changed = false;
if (!object->hasRigidbody) {
object->hasRigidbody = true;
changed = true;
}
RigidbodyComponent& rb = object->rigidbody;
if (!rb.enabled) {
rb.enabled = true;
changed = true;
}
if (rb.useGravity != useGravity) {
rb.useGravity = useGravity;
changed = true;
}
if (rb.isKinematic != kinematic) {
rb.isKinematic = kinematic;
changed = true;
}
if (changed) {
MarkDirty();
}
return true;
}
bool ScriptContext::SetRigidbody2DVelocity(const glm::vec2& velocity) {
if (!object || !HasRigidbody2D()) return false;
object->rigidbody2D.velocity = velocity;
MarkDirty();
return true;
}
bool ScriptContext::GetRigidbody2DVelocity(glm::vec2& outVelocity) const {
if (!object || !HasRigidbody2D()) return false;
outVelocity = object->rigidbody2D.velocity;
return true;
}
bool ScriptContext::SetRigidbodyVelocity(const glm::vec3& velocity) { bool ScriptContext::SetRigidbodyVelocity(const glm::vec3& velocity) {
if (!engine || !object || !HasRigidbody()) return false; if (!engine || !object || !HasRigidbody()) return false;
return engine->setRigidbodyVelocityFromScript(object->id, velocity); return engine->setRigidbodyVelocityFromScript(object->id, velocity);
@@ -140,6 +388,12 @@ bool ScriptContext::GetRigidbodyVelocity(glm::vec3& outVelocity) const {
return engine->getRigidbodyVelocityFromScript(object->id, outVelocity); return engine->getRigidbodyVelocityFromScript(object->id, outVelocity);
} }
bool ScriptContext::AddRigidbodyVelocity(const glm::vec3& deltaVelocity) {
glm::vec3 current;
if (!GetRigidbodyVelocity(current)) return false;
return SetRigidbodyVelocity(current + deltaVelocity);
}
bool ScriptContext::SetRigidbodyAngularVelocity(const glm::vec3& velocity) { bool ScriptContext::SetRigidbodyAngularVelocity(const glm::vec3& velocity) {
if (!engine || !object || !HasRigidbody()) return false; if (!engine || !object || !HasRigidbody()) return false;
return engine->setRigidbodyAngularVelocityFromScript(object->id, velocity); return engine->setRigidbodyAngularVelocityFromScript(object->id, velocity);
@@ -265,6 +519,17 @@ void ScriptContext::SetSettingBool(const std::string& key, bool value) {
SetSetting(key, value ? "1" : "0"); SetSetting(key, value ? "1" : "0");
} }
float ScriptContext::GetSettingFloat(const std::string& key, float fallback) const {
std::string v = GetSetting(key, "");
if (v.empty()) return fallback;
try { return std::stof(v); } catch (...) {}
return fallback;
}
void ScriptContext::SetSettingFloat(const std::string& key, float value) {
SetSetting(key, std::to_string(value));
}
glm::vec3 ScriptContext::GetSettingVec3(const std::string& key, const glm::vec3& fallback) const { glm::vec3 ScriptContext::GetSettingVec3(const std::string& key, const glm::vec3& fallback) const {
std::string v = GetSetting(key, ""); std::string v = GetSetting(key, "");
if (v.empty()) return fallback; if (v.empty()) return fallback;
@@ -315,6 +580,31 @@ void ScriptContext::AutoSetting(const std::string& key, bool& value) {
autoSettings.push_back(entry); autoSettings.push_back(entry);
} }
void ScriptContext::AutoSetting(const std::string& key, float& value) {
if (!script) return;
if (autoSettings.end() != std::find_if(autoSettings.begin(), autoSettings.end(),
[&](const AutoSettingEntry& e){ return e.key == key; })) return;
static std::unordered_map<std::string, float> defaults;
std::string scriptId = makeScriptInstanceKey(*this);
std::string id = scriptId + "|" + key;
float defaultVal = value;
auto itDef = defaults.find(id);
if (itDef != defaults.end()) {
defaultVal = itDef->second;
} else {
defaults[id] = defaultVal;
}
value = GetSettingFloat(key, defaultVal);
AutoSettingEntry entry;
entry.type = AutoSettingType::Float;
entry.key = key;
entry.ptr = &value;
entry.initialFloat = value;
autoSettings.push_back(entry);
}
void ScriptContext::AutoSetting(const std::string& key, glm::vec3& value) { void ScriptContext::AutoSetting(const std::string& key, glm::vec3& value) {
if (!script) return; if (!script) return;
if (autoSettings.end() != std::find_if(autoSettings.begin(), autoSettings.end(), if (autoSettings.end() != std::find_if(autoSettings.begin(), autoSettings.end(),
@@ -378,6 +668,12 @@ void ScriptContext::SaveAutoSettings() {
newVal = cur ? "1" : "0"; newVal = cur ? "1" : "0";
break; break;
} }
case AutoSettingType::Float: {
float cur = *static_cast<float*>(e.ptr);
if (std::abs(cur - e.initialFloat) < 1e-6f) continue;
newVal = std::to_string(cur);
break;
}
case AutoSettingType::Vec3: { case AutoSettingType::Vec3: {
glm::vec3 cur = *static_cast<glm::vec3*>(e.ptr); glm::vec3 cur = *static_cast<glm::vec3*>(e.ptr);
if (glm::all(glm::epsilonEqual(cur, e.initialVec3, 1e-6f))) continue; if (glm::all(glm::epsilonEqual(cur, e.initialVec3, 1e-6f))) continue;

View File

@@ -10,13 +10,14 @@ struct ScriptContext {
Engine* engine = nullptr; Engine* engine = nullptr;
SceneObject* object = nullptr; SceneObject* object = nullptr;
ScriptComponent* script = nullptr; ScriptComponent* script = nullptr;
enum class AutoSettingType { Bool, Vec3, StringBuf }; enum class AutoSettingType { Bool, Float, Vec3, StringBuf };
struct AutoSettingEntry { struct AutoSettingEntry {
AutoSettingType type; AutoSettingType type;
std::string key; std::string key;
void* ptr = nullptr; void* ptr = nullptr;
size_t bufSize = 0; size_t bufSize = 0;
bool initialBool = false; bool initialBool = false;
float initialFloat = 0.0f;
glm::vec3 initialVec3 = glm::vec3(0.0f); glm::vec3 initialVec3 = glm::vec3(0.0f);
std::string initialString; std::string initialString;
}; };
@@ -25,6 +26,7 @@ struct ScriptContext {
// Convenience helpers for scripts // Convenience helpers for scripts
SceneObject* FindObjectByName(const std::string& name); SceneObject* FindObjectByName(const std::string& name);
SceneObject* FindObjectById(int id); SceneObject* FindObjectById(int id);
SceneObject* ResolveObjectRef(const std::string& ref);
bool IsObjectEnabled() const; bool IsObjectEnabled() const;
void SetObjectEnabled(bool enabled); void SetObjectEnabled(bool enabled);
int GetLayer() const; int GetLayer() const;
@@ -34,11 +36,35 @@ struct ScriptContext {
bool HasTag(const std::string& tag) const; bool HasTag(const std::string& tag) const;
bool IsInLayer(int layer) const; bool IsInLayer(int layer) const;
void SetPosition(const glm::vec3& pos); void SetPosition(const glm::vec3& pos);
void SetPosition2D(const glm::vec2& pos);
void SetRotation(const glm::vec3& rot); void SetRotation(const glm::vec3& rot);
void SetScale(const glm::vec3& scl); void SetScale(const glm::vec3& scl);
void GetPlanarYawPitchVectors(float pitchDeg, float yawDeg, glm::vec3& outForward, glm::vec3& outRight) const;
// UI helpers
bool IsUIButtonPressed() const;
bool IsUIInteractable() const;
void SetUIInteractable(bool interactable);
float GetUISliderValue() const;
void SetUISliderValue(float value);
void SetUISliderRange(float minValue, float maxValue);
void SetUILabel(const std::string& label);
void SetUIColor(const glm::vec4& color);
float GetUITextScale() const;
void SetUITextScale(float scale);
void SetUISliderStyle(UISliderStyle style);
void SetUIButtonStyle(UIButtonStyle style);
void SetUIStylePreset(const std::string& name);
void RegisterUIStylePreset(const std::string& name, const ImGuiStyle& style, bool replace = false);
void SetFPSCap(bool enabled, float cap = 120.0f);
bool HasRigidbody() const; bool HasRigidbody() const;
bool HasRigidbody2D() const;
bool EnsureCapsuleCollider(float height, float radius);
bool EnsureRigidbody(bool useGravity = true, bool kinematic = false);
bool SetRigidbody2DVelocity(const glm::vec2& velocity);
bool GetRigidbody2DVelocity(glm::vec2& outVelocity) const;
bool SetRigidbodyVelocity(const glm::vec3& velocity); bool SetRigidbodyVelocity(const glm::vec3& velocity);
bool GetRigidbodyVelocity(glm::vec3& outVelocity) const; bool GetRigidbodyVelocity(glm::vec3& outVelocity) const;
bool AddRigidbodyVelocity(const glm::vec3& deltaVelocity);
bool SetRigidbodyAngularVelocity(const glm::vec3& velocity); bool SetRigidbodyAngularVelocity(const glm::vec3& velocity);
bool GetRigidbodyAngularVelocity(glm::vec3& outVelocity) const; bool GetRigidbodyAngularVelocity(glm::vec3& outVelocity) const;
bool AddRigidbodyForce(const glm::vec3& force); bool AddRigidbodyForce(const glm::vec3& force);
@@ -63,12 +89,15 @@ struct ScriptContext {
void SetSetting(const std::string& key, const std::string& value); void SetSetting(const std::string& key, const std::string& value);
bool GetSettingBool(const std::string& key, bool fallback = false) const; bool GetSettingBool(const std::string& key, bool fallback = false) const;
void SetSettingBool(const std::string& key, bool value); void SetSettingBool(const std::string& key, bool value);
float GetSettingFloat(const std::string& key, float fallback = 0.0f) const;
void SetSettingFloat(const std::string& key, float value);
glm::vec3 GetSettingVec3(const std::string& key, const glm::vec3& fallback = glm::vec3(0.0f)) const; glm::vec3 GetSettingVec3(const std::string& key, const glm::vec3& fallback = glm::vec3(0.0f)) const;
void SetSettingVec3(const std::string& key, const glm::vec3& value); void SetSettingVec3(const std::string& key, const glm::vec3& value);
// Console helper // Console helper
void AddConsoleMessage(const std::string& message, ConsoleMessageType type = ConsoleMessageType::Info); void AddConsoleMessage(const std::string& message, ConsoleMessageType type = ConsoleMessageType::Info);
// Auto-binding helpers: bind once per call, optionally load stored value, then SaveAutoSettings() writes back on change. // Auto-binding helpers: bind once per call, optionally load stored value.
void AutoSetting(const std::string& key, bool& value); void AutoSetting(const std::string& key, bool& value);
void AutoSetting(const std::string& key, float& value);
void AutoSetting(const std::string& key, glm::vec3& value); void AutoSetting(const std::string& key, glm::vec3& value);
void AutoSetting(const std::string& key, char* buffer, size_t bufferSize); void AutoSetting(const std::string& key, char* buffer, size_t bufferSize);
void SaveAutoSettings(); void SaveAutoSettings();