Better Physics a little, New UI! And not only that, More simple scripting, Yey!!!!
This commit is contained in:
44
Scripts/FPSDisplay.cpp
Normal file
44
Scripts/FPSDisplay.cpp
Normal 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));
|
||||
}
|
||||
@@ -36,15 +36,10 @@ extern "C" void Script_OnInspector(ScriptContext& ctx) {
|
||||
ImGui::TextUnformatted("RigidbodyTest");
|
||||
ImGui::Separator();
|
||||
|
||||
bool changed = false;
|
||||
changed |= ImGui::Checkbox("Launch on Begin", &autoLaunch);
|
||||
changed |= ImGui::Checkbox("Show Velocity Readback", &showVelocity);
|
||||
changed |= ImGui::DragFloat3("Launch Velocity", &launchVelocity.x, 0.25f, -50.0f, 50.0f);
|
||||
changed |= ImGui::DragFloat3("Teleport Offset", &teleportOffset.x, 0.1f, -10.0f, 10.0f);
|
||||
|
||||
if (changed) {
|
||||
ctx.SaveAutoSettings();
|
||||
}
|
||||
ImGui::Checkbox("Launch on Begin", &autoLaunch);
|
||||
ImGui::Checkbox("Show Velocity Readback", &showVelocity);
|
||||
ImGui::DragFloat3("Launch Velocity", &launchVelocity.x, 0.25f, -50.0f, 50.0f);
|
||||
ImGui::DragFloat3("Teleport Offset", &teleportOffset.x, 0.1f, -10.0f, 10.0f);
|
||||
|
||||
if (ImGui::Button("Launch Now")) {
|
||||
Launch(ctx);
|
||||
@@ -76,4 +71,4 @@ void Begin(ScriptContext& ctx, float /*deltaTime*/) {
|
||||
if (autoLaunch) {
|
||||
Launch(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,11 @@
|
||||
#include "ThirdParty/imgui/imgui.h"
|
||||
|
||||
namespace {
|
||||
// Script state (persisted by AutoSetting binder)
|
||||
bool autoRotate = false;
|
||||
glm::vec3 spinSpeed = glm::vec3(0.0f, 45.0f, 0.0f); // deg/sec
|
||||
glm::vec3 offset = glm::vec3(0.0f, 1.0f, 0.0f);
|
||||
char targetName[128] = "MyTarget";
|
||||
|
||||
// Runtime behavior
|
||||
static void ApplyAutoRotate(ScriptContext& ctx, float deltaTime) {
|
||||
if (!autoRotate || !ctx.object) return;
|
||||
ctx.SetRotation(ctx.object->rotation + spinSpeed * deltaTime);
|
||||
@@ -17,7 +15,6 @@ namespace {
|
||||
}
|
||||
|
||||
extern "C" void Script_OnInspector(ScriptContext& ctx) {
|
||||
// Auto settings (loaded once, saved only when changed)
|
||||
ctx.AutoSetting("autoRotate", autoRotate);
|
||||
ctx.AutoSetting("spinSpeed", spinSpeed);
|
||||
ctx.AutoSetting("offset", offset);
|
||||
@@ -26,15 +23,10 @@ extern "C" void Script_OnInspector(ScriptContext& ctx) {
|
||||
ImGui::TextUnformatted("SampleInspector");
|
||||
ImGui::Separator();
|
||||
|
||||
bool changed = false;
|
||||
changed |= ImGui::Checkbox("Auto Rotate", &autoRotate);
|
||||
changed |= ImGui::DragFloat3("Spin Speed (deg/s)", &spinSpeed.x, 1.0f, -360.0f, 360.0f);
|
||||
changed |= ImGui::DragFloat3("Offset", &offset.x, 0.1f);
|
||||
changed |= ImGui::InputText("Target Name", targetName, sizeof(targetName));
|
||||
|
||||
if (changed) {
|
||||
ctx.SaveAutoSettings();
|
||||
}
|
||||
ImGui::Checkbox("Auto Rotate", &autoRotate);
|
||||
ImGui::DragFloat3("Spin Speed (deg/s)", &spinSpeed.x, 1.0f, -360.0f, 360.0f);
|
||||
ImGui::DragFloat3("Offset", &offset.x, 0.1f);
|
||||
ImGui::InputText("Target Name", targetName, sizeof(targetName));
|
||||
|
||||
if (ctx.object) {
|
||||
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 (SceneObject* target = ctx.FindObjectByName(targetName)) {
|
||||
if (SceneObject* target = ctx.ResolveObjectRef(targetName)) {
|
||||
target->position += offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Begin(ScriptContext& ctx, float /*deltaTime*/) {
|
||||
}
|
||||
|
||||
void Spec(ScriptContext& ctx, float deltaTime) {
|
||||
ApplyAutoRotate(ctx, deltaTime);
|
||||
}
|
||||
|
||||
@@ -8,101 +8,41 @@
|
||||
#include "ScriptRuntime.h"
|
||||
#include "SceneObject.h"
|
||||
#include "ThirdParty/imgui/imgui.h"
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
|
||||
namespace {
|
||||
bool autoRotate = false;
|
||||
glm::vec3 spinSpeed = glm::vec3(0.0f, 45.0f, 0.0f);
|
||||
glm::vec3 offset = glm::vec3(0.0f, 1.0f, 0.0f);
|
||||
char targetName[128] = "MyTarget";
|
||||
int settingsLoadedForId = -1;
|
||||
ScriptComponent* settingsLoadedForScript = nullptr;
|
||||
|
||||
void setSetting(ScriptContext& ctx, const std::string& key, const std::string& value) {
|
||||
if (!ctx.script) return;
|
||||
auto it = std::find_if(ctx.script->settings.begin(), ctx.script->settings.end(),
|
||||
[&](const ScriptSetting& s) { return s.key == key; });
|
||||
if (it != ctx.script->settings.end()) {
|
||||
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 bindSettings(ScriptContext& ctx) {
|
||||
ctx.AutoSetting("autoRotate", autoRotate);
|
||||
ctx.AutoSetting("spinSpeed", spinSpeed);
|
||||
ctx.AutoSetting("offset", offset);
|
||||
ctx.AutoSetting("targetName", targetName, sizeof(targetName));
|
||||
}
|
||||
|
||||
void applyAutoRotate(ScriptContext& ctx, float deltaTime) {
|
||||
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);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
extern "C" void Script_OnInspector(ScriptContext& ctx) {
|
||||
loadSettings(ctx);
|
||||
bindSettings(ctx);
|
||||
|
||||
ImGui::TextUnformatted("SampleInspector");
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::Checkbox("Auto Rotate", &autoRotate)) {
|
||||
persistSettings(ctx);
|
||||
}
|
||||
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::Checkbox("Auto Rotate", &autoRotate);
|
||||
ImGui::DragFloat3("Spin Speed (deg/s)", &spinSpeed.x, 1.0f, -360.0f, 360.0f);
|
||||
ImGui::DragFloat3("Offset", &offset.x, 0.1f);
|
||||
ImGui::InputText("Target Name", targetName, sizeof(targetName));
|
||||
persistSettings(ctx);
|
||||
|
||||
if (ctx.object) {
|
||||
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 (SceneObject* target = ctx.FindObjectByName(targetName)) {
|
||||
if (SceneObject* target = ctx.ResolveObjectRef(targetName)) {
|
||||
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.
|
||||
void Begin(ScriptContext& ctx, float /*deltaTime*/) {
|
||||
// Initialize per-script state here.
|
||||
loadSettings(ctx);
|
||||
bindSettings(ctx);
|
||||
}
|
||||
|
||||
void Spec(ScriptContext& ctx, float deltaTime) {
|
||||
|
||||
@@ -8,6 +8,8 @@ struct ControllerState {
|
||||
float pitch = 0.0f;
|
||||
float yaw = 0.0f;
|
||||
float verticalVelocity = 0.0f;
|
||||
glm::vec3 debugVelocity = glm::vec3(0.0f);
|
||||
bool debugGrounded = false;
|
||||
bool initialized = false;
|
||||
};
|
||||
|
||||
@@ -38,23 +40,6 @@ void bindSettings(ScriptContext& ctx) {
|
||||
ctx.AutoSetting("enforceRigidbody", enforceRigidbody);
|
||||
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
|
||||
|
||||
extern "C" void Script_OnInspector(ScriptContext& ctx) {
|
||||
@@ -63,19 +48,24 @@ extern "C" void Script_OnInspector(ScriptContext& ctx) {
|
||||
ImGui::TextUnformatted("Standalone Movement Controller");
|
||||
ImGui::Separator();
|
||||
|
||||
bool changed = false;
|
||||
changed |= ImGui::DragFloat3("Walk/Run/Jump", &moveTuning.x, 0.05f, 0.0f, 25.0f, "%.2f");
|
||||
changed |= ImGui::DragFloat2("Look Sens/Clamp", &lookTuning.x, 0.01f, 0.0f, 500.0f, "%.2f");
|
||||
changed |= ImGui::DragFloat3("Height/Radius/Snap", &capsuleTuning.x, 0.02f, 0.0f, 5.0f, "%.2f");
|
||||
changed |= ImGui::DragFloat3("Gravity/Probe/MaxFall", &gravityTuning.x, 0.05f, -50.0f, 50.0f, "%.2f");
|
||||
changed |= ImGui::Checkbox("Enable Mouse Look", &enableMouseLook);
|
||||
changed |= ImGui::Checkbox("Hold RMB to Look", &requireMouseButton);
|
||||
changed |= ImGui::Checkbox("Force Collider", &enforceCollider);
|
||||
changed |= ImGui::Checkbox("Force Rigidbody", &enforceRigidbody);
|
||||
changed |= ImGui::Checkbox("Show Debug", &showDebug);
|
||||
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");
|
||||
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");
|
||||
ImGui::Checkbox("Enable Mouse Look", &enableMouseLook);
|
||||
ImGui::Checkbox("Hold RMB to Look", &requireMouseButton);
|
||||
ImGui::Checkbox("Force Collider", &enforceCollider);
|
||||
ImGui::Checkbox("Force Rigidbody", &enforceRigidbody);
|
||||
ImGui::Checkbox("Show Debug", &showDebug);
|
||||
|
||||
if (changed) {
|
||||
ctx.SaveAutoSettings();
|
||||
if (showDebug && ctx.object) {
|
||||
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.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) {
|
||||
if (!ctx.object) return;
|
||||
|
||||
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 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 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));
|
||||
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 planarForward(0.0f);
|
||||
glm::vec3 planarRight(0.0f);
|
||||
ctx.GetPlanarYawPitchVectors(state.pitch, state.yaw, planarForward, planarRight);
|
||||
|
||||
glm::vec3 move(0.0f);
|
||||
if (ImGui::IsKeyDown(ImGuiKey_W)) move += planarForward;
|
||||
@@ -195,8 +179,8 @@ void TickUpdate(ScriptContext& ctx, float deltaTime) {
|
||||
}
|
||||
|
||||
if (showDebug) {
|
||||
ImGui::Text("Move (%.2f, %.2f, %.2f)", velocity.x, velocity.y, velocity.z);
|
||||
ImGui::Text("Grounded: %s", grounded ? "yes" : "no");
|
||||
state.debugVelocity = velocity;
|
||||
state.debugGrounded = grounded;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,117 +1,400 @@
|
||||
# Modularity C++ Scripting Quickstart
|
||||
---
|
||||
title: C++ Scripting
|
||||
description: Hot-compiled native C++ scripts, per-object state, ImGui inspectors, and runtime/editor hooks.
|
||||
---
|
||||
|
||||
## Project setup
|
||||
- Scripts live under `Scripts/` (configurable via `Scripts.modu`).
|
||||
- The engine generates a wrapper per script when compiling. It exports fixed entry points with `extern "C"` linkage:
|
||||
- `Script_OnInspector(ScriptContext&)`
|
||||
- `Script_Begin(ScriptContext&, float deltaTime)`
|
||||
- `Script_Spec(ScriptContext&, float deltaTime)`
|
||||
- `Script_TestEditor(ScriptContext&, float deltaTime)`
|
||||
- `Script_Update(ScriptContext&, float deltaTime)` (fallback if TickUpdate is absent)
|
||||
- `Script_TickUpdate(ScriptContext&, float deltaTime)`
|
||||
- Build config file: `Scripts.modu` (auto-created per project). Keys:
|
||||
- `scriptsDir`, `outDir`, `includeDir=...`, `define=...`, `linux.linkLib`, `win.linkLib`, `cppStandard`.
|
||||
# C++ Scripting
|
||||
Scripts in Modularity are native C++ code compiled into shared libraries and loaded at runtime. They run per scene object and can optionally draw ImGui UI in the inspector and in custom editor windows.
|
||||
|
||||
> Notes up front:
|
||||
> - Scripts are not sandboxed. They can crash the editor/game if they dereference bad pointers or do unsafe work.
|
||||
> - Always null-check `ctx.object` (objects can be deleted, disabled, or scripts can be detached).
|
||||
|
||||
## Table of contents
|
||||
- [Quickstart](#quickstart)
|
||||
- [Scripts.modu](#scriptsmodu)
|
||||
- [How compilation works](#how-compilation-works)
|
||||
- [Lifecycle hooks](#lifecycle-hooks)
|
||||
- [ScriptContext](#scriptcontext)
|
||||
- [ImGui in scripts](#imgui-in-scripts)
|
||||
- [Per-script settings](#per-script-settings)
|
||||
- [UI scripting](#ui-scripting)
|
||||
- [IEnum tasks](#ienum-tasks)
|
||||
- [Logging](#logging)
|
||||
- [Scripted editor windows](#scripted-editor-windows)
|
||||
- [Manual compile (CLI)](#manual-compile-cli)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Templates](#templates)
|
||||
|
||||
## Quickstart
|
||||
1. Create a script file under `Scripts/` (e.g. `Scripts/MyScript.cpp`).
|
||||
2. Select an object in the scene.
|
||||
3. In the Inspector, add/enable a script component and set its path:
|
||||
- In the **Scripts** section, set `Path` OR click **Use Selection** after selecting the file in the File Browser.
|
||||
4. Compile the script:
|
||||
- In the File Browser, right-click the script file and choose **Compile Script**, or
|
||||
- In the Inspector’s script component menu, choose **Compile**.
|
||||
5. Implement a tick hook (`TickUpdate`) and observe behavior in play mode.
|
||||
|
||||
## Scripts.modu
|
||||
Each project has a `Scripts.modu` file (auto-created if missing). It controls compilation.
|
||||
|
||||
Common keys:
|
||||
- `scriptsDir` - where script source files live (default: `Scripts`)
|
||||
- `outDir` - where compiled binaries go (default: `Cache/ScriptBin`)
|
||||
- `includeDir=...` - add include directories (repeatable)
|
||||
- `define=...` - add preprocessor defines (repeatable)
|
||||
- `linux.linkLib=...` - comma-separated link libs/flags for Linux (e.g. `dl,pthread`)
|
||||
- `win.linkLib=...` - comma-separated link libs for Windows (e.g. `User32,Advapi32`)
|
||||
- `cppStandard` - C++ standard (e.g. `c++20`)
|
||||
|
||||
Example:
|
||||
```ini
|
||||
scriptsDir=Scripts
|
||||
outDir=Cache/ScriptBin
|
||||
includeDir=../src
|
||||
includeDir=../include
|
||||
cppStandard=c++20
|
||||
linux.linkLib=dl,pthread
|
||||
win.linkLib=User32,Advapi32
|
||||
```
|
||||
|
||||
## How compilation works
|
||||
Modularity compiles scripts into shared libraries and loads them by symbol name.
|
||||
|
||||
- Source lives under `Scripts/`.
|
||||
- Output binaries are written to `Cache/ScriptBin/`.
|
||||
- Binaries are platform-specific:
|
||||
- Windows: `.dll`
|
||||
- Linux: `.so`
|
||||
|
||||
### Wrapper generation (important)
|
||||
To reduce boilerplate, Modularity auto-generates a wrapper for these hook names **if it detects them in your script**:
|
||||
- `Begin`
|
||||
- `TickUpdate`
|
||||
- `Update`
|
||||
- `Spec`
|
||||
- `TestEditor`
|
||||
|
||||
That wrapper exports `Script_Begin`, `Script_TickUpdate`, etc. This means you can usually write plain functions like:
|
||||
```cpp
|
||||
void TickUpdate(ScriptContext& ctx, float dt) {
|
||||
(void)dt;
|
||||
if (!ctx.object) return;
|
||||
}
|
||||
```
|
||||
|
||||
However:
|
||||
- `Script_OnInspector` is **not** wrapper-generated. If you want inspector UI, you must export it explicitly with `extern "C"`.
|
||||
- Scripted editor windows (`RenderEditorWindow`, `ExitRenderEditorWindow`) are also **not** wrapper-generated; export them explicitly with `extern "C"`.
|
||||
|
||||
## Lifecycle hooks
|
||||
- **Inspector**: `Script_OnInspector(ScriptContext&)` is called when the script is inspected in the UI.
|
||||
- **Begin**: `Script_Begin` runs once per object instance before ticking.
|
||||
- **Spec/Test**: `Script_Spec` and `Script_TestEditor` run every frame when the global “Spec Mode” / “Test Mode” toggles are enabled (Scripts menu).
|
||||
- **Tick**: `Script_TickUpdate` runs every frame for each script; `Script_Update` is a fallback if TickUpdate is missing.
|
||||
- All tick-style hooks receive `deltaTime` (seconds) and the `ScriptContext`.
|
||||
All hooks are optional. If a hook is missing, it is simply not called.
|
||||
|
||||
Hook list:
|
||||
- `Script_OnInspector(ScriptContext&)` (manual export required)
|
||||
- `Script_Begin(ScriptContext&, float deltaTime)` (wrapper-generated from `Begin`)
|
||||
- `Script_TickUpdate(ScriptContext&, float deltaTime)` (wrapper-generated from `TickUpdate`)
|
||||
- `Script_Update(ScriptContext&, float deltaTime)` (wrapper-generated from `Update`, used only if TickUpdate missing)
|
||||
- `Script_Spec(ScriptContext&, float deltaTime)` (wrapper-generated from `Spec`)
|
||||
- `Script_TestEditor(ScriptContext&, float deltaTime)` (wrapper-generated from `TestEditor`)
|
||||
|
||||
Runtime notes:
|
||||
- `Begin` runs once per object instance (per script component instance).
|
||||
- `TickUpdate` runs every frame (preferred).
|
||||
- `Update` runs only if `TickUpdate` is not exported.
|
||||
- `Spec/TestEditor` run every frame only while their global toggles are enabled (main menu -> Scripts).
|
||||
|
||||
## ScriptContext
|
||||
`ScriptContext` is passed into most hooks and provides access to the engine, the owning object, and helper APIs.
|
||||
|
||||
## ScriptContext helpers
|
||||
Available methods:
|
||||
- `FindObjectByName`, `FindObjectById`
|
||||
- `SetPosition`, `SetRotation`, `SetScale`
|
||||
- `HasRigidbody`
|
||||
- `SetRigidbodyVelocity`, `GetRigidbodyVelocity`
|
||||
- `SetRigidbodyAngularVelocity`, `GetRigidbodyAngularVelocity`
|
||||
- `AddRigidbodyForce`, `AddRigidbodyImpulse`
|
||||
- `AddRigidbodyTorque`, `AddRigidbodyAngularImpulse`
|
||||
- `SetRigidbodyRotation`, `TeleportRigidbody`
|
||||
- `MarkDirty` (flags the project as having unsaved changes)
|
||||
Fields:
|
||||
- `engine`: pointer to the Engine
|
||||
- `object`: pointer to the owning `SceneObject`
|
||||
- `script`: pointer to the owning `ScriptComponent` (gives access to per-script `settings`)
|
||||
- `engine` (`Engine*`) - engine pointer
|
||||
- `object` (`SceneObject*`) - owning object pointer (may be null)
|
||||
- `script` (`ScriptComponent*`) - owning script component (settings storage)
|
||||
|
||||
## Persisting per-script settings
|
||||
- Each `ScriptComponent` has `settings` (key/value strings) serialized with the scene.
|
||||
- You can read/write them via `ctx.script->settings` or helper functions in your script.
|
||||
- After mutating settings or object transforms, call `ctx.MarkDirty()` so Ctrl+S captures changes.
|
||||
### Object lookup
|
||||
- `FindObjectByName(const std::string&)`
|
||||
- `FindObjectById(int)`
|
||||
|
||||
## Example pattern (simplified)
|
||||
### Transform helpers
|
||||
- `SetPosition(const glm::vec3&)`
|
||||
- `SetRotation(const glm::vec3&)` (degrees)
|
||||
- `SetScale(const glm::vec3&)`
|
||||
- `SetPosition2D(const glm::vec2&)` (UI position in pixels)
|
||||
|
||||
### UI helpers (Buttons/Sliders)
|
||||
- `IsUIButtonPressed()`
|
||||
- `IsUIInteractable()`, `SetUIInteractable(bool)`
|
||||
- `GetUISliderValue()`, `SetUISliderValue(float)`
|
||||
- `SetUISliderRange(float min, float max)`
|
||||
- `SetUILabel(const std::string&)`, `SetUIColor(const glm::vec4&)`
|
||||
- `GetUITextScale()`, `SetUITextScale(float)`
|
||||
- `SetUISliderStyle(UISliderStyle)`
|
||||
- `SetUIButtonStyle(UIButtonStyle)`
|
||||
- `SetUIStylePreset(const std::string&)`
|
||||
- `RegisterUIStylePreset(const std::string& name, const ImGuiStyle& style, bool replace = false)`
|
||||
|
||||
### Rigidbody helpers (3D)
|
||||
- `HasRigidbody()`
|
||||
- `SetRigidbodyVelocity(const glm::vec3&)`, `GetRigidbodyVelocity(glm::vec3& out)`
|
||||
- `SetRigidbodyAngularVelocity(const glm::vec3&)`, `GetRigidbodyAngularVelocity(glm::vec3& out)`
|
||||
- `AddRigidbodyForce(const glm::vec3&)`, `AddRigidbodyImpulse(const glm::vec3&)`
|
||||
- `AddRigidbodyTorque(const glm::vec3&)`, `AddRigidbodyAngularImpulse(const glm::vec3&)`
|
||||
- `SetRigidbodyRotation(const glm::vec3& rotDeg)`
|
||||
- `TeleportRigidbody(const glm::vec3& pos, const glm::vec3& rotDeg)`
|
||||
|
||||
### Rigidbody2D helpers (UI/canvas only)
|
||||
- `HasRigidbody2D()`
|
||||
- `SetRigidbody2DVelocity(const glm::vec2&)`, `GetRigidbody2DVelocity(glm::vec2& out)`
|
||||
|
||||
### Audio helpers
|
||||
- `HasAudioSource()`
|
||||
- `PlayAudio()`, `StopAudio()`
|
||||
- `SetAudioLoop(bool)`
|
||||
- `SetAudioVolume(float)`
|
||||
- `SetAudioClip(const std::string& path)`
|
||||
|
||||
### Settings + utility
|
||||
- `GetSetting(key, fallback)`, `SetSetting(key, value)`
|
||||
- `GetSettingBool(key, fallback)`, `SetSettingBool(key, value)`
|
||||
- `GetSettingVec3(key, fallback)`, `SetSettingVec3(key, value)`
|
||||
- `AutoSetting(key, bool|glm::vec3|buffer)`, `SaveAutoSettings()`
|
||||
- `AddConsoleMessage(text, type)`
|
||||
- `MarkDirty()`
|
||||
|
||||
## ImGui in scripts
|
||||
Modularity uses Dear ImGui for editor UI. Scripts can draw ImGui in two places:
|
||||
|
||||
### Inspector UI (per object)
|
||||
Export `Script_OnInspector(ScriptContext&)`:
|
||||
```cpp
|
||||
static bool autoRotate = false;
|
||||
static glm::vec3 speed = {0, 45, 0};
|
||||
#include "ScriptRuntime.h"
|
||||
#include "ThirdParty/imgui/imgui.h"
|
||||
|
||||
void Script_OnInspector(ScriptContext& ctx) {
|
||||
static bool autoRotate = false;
|
||||
|
||||
extern "C" void Script_OnInspector(ScriptContext& ctx) {
|
||||
ImGui::Checkbox("Auto Rotate", &autoRotate);
|
||||
ImGui::DragFloat3("Speed", &speed.x, 1.f, -360.f, 360.f);
|
||||
ctx.MarkDirty();
|
||||
}
|
||||
```
|
||||
|
||||
void Script_Begin(ScriptContext& ctx, float) {
|
||||
ctx.MarkDirty(); // ensure initial state is saved
|
||||
> Tip: `Script_OnInspector` must be exported exactly with `extern "C"` (it is not wrapper-generated).
|
||||
> Important: Do not call ImGui functions (e.g., `ImGui::Text`) from `TickUpdate` or other runtime hooks. Those run before the ImGui frame is active and outside any window, which can crash.
|
||||
|
||||
### Scripted editor windows (custom tabs)
|
||||
See [Scripted editor windows](#scripted-editor-windows).
|
||||
|
||||
## Per-script settings
|
||||
Each `ScriptComponent` owns serialized key/value strings (`ctx.script->settings`). Use them to persist state with the scene.
|
||||
|
||||
### Direct settings
|
||||
```cpp
|
||||
void TickUpdate(ScriptContext& ctx, float) {
|
||||
if (!ctx.script) return;
|
||||
ctx.SetSetting("mode", "hard");
|
||||
ctx.MarkDirty();
|
||||
}
|
||||
```
|
||||
|
||||
void Script_TickUpdate(ScriptContext& ctx, float dt) {
|
||||
if (autoRotate && ctx.object) {
|
||||
ctx.SetRotation(ctx.object->rotation + speed * dt);
|
||||
### AutoSetting (recommended for inspector UI)
|
||||
`AutoSetting` binds a variable to a key and loads/saves automatically when you call `SaveAutoSettings()`.
|
||||
```cpp
|
||||
extern "C" void Script_OnInspector(ScriptContext& ctx) {
|
||||
static bool enabled = false;
|
||||
ctx.AutoSetting("enabled", enabled);
|
||||
ImGui::Checkbox("Enabled", &enabled);
|
||||
ctx.SaveAutoSettings();
|
||||
}
|
||||
```
|
||||
|
||||
## UI scripting
|
||||
UI elements are scene objects (Create -> 2D/UI). They render in the **Game Viewport** overlay.
|
||||
|
||||
### Button clicks
|
||||
`IsUIButtonPressed()` is true only on the frame the click happens.
|
||||
```cpp
|
||||
void TickUpdate(ScriptContext& ctx, float) {
|
||||
if (ctx.IsUIButtonPressed()) {
|
||||
ctx.AddConsoleMessage("Button clicked!");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Runtime behavior
|
||||
- Scripts tick for all objects every frame, even if not selected.
|
||||
- Spec/Test toggles are global (main menu → Scripts).
|
||||
- Compile scripts via the UI “Compile Script” button or run the build command; wrapper generation is automatic.
|
||||
|
||||
## Rigidbody helper usage
|
||||
- `SetRigidbodyAngularVelocity(vec3)` sets angular velocity in radians/sec for dynamic, non-kinematic bodies.
|
||||
### Sliders as meters (health/ammo)
|
||||
Set `Interactable` to false to make a slider read-only.
|
||||
```cpp
|
||||
ctx.SetRigidbodyAngularVelocity({0.0f, 3.0f, 0.0f});
|
||||
```
|
||||
- `GetRigidbodyAngularVelocity(out vec3)` reads current angular velocity into `out`. Returns false if unavailable.
|
||||
```cpp
|
||||
glm::vec3 angVel;
|
||||
if (ctx.GetRigidbodyAngularVelocity(angVel)) {
|
||||
ctx.AddConsoleMessage("AngVel Y: " + std::to_string(angVel.y));
|
||||
void TickUpdate(ScriptContext& ctx, float) {
|
||||
ctx.SetUIInteractable(false);
|
||||
ctx.SetUISliderStyle(UISliderStyle::Fill);
|
||||
ctx.SetUISliderRange(0.0f, 100.0f);
|
||||
ctx.SetUISliderValue(health);
|
||||
}
|
||||
```
|
||||
- `AddRigidbodyForce(vec3)` applies continuous force (mass-aware).
|
||||
|
||||
### Style presets
|
||||
You can register custom ImGui style presets in code and then select them per UI element in the Inspector.
|
||||
```cpp
|
||||
ctx.AddRigidbodyForce({0.0f, 0.0f, 25.0f});
|
||||
void Begin(ScriptContext& ctx, float) {
|
||||
ImGuiStyle style = ImGui::GetStyle();
|
||||
style.Colors[ImGuiCol_Button] = ImVec4(0.20f, 0.50f, 0.90f, 1.00f);
|
||||
style.Colors[ImGuiCol_ButtonHovered] = ImVec4(0.25f, 0.60f, 1.00f, 1.00f);
|
||||
ctx.RegisterUIStylePreset("Ocean", style, true);
|
||||
}
|
||||
```
|
||||
- `AddRigidbodyImpulse(vec3)` applies an instant impulse (mass-aware).
|
||||
Then select **UI -> Style Preset** on a button or slider.
|
||||
|
||||
### Finding other UI objects
|
||||
```cpp
|
||||
ctx.AddRigidbodyImpulse({0.0f, 6.5f, 0.0f});
|
||||
void TickUpdate(ScriptContext& ctx, float) {
|
||||
if (SceneObject* other = ctx.FindObjectByName("UI Button 3")) {
|
||||
if (other->type == ObjectType::UIButton && other->ui.buttonPressed) {
|
||||
ctx.AddConsoleMessage("Other button clicked!");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
- `AddRigidbodyTorque(vec3)` applies continuous torque.
|
||||
|
||||
## IEnum tasks
|
||||
Modularity provides lightweight, opt-in “tasks” you can start/stop per script component instance.
|
||||
|
||||
Important: In this version, an IEnum task is **just a function** with signature `void(ScriptContext&, float)` that is called every frame while it’s registered.
|
||||
|
||||
Start/stop macros:
|
||||
- `IEnum_Start(fn)` / `IEnum_Stop(fn)` / `IEnum_Ensure(fn)`
|
||||
|
||||
Example (toggle rotation without cluttering TickUpdate):
|
||||
```cpp
|
||||
ctx.AddRigidbodyTorque({0.0f, 15.0f, 0.0f});
|
||||
```
|
||||
- `AddRigidbodyAngularImpulse(vec3)` applies an instant angular impulse.
|
||||
```cpp
|
||||
ctx.AddRigidbodyAngularImpulse({0.0f, 4.0f, 0.0f});
|
||||
```
|
||||
- `SetRigidbodyRotation(vec3 degrees)` teleports the rigidbody rotation.
|
||||
```cpp
|
||||
ctx.SetRigidbodyRotation({0.0f, 90.0f, 0.0f});
|
||||
static bool autoRotate = false;
|
||||
static glm::vec3 speed = {0, 45, 0};
|
||||
|
||||
static void RotateTask(ScriptContext& ctx, float dt) {
|
||||
if (!ctx.object) return;
|
||||
ctx.SetRotation(ctx.object->rotation + speed * dt);
|
||||
}
|
||||
|
||||
extern "C" void Script_OnInspector(ScriptContext& ctx) {
|
||||
ImGui::Checkbox("Auto Rotate", &autoRotate);
|
||||
if (autoRotate) IEnum_Ensure(RotateTask);
|
||||
else IEnum_Stop(RotateTask);
|
||||
ctx.MarkDirty();
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- These return false if the object has no enabled rigidbody or is kinematic.
|
||||
- Use force/torque for continuous input and impulses for bursty actions.
|
||||
- `SetRigidbodyRotation` is authoritative; use it sparingly during gameplay.
|
||||
- Tasks are stored per `ScriptComponent` instance.
|
||||
- Don’t spam logs every frame inside a task; use “warn once” patterns.
|
||||
|
||||
## Logging
|
||||
Use `ctx.AddConsoleMessage(text, type)` to write to the editor console.
|
||||
|
||||
Typical types:
|
||||
- `ConsoleMessageType::Info`
|
||||
- `ConsoleMessageType::Success`
|
||||
- `ConsoleMessageType::Warning`
|
||||
- `ConsoleMessageType::Error`
|
||||
|
||||
Warn-once pattern:
|
||||
```cpp
|
||||
static bool warned = false;
|
||||
if (!warned) {
|
||||
ctx.AddConsoleMessage("[MyScript] Something looks off", ConsoleMessageType::Warning);
|
||||
warned = true;
|
||||
}
|
||||
```
|
||||
|
||||
## Scripted editor windows
|
||||
Scripts can expose ImGui-powered editor tabs by exporting:
|
||||
- `RenderEditorWindow(ScriptContext& ctx)` (called every frame while tab is open)
|
||||
- `ExitRenderEditorWindow(ScriptContext& ctx)` (called once when tab closes)
|
||||
|
||||
Example:
|
||||
```cpp
|
||||
#include "ScriptRuntime.h"
|
||||
#include "ThirdParty/imgui/imgui.h"
|
||||
|
||||
extern "C" void RenderEditorWindow(ScriptContext& ctx) {
|
||||
ImGui::TextUnformatted("Hello from script!");
|
||||
if (ImGui::Button("Log")) {
|
||||
ctx.AddConsoleMessage("Editor window clicked");
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void ExitRenderEditorWindow(ScriptContext& ctx) {
|
||||
(void)ctx;
|
||||
}
|
||||
```
|
||||
|
||||
How to open:
|
||||
1. Compile the script so the binary is updated under `Cache/ScriptBin/`.
|
||||
2. In the main menu, go to **View -> Scripted Windows** and toggle the entry.
|
||||
|
||||
## Manual compile (CLI)
|
||||
Linux example:
|
||||
Linux:
|
||||
```bash
|
||||
g++ -std=c++20 -fPIC -O0 -g -I../src -I../include -c SampleInspector.cpp -o ../Cache/ScriptBin/SampleInspector.o
|
||||
g++ -shared ../Cache/ScriptBin/SampleInspector.o -o ../Cache/ScriptBin/SampleInspector.so -ldl -lpthread
|
||||
```
|
||||
Windows example:
|
||||
|
||||
Windows:
|
||||
```bat
|
||||
cl /nologo /std:c++20 /EHsc /MD /Zi /Od /I ..\\src /I ..\\include /c SampleInspector.cpp /Fo ..\\Cache\\ScriptBin\\SampleInspector.obj
|
||||
link /nologo /DLL ..\\Cache\\ScriptBin\\SampleInspector.obj /OUT:..\\Cache\\ScriptBin\\SampleInspector.dll User32.lib Advapi32.lib
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
- **Script not running**
|
||||
- Ensure the object is enabled and the script component is enabled.
|
||||
- Ensure the script path points to a real file and the compiled binary exists.
|
||||
- **No inspector UI**
|
||||
- `Script_OnInspector` must be exported with `extern "C"` (no wrapper is generated for it).
|
||||
- **Changes not saved**
|
||||
- Call `ctx.MarkDirty()` after mutating transforms/settings you want to persist.
|
||||
- **Editor window not showing**
|
||||
- Ensure `RenderEditorWindow` is exported with `extern "C"` and the binary is up to date.
|
||||
- **Custom UI style preset not listed**
|
||||
- Ensure `RegisterUIStylePreset(...)` ran (e.g. in `Begin`) before selecting it in the Inspector.
|
||||
- **Hard crash**
|
||||
- Add null checks, avoid static pointers to scene objects, and don’t hold references across frames unless you can validate them.
|
||||
|
||||
## Templates
|
||||
### Minimal runtime script (wrapper-based)
|
||||
```cpp
|
||||
#include "ScriptRuntime.h"
|
||||
|
||||
void TickUpdate(ScriptContext& ctx, float /*dt*/) {
|
||||
if (!ctx.object) return;
|
||||
}
|
||||
```
|
||||
|
||||
### Minimal script with inspector (manual export)
|
||||
```cpp
|
||||
#include "ScriptRuntime.h"
|
||||
#include "ThirdParty/imgui/imgui.h"
|
||||
|
||||
void TickUpdate(ScriptContext& ctx, float /*dt*/) {
|
||||
if (!ctx.object) return;
|
||||
}
|
||||
|
||||
extern "C" void Script_OnInspector(ScriptContext& ctx) {
|
||||
ImGui::TextDisabled("Hello from inspector!");
|
||||
(void)ctx;
|
||||
}
|
||||
```
|
||||
### Text
|
||||
Use **UI Text** objects for on-screen text. Update their `label` and size from scripts:
|
||||
```cpp
|
||||
void TickUpdate(ScriptContext& ctx, float) {
|
||||
if (SceneObject* text = ctx.FindObjectByName("UI Text 2")) {
|
||||
if (text->type == ObjectType::UIText) {
|
||||
text->ui.label = "Speed: 12.4";
|
||||
text->ui.textScale = 1.4f;
|
||||
ctx.MarkDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### FPS display example
|
||||
Attach `Scripts/FPSDisplay.cpp` to a **UI Text** object to show FPS. The inspector exposes a checkbox to clamp FPS to 120.
|
||||
|
||||
@@ -188,7 +188,7 @@ void Engine::renderLauncher() {
|
||||
ImGui::SetWindowFontScale(1.4f);
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.96f, 0.98f, 1.0f), "Modularity");
|
||||
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();
|
||||
@@ -349,13 +349,10 @@ void Engine::renderLauncher() {
|
||||
ImGui::Spacing();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::TextDisabled("Modularity Engine - Version 0.6.8");
|
||||
|
||||
ImGui::TextDisabled("Modularity Engine - Beta V1.0");
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
|
||||
@@ -29,10 +29,17 @@ namespace {
|
||||
case ObjectType::AreaLight: return IM_COL32(255, 200, 90, 220);
|
||||
case ObjectType::PostFXNode: return IM_COL32(200, 140, 230, 220);
|
||||
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::Plane: return IM_COL32(170, 180, 190, 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);
|
||||
}
|
||||
}
|
||||
@@ -194,6 +201,25 @@ void Engine::renderHierarchyPanel() {
|
||||
ImGuiPopupFlags_MouseButtonRight |
|
||||
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"))
|
||||
{
|
||||
// ── Primitives ─────────────────────────────
|
||||
@@ -204,10 +230,23 @@ void Engine::renderHierarchyPanel() {
|
||||
if (ImGui::MenuItem("Capsule")) addObject(ObjectType::Capsule, "Capsule");
|
||||
if (ImGui::MenuItem("Plane")) addObject(ObjectType::Plane, "Plane");
|
||||
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");
|
||||
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 ────────────────────────────────
|
||||
if (ImGui::BeginMenu("Lights"))
|
||||
{
|
||||
@@ -224,6 +263,16 @@ void Engine::renderHierarchyPanel() {
|
||||
if (ImGui::MenuItem("Post FX Node")) addObject(ObjectType::PostFXNode, "Post FX");
|
||||
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");
|
||||
|
||||
ImGui::EndMenu();
|
||||
@@ -341,6 +390,20 @@ void Engine::renderObjectNode(SceneObject& obj, const std::string& filter,
|
||||
deleteSelected();
|
||||
}
|
||||
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) {
|
||||
setParent(obj.id, -1);
|
||||
}
|
||||
@@ -901,6 +964,14 @@ void Engine::renderInspectorPanel() {
|
||||
|
||||
SceneObject& obj = *it;
|
||||
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) {
|
||||
ImGui::Text("Multiple objects selected: %zu", selectedObjectIds.size());
|
||||
@@ -931,6 +1002,13 @@ void Engine::renderInspectorPanel() {
|
||||
case ObjectType::Capsule: typeLabel = "Capsule"; break;
|
||||
case ObjectType::OBJMesh: typeLabel = "OBJ Mesh"; 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::DirectionalLight: typeLabel = "Directional Light"; break;
|
||||
case ObjectType::PointLight: typeLabel = "Point Light"; break;
|
||||
@@ -986,6 +1064,9 @@ void Engine::renderInspectorPanel() {
|
||||
if (obj.type == ObjectType::PostFXNode) {
|
||||
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::PushItemWidth(-1);
|
||||
@@ -1032,6 +1113,138 @@ void Engine::renderInspectorPanel() {
|
||||
|
||||
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) {
|
||||
ImGui::Spacing();
|
||||
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.");
|
||||
} 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;
|
||||
}
|
||||
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));
|
||||
bool removeRigidbody = 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")) {
|
||||
removeRigidbody = true;
|
||||
}
|
||||
@@ -1173,9 +1386,12 @@ void Engine::renderInspectorPanel() {
|
||||
changed = true;
|
||||
}
|
||||
if (header.open) {
|
||||
ImGui::PushID("Rigidbody");
|
||||
ImGui::PushID("Rigidbody3D");
|
||||
ImGui::Indent(10.0f);
|
||||
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")) {
|
||||
obj.rigidbody.mass = std::max(0.01f, obj.rigidbody.mass);
|
||||
@@ -1218,6 +1434,52 @@ void Engine::renderInspectorPanel() {
|
||||
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) {
|
||||
ImGui::Spacing();
|
||||
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)
|
||||
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::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.35f, 0.55f, 1.0f));
|
||||
|
||||
@@ -1951,6 +2213,7 @@ void Engine::renderInspectorPanel() {
|
||||
ImGui::PushID(inspectorId.c_str());
|
||||
inspector(ctx);
|
||||
ImGui::PopID();
|
||||
ctx.SaveAutoSettings();
|
||||
} else if (!scriptRuntime.getLastError().empty()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.6f, 1.0f), "Inspector load failed");
|
||||
ImGui::TextWrapped("%s", scriptRuntime.getLastError().c_str());
|
||||
@@ -2052,11 +2315,21 @@ void Engine::renderInspectorPanel() {
|
||||
ImGui::OpenPopup("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.rigidbody = RigidbodyComponent{};
|
||||
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")) {
|
||||
obj.hasPlayerController = true;
|
||||
obj.playerController = PlayerControllerComponent{};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
320
src/Engine.cpp
320
src/Engine.cpp
@@ -5,6 +5,7 @@
|
||||
#include <functional>
|
||||
#include <unordered_set>
|
||||
#include <unordered_map>
|
||||
#include "ThirdParty/glm/gtc/constants.hpp"
|
||||
|
||||
#pragma region Material File IO Helpers
|
||||
namespace {
|
||||
@@ -78,6 +79,122 @@ bool writeMaterialFile(const MaterialFileData& data, const std::string& path) {
|
||||
f << "fragmentShader=" << data.fragmentShader << "\n";
|
||||
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
|
||||
#pragma endregion
|
||||
|
||||
@@ -310,6 +427,7 @@ void Engine::run() {
|
||||
std::cerr << "[DEBUG] Entering main loop, showLauncher=" << showLauncher << std::endl;
|
||||
|
||||
while (!glfwWindowShouldClose(editorWindow)) {
|
||||
double frameStart = glfwGetTime();
|
||||
if (glfwGetWindowAttrib(editorWindow, GLFW_ICONIFIED)) {
|
||||
ImGui_ImplGlfw_Sleep(10);
|
||||
continue;
|
||||
@@ -362,6 +480,11 @@ void Engine::run() {
|
||||
updatePlayerController(deltaTime);
|
||||
}
|
||||
|
||||
bool simulate2D = (isPlaying && !isPaused) || (!isPlaying && specMode) || (!isPlaying && testMode);
|
||||
if (simulate2D) {
|
||||
updateRigidbody2D(deltaTime);
|
||||
}
|
||||
|
||||
updateHierarchyWorldTransforms();
|
||||
|
||||
bool simulatePhysics = physics.isReady() && ((isPlaying && !isPaused) || (!isPlaying && specMode));
|
||||
@@ -465,6 +588,16 @@ void Engine::run() {
|
||||
}
|
||||
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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 region Mesh Editing
|
||||
@@ -600,6 +772,7 @@ bool Engine::ensureMeshEditTarget(SceneObject* obj) {
|
||||
}
|
||||
meshEditLoaded = true;
|
||||
meshEditPath = obj->meshPath;
|
||||
meshEditDirty = false;
|
||||
meshEditSelectedVertices.clear();
|
||||
meshEditSelectedEdges.clear();
|
||||
meshEditSelectedFaces.clear();
|
||||
@@ -617,6 +790,23 @@ bool Engine::syncMeshEditToGPU(SceneObject* obj) {
|
||||
projectManager.currentProject.hasUnsavedChanges = 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 region Material IO
|
||||
@@ -942,6 +1132,33 @@ void Engine::updatePlayerController(float delta) {
|
||||
}
|
||||
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 region Transform Hierarchy
|
||||
@@ -1071,6 +1288,13 @@ void Engine::updateHierarchyWorldTransforms() {
|
||||
worldPos = obj.position;
|
||||
worldRot = QuatFromEulerXYZ(obj.rotation);
|
||||
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 {
|
||||
glm::quat localRot = QuatFromEulerXYZ(obj.localRotation);
|
||||
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);
|
||||
} else if (type == ObjectType::Plane) {
|
||||
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().localRotation = NormalizeEulerDegrees(sceneObjects.back().rotation);
|
||||
@@ -1369,6 +1614,8 @@ void Engine::duplicateSelected() {
|
||||
newObj.postFx = it->postFx;
|
||||
newObj.hasRigidbody = it->hasRigidbody;
|
||||
newObj.rigidbody = it->rigidbody;
|
||||
newObj.hasRigidbody2D = it->hasRigidbody2D;
|
||||
newObj.rigidbody2D = it->rigidbody2D;
|
||||
newObj.hasCollider = it->hasCollider;
|
||||
newObj.collider = it->collider;
|
||||
newObj.hasPlayerController = it->hasPlayerController;
|
||||
@@ -1379,6 +1626,7 @@ void Engine::duplicateSelected() {
|
||||
newObj.localInitialized = true;
|
||||
newObj.hasAudioSource = it->hasAudioSource;
|
||||
newObj.audioSource = it->audioSource;
|
||||
newObj.ui = it->ui;
|
||||
|
||||
sceneObjects.push_back(newObj);
|
||||
setPrimarySelection(id);
|
||||
@@ -1565,6 +1813,13 @@ bool Engine::raycastClosestFromScript(const glm::vec3& origin, const glm::vec3&
|
||||
}
|
||||
|
||||
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::quat parentRot(1.0f, 0.0f, 0.0f, 0.0f);
|
||||
glm::vec3 parentScale(1.0f);
|
||||
@@ -1578,6 +1833,11 @@ void Engine::syncLocalTransform(SceneObject& obj) {
|
||||
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) {
|
||||
SceneObject* obj = findObjectById(id);
|
||||
if (!obj || !obj->hasAudioSource) return false;
|
||||
@@ -1820,6 +2080,7 @@ void Engine::setupImGui() {
|
||||
style.WindowRounding = 0.0f;
|
||||
style.Colors[ImGuiCol_WindowBg].w = 1.0f;
|
||||
}
|
||||
initUIStylePresets();
|
||||
|
||||
std::cerr << "[DEBUG] setupImGui: initializing ImGui GLFW backend..." << std::endl;
|
||||
ImGui_ImplGlfw_InitForOpenGL(editorWindow, true);
|
||||
@@ -1832,3 +2093,62 @@ void Engine::setupImGui() {
|
||||
std::cerr << "[DEBUG] setupImGui: complete!" << std::endl;
|
||||
}
|
||||
#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);
|
||||
}
|
||||
|
||||
21
src/Engine.h
21
src/Engine.h
@@ -121,6 +121,8 @@ private:
|
||||
char meshBuilderFaceInput[128] = "";
|
||||
bool meshEditMode = false;
|
||||
bool meshEditLoaded = false;
|
||||
bool meshEditDirty = false;
|
||||
bool meshEditExtrudeMode = false;
|
||||
std::string meshEditPath;
|
||||
RawMeshAsset meshEditAsset;
|
||||
std::vector<int> meshEditSelectedVertices;
|
||||
@@ -139,6 +141,15 @@ private:
|
||||
bool specMode = false;
|
||||
bool testMode = 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
|
||||
SceneObject* getSelectedObject();
|
||||
glm::vec3 getSelectionCenterWorld(bool worldSpace) const;
|
||||
@@ -154,8 +165,10 @@ private:
|
||||
void importOBJToScene(const std::string& filepath, const std::string& objectName);
|
||||
void importModelToScene(const std::string& filepath, const std::string& objectName); // Assimp import
|
||||
void convertModelToRawMesh(const std::string& filepath);
|
||||
void createRMeshPrimitive(const std::string& primitiveName);
|
||||
bool ensureMeshEditTarget(SceneObject* obj);
|
||||
bool syncMeshEditToGPU(SceneObject* obj);
|
||||
bool saveMeshEditAsset(std::string& error);
|
||||
void handleKeyboardShortcuts();
|
||||
void OpenProjectPath(const std::string& path);
|
||||
|
||||
@@ -184,6 +197,11 @@ private:
|
||||
void compileScriptFile(const fs::path& scriptPath);
|
||||
void updateScripts(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 renderFileBrowserBreadcrumb();
|
||||
@@ -265,4 +283,7 @@ public:
|
||||
bool setAudioVolumeFromScript(int id, float volume);
|
||||
bool setAudioClipFromScript(int id, const std::string& path);
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -16,8 +16,12 @@ PxVec3 ToPxVec3(const glm::vec3& v) {
|
||||
}
|
||||
|
||||
PxQuat ToPxQuat(const glm::vec3& eulerDeg) {
|
||||
glm::vec3 radians = glm::radians(eulerDeg);
|
||||
glm::quat q = glm::quat(radians);
|
||||
glm::vec3 r = glm::radians(eulerDeg);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -25,9 +29,20 @@ glm::vec3 ToGlmVec3(const PxVec3& v) {
|
||||
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::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
|
||||
|
||||
@@ -234,6 +249,13 @@ bool PhysicsSystem::attachPrimitiveShape(PxRigidActor* actor, const SceneObject&
|
||||
tuneShape(shape, std::min({halfExtents.x, halfExtents.y, halfExtents.z}) * 2.0f, isDynamic);
|
||||
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: {
|
||||
float radius = std::max({obj.scale.x, obj.scale.y, obj.scale.z}) * 0.5f;
|
||||
radius = std::max(radius, 0.01f);
|
||||
|
||||
@@ -299,7 +299,7 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
|
||||
if (!file.is_open()) return false;
|
||||
|
||||
file << "# Scene File\n";
|
||||
file << "version=10\n";
|
||||
file << "version=11\n";
|
||||
file << "nextId=" << nextId << "\n";
|
||||
file << "objectCount=" << objects.size() << "\n";
|
||||
file << "\n";
|
||||
@@ -328,6 +328,14 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
|
||||
file << "rbLockRotY=" << (obj.rigidbody.lockRotationY ? 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";
|
||||
if (obj.hasCollider) {
|
||||
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 << "cameraFar=" << obj.camera.farClip << "\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) {
|
||||
file << "postEnabled=" << (obj.postFx.enabled ? 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;
|
||||
} else if (key == "rbLockRotZ") {
|
||||
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",
|
||||
¤tObj->rigidbody2D.velocity.x,
|
||||
¤tObj->rigidbody2D.velocity.y);
|
||||
} else if (key == "hasCollider") {
|
||||
currentObj->hasCollider = std::stoi(value) != 0;
|
||||
} else if (key == "colliderEnabled") {
|
||||
@@ -701,6 +736,40 @@ bool SceneSerializer::loadScene(const fs::path& filePath,
|
||||
currentObj->camera.farClip = std::stof(value);
|
||||
} else if (key == "cameraPostFX") {
|
||||
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",
|
||||
¤tObj->ui.position.x,
|
||||
¤tObj->ui.position.y);
|
||||
} else if (key == "uiSize") {
|
||||
sscanf(value.c_str(), "%f,%f",
|
||||
¤tObj->ui.size.x,
|
||||
¤tObj->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",
|
||||
¤tObj->ui.color.r,
|
||||
¤tObj->ui.color.g,
|
||||
¤tObj->ui.color.b,
|
||||
¤tObj->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") {
|
||||
currentObj->postFx.enabled = (std::stoi(value) != 0);
|
||||
} else if (key == "postBloomEnabled") {
|
||||
|
||||
@@ -993,7 +993,7 @@ void Renderer::renderObject(const SceneObject& obj) {
|
||||
shader->setFloat("specularStrength", obj.material.specularStrength);
|
||||
shader->setFloat("shininess", obj.material.shininess);
|
||||
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;
|
||||
if (!obj.albedoTexturePath.empty()) {
|
||||
@@ -1046,6 +1046,9 @@ void Renderer::renderObject(const SceneObject& obj) {
|
||||
case ObjectType::Mirror:
|
||||
if (planeMesh) planeMesh->draw();
|
||||
break;
|
||||
case ObjectType::Sprite:
|
||||
if (planeMesh) planeMesh->draw();
|
||||
break;
|
||||
case ObjectType::Torus:
|
||||
if (torusMesh) torusMesh->draw();
|
||||
break;
|
||||
@@ -1078,6 +1081,14 @@ void Renderer::renderObject(const SceneObject& obj) {
|
||||
break;
|
||||
case ObjectType::PostFXNode:
|
||||
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 (!drawMirrorObjects && obj.type == ObjectType::Mirror) continue;
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -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::Plane) 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::OBJMesh && obj.meshId != -1) {
|
||||
meshToDraw = g_objLoader.getMesh(obj.meshId);
|
||||
@@ -1461,7 +1473,7 @@ unsigned int Renderer::applyPostProcessing(const std::vector<SceneObject>& scene
|
||||
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);
|
||||
renderSceneInternal(camera, sceneObjects, currentWidth, currentHeight, true, fovDeg, nearPlane, farPlane, true);
|
||||
if (drawColliders) {
|
||||
@@ -1470,6 +1482,7 @@ void Renderer::renderScene(const Camera& camera, const std::vector<SceneObject>&
|
||||
renderCollisionOverlay(camera, sceneObjects, currentWidth, currentHeight, fovDeg, nearPlane, farPlane);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
renderSelectionOutline(camera, sceneObjects, selectedId, fovDeg, nearPlane, farPlane);
|
||||
unsigned int result = applyPostProcessing(sceneObjects, viewportTexture, currentWidth, currentHeight, true);
|
||||
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 };
|
||||
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 polyOffsetLine = glIsEnabled(GL_POLYGON_OFFSET_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:
|
||||
meshToDraw = planeMesh;
|
||||
break;
|
||||
case ObjectType::Sprite:
|
||||
meshToDraw = planeMesh;
|
||||
break;
|
||||
case ObjectType::Torus:
|
||||
meshToDraw = sphereMesh;
|
||||
break;
|
||||
@@ -1605,6 +1625,153 @@ void Renderer::renderCollisionOverlay(const Camera& camera, const std::vector<Sc
|
||||
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() {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
@@ -147,6 +147,7 @@ public:
|
||||
void renderSkybox(const glm::mat4& view, const glm::mat4& proj);
|
||||
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 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);
|
||||
void renderCollisionOverlay(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane);
|
||||
void endRender();
|
||||
|
||||
@@ -3,20 +3,27 @@
|
||||
#include "Common.h"
|
||||
|
||||
enum class ObjectType {
|
||||
Cube,
|
||||
Sphere,
|
||||
Capsule,
|
||||
OBJMesh,
|
||||
Model, // New type for Assimp-loaded models (FBX, GLTF, etc.)
|
||||
DirectionalLight,
|
||||
PointLight,
|
||||
SpotLight,
|
||||
AreaLight,
|
||||
Camera,
|
||||
PostFXNode,
|
||||
Mirror,
|
||||
Plane,
|
||||
Torus
|
||||
Cube = 0,
|
||||
Sphere = 1,
|
||||
Capsule = 2,
|
||||
OBJMesh = 3,
|
||||
Model = 4, // New type for Assimp-loaded models (FBX, GLTF, etc.)
|
||||
DirectionalLight = 5,
|
||||
PointLight = 6,
|
||||
SpotLight = 7,
|
||||
AreaLight = 8,
|
||||
Camera = 9,
|
||||
PostFXNode = 10,
|
||||
Mirror = 11,
|
||||
Plane = 12,
|
||||
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 {
|
||||
@@ -34,6 +41,25 @@ enum class LightType {
|
||||
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 {
|
||||
LightType type = LightType::Point;
|
||||
glm::vec3 color = glm::vec3(1.0f);
|
||||
@@ -142,6 +168,31 @@ struct PlayerControllerComponent {
|
||||
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 {
|
||||
bool enabled = true;
|
||||
std::string clipPath;
|
||||
@@ -188,12 +239,15 @@ public:
|
||||
std::vector<std::string> additionalMaterialPaths;
|
||||
bool hasRigidbody = false;
|
||||
RigidbodyComponent rigidbody;
|
||||
bool hasRigidbody2D = false;
|
||||
Rigidbody2DComponent rigidbody2D;
|
||||
bool hasCollider = false;
|
||||
ColliderComponent collider;
|
||||
bool hasPlayerController = false;
|
||||
PlayerControllerComponent playerController;
|
||||
bool hasAudioSource = false;
|
||||
AudioSourceComponent audioSource;
|
||||
UIElementComponent ui;
|
||||
|
||||
SceneObject(const std::string& name, ObjectType type, int id)
|
||||
: name(name),
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#include "Engine.h"
|
||||
#include "SceneObject.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cctype>
|
||||
#include <iterator>
|
||||
#include <unordered_map>
|
||||
|
||||
@@ -27,6 +29,27 @@ std::string makeScriptInstanceKey(const ScriptContext& ctx) {
|
||||
}
|
||||
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) {
|
||||
@@ -39,6 +62,31 @@ SceneObject* ScriptContext::FindObjectById(int 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 {
|
||||
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) {
|
||||
if (object) {
|
||||
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 {
|
||||
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) {
|
||||
if (!engine || !object || !HasRigidbody()) return false;
|
||||
return engine->setRigidbodyVelocityFromScript(object->id, velocity);
|
||||
@@ -140,6 +388,12 @@ bool ScriptContext::GetRigidbodyVelocity(glm::vec3& outVelocity) const {
|
||||
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) {
|
||||
if (!engine || !object || !HasRigidbody()) return false;
|
||||
return engine->setRigidbodyAngularVelocityFromScript(object->id, velocity);
|
||||
@@ -265,6 +519,17 @@ void ScriptContext::SetSettingBool(const std::string& key, bool value) {
|
||||
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 {
|
||||
std::string v = GetSetting(key, "");
|
||||
if (v.empty()) return fallback;
|
||||
@@ -315,6 +580,31 @@ void ScriptContext::AutoSetting(const std::string& key, bool& value) {
|
||||
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) {
|
||||
if (!script) return;
|
||||
if (autoSettings.end() != std::find_if(autoSettings.begin(), autoSettings.end(),
|
||||
@@ -378,6 +668,12 @@ void ScriptContext::SaveAutoSettings() {
|
||||
newVal = cur ? "1" : "0";
|
||||
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: {
|
||||
glm::vec3 cur = *static_cast<glm::vec3*>(e.ptr);
|
||||
if (glm::all(glm::epsilonEqual(cur, e.initialVec3, 1e-6f))) continue;
|
||||
|
||||
@@ -10,13 +10,14 @@ struct ScriptContext {
|
||||
Engine* engine = nullptr;
|
||||
SceneObject* object = nullptr;
|
||||
ScriptComponent* script = nullptr;
|
||||
enum class AutoSettingType { Bool, Vec3, StringBuf };
|
||||
enum class AutoSettingType { Bool, Float, Vec3, StringBuf };
|
||||
struct AutoSettingEntry {
|
||||
AutoSettingType type;
|
||||
std::string key;
|
||||
void* ptr = nullptr;
|
||||
size_t bufSize = 0;
|
||||
bool initialBool = false;
|
||||
float initialFloat = 0.0f;
|
||||
glm::vec3 initialVec3 = glm::vec3(0.0f);
|
||||
std::string initialString;
|
||||
};
|
||||
@@ -25,6 +26,7 @@ struct ScriptContext {
|
||||
// Convenience helpers for scripts
|
||||
SceneObject* FindObjectByName(const std::string& name);
|
||||
SceneObject* FindObjectById(int id);
|
||||
SceneObject* ResolveObjectRef(const std::string& ref);
|
||||
bool IsObjectEnabled() const;
|
||||
void SetObjectEnabled(bool enabled);
|
||||
int GetLayer() const;
|
||||
@@ -34,11 +36,35 @@ struct ScriptContext {
|
||||
bool HasTag(const std::string& tag) const;
|
||||
bool IsInLayer(int layer) const;
|
||||
void SetPosition(const glm::vec3& pos);
|
||||
void SetPosition2D(const glm::vec2& pos);
|
||||
void SetRotation(const glm::vec3& rot);
|
||||
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 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 GetRigidbodyVelocity(glm::vec3& outVelocity) const;
|
||||
bool AddRigidbodyVelocity(const glm::vec3& deltaVelocity);
|
||||
bool SetRigidbodyAngularVelocity(const glm::vec3& velocity);
|
||||
bool GetRigidbodyAngularVelocity(glm::vec3& outVelocity) const;
|
||||
bool AddRigidbodyForce(const glm::vec3& force);
|
||||
@@ -63,12 +89,15 @@ struct ScriptContext {
|
||||
void SetSetting(const std::string& key, const std::string& value);
|
||||
bool GetSettingBool(const std::string& key, bool fallback = false) const;
|
||||
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;
|
||||
void SetSettingVec3(const std::string& key, const glm::vec3& value);
|
||||
// Console helper
|
||||
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, float& value);
|
||||
void AutoSetting(const std::string& key, glm::vec3& value);
|
||||
void AutoSetting(const std::string& key, char* buffer, size_t bufferSize);
|
||||
void SaveAutoSettings();
|
||||
|
||||
Reference in New Issue
Block a user