Made Play mode functionable in all API's, added new settings to the project settings, improved the building system to now build faster using CPack like Modularity's build process.

This commit is contained in:
2026-02-17 23:31:52 -05:00
parent 7ac3bdf432
commit 4b35d36eb7
21 changed files with 1863 additions and 400 deletions

6
AGENTS.md Normal file
View File

@@ -0,0 +1,6 @@
<INSTRUCTIONS>Focus on advanced coding tasks (Lua, C#, CSS, architecture, debugging, and performance).
Default to concise answers; expand only when complexity requires it.
Be conversational but technically precise.
Offer clear opinions when there are tradeoffs, and justify them briefly.
Prefer proven, conventional engineering patterns by default, but recommend modern alternatives when they are clearly better for maintainability, safety, or performance.
</INSTRUCTIONS>

View File

@@ -57,6 +57,9 @@ option(MODULARITY_BUILD_EDITOR "Build the Modularity editor target" ON)
# ==================== Third-party libraries ====================
set(GLFW_BUILD_DOCS OFF CACHE BOOL "Build the GLFW documentation" FORCE)
set(ASSIMP_BUILD_DOCS OFF CACHE BOOL "Build Assimp documentation" FORCE)
add_subdirectory(src/ThirdParty/glfw EXCLUDE_FROM_ALL)
find_package(OpenGL REQUIRED)
@@ -353,4 +356,4 @@ set(CPACK_INSTALL_CMAKE_PROJECTS
"${CMAKE_BINARY_DIR};${CMAKE_PROJECT_NAME};ALL;/"
)
include(CPack)
include(CPack)

View File

@@ -5,6 +5,7 @@ namespace ModuCPP;
public class SampleInspector
{
private bool autoRotate = false;
private bool ensureRigidbody = false;
[DragSpeed(1.0f)] private Vec3 spinSpeed = new Vec3(0f, 45f, 0f);
[DragSpeed(0.1f)] private Vec3 offset = new Vec3(0f, 1f, 0f);
private ModuObject targetObject;
@@ -13,7 +14,9 @@ public class SampleInspector
{
var context = new Context(ctx);
context.AutoSettingsFrom(this, save: false);
context.EnsureRigidbody(useGravity: true, kinematic: false);
if (ensureRigidbody) {
context.EnsureRigidbody(useGravity: true, kinematic: false);
}
context.AddConsoleMessage("Managed script begin (C#)", ConsoleMessageType.Info);
}

View File

@@ -5,6 +5,7 @@ namespace ModuCPP;
[HeadText("Sample Inspector")]
public class SampleInspectorManaged {
private bool autoRotate = false;
private bool ensureRigidbody = false;
[DragSpeed(1.0f)]
private Vec3 spinSpeed = new Vec3(0f, 45f, 0f);
[DragSpeed(0.1f)]
@@ -19,7 +20,9 @@ public class SampleInspectorManaged {
public void Begin(IntPtr ctx, float deltaTime) {
var context = new Context(ctx);
context.AutoSettingsFrom(this, save: false);
context.EnsureRigidbody(useGravity: true, kinematic: false);
if (ensureRigidbody) {
context.EnsureRigidbody(useGravity: true, kinematic: false);
}
context.AddConsoleMessage("Managed script begin (C#)", ConsoleMessageType.Info);
}

View File

@@ -1,40 +1,76 @@
#include "ScriptRuntime.h"
#include "SceneObject.h"
#include <unordered_map>
namespace
{
struct ControllerState
{
ScriptContext::StandaloneMovementState movement; ScriptContext::StandaloneMovementDebug debug;
ScriptContext::StandaloneMovementState movement;
ScriptContext::StandaloneMovementDebug debug;
bool initialized = false;
};
std::unordered_map<int, ControllerState> g_states;
ScriptContext::StandaloneMovementSettings g_settings;
ControllerState& getState(int id) {return g_states[id];}
// aliases for readability
glm::vec3& capsuleTuning = g_settings.capsuleTuning;
bool& enforceCollider = g_settings.enforceCollider;
bool& enforceRigidbody = g_settings.enforceRigidbody;
}
extern "C" void Script_OnInspector(ScriptContext& ctx)
{
ctx.DrawStandaloneMovementInspector(g_settings, nullptr);
ScriptContext::StandaloneMovementSettings g_settings = {
glm::vec3(4.5f, 7.5f, 6.5f), // Walk / Run / Jump
glm::vec3(0.12f, 200.0f, 0.0f), // Look sensitivity / max mouse delta
glm::vec3(1.8f, 0.4f, 0.2f), // Height / Radius / Ground snap
glm::vec3(-9.81f, 0.4f, 30.0f), // Gravity / Probe extra / Max fall
glm::vec3(24.0f, 8.0f, 16.0f), // Ground accel / Air accel / Braking
glm::vec3(0.2f, 40.0f, 1.0f), // Min control / Slide gravity / Platform carry
true, // Enable mouse look
false, // Require mouse button for look
true, // Ensure collider
true // Ensure rigidbody
};
bool g_showDebug = false;
ControllerState& getState(int id) { return g_states[id]; }
void resetStateFromObject(ControllerState& state, const SceneObject& object)
{
state.movement.pitch = object.rotation.x;
state.movement.yaw = object.rotation.y;
state.movement.verticalVelocity = 0.0f;
state.movement.localVelocity = glm::vec2(0.0f);
state.movement.slideVelocity = glm::vec3(0.0f);
state.movement.lastGroundHitPos = glm::vec3(0.0f);
state.movement.hasGroundSample = false;
state.debug = ScriptContext::StandaloneMovementDebug{};
state.initialized = true;
}
}
void Begin(ScriptContext& ctx, float)
extern "C" void Script_OnInspector(ScriptContext& ctx)
{
if (!ctx.object) return; ControllerState& s = getState(ctx.object->id);
if (!s.initialized)
{
s.movement.pitch = ctx.object->rotation.x;
s.movement.yaw = ctx.object->rotation.y;
s.initialized = true;
ctx.DrawStandaloneMovementInspector(g_settings, &g_showDebug);
}
void Begin(ScriptContext& ctx, float /*deltaTime*/)
{
if (!ctx.object) return;
ControllerState& state = getState(ctx.object->id);
resetStateFromObject(state, *ctx.object);
if (g_settings.enforceCollider) {
ctx.EnsureCapsuleCollider(g_settings.capsuleTuning.x, g_settings.capsuleTuning.y);
}
if (g_settings.enforceRigidbody) {
ctx.EnsureRigidbody(true, false);
}
if (enforceCollider) ctx.EnsureCapsuleCollider(capsuleTuning.x, capsuleTuning.y);
if (enforceRigidbody) ctx.EnsureRigidbody(true, false);
}
void TickUpdate(ScriptContext& ctx, float dt)
{
if (!ctx.object) return; ControllerState& s = getState(ctx.object->id); ctx.TickStandaloneMovement(s.movement, g_settings, dt, nullptr);
if (!ctx.object || dt <= 0.0f) return;
ControllerState& state = getState(ctx.object->id);
if (!state.initialized) {
resetStateFromObject(state, *ctx.object);
}
ctx.TickStandaloneMovement(state.movement, g_settings, dt, &state.debug);
}

View File

@@ -6,7 +6,7 @@ This document explains how the Modularity C++ engine is structured, how projects
Modularity is a native C++ engine with an integrated editor. The core is built around:
- A scene graph made of `SceneObject` instances with component-style flags/data.
- An OpenGL renderer with post-processing and UI rendering.
- A scripting system that hot-compiles C++ into shared libraries and optionally hosts managed C# via Mono.
- A scripting system that hot-compiles native C++/C into shared libraries and optionally hosts managed C# via Mono.
- Optional PhysX-based 3D physics, plus a lightweight built-in 2D simulation.
- Audio via miniaudio with spatial playback and reverb zones.
@@ -53,6 +53,8 @@ YourProject/
ProjectUserSettings/
ProjectLayout/
ScriptSettings/
Scripts/
Managed/ # optional, created when managed C# scripting is set up
project.modu
scripts.modu
packages.modu
@@ -60,7 +62,7 @@ YourProject/
Key files:
- `project.modu`: project name + last opened scene.
- `scripts.modu`: script build configuration.
- `scripts.modu`: native script build configuration (`Scripts.modu` legacy name is still detected).
- `packages.modu`: script dependency manifest.
- Scenes live in `Assets/Scenes` with extension `.scene`.
@@ -92,7 +94,7 @@ Examples of built-in types:
- **Light**: light type, color, intensity, range, and light-specific parameters.
- **Camera**: FOV, near/far, 2D settings, post-FX toggle.
- **PostFX**: global effects settings (bloom, color adjust, motion blur, vignette, chromatic aberration, AO).
- **Scripts**: one or more script components (C++ or managed C#).
- **Scripts**: one or more script components (`C++`, `C`, or managed `C#`).
### Physics components
3D (PhysX, optional):
@@ -171,6 +173,7 @@ For full details, see:
### Managed C# scripting (experimental)
Modularity can host managed scripts using Mono, with a minimal API surface.
Managed project files are under `Scripts/Managed` (separate from `Assets/Scripts` used for native scripts).
For setup and caveats, see:
- `docs/Scripting.md`

View File

@@ -1,10 +1,10 @@
---
title: Native Scripting (C++ and C)
description: Hot-compiled native C++ and C scripts, per-object state, ImGui inspectors, and runtime/editor hooks.
title: Scripting (C++, C, and C#)
description: Hot-compiled native C++/C scripts plus managed C# scripting, with per-object state, ImGui inspectors, and runtime/editor hooks.
---
# Native Scripting (C++ and C)
Scripts in Modularity are native C++ or 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.
# Scripting (C++, C, and C#)
Scripts in Modularity can be native C++/C (compiled to shared libraries) or managed C# (loaded through the embedded Mono host). 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.
@@ -14,10 +14,11 @@ Scripts in Modularity are native C++ or C code compiled into shared libraries an
- [Quickstart](#quickstart)
- [C scripting (C API bridge)](#c-scripting-c-api-bridge)
- [C# managed scripting (experimental)](#c-managed-scripting-experimental)
- [Scripts.modu](#scriptsmodu)
- [scripts.modu](#scriptsmodu)
- [How compilation works](#how-compilation-works)
- [Lifecycle hooks](#lifecycle-hooks)
- [ScriptContext](#scriptcontext)
- [C API quick reference](#c-api-quick-reference)
- [ImGui in scripts](#imgui-in-scripts)
- [Per-script settings](#per-script-settings)
- [UI scripting](#ui-scripting)
@@ -29,7 +30,7 @@ Scripts in Modularity are native C++ or C code compiled into shared libraries an
- [Templates](#templates)
## Quickstart
1. Create a script file under `Scripts/` (e.g. `Scripts/MyScript.cpp`).
1. Create a script file under `Assets/Scripts/` (e.g. `Assets/Scripts/Runtime/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.
@@ -38,10 +39,15 @@ Scripts in Modularity are native C++ or C code compiled into shared libraries an
- In the Inspectors script component menu, choose **Compile**.
5. Implement a tick hook (`TickUpdate`) and observe behavior in play mode.
Language options in the Script component:
- `C++` - native script with `ScriptContext` helpers.
- `C` - native script through `ScriptRuntimeCAPI.h` + `Modu_*` hooks.
- `C#` - managed script loaded from an assembly + type name.
## C scripting (C API bridge)
You can also write native scripts in plain C (`.c`). The compiler generates a C++ bridge automatically and links it with your C object file.
1. Create `Scripts/MyScript.c` (or use **Project window -> New -> C Script**).
1. Create `Assets/Scripts/Runtime/MyScript.c` (or use **Project window -> New -> C Script**).
2. In Inspector -> Script component, set `Language` to **C** and assign the file.
3. Compile as usual (right-click file -> **Compile Script** or script component menu -> **Compile**).
@@ -68,56 +74,59 @@ Supported C hook names (all optional):
- `Modu_ExitRenderEditorWindow`
## C# managed scripting (experimental)
Modularity can host managed C# scripts via the .NET runtime. This is an early, minimal integration
Modularity can host managed C# scripts via Mono. This is an early, minimal integration
intended for movement/transform tests and simple Rigidbody control.
1. Build the managed project (this now happens automatically when you compile a C# script):
1. Build the managed project (this also happens automatically when you compile a C# script):
- `dotnet build Scripts/Managed/ModuCPP.csproj`
2. In the Inspector, add a Script component and set:
- `Language` = **C#**
- `Assembly Path` = `Scripts/Managed/bin/Debug/net10.0/ModuCPP.dll` (or point at `Scripts/Managed/SampleInspector.cs`)
- `Assembly Path` = `Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.dll` (or point at a `.cs` file in `Scripts/Managed/`)
- `Type` = `ModuCPP.SampleInspector`
3. Enter play mode. The sample script will auto-rotate the object.
Notes:
- The `ModuCPP.runtimeconfig.json` produced by `dotnet build` must sit next to the DLL.
- The managed host currently expects the script assembly to also contain `ModuCPP.Host`
(use the provided `Scripts/Managed/ModuCPP.csproj` as the entry assembly).
- The managed API surface is tiny for now: position/rotation/scale, basic Rigidbody velocity/forces,
settings, and console logging.
- Requires a local .NET runtime (Windows/Linux). If the runtime is missing, the engine will fail to
- Requires Mono runtime support in the engine build plus `dotnet` to compile the managed project.
If runtime setup is missing, the engine will fail to
initialize managed scripts and report the error in the inspector.
- Managed hooks should be exported as `Script_Begin`, `Script_TickUpdate`, etc. via
`[UnmanagedCallersOnly]` in the C# script class.
- Managed hooks are discovered by method name (`Script_Begin` or `Begin`, `Script_TickUpdate` or `TickUpdate`, etc.)
with signatures that accept a context pointer and optional delta time.
## Scripts.modu
Each project has a `Scripts.modu` file (auto-created if missing). It controls compilation.
## scripts.modu
Each project has a `scripts.modu` file (auto-created if missing). It controls native compilation.
Legacy `Scripts.modu` is still detected for older projects.
Common keys:
- `scriptsDir` - where script source files live (default: `Scripts`)
- `outDir` - where compiled binaries go (default: `Cache/ScriptBin`)
- `scriptsDir` - where script source files live (default: `Assets/Scripts`)
- `outDir` - where compiled binaries go (default: `Library/CompiledScripts`)
- `includeDir=...` - add include directories (repeatable)
- `define=...` - add preprocessor defines (repeatable)
- `linux.linkLib=...` - comma-separated link libs/flags for Linux (e.g. `dl,pthread`)
- `win.linkLib=...` - comma-separated link libs for Windows (e.g. `User32,Advapi32`)
- `linux.linkLib=...` - add one Linux link lib/flag per line (repeatable)
- `win.linkLib=...` - add one Windows link lib per line (repeatable)
- `cppStandard` - C++ standard (e.g. `c++20`)
Example:
```ini
scriptsDir=Scripts
outDir=Cache/ScriptBin
scriptsDir=Assets/Scripts
outDir=Library/CompiledScripts
includeDir=../src
includeDir=../include
cppStandard=c++20
linux.linkLib=dl,pthread
win.linkLib=User32,Advapi32
linux.linkLib=pthread
linux.linkLib=dl
win.linkLib=User32.lib
win.linkLib=Advapi32.lib
```
## 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/`.
- Source lives under `scriptsDir` (default `Assets/Scripts/`).
- Output binaries are written to `outDir` (default `Library/CompiledScripts/`).
- Binaries are platform-specific:
- Windows: `.dll`
- Linux: `.so`
@@ -168,12 +177,29 @@ Fields:
### Object lookup
- `FindObjectByName(const std::string&)`
- `FindObjectById(int)`
- `ResolveObjectRef(const std::string&)`
### Object state helpers
- `IsObjectEnabled()`, `SetObjectEnabled(bool)`
- `GetLayer()`, `SetLayer(int)`
- `GetTag()`, `SetTag(const std::string&)`
- `HasTag(const std::string&)`, `IsInLayer(int)`
### Transform helpers
- `SetPosition(const glm::vec3&)`
- `SetRotation(const glm::vec3&)` (degrees)
- `SetScale(const glm::vec3&)`
- `SetPosition2D(const glm::vec2&)` (UI position in pixels)
- `GetPlanarYawPitchVectors(...)`
- `GetMoveInputWASD(float pitchDeg, float yawDeg)`
- `ApplyMouseLook(...)`
- `ApplyVelocity(...)`
### Ground + movement helpers
- `ResolveGround(...)`
- `BindStandaloneMovementSettings(...)`
- `DrawStandaloneMovementInspector(...)`
- `TickStandaloneMovement(...)`
### UI helpers (Buttons/Sliders)
- `IsUIButtonPressed()`
@@ -186,13 +212,19 @@ Fields:
- `SetUIButtonStyle(UIButtonStyle)`
- `SetUIStylePreset(const std::string&)`
- `RegisterUIStylePreset(const std::string& name, const ImGuiStyle& style, bool replace = false)`
- `SetFPSCap(bool enabled, float cap = 120.0f)`
### Rigidbody helpers (3D)
- `HasRigidbody()`
- `EnsureCapsuleCollider(float height, float radius)`
- `EnsureRigidbody(bool useGravity, bool kinematic)`
- `SetRigidbodyVelocity(const glm::vec3&)`, `GetRigidbodyVelocity(glm::vec3& out)`
- `AddRigidbodyVelocity(const glm::vec3&)`
- `SetRigidbodyAngularVelocity(const glm::vec3&)`, `GetRigidbodyAngularVelocity(glm::vec3& out)`
- `AddRigidbodyForce(const glm::vec3&)`, `AddRigidbodyImpulse(const glm::vec3&)`
- `AddRigidbodyTorque(const glm::vec3&)`, `AddRigidbodyAngularImpulse(const glm::vec3&)`
- `SetRigidbodyYaw(float yawDegrees)`
- `RaycastClosest(...)`, `RaycastClosestDetailed(...)`
- `SetRigidbodyRotation(const glm::vec3& rotDeg)`
- `TeleportRigidbody(const glm::vec3& pos, const glm::vec3& rotDeg)`
@@ -210,11 +242,23 @@ Fields:
### Settings + utility
- `GetSetting(key, fallback)`, `SetSetting(key, value)`
- `GetSettingBool(key, fallback)`, `SetSettingBool(key, value)`
- `GetSettingFloat(key, fallback)`, `SetSettingFloat(key, value)`
- `GetSettingVec3(key, fallback)`, `SetSettingVec3(key, value)`
- `AutoSetting(key, bool|glm::vec3|buffer)`, `SaveAutoSettings()`
- `AutoSetting(key, bool|float|glm::vec3|buffer)`, `SaveAutoSettings()`
- `AddConsoleMessage(text, type)`
- `MarkDirty()`
## C API quick reference
Include `ScriptRuntimeCAPI.h` in `.c` scripts. The wrapper maps `Modu_*` calls to the same runtime systems used by C++ scripts.
Available groups:
- Object + transform: `Modu_GetObjectId`, `Modu_IsObjectEnabled`, `Modu_SetObjectEnabled`, `Modu_Get/SetPosition`, `Modu_Get/SetRotation`, `Modu_Get/SetScale`
- Rigidbody + collision: `Modu_SetRigidbodyVelocity`, `Modu_GetRigidbodyVelocity`, `Modu_AddRigidbodyForce`, `Modu_SetRigidbodyRotation`, `Modu_EnsureCapsuleCollider`, `Modu_EnsureRigidbody`
- Input + movement: `Modu_IsSprintDown`, `Modu_IsJumpDown`, `Modu_GetMoveInputWASD`, `Modu_ApplyMouseLook`, `Modu_RaycastClosestDetailed`
- Script settings: `Modu_Get/SetSettingFloat`, `Modu_Get/SetSettingBool`, `Modu_Get/SetSettingString`
- Inspector helpers: `Modu_InspectorText`, `Modu_InspectorSeparator`, `Modu_InspectorDragFloat(1/2/3)`, `Modu_InspectorCheckbox`
- Console: `Modu_AddConsoleMessage`
## ImGui in scripts
Modularity uses Dear ImGui for editor UI. Scripts can draw ImGui in two places:
@@ -379,20 +423,20 @@ extern "C" void ExitRenderEditorWindow(ScriptContext& ctx) {
```
How to open:
1. Compile the script so the binary is updated under `Cache/ScriptBin/`.
1. Compile the script so the binary is updated under `outDir` (default `Library/CompiledScripts/`).
2. In the main menu, go to **View -> Scripted Windows** and toggle the entry.
## Manual compile (CLI)
Linux:
```bash
g++ -std=c++20 -fPIC -O0 -g -I../src -I../include -c SampleInspector.cpp -o ../Cache/ScriptBin/SampleInspector.o
g++ -shared ../Cache/ScriptBin/SampleInspector.o -o ../Cache/ScriptBin/SampleInspector.so -ldl -lpthread
g++ -std=c++20 -fPIC -O0 -g -I../src -I../include -c SampleInspector.cpp -o ../Library/CompiledScripts/SampleInspector.o
g++ -shared ../Library/CompiledScripts/SampleInspector.o -o ../Library/CompiledScripts/SampleInspector.so -ldl -lpthread
```
Windows:
```bat
cl /nologo /std:c++20 /EHsc /MD /Zi /Od /I ..\\src /I ..\\include /c SampleInspector.cpp /Fo ..\\Cache\\ScriptBin\\SampleInspector.obj
link /nologo /DLL ..\\Cache\\ScriptBin\\SampleInspector.obj /OUT:..\\Cache\\ScriptBin\\SampleInspector.dll User32.lib Advapi32.lib
cl /nologo /std:c++20 /EHsc /MD /Zi /Od /I ..\\src /I ..\\include /c SampleInspector.cpp /Fo ..\\Library\\CompiledScripts\\SampleInspector.obj
link /nologo /DLL ..\\Library\\CompiledScripts\\SampleInspector.obj /OUT:..\\Library\\CompiledScripts\\SampleInspector.dll User32.lib Advapi32.lib
```
## Troubleshooting
@@ -449,4 +493,4 @@ void TickUpdate(ScriptContext& ctx, float) {
```
### 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.
Attach an FPS script (for example `Assets/Scripts/Runtime/FPSDisplay.cpp`) to a **UI Text** object to show FPS. The inspector exposes a checkbox to clamp FPS to 120.

View File

@@ -15,3 +15,5 @@ You can override the runtime location at runtime with:
Build notes:
- The CMake cache variable `MONO_ROOT` controls where headers/libs are found.
- Managed scripts target `netstandard2.0` and are built with `dotnet build`.
- Project-managed C# sources/build files live under `<ProjectRoot>/Scripts/Managed/`.
- Default managed output is `<ProjectRoot>/Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.dll`.

View File

@@ -10,8 +10,9 @@ void Camera::processMouse(double xpos, double ypos) {
firstMouse = false;
}
float xoffset = (xpos - lastX) * SENSITIVITY;
float yoffset = (lastY - ypos) * SENSITIVITY;
float sens = std::max(0.001f, mouseSensitivity);
float xoffset = (xpos - lastX) * sens;
float yoffset = (lastY - ypos) * sens;
lastX = xpos;
lastY = ypos;

View File

@@ -13,6 +13,7 @@ public:
float sprintSpeed = 10.0f;
float acceleration = 15.0f;
bool smoothMovement = true;
float mouseSensitivity = SENSITIVITY;
float yaw = -90.0f;
float pitch = 0.0f;
float speed = CAMERA_SPEED;

View File

@@ -16,110 +16,183 @@ void Engine::renderBuildSettingsWindow() {
}
bool changed = false;
ImGui::BeginChild("BuildScenesList", ImVec2(0, 150), true);
ImGui::Text("Scenes In Build");
ImGui::Separator();
for (int i = 0; i < static_cast<int>(buildSettings.scenes.size()); ++i) {
BuildSceneEntry& entry = buildSettings.scenes[i];
ImGui::PushID(i);
bool enabled = entry.enabled;
if (ImGui::Checkbox("##enabled", &enabled)) {
entry.enabled = enabled;
changed = true;
}
ImGui::SameLine();
bool selected = (buildSettingsSelectedIndex == i);
if (ImGui::Selectable(entry.name.c_str(), selected, ImGuiSelectableFlags_SpanAllColumns)) {
buildSettingsSelectedIndex = i;
}
float rightX = ImGui::GetWindowContentRegionMax().x;
ImGui::SameLine(rightX - 24.0f);
ImGui::TextDisabled("%d", i);
ImGui::PopID();
}
ImGui::EndChild();
float buttonSpacing = ImGui::GetStyle().ItemSpacing.x;
float addWidth = 150.0f;
float removeWidth = 130.0f;
float totalButtons = addWidth + removeWidth + buttonSpacing;
float buttonStart = ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - totalButtons;
if (buttonStart > ImGui::GetCursorPosX()) {
ImGui::SetCursorPosX(buttonStart);
}
if (ImGui::Button("Remove Selected", ImVec2(removeWidth, 0.0f))) {
if (buildSettingsSelectedIndex >= 0 &&
buildSettingsSelectedIndex < static_cast<int>(buildSettings.scenes.size())) {
buildSettings.scenes.erase(buildSettings.scenes.begin() + buildSettingsSelectedIndex);
if (buildSettingsSelectedIndex >= static_cast<int>(buildSettings.scenes.size())) {
buildSettingsSelectedIndex = static_cast<int>(buildSettings.scenes.size()) - 1;
if (ImGui::BeginTabBar("BuildSettingsTabs")) {
if (ImGui::BeginTabItem("Build")) {
ImGui::BeginChild("BuildScenesList", ImVec2(0, 150), true);
ImGui::Text("Scenes In Build");
ImGui::Separator();
for (int i = 0; i < static_cast<int>(buildSettings.scenes.size()); ++i) {
BuildSceneEntry& entry = buildSettings.scenes[i];
ImGui::PushID(i);
bool enabled = entry.enabled;
if (ImGui::Checkbox("##enabled", &enabled)) {
entry.enabled = enabled;
changed = true;
}
ImGui::SameLine();
bool selected = (buildSettingsSelectedIndex == i);
if (ImGui::Selectable(entry.name.c_str(), selected, ImGuiSelectableFlags_SpanAllColumns)) {
buildSettingsSelectedIndex = i;
}
float rightX = ImGui::GetWindowContentRegionMax().x;
ImGui::SameLine(rightX - 24.0f);
ImGui::TextDisabled("%d", i);
ImGui::PopID();
}
changed = true;
ImGui::EndChild();
float addWidth = 150.0f;
float removeWidth = 130.0f;
float totalButtons = addWidth + removeWidth + buttonSpacing;
float buttonStart = ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - totalButtons;
if (buttonStart > ImGui::GetCursorPosX()) {
ImGui::SetCursorPosX(buttonStart);
}
if (ImGui::Button("Remove Selected", ImVec2(removeWidth, 0.0f))) {
if (buildSettingsSelectedIndex >= 0 &&
buildSettingsSelectedIndex < static_cast<int>(buildSettings.scenes.size())) {
buildSettings.scenes.erase(buildSettings.scenes.begin() + buildSettingsSelectedIndex);
if (buildSettingsSelectedIndex >= static_cast<int>(buildSettings.scenes.size())) {
buildSettingsSelectedIndex = static_cast<int>(buildSettings.scenes.size()) - 1;
}
changed = true;
}
}
ImGui::SameLine();
if (ImGui::Button("Add Open Scenes", ImVec2(addWidth, 0.0f))) {
if (addSceneToBuildSettings(projectManager.currentProject.currentSceneName, true)) {
changed = true;
}
}
ImGui::Spacing();
ImGui::Text("Platform");
ImGui::Separator();
ImGui::BeginChild("BuildPlatforms", ImVec2(220, 0), true);
ImGui::Selectable("Windows & Linux Standalone", true);
ImGui::BeginDisabled(true);
ImGui::Selectable("Android", false);
ImGui::Selectable("Android | Meta Quest", false);
ImGui::EndDisabled();
ImGui::EndChild();
ImGui::SameLine();
ImGui::BeginChild("BuildPlatformSettings", ImVec2(0, 0), true);
ImGui::Text("Target Platform");
const char* targets[] = {"Windows", "Linux"};
int targetIndex = (buildSettings.platform == BuildPlatform::Linux) ? 1 : 0;
if (ImGui::Combo("##target-platform", &targetIndex, targets, 2)) {
buildSettings.platform = (targetIndex == 1) ? BuildPlatform::Linux : BuildPlatform::Windows;
changed = true;
}
ImGui::Text("Architecture");
const char* arches[] = {"x86_64", "x86"};
int archIndex = (buildSettings.architecture == "x86") ? 1 : 0;
if (ImGui::Combo("##architecture", &archIndex, arches, 2)) {
buildSettings.architecture = arches[archIndex];
changed = true;
}
ImGui::Spacing();
if (ImGui::Checkbox("Server Build", &buildSettings.serverBuild)) changed = true;
if (ImGui::Checkbox("Development Build", &buildSettings.developmentBuild)) changed = true;
if (ImGui::Checkbox("Autoconnect Profiler", &buildSettings.autoConnectProfiler)) changed = true;
if (ImGui::Checkbox("Deep Profiling Support", &buildSettings.deepProfiling)) changed = true;
if (ImGui::Checkbox("Script Debugging", &buildSettings.scriptDebugging)) changed = true;
if (ImGui::Checkbox("Scripts Only Build", &buildSettings.scriptsOnlyBuild)) changed = true;
ImGui::Spacing();
ImGui::Text("Compression Method");
const char* compressionOptions[] = {"Default", "None", "LZ4", "LZ4HC"};
int compressionIndex = 0;
for (int i = 0; i < 4; ++i) {
if (buildSettings.compressionMethod == compressionOptions[i]) {
compressionIndex = i;
break;
}
}
if (ImGui::Combo("##compression", &compressionIndex, compressionOptions, 4)) {
buildSettings.compressionMethod = compressionOptions[compressionIndex];
changed = true;
}
if (ImGui::Checkbox("Create Standalone Archive (.tar.gz)", &buildSettings.packageStandaloneArchive)) {
changed = true;
}
ImGui::TextDisabled("Android support will unlock after OpenGLES is available.");
ImGui::EndChild();
ImGui::EndTabItem();
}
}
ImGui::SameLine();
if (ImGui::Button("Add Open Scenes", ImVec2(addWidth, 0.0f))) {
if (addSceneToBuildSettings(projectManager.currentProject.currentSceneName, true)) {
changed = true;
if (ImGui::BeginTabItem("Project")) {
char companyBuf[256];
char buildNameBuf[256];
char versionBuf[128];
char splashBuf[512];
std::snprintf(companyBuf, sizeof(companyBuf), "%s", buildSettings.companyName.c_str());
std::snprintf(buildNameBuf, sizeof(buildNameBuf), "%s", buildSettings.buildName.c_str());
std::snprintf(versionBuf, sizeof(versionBuf), "%s", buildSettings.version.c_str());
std::snprintf(splashBuf, sizeof(splashBuf), "%s", buildSettings.splashImagePath.c_str());
if (ImGui::InputText("Company Name", companyBuf, sizeof(companyBuf))) {
buildSettings.companyName = companyBuf;
changed = true;
}
if (ImGui::InputText("Build Name", buildNameBuf, sizeof(buildNameBuf))) {
buildSettings.buildName = buildNameBuf;
changed = true;
}
if (ImGui::InputText("Version", versionBuf, sizeof(versionBuf))) {
buildSettings.version = versionBuf;
changed = true;
}
ImGui::Spacing();
if (ImGui::Checkbox("Enable Startup Splash", &buildSettings.splashEnabled)) {
changed = true;
}
ImGui::BeginDisabled(!buildSettings.splashEnabled);
if (ImGui::InputText("Splash Image", splashBuf, sizeof(splashBuf))) {
buildSettings.splashImagePath = splashBuf;
changed = true;
}
if (ImGui::Button("Use Selected File")) {
if (!fileBrowser.selectedFile.empty() && fs::is_regular_file(fileBrowser.selectedFile)) {
fs::path splashPath = fs::absolute(fileBrowser.selectedFile);
std::error_code relEc;
fs::path rel = fs::relative(splashPath, projectManager.currentProject.projectPath, relEc);
bool outsideProject = static_cast<bool>(relEc);
if (!outsideProject) {
for (const auto& part : rel) {
if (part == "..") {
outsideProject = true;
break;
}
}
}
buildSettings.splashImagePath = outsideProject ? splashPath.string() : rel.generic_string();
changed = true;
}
}
ImGui::SameLine();
if (ImGui::Button("Clear Splash")) {
buildSettings.splashImagePath.clear();
changed = true;
}
float splashDuration = buildSettings.splashDurationSeconds;
if (ImGui::SliderFloat("Splash Duration (sec)", &splashDuration, 0.5f, 10.0f, "%.1f")) {
buildSettings.splashDurationSeconds = std::clamp(splashDuration, 0.5f, 10.0f);
changed = true;
}
ImGui::EndDisabled();
ImGui::TextDisabled("Tip: use a path inside the project so export can include it.");
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::Spacing();
ImGui::Text("Platform");
ImGui::Separator();
ImGui::BeginChild("BuildPlatforms", ImVec2(220, 0), true);
ImGui::Selectable("Windows & Linux Standalone", true);
ImGui::BeginDisabled(true);
ImGui::Selectable("Android", false);
ImGui::Selectable("Android | Meta Quest", false);
ImGui::EndDisabled();
ImGui::EndChild();
ImGui::SameLine();
ImGui::BeginChild("BuildPlatformSettings", ImVec2(0, 0), true);
ImGui::Text("Target Platform");
const char* targets[] = {"Windows", "Linux"};
int targetIndex = (buildSettings.platform == BuildPlatform::Linux) ? 1 : 0;
if (ImGui::Combo("##target-platform", &targetIndex, targets, 2)) {
buildSettings.platform = (targetIndex == 1) ? BuildPlatform::Linux : BuildPlatform::Windows;
changed = true;
}
ImGui::Text("Architecture");
const char* arches[] = {"x86_64", "x86"};
int archIndex = (buildSettings.architecture == "x86") ? 1 : 0;
if (ImGui::Combo("##architecture", &archIndex, arches, 2)) {
buildSettings.architecture = arches[archIndex];
changed = true;
}
ImGui::Spacing();
if (ImGui::Checkbox("Server Build", &buildSettings.serverBuild)) changed = true;
if (ImGui::Checkbox("Development Build", &buildSettings.developmentBuild)) changed = true;
if (ImGui::Checkbox("Autoconnect Profiler", &buildSettings.autoConnectProfiler)) changed = true;
if (ImGui::Checkbox("Deep Profiling Support", &buildSettings.deepProfiling)) changed = true;
if (ImGui::Checkbox("Script Debugging", &buildSettings.scriptDebugging)) changed = true;
if (ImGui::Checkbox("Scripts Only Build", &buildSettings.scriptsOnlyBuild)) changed = true;
ImGui::Spacing();
ImGui::Text("Compression Method");
const char* compressionOptions[] = {"Default", "None", "LZ4", "LZ4HC"};
int compressionIndex = 0;
for (int i = 0; i < 4; ++i) {
if (buildSettings.compressionMethod == compressionOptions[i]) {
compressionIndex = i;
break;
}
}
if (ImGui::Combo("##compression", &compressionIndex, compressionOptions, 4)) {
buildSettings.compressionMethod = compressionOptions[compressionIndex];
changed = true;
}
ImGui::TextDisabled("Android support will unlock after OpenGLES is available.");
ImGui::EndChild();
ImGui::Separator();
float buildWidth = 90.0f;
float buildRunWidth = 120.0f;
@@ -128,7 +201,7 @@ void Engine::renderBuildSettingsWindow() {
if (buildStart > ImGui::GetCursorPosX()) {
ImGui::SetCursorPosX(buildStart);
}
if (ImGui::Button("Export Game", ImVec2(buildWidth, 0.0f))) {
if (ImGui::Button("Bake Game", ImVec2(buildWidth, 0.0f))) {
exportRunAfter = false;
if (exportOutputPath[0] == '\0') {
fs::path defaultOut = projectManager.currentProject.projectPath / "Builds";
@@ -137,7 +210,7 @@ void Engine::renderBuildSettingsWindow() {
showExportDialog = true;
}
ImGui::SameLine();
if (ImGui::Button("Export & Run", ImVec2(buildRunWidth, 0.0f))) {
if (ImGui::Button("Bake & Run", ImVec2(buildRunWidth, 0.0f))) {
exportRunAfter = true;
if (exportOutputPath[0] == '\0') {
fs::path defaultOut = projectManager.currentProject.projectPath / "Builds";
@@ -165,6 +238,8 @@ void Engine::renderBuildSettingsWindow() {
std::string exportStatus;
std::string exportLog;
fs::path exportDir;
std::string exportExeName;
fs::path exportArchivePath;
{
std::lock_guard<std::mutex> lock(exportMutex);
exportActive = exportJob.active;
@@ -174,6 +249,8 @@ void Engine::renderBuildSettingsWindow() {
exportStatus = exportJob.status;
exportLog = exportJob.log;
exportDir = exportJob.outputDir;
exportExeName = exportJob.executableName;
exportArchivePath = exportJob.archivePath;
}
bool allowClose = !exportActive;
if (ImGui::BeginPopupModal("Export Game", allowClose ? &exportPopupOpen : nullptr, popupFlags)) {
@@ -232,6 +309,12 @@ void Engine::renderBuildSettingsWindow() {
} else if (!exportActive && exportDone) {
if (exportSuccess && !exportDir.empty()) {
ImGui::TextDisabled("Exported to: %s", exportDir.string().c_str());
if (!exportExeName.empty()) {
ImGui::TextDisabled("Executable: %s", exportExeName.c_str());
}
if (!exportArchivePath.empty()) {
ImGui::TextDisabled("Archive: %s", exportArchivePath.filename().string().c_str());
}
}
if (ImGui::Button("Close", ImVec2(100, 0))) {
ImGui::CloseCurrentPopup();

View File

@@ -215,6 +215,55 @@ static void DrawBlurredImageCover(ImDrawList* list, ImTextureID texId, const ImV
texW, texH, tint, 0.0f);
}
}
static fs::path MakeRelativeIfInside(const fs::path& absolutePath, const fs::path& baseRoot) {
if (absolutePath.empty()) return absolutePath;
std::error_code ec;
fs::path abs = absolutePath.is_absolute() ? absolutePath : fs::absolute(absolutePath, ec);
if (ec) abs = absolutePath;
fs::path rel = fs::relative(abs, baseRoot, ec);
if (ec || rel.empty()) return abs;
for (const auto& part : rel) {
if (part == "..") return abs;
}
return rel;
}
static bool SaveScriptsConfig(const fs::path& path, const ScriptBuildConfig& config, const fs::path& projectRoot, std::string& error) {
std::error_code ec;
fs::create_directories(path.parent_path(), ec);
if (ec) {
error = "Failed to create config folder: " + ec.message();
return false;
}
std::ofstream file(path);
if (!file.is_open()) {
error = "Failed to open scripts config for writing: " + path.string();
return false;
}
file << "# scripts.modu\n";
file << "cppStandard=" << config.cppStandard << "\n";
file << "scriptsDir=" << MakeRelativeIfInside(config.scriptsDir, projectRoot).generic_string() << "\n";
file << "outDir=" << MakeRelativeIfInside(config.outDir, projectRoot).generic_string() << "\n";
for (const auto& include : config.includeDirs) {
file << "includeDir=" << MakeRelativeIfInside(include, projectRoot).generic_string() << "\n";
}
for (const auto& define : config.defines) {
if (define.empty()) continue;
file << "define=" << define << "\n";
}
for (const auto& lib : config.linuxLinkLibs) {
if (lib.empty()) continue;
file << "linux.linkLib=" << lib << "\n";
}
for (const auto& lib : config.windowsLinkLibs) {
if (lib.empty()) continue;
file << "win.linkLib=" << lib << "\n";
}
return true;
}
} // namespace
#pragma endregion
@@ -926,10 +975,16 @@ void Engine::renderProjectBrowserPanel() {
ImGui::Separator();
static int selectedTab = 0;
const char* tabs[] = { "Scenes", "Packages", "Assets" };
const char* tabs[] = { "Scenes", "Packages", "Assets", "Editor", "Build", "Compilation" };
constexpr int tabCount = static_cast<int>(IM_ARRAYSIZE(tabs));
if (selectedTab < 0 || selectedTab >= tabCount) {
selectedTab = 0;
}
ImGui::BeginChild("SettingsNav", ImVec2(180, 0), true);
for (int i = 0; i < 3; ++i) {
ImGui::TextDisabled("Categories");
ImGui::Separator();
for (int i = 0; i < tabCount; ++i) {
if (ImGui::Selectable(tabs[i], selectedTab == i, 0, ImVec2(0, 32))) {
selectedTab = i;
}
@@ -1185,6 +1240,403 @@ void Engine::renderProjectBrowserPanel() {
}
}
}
} else if (selectedTab == 3) {
bool editorSettingsChanged = false;
bool buildSettingsChanged = false;
if (ImGui::CollapsingHeader("Player / Viewport", ImGuiTreeNodeFlags_DefaultOpen)) {
const char* resolutionOptions[] = { "Window", "1080p", "720p", "1440p", "Custom" };
if (gameViewportResolutionIndex < 0 || gameViewportResolutionIndex >= static_cast<int>(IM_ARRAYSIZE(resolutionOptions))) {
gameViewportResolutionIndex = 0;
}
int resolutionIndex = gameViewportResolutionIndex;
if (ImGui::Combo("Preview Resolution", &resolutionIndex, resolutionOptions, IM_ARRAYSIZE(resolutionOptions)))
{
gameViewportResolutionIndex = resolutionIndex;
editorSettingsChanged = true;
}
if (gameViewportResolutionIndex == 4) {
if (ImGui::DragInt("Custom Width", &gameViewportCustomWidth, 1.0f, 64, 8192)) {
gameViewportCustomWidth = std::clamp(gameViewportCustomWidth, 64, 8192);
editorSettingsChanged = true;
}
if (ImGui::DragInt("Custom Height", &gameViewportCustomHeight, 1.0f, 64, 8192)) {
gameViewportCustomHeight = std::clamp(gameViewportCustomHeight, 64, 8192);
editorSettingsChanged = true;
}
}
if (ImGui::Checkbox("Auto Fit Preview", &gameViewportAutoFit)) {
editorSettingsChanged = true;
}
ImGui::BeginDisabled(gameViewportAutoFit);
float zoomPercent = gameViewportZoom * 100.0f;
if (ImGui::SliderFloat("Preview Zoom", &zoomPercent, 20.0f, 400.0f, "%.0f%%")) {
gameViewportZoom = std::clamp(zoomPercent / 100.0f, 0.2f, 4.0f);
editorSettingsChanged = true;
}
ImGui::EndDisabled();
if (ImGui::Checkbox("Show Game Profiler", &showGameProfiler)) editorSettingsChanged = true;
if (ImGui::Checkbox("Canvas Guides", &showCanvasOverlay)) editorSettingsChanged = true;
if (ImGui::Checkbox("UI World Grid", &showUIWorldGrid)) editorSettingsChanged = true;
}
if (ImGui::CollapsingHeader("Renderer", ImGuiTreeNodeFlags_DefaultOpen)) {
glm::vec3 ambient = renderer.getAmbientColor();
if (ImGui::ColorEdit3("Ambient Color", &ambient.x)) {
renderer.setAmbientColor(ambient);
buildSettings.rendererAmbientColor = ambient;
buildSettingsChanged = true;
}
int shadowResolution = renderer.getShadowMapResolution();
if (ImGui::SliderInt("Shadow Resolution", &shadowResolution, 128, 4096)) {
renderer.setShadowMapResolution(shadowResolution);
buildSettings.rendererShadowResolution = renderer.getShadowMapResolution();
buildSettingsChanged = true;
}
bool shaderAutoReload = renderer.isShaderAutoReloadEnabled();
if (ImGui::Checkbox("Auto Reload Shaders", &shaderAutoReload)) {
renderer.setShaderAutoReload(shaderAutoReload);
buildSettings.rendererAutoReloadShaders = shaderAutoReload;
buildSettingsChanged = true;
}
}
if (ImGui::CollapsingHeader("Editor Camera", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::DragFloat("Move Speed", &camera.moveSpeed, 0.1f, 0.01f, 100.0f, "%.2f")) {
camera.moveSpeed = std::max(0.01f, camera.moveSpeed);
camera.sprintSpeed = std::max(camera.moveSpeed, camera.sprintSpeed);
editorSettingsChanged = true;
}
if (ImGui::DragFloat("Sprint Speed", &camera.sprintSpeed, 0.1f, 0.01f, 200.0f, "%.2f")) {
camera.sprintSpeed = std::max(camera.moveSpeed, camera.sprintSpeed);
editorSettingsChanged = true;
}
if (ImGui::Checkbox("Smooth Movement", &camera.smoothMovement)) editorSettingsChanged = true;
ImGui::BeginDisabled(!camera.smoothMovement);
if (ImGui::DragFloat("Acceleration", &camera.acceleration, 0.1f, 0.1f, 200.0f, "%.2f")) {
camera.acceleration = std::max(0.1f, camera.acceleration);
editorSettingsChanged = true;
}
ImGui::EndDisabled();
if (ImGui::SliderFloat("Mouse Sensitivity", &camera.mouseSensitivity, 0.001f, 1.0f, "%.3f")) {
camera.mouseSensitivity = std::clamp(camera.mouseSensitivity, 0.001f, 1.0f);
editorSettingsChanged = true;
}
ImGui::Separator();
if (ImGui::SliderFloat("Projection FOV", &buildSettings.editorCameraFov, 20.0f, 140.0f, "%.1f")) {
buildSettingsChanged = true;
}
if (ImGui::DragFloat("Near Clip", &buildSettings.editorCameraNear, 0.01f, 0.01f, buildSettings.editorCameraFar - 0.01f, "%.3f")) {
buildSettings.editorCameraNear = std::max(0.01f, std::min(buildSettings.editorCameraNear, buildSettings.editorCameraFar - 0.01f));
buildSettingsChanged = true;
}
if (ImGui::DragFloat("Far Clip", &buildSettings.editorCameraFar, 0.1f, buildSettings.editorCameraNear + 0.05f, 5000.0f, "%.1f")) {
buildSettings.editorCameraFar = std::max(buildSettings.editorCameraNear + 0.05f, buildSettings.editorCameraFar);
buildSettingsChanged = true;
}
}
if (ImGui::CollapsingHeader("Debug / Performance", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::Checkbox("Scene Gizmos", &showSceneGizmos)) editorSettingsChanged = true;
if (ImGui::Checkbox("3D Grid", &showSceneGrid3D)) editorSettingsChanged = true;
if (ImGui::Checkbox("Collision Wireframe", &collisionWireframe)) editorSettingsChanged = true;
if (ImGui::Checkbox("FPS Cap", &fpsCapEnabled)) editorSettingsChanged = true;
ImGui::BeginDisabled(!fpsCapEnabled);
if (ImGui::DragFloat("FPS Target", &fpsCap, 1.0f, 1.0f, 500.0f, "%.0f")) {
fpsCap = std::max(1.0f, fpsCap);
editorSettingsChanged = true;
}
ImGui::EndDisabled();
}
if (editorSettingsChanged) {
saveEditorUserSettings();
}
if (buildSettingsChanged) {
saveBuildSettings();
}
} else if (selectedTab == 4) {
bool changed = false;
if (ImGui::CollapsingHeader("Build Targets", ImGuiTreeNodeFlags_DefaultOpen)) {
const char* targets[] = {"Windows", "Linux", "Android"};
int targetIndex = static_cast<int>(buildSettings.platform);
if (ImGui::Combo("Platform", &targetIndex, targets, IM_ARRAYSIZE(targets))) {
buildSettings.platform = static_cast<BuildPlatform>(targetIndex);
changed = true;
}
const char* arches[] = {"x86_64", "x86"};
int archIndex = (buildSettings.architecture == "x86") ? 1 : 0;
if (ImGui::Combo("Architecture", &archIndex, arches, IM_ARRAYSIZE(arches))) {
buildSettings.architecture = arches[archIndex];
changed = true;
}
if (ImGui::Checkbox("Development Build", &buildSettings.developmentBuild)) changed = true;
if (ImGui::Checkbox("Script Debugging", &buildSettings.scriptDebugging)) changed = true;
if (ImGui::Checkbox("Auto-connect Profiler", &buildSettings.autoConnectProfiler)) changed = true;
if (ImGui::Checkbox("Deep Profiling", &buildSettings.deepProfiling)) changed = true;
if (ImGui::Checkbox("Scripts Only Build", &buildSettings.scriptsOnlyBuild)) changed = true;
if (ImGui::Checkbox("Server Build", &buildSettings.serverBuild)) changed = true;
const char* compressionOptions[] = {"Default", "None", "LZ4", "LZ4HC"};
int compressionIndex = 0;
for (int i = 0; i < IM_ARRAYSIZE(compressionOptions); ++i) {
if (buildSettings.compressionMethod == compressionOptions[i]) {
compressionIndex = i;
break;
}
}
if (ImGui::Combo("Compression", &compressionIndex, compressionOptions, IM_ARRAYSIZE(compressionOptions))) {
buildSettings.compressionMethod = compressionOptions[compressionIndex];
changed = true;
}
}
if (ImGui::CollapsingHeader("Scenes In Build", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::BeginChild("ProjectBuildScenes", ImVec2(0, 180), true);
for (int i = 0; i < static_cast<int>(buildSettings.scenes.size()); ++i) {
BuildSceneEntry& entry = buildSettings.scenes[i];
ImGui::PushID(i);
bool enabled = entry.enabled;
if (ImGui::Checkbox("##enabled", &enabled)) {
entry.enabled = enabled;
changed = true;
}
ImGui::SameLine();
if (ImGui::Selectable(entry.name.c_str(), buildSettingsSelectedIndex == i, ImGuiSelectableFlags_SpanAllColumns)) {
buildSettingsSelectedIndex = i;
}
ImGui::PopID();
}
ImGui::EndChild();
if (ImGui::Button("Add Current Scene")) {
if (addSceneToBuildSettings(projectManager.currentProject.currentSceneName, true)) {
changed = true;
}
}
ImGui::SameLine();
if (ImGui::Button("Add All Scenes")) {
auto scenes = projectManager.currentProject.getSceneList();
for (const auto& scene : scenes) {
if (addSceneToBuildSettings(scene, scene == projectManager.currentProject.currentSceneName)) {
changed = true;
}
}
}
ImGui::SameLine();
if (ImGui::Button("Remove Selected")) {
if (buildSettingsSelectedIndex >= 0 &&
buildSettingsSelectedIndex < static_cast<int>(buildSettings.scenes.size())) {
buildSettings.scenes.erase(buildSettings.scenes.begin() + buildSettingsSelectedIndex);
if (buildSettingsSelectedIndex >= static_cast<int>(buildSettings.scenes.size())) {
buildSettingsSelectedIndex = static_cast<int>(buildSettings.scenes.size()) - 1;
}
changed = true;
}
}
}
ImGui::Separator();
ImGui::TextDisabled("Export");
if (ImGui::Button("Open Advanced Build Window")) {
showBuildSettings = true;
}
ImGui::SameLine();
if (ImGui::Button("Save Build Profile")) {
saveBuildSettings();
}
if (changed) {
saveBuildSettings();
}
} else if (selectedTab == 5) {
struct CompilationUiState {
fs::path configPath;
ScriptBuildConfig config;
bool loaded = false;
bool dirty = false;
std::string status;
};
static CompilationUiState ui;
fs::path configPath = resolveScriptsConfigPath(projectManager.currentProject);
if (!ui.loaded || ui.configPath != configPath) {
ui = CompilationUiState{};
ui.configPath = configPath;
std::string error;
if (!scriptCompiler.loadConfig(configPath, ui.config, error)) {
ui.status = "Creating new scripts config (previous load failed): " + error;
ui.config = ScriptBuildConfig{};
ui.config.scriptsDir = projectManager.currentProject.projectPath / "Assets" / "Scripts";
ui.config.outDir = projectManager.currentProject.projectPath / "Library" / "CompiledScripts";
ui.loaded = true;
} else {
ui.status = "Loaded " + configPath.filename().string();
ui.loaded = true;
}
}
bool editorSettingsChanged = false;
if (ImGui::CollapsingHeader("Compiler Workflow", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::Checkbox("Auto-compile on Save", &scriptEditorState.autoCompileOnSave)) {
editorSettingsChanged = true;
}
float interval = static_cast<float>(scriptAutoCompileInterval);
if (ImGui::SliderFloat("Auto-compile Scan Interval (s)", &interval, 0.1f, 10.0f, "%.2f")) {
scriptAutoCompileInterval = std::clamp(static_cast<double>(interval), 0.1, 10.0);
editorSettingsChanged = true;
}
}
if (ImGui::CollapsingHeader("scripts.modu", ImGuiTreeNodeFlags_DefaultOpen)) {
auto editString = [&](const char* label, std::string& value, size_t capacity = 1024) {
std::vector<char> buf(capacity, '\0');
std::snprintf(buf.data(), buf.size(), "%s", value.c_str());
if (ImGui::InputText(label, buf.data(), buf.size())) {
value = buf.data();
ui.dirty = true;
}
};
auto editPath = [&](const char* label, fs::path& value, size_t capacity = 1024) {
std::string text = value.generic_string();
std::vector<char> buf(capacity, '\0');
std::snprintf(buf.data(), buf.size(), "%s", text.c_str());
if (ImGui::InputText(label, buf.data(), buf.size())) {
value = fs::path(buf.data());
ui.dirty = true;
}
};
const char* cppStd[] = {"c++17", "c++20", "c++23", "gnu++20"};
int cppIdx = 1;
for (int i = 0; i < IM_ARRAYSIZE(cppStd); ++i) {
if (ui.config.cppStandard == cppStd[i]) {
cppIdx = i;
break;
}
}
if (ImGui::Combo("C++ Standard", &cppIdx, cppStd, IM_ARRAYSIZE(cppStd))) {
ui.config.cppStandard = cppStd[cppIdx];
ui.dirty = true;
}
editPath("Scripts Directory", ui.config.scriptsDir);
editPath("Output Directory", ui.config.outDir);
ImGui::Separator();
ImGui::TextDisabled("Include Directories");
for (size_t i = 0; i < ui.config.includeDirs.size(); ++i) {
ImGui::PushID(static_cast<int>(i));
editPath("##inc", ui.config.includeDirs[i], 1200);
ImGui::SameLine();
if (ImGui::SmallButton("Remove")) {
ui.config.includeDirs.erase(ui.config.includeDirs.begin() + static_cast<long>(i));
ui.dirty = true;
ImGui::PopID();
break;
}
ImGui::PopID();
}
if (ImGui::SmallButton("Add Include Directory")) {
ui.config.includeDirs.emplace_back(fs::path());
ui.dirty = true;
}
ImGui::Separator();
ImGui::TextDisabled("Defines");
for (size_t i = 0; i < ui.config.defines.size(); ++i) {
ImGui::PushID(static_cast<int>(1000 + i));
editString("##def", ui.config.defines[i], 1024);
ImGui::SameLine();
if (ImGui::SmallButton("Remove")) {
ui.config.defines.erase(ui.config.defines.begin() + static_cast<long>(i));
ui.dirty = true;
ImGui::PopID();
break;
}
ImGui::PopID();
}
if (ImGui::SmallButton("Add Define")) {
ui.config.defines.push_back("");
ui.dirty = true;
}
ImGui::Separator();
ImGui::TextDisabled("Linux Link Libraries");
for (size_t i = 0; i < ui.config.linuxLinkLibs.size(); ++i) {
ImGui::PushID(static_cast<int>(2000 + i));
editString("##linlib", ui.config.linuxLinkLibs[i], 512);
ImGui::SameLine();
if (ImGui::SmallButton("Remove")) {
ui.config.linuxLinkLibs.erase(ui.config.linuxLinkLibs.begin() + static_cast<long>(i));
ui.dirty = true;
ImGui::PopID();
break;
}
ImGui::PopID();
}
if (ImGui::SmallButton("Add Linux Lib")) {
ui.config.linuxLinkLibs.push_back("");
ui.dirty = true;
}
ImGui::Separator();
ImGui::TextDisabled("Windows Link Libraries");
for (size_t i = 0; i < ui.config.windowsLinkLibs.size(); ++i) {
ImGui::PushID(static_cast<int>(3000 + i));
editString("##winlib", ui.config.windowsLinkLibs[i], 512);
ImGui::SameLine();
if (ImGui::SmallButton("Remove")) {
ui.config.windowsLinkLibs.erase(ui.config.windowsLinkLibs.begin() + static_cast<long>(i));
ui.dirty = true;
ImGui::PopID();
break;
}
ImGui::PopID();
}
if (ImGui::SmallButton("Add Windows Lib")) {
ui.config.windowsLinkLibs.push_back("");
ui.dirty = true;
}
ImGui::Spacing();
if (ImGui::Button("Reload scripts.modu")) {
ui.loaded = false;
}
ImGui::SameLine();
ImGui::BeginDisabled(!ui.dirty);
if (ImGui::Button("Save scripts.modu")) {
std::string error;
if (SaveScriptsConfig(ui.configPath, ui.config, projectManager.currentProject.projectPath, error)) {
ui.status = "Saved " + ui.configPath.filename().string();
ui.dirty = false;
scriptingFilesDirty = true;
scriptLastAutoCompileTime.clear();
autoCompileQueue.clear();
autoCompileQueued.clear();
addConsoleMessage("Saved scripts config: " + ui.configPath.string(), ConsoleMessageType::Success);
} else {
ui.status = error;
addConsoleMessage(error, ConsoleMessageType::Error);
}
}
ImGui::EndDisabled();
if (!ui.status.empty()) {
ImGui::TextDisabled("%s", ui.status.c_str());
}
}
if (editorSettingsChanged) {
saveEditorUserSettings();
}
}
ImGui::EndChild();

View File

@@ -624,26 +624,6 @@ void Engine::renderHierarchyPanel() {
ImGui::InputTextWithHint("##Search", "Search...", searchBuffer, sizeof(searchBuffer));
ImGui::Spacing();
ImGui::Checkbox("Texture Preview", &hierarchyShowTexturePreview);
ImGui::SameLine();
ImGui::BeginDisabled(!hierarchyShowTexturePreview);
ImGui::TextDisabled("Filter");
ImGui::SameLine();
const char* filterOptions[] = { "Bilinear", "Nearest" };
int filterIndex = hierarchyPreviewNearest ? 1 : 0;
ImGui::SetNextItemWidth(120.0f);
ImGui::SetNextWindowBgAlpha(0.85f);
if (ImGui::BeginCombo("##HierarchyTexFilter", filterOptions[filterIndex])) {
for (int i = 0; i < IM_ARRAYSIZE(filterOptions); ++i) {
bool selected = (i == filterIndex);
if (ImGui::Selectable(filterOptions[i], selected)) {
filterIndex = i;
hierarchyPreviewNearest = (filterIndex == 1);
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
ImGui::EndDisabled();
ImGui::EndChild();
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
@@ -985,11 +965,7 @@ void Engine::renderObjectNode(SceneObject& obj, const std::string& filter,
}
if (previewPath) {
auto overrideIt = texturePreviewFilterOverrides.find(*previewPath);
bool previewNearest = (overrideIt != texturePreviewFilterOverrides.end())
? overrideIt->second
: hierarchyPreviewNearest;
Texture* previewTex = renderer.getTexturePreview(*previewPath, previewNearest);
Texture* previewTex = renderer.getTexture(*previewPath, obj.material.textureFilter);
if (previewTex && previewTex->GetID()) {
ImGuiStyle& style = ImGui::GetStyle();
ImVec2 itemMin = ImGui::GetItemRectMin();
@@ -1398,6 +1374,14 @@ void Engine::renderInspectorPanel() {
if (ImGui::SliderFloat("Detail Mix", &inspectedMaterial.textureMix, 0.0f, 1.0f)) {
matChanged = true;
}
const char* texFilterOptions[] = { "Bilinear", "Point" };
int texFilterIndex = (inspectedMaterial.textureFilter == MaterialProperties::TextureFilter::Point) ? 1 : 0;
if (ImGui::Combo("Texture Filter", &texFilterIndex, texFilterOptions, IM_ARRAYSIZE(texFilterOptions))) {
inspectedMaterial.textureFilter =
(texFilterIndex == 1) ? MaterialProperties::TextureFilter::Point
: MaterialProperties::TextureFilter::Bilinear;
matChanged = true;
}
ImGui::Spacing();
matChanged |= textureField("Base Map", "PreviewAlbedo", inspectedAlbedo);
@@ -1610,9 +1594,7 @@ void Engine::renderInspectorPanel() {
ImGui::TextDisabled("%s", selectedTexturePath.filename().string().c_str());
ImGui::TextColored(ImVec4(0.8f, 0.65f, 0.95f, 1.0f), "%s", selectedTexturePath.string().c_str());
bool hasOverride = texturePreviewFilterOverrides.find(selectedTexturePath.string()) != texturePreviewFilterOverrides.end();
bool previewNearest = hasOverride ? texturePreviewFilterOverrides[selectedTexturePath.string()] : hierarchyPreviewNearest;
Texture* previewTex = renderer.getTexturePreview(selectedTexturePath.string(), previewNearest);
Texture* previewTex = renderer.getTexture(selectedTexturePath.string());
ImGui::Spacing();
if (previewTex && previewTex->GetID()) {
@@ -1631,25 +1613,6 @@ void Engine::renderInspectorPanel() {
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.6f, 1.0f), "Unable to load texture preview.");
}
ImGui::Spacing();
if (ImGui::Checkbox("Override Hierarchy Filter", &hasOverride)) {
if (hasOverride) {
texturePreviewFilterOverrides[selectedTexturePath.string()] = hierarchyPreviewNearest;
} else {
texturePreviewFilterOverrides.erase(selectedTexturePath.string());
}
}
ImGui::BeginDisabled(!hasOverride);
const char* filterOptions[] = { "Bilinear", "Nearest" };
int filterIndex = previewNearest ? 1 : 0;
if (ImGui::Combo("Preview Filter", &filterIndex, filterOptions, IM_ARRAYSIZE(filterOptions))) {
texturePreviewFilterOverrides[selectedTexturePath.string()] = (filterIndex == 1);
}
ImGui::EndDisabled();
if (!hasOverride) {
ImGui::TextDisabled("Using global: %s", hierarchyPreviewNearest ? "Nearest" : "Bilinear");
}
ImGui::Unindent(8.0f);
}
ImGui::PopStyleColor();
@@ -2148,12 +2111,14 @@ void Engine::renderInspectorPanel() {
changed = true;
}
if (changed) {
obj.hasCollider = true;
obj.collider.type = ColliderType::Capsule;
obj.collider.convex = true;
obj.hasRigidbody = true;
obj.rigidbody.enabled = true;
obj.rigidbody.useGravity = true;
if (obj.hasPlayerController) {
obj.hasCollider = true;
obj.collider.type = ColliderType::Capsule;
obj.collider.convex = true;
obj.hasRigidbody = true;
obj.rigidbody.enabled = true;
obj.rigidbody.useGravity = true;
}
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::PopStyleColor();
@@ -3185,6 +3150,14 @@ void Engine::renderInspectorPanel() {
if (ImGui::SliderFloat("Detail Mix", &obj.material.textureMix, 0.0f, 1.0f)) {
materialChanged = true;
}
const char* texFilterOptions[] = { "Bilinear", "Point" };
int texFilterIndex = (obj.material.textureFilter == MaterialProperties::TextureFilter::Point) ? 1 : 0;
if (ImGui::Combo("Texture Filter", &texFilterIndex, texFilterOptions, IM_ARRAYSIZE(texFilterOptions))) {
obj.material.textureFilter =
(texFilterIndex == 1) ? MaterialProperties::TextureFilter::Point
: MaterialProperties::TextureFilter::Bilinear;
materialChanged = true;
}
ImGui::Spacing();
ImGui::TextDisabled("Maps");

View File

@@ -2294,15 +2294,19 @@ void Engine::renderViewport() {
if (rendererInitialized) {
glm::mat4 proj = glm::perspective(
glm::radians(FOV),
glm::radians(buildSettings.editorCameraFov),
(float)viewportWidth / (float)viewportHeight,
NEAR_PLANE, FAR_PLANE
buildSettings.editorCameraNear, buildSettings.editorCameraFar
);
glm::mat4 view = camera.getViewMatrix();
renderer.beginRender(view, proj, camera.position);
renderer.renderScene(camera, sceneObjects, selectedObjectId, FOV, NEAR_PLANE, FAR_PLANE, collisionWireframe);
renderer.renderScene(camera, sceneObjects, selectedObjectId,
buildSettings.editorCameraFov,
buildSettings.editorCameraNear,
buildSettings.editorCameraFar,
collisionWireframe);
unsigned int tex = renderer.getViewportTexture();
ImGui::Image((void*)(intptr_t)tex, imageSize, ImVec2(0, 1), ImVec2(1, 0));
@@ -2327,7 +2331,7 @@ void Engine::renderViewport() {
auto clipLineToScreen = [&](glm::vec3 a, glm::vec3 b, ImVec2& outA, ImVec2& outB) -> bool {
glm::vec4 va = view * glm::vec4(a, 1.0f);
glm::vec4 vb = view * glm::vec4(b, 1.0f);
const float nearZ = -NEAR_PLANE;
const float nearZ = -buildSettings.editorCameraNear;
if (va.z > nearZ && vb.z > nearZ) {
return false;
}
@@ -5557,6 +5561,15 @@ void Engine::renderPlayerViewport() {
ImVec2 imageMin = ImGui::GetItemRectMin();
ImVec2 imageMax = ImGui::GetItemRectMax();
bool imageHovered = ImGui::IsItemHovered();
bool showingStartupSplash = false;
if (playerMode && buildSettings.splashEnabled && buildSettings.splashDurationSeconds > 0.0f) {
if (startupSplashStartTime < 0.0) {
startupSplashStartTime = glfwGetTime();
}
const double elapsed = glfwGetTime() - startupSplashStartTime;
showingStartupSplash = elapsed < static_cast<double>(buildSettings.splashDurationSeconds);
}
auto updateUiCanvas3DInput = [&](const Camera& cam, float fovDeg, float nearPlane, float farPlane) {
if (!imageHovered) return;
@@ -5617,7 +5630,26 @@ void Engine::renderPlayerViewport() {
}
};
updateUiCanvas3DInput(camera, FOV, NEAR_PLANE, FAR_PLANE);
float runtimeFov = buildSettings.editorCameraFov;
float runtimeNear = buildSettings.editorCameraNear;
float runtimeFar = buildSettings.editorCameraFar;
if (playerMode) {
const SceneObject* runtimeCam = nullptr;
for (const auto& obj : sceneObjects) {
if (!obj.enabled || !obj.hasCamera) continue;
if (!runtimeCam) runtimeCam = &obj;
if (obj.camera.type == SceneCameraType::Player) {
runtimeCam = &obj;
break;
}
}
if (runtimeCam) {
runtimeFov = runtimeCam->camera.fov;
runtimeNear = std::max(0.01f, runtimeCam->camera.nearClip);
runtimeFar = std::max(runtimeNear + 0.01f, runtimeCam->camera.farClip);
}
}
updateUiCanvas3DInput(camera, runtimeFov, runtimeNear, runtimeFar);
ImDrawList* drawList = ImGui::GetWindowDrawList();
float uiScaleX = (viewportWidth > 0) ? (imageSize.x / (float)viewportWidth) : 1.0f;
@@ -6112,7 +6144,50 @@ void Engine::renderPlayerViewport() {
ImGui::EndChild();
ImGui::PopStyleVar();
bool clicked = imageHovered && isPlaying && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !uiInteracting;
if (showingStartupSplash) {
ImDrawList* splashDraw = ImGui::GetWindowDrawList();
splashDraw->AddRectFilled(imageMin, imageMax, IM_COL32(0, 0, 0, 230));
fs::path splashPath = resolveSplashImagePath();
Texture* splashTex = nullptr;
if (!splashPath.empty() && fs::exists(splashPath)) {
splashTex = renderer.getTexture(splashPath.string());
}
if (splashTex) {
float availW = imageMax.x - imageMin.x;
float availH = imageMax.y - imageMin.y;
float texW = static_cast<float>(std::max(1, splashTex->GetWidth()));
float texH = static_cast<float>(std::max(1, splashTex->GetHeight()));
float scale = std::min(availW / texW, availH / texH);
scale = std::min(scale, 1.0f);
ImVec2 drawSize(texW * scale, texH * scale);
ImVec2 drawMin(imageMin.x + (availW - drawSize.x) * 0.5f,
imageMin.y + (availH - drawSize.y) * 0.5f);
ImVec2 drawMax(drawMin.x + drawSize.x, drawMin.y + drawSize.y);
splashDraw->AddImage((ImTextureID)(intptr_t)splashTex->GetID(), drawMin, drawMax,
ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 255));
} else {
const char* fallback = "Loading...";
ImVec2 textSize = ImGui::CalcTextSize(fallback);
ImVec2 textPos((imageMin.x + imageMax.x) * 0.5f - textSize.x * 0.5f,
(imageMin.y + imageMax.y) * 0.5f - textSize.y * 0.5f);
splashDraw->AddText(textPos, IM_COL32(240, 240, 240, 255), fallback);
}
std::string splashTitle = buildSettings.buildName;
if (splashTitle.empty()) splashTitle = "Game";
if (!buildSettings.version.empty()) splashTitle += " " + buildSettings.version;
ImVec2 titleSize = ImGui::CalcTextSize(splashTitle.c_str());
splashDraw->AddText(ImVec2((imageMin.x + imageMax.x) * 0.5f - titleSize.x * 0.5f,
imageMax.y - titleSize.y - 32.0f),
IM_COL32(240, 240, 240, 230), splashTitle.c_str());
}
if (showingStartupSplash && gameViewCursorLocked) {
gameViewCursorLocked = false;
}
bool clicked = imageHovered && isPlaying && !showingStartupSplash &&
ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !uiInteracting;
if (clicked && !gameViewCursorLocked) {
gameViewCursorLocked = true;
}

File diff suppressed because it is too large Load Diff

View File

@@ -160,8 +160,6 @@ private:
bool animationIsPlaying = false;
float animationLastAppliedTime = -1.0f;
bool hierarchyShowTexturePreview = false;
bool hierarchyPreviewNearest = false;
std::unordered_map<std::string, bool> texturePreviewFilterOverrides;
bool audioPreviewLoop = false;
bool audioPreviewAutoPlay = false;
std::string audioPreviewSelectedPath;
@@ -266,6 +264,13 @@ private:
struct BuildSettings {
BuildPlatform platform = BuildPlatform::Windows;
std::string architecture = "x86_64";
std::string companyName = "DefaultCompany";
std::string buildName = "MyProject";
std::string version = "0.1.0";
std::string splashImagePath;
bool splashEnabled = false;
float splashDurationSeconds = 2.5f;
bool packageStandaloneArchive = true;
bool developmentBuild = false;
bool autoConnectProfiler = false;
bool scriptDebugging = false;
@@ -273,6 +278,12 @@ private:
bool scriptsOnlyBuild = false;
bool serverBuild = false;
std::string compressionMethod = "Default";
glm::vec3 rendererAmbientColor = glm::vec3(0.2f, 0.2f, 0.2f);
int rendererShadowResolution = 512;
bool rendererAutoReloadShaders = true;
float editorCameraFov = FOV;
float editorCameraNear = NEAR_PLANE;
float editorCameraFar = FAR_PLANE;
std::vector<BuildSceneEntry> scenes;
};
BuildSettings buildSettings;
@@ -282,6 +293,8 @@ private:
bool success = false;
std::string message;
fs::path outputDir;
std::string executableName;
fs::path archivePath;
};
struct ExportJobState {
bool active = false;
@@ -292,6 +305,8 @@ private:
std::string status;
std::string log;
fs::path outputDir;
std::string executableName;
fs::path archivePath;
bool runAfter = false;
std::future<ExportJobResult> future;
};
@@ -319,6 +334,8 @@ private:
std::unordered_map<std::string, fs::file_time_type> scriptLastAutoCompileTime;
std::deque<fs::path> autoCompileQueue;
std::unordered_set<std::string> autoCompileQueued;
std::unordered_set<std::string> nativeScriptMissingLogged;
std::unordered_set<std::string> nativeScriptLoadErrorLogged;
bool managedAutoCompileQueued = false;
double scriptAutoCompileLastCheck = 0.0;
double scriptAutoCompileInterval = 0.5;
@@ -332,6 +349,7 @@ private:
double projectLoadStartTime = 0.0;
std::string projectLoadPath;
std::future<ProjectLoadResult> projectLoadFuture;
double startupSplashStartTime = -1.0;
bool sceneLoadInProgress = false;
float sceneLoadProgress = 0.0f;
std::string sceneLoadStatus;
@@ -458,6 +476,8 @@ private:
void loadBuildSettings();
void saveBuildSettings();
bool addSceneToBuildSettings(const std::string& sceneName, bool enabled);
fs::path resolveSplashImagePath() const;
void applyBuildWindowTitle();
void loadAutoStartConfig();
void applyAutoStartMode();
void startExportBuild(const fs::path& outputDir, bool runAfter);
@@ -529,6 +549,10 @@ public:
void markProjectDirty();
// Script-accessible logging wrapper
void addConsoleMessageFromScript(const std::string& message, ConsoleMessageType type);
// Runtime input queries for script helpers.
bool isRuntimeKeyDown(int key) const;
bool isRuntimeMouseDown(int button) const;
glm::vec2 getRuntimeMouseDelta() const;
int getSelectedObjectId() const;
// Script-accessible physics helpers
bool setRigidbodyVelocityFromScript(int id, const glm::vec3& velocity);

View File

@@ -304,7 +304,7 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
if (!file.is_open()) return false;
file << "# Scene File\n";
file << "version=17\n";
file << "version=18\n";
file << "nextId=" << nextId << "\n";
file << "timeOfDay=" << timeOfDay << "\n";
file << "objectCount=" << objects.size() << "\n";
@@ -483,6 +483,7 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
file << "materialSpecular=" << obj.material.specularStrength << "\n";
file << "materialShininess=" << obj.material.shininess << "\n";
file << "materialTextureMix=" << obj.material.textureMix << "\n";
file << "materialTextureFilter=" << static_cast<int>(obj.material.textureFilter) << "\n";
file << "materialPath=" << obj.materialPath << "\n";
file << "albedoTex=" << obj.albedoTexturePath << "\n";
file << "overlayTex=" << obj.overlayTexturePath << "\n";
@@ -927,6 +928,12 @@ const std::unordered_map<std::string, KeyHandler>& GetSceneObjectKeyHandlers() {
{"materialSpecular", +[](SceneObject& obj, const std::string& value) { obj.material.specularStrength = std::stof(value); }},
{"materialShininess", +[](SceneObject& obj, const std::string& value) { obj.material.shininess = std::stof(value); }},
{"materialTextureMix", +[](SceneObject& obj, const std::string& value) { obj.material.textureMix = std::stof(value); }},
{"materialTextureFilter", +[](SceneObject& obj, const std::string& value) {
int filterValue = std::stoi(value);
obj.material.textureFilter = (filterValue == 1)
? MaterialProperties::TextureFilter::Point
: MaterialProperties::TextureFilter::Bilinear;
}},
{"materialPath", +[](SceneObject& obj, const std::string& value) { obj.materialPath = value; }},
{"albedoTex", +[](SceneObject& obj, const std::string& value) { obj.albedoTexturePath = value; }},
{"overlayTex", +[](SceneObject& obj, const std::string& value) { obj.overlayTexturePath = value; }},

View File

@@ -649,28 +649,15 @@ Renderer::~Renderer() {
if (debugWhiteTexture) glDeleteTextures(1, &debugWhiteTexture);
}
Texture* Renderer::getTexture(const std::string& path) {
Texture* Renderer::getTexture(const std::string& path, MaterialProperties::TextureFilter filter) {
if (path.empty()) return nullptr;
auto it = textureCache.find(path);
if (it != textureCache.end()) return it->second.get();
auto tex = std::make_unique<Texture>(path);
if (!tex->GetID()) {
return nullptr;
}
Texture* raw = tex.get();
textureCache[path] = std::move(tex);
return raw;
}
Texture* Renderer::getTexturePreview(const std::string& path, bool nearest) {
if (path.empty()) return nullptr;
auto& cache = nearest ? previewTextureCacheNearest : previewTextureCacheLinear;
bool point = (filter == MaterialProperties::TextureFilter::Point);
auto& cache = point ? textureCachePoint : textureCacheBilinear;
auto it = cache.find(path);
if (it != cache.end()) return it->second.get();
GLenum minFilter = nearest ? GL_NEAREST : GL_LINEAR_MIPMAP_LINEAR;
GLenum magFilter = nearest ? GL_NEAREST : GL_LINEAR;
GLenum minFilter = point ? GL_NEAREST_MIPMAP_NEAREST : GL_LINEAR_MIPMAP_LINEAR;
GLenum magFilter = point ? GL_NEAREST : GL_LINEAR;
auto tex = std::make_unique<Texture>(path, GL_REPEAT, GL_REPEAT, minFilter, magFilter);
if (!tex->GetID()) {
return nullptr;
@@ -1160,7 +1147,7 @@ void Renderer::renderObject(const SceneObject& obj) {
Texture* baseTex = texture1;
if (!obj.albedoTexturePath.empty()) {
if (auto* t = getTexture(obj.albedoTexturePath)) baseTex = t;
if (auto* t = getTexture(obj.albedoTexturePath, obj.material.textureFilter)) baseTex = t;
}
if (baseTex) baseTex->Bind(GL_TEXTURE0);
@@ -1174,7 +1161,7 @@ void Renderer::renderObject(const SceneObject& obj) {
}
}
if (!overlayUsed && obj.useOverlay && !obj.overlayTexturePath.empty()) {
if (auto* t = getTexture(obj.overlayTexturePath)) {
if (auto* t = getTexture(obj.overlayTexturePath, obj.material.textureFilter)) {
t->Bind(GL_TEXTURE1);
overlayUsed = true;
}
@@ -1186,7 +1173,7 @@ void Renderer::renderObject(const SceneObject& obj) {
bool normalUsed = false;
if (!obj.normalMapPath.empty()) {
if (auto* t = getTexture(obj.normalMapPath)) {
if (auto* t = getTexture(obj.normalMapPath, obj.material.textureFilter)) {
t->Bind(GL_TEXTURE2);
normalUsed = true;
}
@@ -1647,7 +1634,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
Texture* baseTex = texture1;
if (!usingUiTargetTex) {
if (!obj.albedoTexturePath.empty()) {
if (auto* t = getTexture(obj.albedoTexturePath)) baseTex = t;
if (auto* t = getTexture(obj.albedoTexturePath, obj.material.textureFilter)) baseTex = t;
}
if (baseTex) baseTex->Bind(GL_TEXTURE0);
}
@@ -1662,7 +1649,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
}
}
if (!overlayUsed && obj.useOverlay && !obj.overlayTexturePath.empty()) {
if (auto* t = getTexture(obj.overlayTexturePath)) {
if (auto* t = getTexture(obj.overlayTexturePath, obj.material.textureFilter)) {
t->Bind(GL_TEXTURE1);
overlayUsed = true;
}
@@ -1674,7 +1661,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
bool normalUsed = false;
if (!obj.normalMapPath.empty()) {
if (auto* t = getTexture(obj.normalMapPath)) {
if (auto* t = getTexture(obj.normalMapPath, obj.material.textureFilter)) {
t->Bind(GL_TEXTURE2);
normalUsed = true;
}

View File

@@ -117,9 +117,8 @@ private:
Texture* texture1 = nullptr;
Texture* texture2 = nullptr;
unsigned int debugWhiteTexture = 0;
std::unordered_map<std::string, std::unique_ptr<Texture>> textureCache;
std::unordered_map<std::string, std::unique_ptr<Texture>> previewTextureCacheLinear;
std::unordered_map<std::string, std::unique_ptr<Texture>> previewTextureCacheNearest;
std::unordered_map<std::string, std::unique_ptr<Texture>> textureCacheBilinear;
std::unordered_map<std::string, std::unique_ptr<Texture>> textureCachePoint;
struct ShaderEntry {
std::unique_ptr<Shader> shader;
fs::file_time_type vertTime;
@@ -179,12 +178,15 @@ public:
~Renderer();
void initialize();
Texture* getTexture(const std::string& path);
Texture* getTexturePreview(const std::string& path, bool nearest);
Texture* getTexture(const std::string& path, MaterialProperties::TextureFilter filter = MaterialProperties::TextureFilter::Bilinear);
Shader* getShader(const std::string& vert, const std::string& frag);
bool forceReloadShader(const std::string& vert, const std::string& frag);
void setAmbientColor(const glm::vec3& color) { ambientColor = color; }
glm::vec3 getAmbientColor() const { return ambientColor; }
void setShadowMapResolution(int resolution) { shadowMapResolution = std::clamp(resolution, 128, 4096); }
int getShadowMapResolution() const { return shadowMapResolution; }
void setShaderAutoReload(bool enabled) { autoReloadShaders = enabled; }
bool isShaderAutoReloadEnabled() const { return autoReloadShaders; }
void resize(int w, int h);
int getWidth() const { return currentWidth; }
int getHeight() const { return currentHeight; }

View File

@@ -51,11 +51,17 @@ enum class UIElementType {
};
struct MaterialProperties {
enum class TextureFilter {
Bilinear = 0,
Point = 1
};
glm::vec3 color = glm::vec3(1.0f);
float ambientStrength = 0.2f;
float specularStrength = 0.5f;
float shininess = 32.0f;
float textureMix = 0.3f; // Blend factor between albedo and overlay
TextureFilter textureFilter = TextureFilter::Bilinear;
};
enum class LightType {

View File

@@ -64,6 +64,72 @@ glm::vec3 sanitizePlanar(const glm::vec3& value) {
}
return out;
}
bool isGlfwKeyDownFallback(int key) {
GLFWwindow* window = glfwGetCurrentContext();
if (!window) return false;
return glfwGetKey(window, key) == GLFW_PRESS;
}
bool isGlfwMouseDownFallback(int button) {
GLFWwindow* window = glfwGetCurrentContext();
if (!window) return false;
return glfwGetMouseButton(window, button) == GLFW_PRESS;
}
bool isMoveKeyDown(const ScriptContext* ctx, ImGuiKey imguiKey, int glfwKey) {
if (ImGui::IsKeyDown(imguiKey)) return true;
if (ctx && ctx->engine && ctx->engine->isRuntimeKeyDown(glfwKey)) return true;
return isGlfwKeyDownFallback(glfwKey);
}
bool isScriptMouseDown(const ScriptContext* ctx, int glfwButton) {
if (ctx && ctx->engine && ctx->engine->isRuntimeMouseDown(glfwButton)) return true;
return isGlfwMouseDownFallback(glfwButton);
}
glm::vec2 getScriptMouseDelta(const ScriptContext* ctx) {
if (ctx && ctx->engine) {
return ctx->engine->getRuntimeMouseDelta();
}
GLFWwindow* window = glfwGetCurrentContext();
if (!window) {
return glm::vec2(0.0f);
}
struct CursorCache {
int frame = -1;
bool hasPos = false;
double lastX = 0.0;
double lastY = 0.0;
glm::vec2 delta = glm::vec2(0.0f);
};
static std::unordered_map<GLFWwindow*, CursorCache> cacheByWindow;
CursorCache& cache = cacheByWindow[window];
int frame = ImGui::GetFrameCount();
if (cache.frame == frame) {
return cache.delta;
}
double x = 0.0;
double y = 0.0;
glfwGetCursorPos(window, &x, &y);
glm::vec2 computed(0.0f);
if (cache.hasPos) {
computed.x = static_cast<float>(x - cache.lastX);
computed.y = static_cast<float>(y - cache.lastY);
}
cache.lastX = x;
cache.lastY = y;
cache.hasPos = true;
cache.frame = frame;
cache.delta = computed;
return computed;
}
}
SceneObject* ScriptContext::FindObjectByName(const std::string& name) {
@@ -214,19 +280,21 @@ glm::vec3 ScriptContext::GetMoveInputWASD(float pitchDeg, float yawDeg) const {
glm::vec3 right(0.0f);
glm::vec3 move(0.0f);
GetPlanarYawPitchVectors(pitchDeg, yawDeg, forward, right);
if (ImGui::IsKeyDown(ImGuiKey_W)) move += forward;
if (ImGui::IsKeyDown(ImGuiKey_S)) move -= forward;
if (ImGui::IsKeyDown(ImGuiKey_D)) move += right;
if (ImGui::IsKeyDown(ImGuiKey_A)) move -= right;
if (isMoveKeyDown(this, ImGuiKey_W, GLFW_KEY_W)) move += forward;
if (isMoveKeyDown(this, ImGuiKey_S, GLFW_KEY_S)) move -= forward;
if (isMoveKeyDown(this, ImGuiKey_D, GLFW_KEY_D)) move += right;
if (isMoveKeyDown(this, ImGuiKey_A, GLFW_KEY_A)) move -= right;
if (glm::length(move) > 0.001f) move = glm::normalize(move);
return move;
}
bool ScriptContext::ApplyMouseLook(float& pitchDeg, float& yawDeg, float sensitivity, float maxDelta,
float deltaTime, bool requireMouseButton) const {
if (requireMouseButton && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) return false;
ImGuiIO& io = ImGui::GetIO();
glm::vec2 delta(io.MouseDelta.x, io.MouseDelta.y);
if (requireMouseButton &&
!(ImGui::IsMouseDown(ImGuiMouseButton_Right) || isScriptMouseDown(this, GLFW_MOUSE_BUTTON_RIGHT))) {
return false;
}
glm::vec2 delta = getScriptMouseDelta(this);
float len = glm::length(delta);
if (len > maxDelta) delta *= (maxDelta / len);
yawDeg -= delta.x * 50.0f * sensitivity * deltaTime;
@@ -241,11 +309,12 @@ int ScriptContext::GetSelectedObjectId() const {
}
bool ScriptContext::IsSprintDown() const {
return ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift);
return isMoveKeyDown(this, ImGuiKey_LeftShift, GLFW_KEY_LEFT_SHIFT) ||
isMoveKeyDown(this, ImGuiKey_RightShift, GLFW_KEY_RIGHT_SHIFT);
}
bool ScriptContext::IsJumpDown() const {
return ImGui::IsKeyDown(ImGuiKey_Space);
return isMoveKeyDown(this, ImGuiKey_Space, GLFW_KEY_SPACE);
}
bool ScriptContext::ResolveGround(float capsuleHalf, float probeExtra, float groundSnap, float verticalVelocity,