Added Hot loading and fixed the engine freezing when compiling scripts, Yey!

This commit is contained in:
Anemunt
2026-01-01 18:58:13 -05:00
parent b5bbbc2937
commit 2061d588e7
10 changed files with 828 additions and 289 deletions

View File

@@ -2,52 +2,31 @@
#include "SceneObject.h" #include "SceneObject.h"
#include "ThirdParty/imgui/imgui.h" #include "ThirdParty/imgui/imgui.h"
#include <unordered_map> #include <unordered_map>
namespace
namespace { {
struct ControllerState { struct ControllerState
float pitch = 0.0f; {
float yaw = 0.0f; ScriptContext::StandaloneMovementState movement; ScriptContext::StandaloneMovementDebug debug;
float verticalVelocity = 0.0f; bool initialized = false;
glm::vec3 debugVelocity = glm::vec3(0.0f); };
bool debugGrounded = false; std::unordered_map<int, ControllerState> g_states;
bool initialized = false; ScriptContext::StandaloneMovementSettings g_settings;
}; ControllerState& getState(int id) {return g_states[id];}
// aliases for readability
std::unordered_map<int, ControllerState> g_states; glm::vec3& moveTuning = g_settings.moveTuning;
glm::vec3& lookTuning = g_settings.lookTuning;
glm::vec3 moveTuning = glm::vec3(4.5f, 7.5f, 6.5f); // walk speed, run speed, jump glm::vec3& capsuleTuning = g_settings.capsuleTuning;
glm::vec3 lookTuning = glm::vec3(0.12f, 200.0f, 0.0f); // sensitivity, max delta clamp, reserved glm::vec3& gravityTuning = g_settings.gravityTuning;
glm::vec3 capsuleTuning = glm::vec3(1.8f, 0.4f, 0.2f); // height, radius, ground snap bool& enableMouseLook = g_settings.enableMouseLook;
glm::vec3 gravityTuning = glm::vec3(-9.81f, 0.4f, 30.0f); // gravity, probe extra, max fall speed bool& requireMouseButton = g_settings.requireMouseButton;
bool enableMouseLook = true; bool& enforceCollider = g_settings.enforceCollider;
bool requireMouseButton = false; bool& enforceRigidbody = g_settings.enforceRigidbody;
bool enforceCollider = true;
bool enforceRigidbody = true;
bool showDebug = false;
ControllerState& getState(int id) {
return g_states[id];
} }
extern "C" void Script_OnInspector(ScriptContext& ctx)
void bindSettings(ScriptContext& ctx) { {
ctx.AutoSetting("moveTuning", moveTuning); ctx.BindStandaloneMovementSettings(g_settings);
ctx.AutoSetting("lookTuning", lookTuning);
ctx.AutoSetting("capsuleTuning", capsuleTuning);
ctx.AutoSetting("gravityTuning", gravityTuning);
ctx.AutoSetting("enableMouseLook", enableMouseLook);
ctx.AutoSetting("requireMouseButton", requireMouseButton);
ctx.AutoSetting("enforceCollider", enforceCollider);
ctx.AutoSetting("enforceRigidbody", enforceRigidbody);
ctx.AutoSetting("showDebug", showDebug);
}
} // namespace
extern "C" void Script_OnInspector(ScriptContext& ctx) {
bindSettings(ctx);
ImGui::TextUnformatted("Standalone Movement Controller"); ImGui::TextUnformatted("Standalone Movement Controller");
ImGui::Separator(); ImGui::Separator();
ImGui::DragFloat3("Walk/Run/Jump", &moveTuning.x, 0.05f, 0.0f, 25.0f, "%.2f"); 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::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("Height/Radius/Snap", &capsuleTuning.x, 0.02f, 0.0f, 5.0f, "%.2f");
@@ -56,133 +35,22 @@ extern "C" void Script_OnInspector(ScriptContext& ctx) {
ImGui::Checkbox("Hold RMB to Look", &requireMouseButton); ImGui::Checkbox("Hold RMB to Look", &requireMouseButton);
ImGui::Checkbox("Force Collider", &enforceCollider); ImGui::Checkbox("Force Collider", &enforceCollider);
ImGui::Checkbox("Force Rigidbody", &enforceRigidbody); ImGui::Checkbox("Force Rigidbody", &enforceRigidbody);
ImGui::Checkbox("Show Debug", &showDebug);
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");
}
}
} }
void Begin(ScriptContext& ctx, float /*deltaTime*/) { void Begin(ScriptContext& ctx, float)
if (!ctx.object) return; {
bindSettings(ctx); if (!ctx.object) return; ControllerState& s = getState(ctx.object->id);
ControllerState& state = getState(ctx.object->id); if (!s.initialized)
if (!state.initialized) { {
state.pitch = ctx.object->rotation.x; s.movement.pitch = ctx.object->rotation.x;
state.yaw = ctx.object->rotation.y; s.movement.yaw = ctx.object->rotation.y;
state.verticalVelocity = 0.0f; s.initialized = true;
state.initialized = true;
} }
if (enforceCollider) ctx.EnsureCapsuleCollider(capsuleTuning.x, capsuleTuning.y); if (enforceCollider) ctx.EnsureCapsuleCollider(capsuleTuning.x, capsuleTuning.y);
if (enforceRigidbody) ctx.EnsureRigidbody(true, false); if (enforceRigidbody) ctx.EnsureRigidbody(true, false);
} }
void TickUpdate(ScriptContext& ctx, float deltaTime) { void TickUpdate(ScriptContext& ctx, float dt)
if (!ctx.object) return; {
if (!ctx.object) return; ControllerState& s = getState(ctx.object->id); ctx.TickStandaloneMovement(s.movement, g_settings, dt, nullptr);
ControllerState& state = getState(ctx.object->id);
if (enforceCollider) ctx.EnsureCapsuleCollider(capsuleTuning.x, capsuleTuning.y);
if (enforceRigidbody) ctx.EnsureRigidbody(true, false);
const float walkSpeed = moveTuning.x;
const float runSpeed = moveTuning.y;
const float jumpStrength = moveTuning.z;
const float lookSensitivity = lookTuning.x;
const float maxMouseDelta = glm::max(5.0f, lookTuning.y);
const float height = capsuleTuning.x;
const float radius = capsuleTuning.y;
const float groundSnap = capsuleTuning.z;
const float gravity = gravityTuning.x;
const float probeExtra = gravityTuning.y;
const float maxFall = glm::max(1.0f, gravityTuning.z);
if (enableMouseLook) {
bool allowLook = !requireMouseButton || ImGui::IsMouseDown(ImGuiMouseButton_Right);
if (allowLook) {
ImGuiIO& io = ImGui::GetIO();
glm::vec2 delta(io.MouseDelta.x, io.MouseDelta.y);
float len = glm::length(delta);
if (len > maxMouseDelta) {
delta *= (maxMouseDelta / len);
}
state.yaw -= delta.x * 50.0f * lookSensitivity * deltaTime;
state.pitch -= delta.y * 50.0f * lookSensitivity * deltaTime;
state.pitch = std::clamp(state.pitch, -89.0f, 89.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;
if (ImGui::IsKeyDown(ImGuiKey_S)) move -= planarForward;
if (ImGui::IsKeyDown(ImGuiKey_D)) move += planarRight;
if (ImGui::IsKeyDown(ImGuiKey_A)) move -= planarRight;
if (glm::length(move) > 0.001f) move = glm::normalize(move);
bool sprint = ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift);
float targetSpeed = sprint ? runSpeed : walkSpeed;
glm::vec3 velocity = move * targetSpeed;
float capsuleHalf = std::max(0.1f, height * 0.5f);
glm::vec3 physVel;
bool havePhysVel = ctx.GetRigidbodyVelocity(physVel);
if (havePhysVel) state.verticalVelocity = physVel.y;
glm::vec3 hitPos(0.0f);
glm::vec3 hitNormal(0.0f, 1.0f, 0.0f);
float hitDist = 0.0f;
float probeDist = capsuleHalf + probeExtra;
glm::vec3 rayStart = ctx.object->position + glm::vec3(0.0f, 0.1f, 0.0f);
bool hitGround = ctx.RaycastClosest(rayStart, glm::vec3(0.0f, -1.0f, 0.0f), probeDist,
&hitPos, &hitNormal, &hitDist);
bool grounded = hitGround && hitNormal.y > 0.25f &&
hitDist <= capsuleHalf + groundSnap &&
state.verticalVelocity <= 0.35f;
if (!hitGround) {
grounded = ctx.object->position.y <= capsuleHalf + 0.12f && state.verticalVelocity <= 0.35f;
}
if (grounded) {
state.verticalVelocity = 0.0f;
if (!havePhysVel) {
if (hitGround) {
ctx.object->position.y = std::max(ctx.object->position.y, hitPos.y + capsuleHalf);
} else {
ctx.object->position.y = capsuleHalf;
}
}
if (ImGui::IsKeyDown(ImGuiKey_Space)) {
state.verticalVelocity = jumpStrength;
}
} else {
state.verticalVelocity += gravity * deltaTime;
}
state.verticalVelocity = std::clamp(state.verticalVelocity, -maxFall, maxFall);
velocity.y = state.verticalVelocity;
glm::vec3 rotation(state.pitch, state.yaw, 0.0f);
ctx.object->rotation = rotation;
ctx.SetRigidbodyRotation(rotation);
if (!ctx.SetRigidbodyVelocity(velocity)) {
ctx.object->position += velocity * deltaTime;
}
if (showDebug) {
state.debugVelocity = velocity;
state.debugGrounded = grounded;
}
} }
void Spec(ScriptContext& /*ctx*/, float /*deltaTime*/) {}
void TestEditor(ScriptContext& /*ctx*/, float /*deltaTime*/) {}

View File

@@ -364,6 +364,54 @@ void Engine::renderLauncher() {
renderNewProjectDialog(); renderNewProjectDialog();
if (projectManager.showOpenProjectDialog) if (projectManager.showOpenProjectDialog)
renderOpenProjectDialog(); renderOpenProjectDialog();
if (projectLoadInProgress) {
float elapsed = static_cast<float>(glfwGetTime() - projectLoadStartTime);
if (elapsed > 0.15f) {
ImGuiIO& io = ImGui::GetIO();
ImGui::SetNextWindowPos(ImVec2(0, 0));
ImGui::SetNextWindowSize(io.DisplaySize);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.05f, 0.06f, 0.08f, 0.65f));
ImGui::Begin("ProjectLoadOverlay", nullptr,
ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoDocking |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoInputs);
ImGui::End();
ImGui::PopStyleColor();
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(420, 160));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 16.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(24.0f, 20.0f));
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.12f, 0.13f, 0.18f, 0.98f));
ImGui::Begin("ProjectLoadCard", nullptr,
ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoDocking |
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoSavedSettings);
ImGui::TextColored(ImVec4(0.88f, 0.90f, 0.96f, 1.0f), "Loading project...");
ImGui::Spacing();
ImGui::TextDisabled("%s", projectLoadPath.c_str());
ImGui::Spacing();
ImGui::Spinner("##project_load_spinner", 16.0f, 4, ImGui::GetColorU32(ImGuiCol_ButtonHovered));
ImGui::SameLine();
ImGui::BufferingBar("##project_load_bar", std::fmod(elapsed * 0.25f, 1.0f),
ImVec2(ImGui::GetContentRegionAvail().x - 40.0f, 8.0f),
ImGui::GetColorU32(ImGuiCol_Button),
ImGui::GetColorU32(ImGuiCol_ButtonHovered));
ImGui::End();
ImGui::PopStyleColor();
ImGui::PopStyleVar(2);
}
}
} }
#pragma endregion #pragma endregion

View File

@@ -2624,23 +2624,41 @@ void Engine::renderDialogs() {
} }
if (showCompilePopup) { if (showCompilePopup) {
if (!compilePopupOpened) {
ImGui::OpenPopup("Script Compile");
compilePopupOpened = true;
}
ImGuiIO& io = ImGui::GetIO(); ImGuiIO& io = ImGui::GetIO();
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f); ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(520, 240), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(520, 260), ImGuiCond_FirstUseEver);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoSavedSettings; ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
if (ImGui::Begin("Script Compile", &showCompilePopup, flags)) { ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoSavedSettings;
bool allowClose = !compileInProgress;
if (ImGui::BeginPopupModal("Script Compile", allowClose ? &showCompilePopup : nullptr, flags)) {
ImGui::TextWrapped("%s", lastCompileStatus.c_str()); ImGui::TextWrapped("%s", lastCompileStatus.c_str());
float t = static_cast<float>(glfwGetTime());
float pulse = 0.5f + 0.5f * std::sin(t * 2.5f);
ImGui::ProgressBar(compileInProgress ? pulse : 1.0f, ImVec2(-1, 0),
compileInProgress ? "Working..." : "Done");
ImGui::Separator(); ImGui::Separator();
ImGui::BeginChild("CompileLog", ImVec2(0, -40), true); ImGui::BeginChild("CompileLog", ImVec2(0, -40), true);
ImGui::TextUnformatted(lastCompileLog.c_str()); if (lastCompileLog.empty() && compileInProgress) {
ImGui::TextUnformatted("Waiting for compiler output...");
} else {
ImGui::TextUnformatted(lastCompileLog.c_str());
}
ImGui::EndChild(); ImGui::EndChild();
ImGui::Spacing(); ImGui::Spacing();
if (ImGui::Button("Close", ImVec2(80, 0))) { if (allowClose && ImGui::Button("Close", ImVec2(80, 0))) {
showCompilePopup = false; showCompilePopup = false;
ImGui::CloseCurrentPopup();
compilePopupOpened = false;
} }
ImGui::EndPopup();
} }
ImGui::End(); } else {
compilePopupOpened = false;
} }
if (showSaveSceneAsDialog) { if (showSaveSceneAsDialog) {

View File

@@ -4,6 +4,7 @@
#include <array> #include <array>
#include <cstring> #include <cstring>
#include <cstdlib> #include <cstdlib>
#include <cstdio>
#include <cfloat> #include <cfloat>
#include <cmath> #include <cmath>
#include <functional> #include <functional>
@@ -261,9 +262,23 @@ void Engine::renderGameViewportWindow() {
ImGui::Begin("Game Viewport", &showGameViewport, ImGuiWindowFlags_NoScrollbar); ImGui::Begin("Game Viewport", &showGameViewport, ImGuiWindowFlags_NoScrollbar);
bool windowFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); bool windowFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows);
ImVec2 avail = ImGui::GetContentRegionAvail(); struct GameResolutionOption {
int width = std::max(160, (int)avail.x); const char* label;
int height = std::max(120, (int)avail.y); int width;
int height;
bool useWindow;
bool custom;
};
static const std::array<GameResolutionOption, 5> kGameResolutions = {{
{ "Window", 0, 0, true, false },
{ "1920x1080 (1080p)", 1920, 1080, false, false },
{ "1280x720 (720p)", 1280, 720, false, false },
{ "2560x1440 (1440p)", 2560, 1440, false, false },
{ "Custom", 0, 0, false, true }
}};
if (gameViewportResolutionIndex < 0 || gameViewportResolutionIndex >= (int)kGameResolutions.size()) {
gameViewportResolutionIndex = 0;
}
SceneObject* playerCam = nullptr; SceneObject* playerCam = nullptr;
for (auto& obj : sceneObjects) { for (auto& obj : sceneObjects) {
@@ -288,12 +303,69 @@ void Engine::renderGameViewportWindow() {
ImGui::Checkbox("Post FX", &dummyToggle); ImGui::Checkbox("Post FX", &dummyToggle);
} }
ImGui::SameLine(); ImGui::SameLine();
ImGui::Checkbox("Text", &showUITextOverlay); ImGui::Checkbox("Profiler", &showGameProfiler);
ImGui::SameLine(); ImGui::SameLine();
ImGui::Checkbox("Canvas Guides", &showCanvasOverlay); ImGui::Checkbox("Canvas Guides", &showCanvasOverlay);
ImGui::EndDisabled(); ImGui::EndDisabled();
ImGui::PopStyleColor(3); ImGui::PopStyleColor(3);
ImGui::Spacing();
const GameResolutionOption& resOption = kGameResolutions[gameViewportResolutionIndex];
ImGui::SetNextItemWidth(180.0f);
if (ImGui::BeginCombo("Resolution", resOption.label)) {
for (int i = 0; i < (int)kGameResolutions.size(); ++i) {
bool selected = (i == gameViewportResolutionIndex);
if (ImGui::Selectable(kGameResolutions[i].label, selected)) {
gameViewportResolutionIndex = i;
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (kGameResolutions[gameViewportResolutionIndex].custom) {
ImGui::SameLine();
ImGui::SetNextItemWidth(90.0f);
ImGui::DragInt("W", &gameViewportCustomWidth, 1.0f, 64, 8192);
ImGui::SameLine();
ImGui::SetNextItemWidth(90.0f);
ImGui::DragInt("H", &gameViewportCustomHeight, 1.0f, 64, 8192);
}
ImGui::SameLine();
ImGui::Checkbox("Auto Fit", &gameViewportAutoFit);
ImGui::SameLine();
ImGui::BeginDisabled(gameViewportAutoFit);
float zoomPercent = gameViewportZoom * 100.0f;
ImGui::SetNextItemWidth(140.0f);
if (ImGui::SliderFloat("Zoom", &zoomPercent, 10.0f, 200.0f, "%.0f%%")) {
gameViewportZoom = zoomPercent / 100.0f;
}
ImGui::EndDisabled();
ImVec2 avail = ImGui::GetContentRegionAvail();
int renderWidth = 0;
int renderHeight = 0;
if (kGameResolutions[gameViewportResolutionIndex].useWindow) {
renderWidth = std::max(160, (int)avail.x);
renderHeight = std::max(120, (int)avail.y);
} else if (kGameResolutions[gameViewportResolutionIndex].custom) {
renderWidth = std::clamp(gameViewportCustomWidth, 64, 8192);
renderHeight = std::clamp(gameViewportCustomHeight, 64, 8192);
} else {
renderWidth = kGameResolutions[gameViewportResolutionIndex].width;
renderHeight = kGameResolutions[gameViewportResolutionIndex].height;
}
float zoom = gameViewportZoom;
if (gameViewportAutoFit) {
if (kGameResolutions[gameViewportResolutionIndex].useWindow) {
zoom = 1.0f;
} else {
float fitX = (renderWidth > 0) ? (avail.x / (float)renderWidth) : 1.0f;
float fitY = (renderHeight > 0) ? (avail.y / (float)renderHeight) : 1.0f;
zoom = std::min(1.0f, std::min(fitX, fitY));
zoom = std::max(0.01f, zoom);
}
}
if (playerCam && postFxChanged) { if (playerCam && postFxChanged) {
projectManager.currentProject.hasUnsavedChanges = true; projectManager.currentProject.hasUnsavedChanges = true;
} }
@@ -302,38 +374,68 @@ void Engine::renderGameViewportWindow() {
gameViewCursorLocked = false; gameViewCursorLocked = false;
} }
if (playerCam && rendererInitialized) { if (playerCam && rendererInitialized) {
unsigned int tex = renderer.renderScenePreview( unsigned int tex = renderer.renderScenePreview(
makeCameraFromObject(*playerCam), makeCameraFromObject(*playerCam),
sceneObjects, sceneObjects,
width, renderWidth,
height, renderHeight,
playerCam->camera.fov, playerCam->camera.fov,
playerCam->camera.nearClip, playerCam->camera.nearClip,
playerCam->camera.farClip, playerCam->camera.farClip,
playerCam->camera.applyPostFX playerCam->camera.applyPostFX
); );
ImGui::Image((void*)(intptr_t)tex, ImVec2((float)width, (float)height), ImVec2(0, 1), ImVec2(1, 0)); ImVec2 imageSize(std::max(1.0f, renderWidth * zoom), std::max(1.0f, renderHeight * zoom));
ImVec2 cursorPos = ImGui::GetCursorPos();
float offsetX = std::max(0.0f, (avail.x - imageSize.x) * 0.5f);
float offsetY = std::max(0.0f, (avail.y - imageSize.y) * 0.5f);
ImGui::SetCursorPos(ImVec2(cursorPos.x + offsetX, cursorPos.y + offsetY));
ImGui::Image((void*)(intptr_t)tex, imageSize, ImVec2(0, 1), ImVec2(1, 0));
bool imageHovered = ImGui::IsItemHovered(); bool imageHovered = ImGui::IsItemHovered();
ImVec2 imageMin = ImGui::GetItemRectMin(); ImVec2 imageMin = ImGui::GetItemRectMin();
ImVec2 imageMax = ImGui::GetItemRectMax(); ImVec2 imageMax = ImGui::GetItemRectMax();
ImDrawList* drawList = ImGui::GetWindowDrawList(); ImDrawList* drawList = ImGui::GetWindowDrawList();
float uiScaleX = (renderWidth > 0) ? (imageSize.x / (float)renderWidth) : 1.0f;
float uiScaleY = (renderHeight > 0) ? (imageSize.y / (float)renderHeight) : 1.0f;
if (showCanvasOverlay) { if (showCanvasOverlay) {
ImVec2 pad(8.0f, 8.0f); ImVec2 pad(8.0f, 8.0f);
ImVec2 tl(imageMin.x + pad.x, imageMin.y + pad.y); ImVec2 tl(imageMin.x + pad.x, imageMin.y + pad.y);
ImVec2 br(imageMax.x - pad.x, imageMax.y - pad.y); ImVec2 br(imageMax.x - pad.x, imageMax.y - pad.y);
drawList->AddRect(tl, br, IM_COL32(110, 170, 255, 180), 8.0f, 0, 2.0f); drawList->AddRect(tl, br, IM_COL32(110, 170, 255, 180), 8.0f, 0, 2.0f);
} }
if (showUITextOverlay) { if (showGameProfiler) {
const char* textLabel = "Text Overlay"; float fps = ImGui::GetIO().Framerate;
ImVec2 textPos(imageMin.x + 16.0f, imageMin.y + 16.0f); float frameMs = (fps > 0.0f) ? (1000.0f / fps) : 0.0f;
ImVec2 size = ImGui::CalcTextSize(textLabel); int zoomPercent = (int)std::round(zoom * 100.0f);
ImVec2 bgPad(6.0f, 4.0f); const Renderer::RenderStats& stats = renderer.getLastPreviewStats();
ImVec2 bgMin(textPos.x - bgPad.x, textPos.y - bgPad.y);
ImVec2 bgMax(textPos.x + size.x + bgPad.x, textPos.y + size.y + bgPad.y); char line1[128];
drawList->AddRectFilled(bgMin, bgMax, IM_COL32(20, 20, 24, 200), 4.0f); char line2[128];
drawList->AddText(textPos, IM_COL32(235, 235, 245, 255), textLabel); char line3[128];
char line4[128];
std::snprintf(line1, sizeof(line1), "FPS: %.0f (%.1f ms)", fps, frameMs);
std::snprintf(line2, sizeof(line2), "Batches: %d", stats.drawCalls);
std::snprintf(line3, sizeof(line3), "Meshes: %d", stats.meshDraws);
std::snprintf(line4, sizeof(line4), "Render: %dx%d @ %d%%", renderWidth, renderHeight, zoomPercent);
const char* lines[] = { line1, line2, line3, line4 };
float lineHeight = ImGui::GetFontSize() + 2.0f;
float maxWidth = 0.0f;
for (const char* line : lines) {
ImVec2 size = ImGui::CalcTextSize(line);
maxWidth = std::max(maxWidth, size.x);
}
ImVec2 pad(8.0f, 6.0f);
ImVec2 panelMin(imageMin.x + 14.0f, imageMin.y + 14.0f);
ImVec2 panelMax(panelMin.x + maxWidth + pad.x * 2.0f,
panelMin.y + lineHeight * (float)(sizeof(lines) / sizeof(lines[0])) + pad.y * 2.0f);
drawList->AddRectFilled(panelMin, panelMax, IM_COL32(18, 18, 24, 210), 6.0f);
drawList->AddRect(panelMin, panelMax, IM_COL32(255, 255, 255, 40), 6.0f);
for (int i = 0; i < (int)(sizeof(lines) / sizeof(lines[0])); ++i) {
ImVec2 textPos(panelMin.x + pad.x, panelMin.y + pad.y + lineHeight * i);
drawList->AddText(textPos, IM_COL32(235, 235, 245, 255), lines[i]);
}
} }
bool uiInteracting = false; bool uiInteracting = false;
auto isUIType = [](ObjectType type) { auto isUIType = [](ObjectType type) {
@@ -401,9 +503,9 @@ void Engine::renderGameViewportWindow() {
*parentMin = regionMin; *parentMin = regionMin;
*parentMax = regionMax; *parentMax = regionMax;
} }
ImVec2 size = ImVec2(std::max(1.0f, node->ui.size.x), std::max(1.0f, node->ui.size.y)); ImVec2 size = ImVec2(std::max(1.0f, node->ui.size.x * uiScaleX), std::max(1.0f, node->ui.size.y * uiScaleY));
ImVec2 anchorPoint = anchorToPoint(node->ui.anchor, regionMin, regionMax); ImVec2 anchorPoint = anchorToPoint(node->ui.anchor, regionMin, regionMax);
ImVec2 pivot(anchorPoint.x + node->ui.position.x, anchorPoint.y + node->ui.position.y); ImVec2 pivot(anchorPoint.x + node->ui.position.x * uiScaleX, anchorPoint.y + node->ui.position.y * uiScaleY);
ImVec2 pivotOffset = anchorToPivot(node->ui.anchor, size); ImVec2 pivotOffset = anchorToPivot(node->ui.anchor, size);
regionMin = ImVec2(pivot.x - pivotOffset.x, pivot.y - pivotOffset.y); regionMin = ImVec2(pivot.x - pivotOffset.x, pivot.y - pivotOffset.y);
regionMax = ImVec2(regionMin.x + size.x, regionMin.y + size.y); regionMax = ImVec2(regionMin.x + size.x, regionMin.y + size.y);
@@ -590,7 +692,8 @@ void Engine::renderGameViewportWindow() {
ImDrawList* dl = ImGui::GetWindowDrawList(); ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec4 tint(obj.ui.color.r, obj.ui.color.g, obj.ui.color.b, obj.ui.color.a); ImVec4 tint(obj.ui.color.r, obj.ui.color.g, obj.ui.color.b, obj.ui.color.a);
float scale = std::max(0.1f, obj.ui.textScale); float scale = std::max(0.1f, obj.ui.textScale);
float fontSize = std::max(1.0f, ImGui::GetFontSize() * scale); float scaleFactor = std::min(uiScaleX, uiScaleY);
float fontSize = std::max(1.0f, ImGui::GetFontSize() * scale * scaleFactor);
ImVec2 textPos = ImVec2(clippedMin.x + 4.0f, clippedMin.y + 2.0f); ImVec2 textPos = ImVec2(clippedMin.x + 4.0f, clippedMin.y + 2.0f);
ImGui::PushClipRect(clippedMin, clippedMax, true); ImGui::PushClipRect(clippedMin, clippedMax, true);
dl->AddText(ImGui::GetFont(), fontSize, textPos, ImGui::GetColorU32(tint), obj.ui.label.c_str()); dl->AddText(ImGui::GetFont(), fontSize, textPos, ImGui::GetColorU32(tint), obj.ui.label.c_str());
@@ -630,12 +733,15 @@ void Engine::renderGameViewportWindow() {
DecomposeMatrix(model, pos, rot, scl); DecomposeMatrix(model, pos, rot, scl);
(void)rot; (void)rot;
ImVec2 newMin(imageMin.x + pos.x, imageMin.y + pos.y); ImVec2 newMin(imageMin.x + pos.x, imageMin.y + pos.y);
ImVec2 newSize(std::max(1.0f, scl.x), std::max(1.0f, scl.y)); ImVec2 newSize(std::max(1.0f, scl.x), std::max(1.0f, scl.y));
ImVec2 anchorPoint = anchorToPoint(selected->ui.anchor, parentMin, parentMax); ImVec2 anchorPoint = anchorToPoint(selected->ui.anchor, parentMin, parentMax);
ImVec2 pivotOffset = anchorToPivot(selected->ui.anchor, newSize); ImVec2 pivotOffset = anchorToPivot(selected->ui.anchor, newSize);
ImVec2 pivot(newMin.x + pivotOffset.x, newMin.y + pivotOffset.y); ImVec2 pivot(newMin.x + pivotOffset.x, newMin.y + pivotOffset.y);
selected->ui.position = glm::vec2(pivot.x - anchorPoint.x, pivot.y - anchorPoint.y); float invScaleX = (uiScaleX > 0.0f) ? 1.0f / uiScaleX : 1.0f;
selected->ui.size = glm::vec2(newSize.x, newSize.y); float invScaleY = (uiScaleY > 0.0f) ? 1.0f / uiScaleY : 1.0f;
selected->ui.position = glm::vec2((pivot.x - anchorPoint.x) * invScaleX,
(pivot.y - anchorPoint.y) * invScaleY);
selected->ui.size = glm::vec2(newSize.x * invScaleX, newSize.y * invScaleY);
projectManager.currentProject.hasUnsavedChanges = true; projectManager.currentProject.hasUnsavedChanges = true;
gizmoUsed = true; gizmoUsed = true;
} }
@@ -656,7 +762,6 @@ void Engine::renderGameViewportWindow() {
} }
gameViewportFocused = windowFocused && gameViewCursorLocked; gameViewportFocused = windowFocused && gameViewCursorLocked;
ImGui::TextDisabled(gameViewCursorLocked ? "Camera captured (ESC to release)" : "Click to capture");
} else { } else {
ImGui::TextDisabled("No player camera found (Camera Type: Player)."); ImGui::TextDisabled("No player camera found (Camera Type: Player).");
gameViewportFocused = ImGui::IsWindowFocused(); gameViewportFocused = ImGui::IsWindowFocused();

View File

@@ -3,6 +3,7 @@
#include <iostream> #include <iostream>
#include <fstream> #include <fstream>
#include <functional> #include <functional>
#include <chrono>
#include <unordered_set> #include <unordered_set>
#include <unordered_map> #include <unordered_map>
#include "ThirdParty/glm/gtc/constants.hpp" #include "ThirdParty/glm/gtc/constants.hpp"
@@ -439,6 +440,7 @@ void Engine::run() {
deltaTime = std::min(deltaTime, 1.0f / 30.0f); deltaTime = std::min(deltaTime, 1.0f / 30.0f);
glfwPollEvents(); glfwPollEvents();
pollProjectLoad();
if (!showLauncher) { if (!showLauncher) {
handleKeyboardShortcuts(); handleKeyboardShortcuts();
@@ -505,6 +507,10 @@ void Engine::run() {
} }
audio.update(sceneObjects, listenerCamera, audioShouldPlay); audio.update(sceneObjects, listenerCamera, audioShouldPlay);
updateCompileJob();
updateAutoCompileScripts();
processAutoCompileQueue();
if (!showLauncher && projectManager.currentProject.isLoaded && rendererInitialized) { if (!showLauncher && projectManager.currentProject.isLoaded && rendererInitialized) {
glm::mat4 view = camera.getViewMatrix(); glm::mat4 view = camera.getViewMatrix();
float aspect = static_cast<float>(viewportWidth) / static_cast<float>(viewportHeight); float aspect = static_cast<float>(viewportWidth) / static_cast<float>(viewportHeight);
@@ -613,6 +619,10 @@ void Engine::shutdown() {
saveCurrentScene(); saveCurrentScene();
} }
if (compileWorker.joinable()) {
compileWorker.join();
}
physics.onPlayStop(); physics.onPlayStop();
audio.onPlayStop(); audio.onPlayStop();
audio.shutdown(); audio.shutdown();
@@ -1022,6 +1032,112 @@ void Engine::updateScripts(float delta) {
} }
} }
void Engine::queueAutoCompile(const fs::path& scriptPath, const fs::file_time_type& sourceTime) {
std::error_code ec;
fs::path scriptAbs = fs::absolute(scriptPath, ec);
if (ec) scriptAbs = scriptPath;
std::string key = scriptAbs.lexically_normal().string();
if (!autoCompileQueued.insert(key).second) return;
autoCompileQueue.push_back(scriptAbs);
scriptLastAutoCompileTime[key] = sourceTime;
}
void Engine::updateAutoCompileScripts() {
if (!projectManager.currentProject.isLoaded) return;
if (showLauncher) return;
double now = glfwGetTime();
if (now - scriptAutoCompileLastCheck < scriptAutoCompileInterval) return;
scriptAutoCompileLastCheck = now;
fs::path configPath = projectManager.currentProject.scriptsConfigPath;
if (configPath.empty()) {
configPath = projectManager.currentProject.projectPath / "Scripts.modu";
}
ScriptBuildConfig config;
std::string error;
if (!scriptCompiler.loadConfig(configPath, config, error)) {
return;
}
packageManager.applyToBuildConfig(config);
std::unordered_set<std::string> sources;
auto addSource = [&](const fs::path& path) {
if (path.empty()) return;
std::error_code ec;
fs::path absPath = fs::absolute(path, ec);
if (ec) absPath = path;
sources.insert(absPath.lexically_normal().string());
};
for (const auto& obj : sceneObjects) {
for (const auto& sc : obj.scripts) {
if (sc.path.empty()) continue;
addSource(sc.path);
}
}
fs::path scriptsDir = config.scriptsDir;
if (!scriptsDir.is_absolute()) {
scriptsDir = projectManager.currentProject.projectPath / scriptsDir;
}
std::error_code dirEc;
if (fs::exists(scriptsDir, dirEc)) {
for (auto it = fs::recursive_directory_iterator(scriptsDir, dirEc);
it != fs::recursive_directory_iterator(); ++it) {
if (it->is_directory()) continue;
std::string ext = it->path().extension().string();
if (ext == ".cpp" || ext == ".cc" || ext == ".cxx" || ext == ".c") {
addSource(it->path());
}
}
}
for (const auto& sourceKey : sources) {
fs::path sourcePath = sourceKey;
std::error_code sourceEc;
if (!fs::exists(sourcePath, sourceEc)) continue;
auto sourceTime = fs::last_write_time(sourcePath, sourceEc);
if (sourceEc) continue;
ScriptBuildCommands commands;
if (!scriptCompiler.makeCommands(config, sourcePath, commands, error)) {
continue;
}
std::error_code binEc;
bool binaryExists = fs::exists(commands.binaryPath, binEc);
fs::file_time_type binaryTime{};
if (binaryExists && !binEc) {
binaryTime = fs::last_write_time(commands.binaryPath, binEc);
}
bool needsCompile = !binaryExists || (!binEc && sourceTime > binaryTime);
if (!needsCompile) continue;
auto it = scriptLastAutoCompileTime.find(sourceKey);
if (it != scriptLastAutoCompileTime.end() && sourceTime <= it->second) continue;
queueAutoCompile(sourcePath, sourceTime);
}
}
void Engine::processAutoCompileQueue() {
if (compileInProgress) return;
if (autoCompileQueue.empty()) return;
fs::path next = autoCompileQueue.front();
autoCompileQueue.pop_front();
std::error_code ec;
fs::path absPath = fs::absolute(next, ec);
if (ec) absPath = next;
autoCompileQueued.erase(absPath.lexically_normal().string());
compileScriptFile(next);
}
void Engine::updatePlayerController(float delta) { void Engine::updatePlayerController(float delta) {
if (!isPlaying) return; if (!isPlaying) return;
@@ -1327,49 +1443,98 @@ void Engine::updateHierarchyWorldTransforms() {
#pragma region Project Lifecycle #pragma region Project Lifecycle
void Engine::OpenProjectPath(const std::string& path) { void Engine::OpenProjectPath(const std::string& path) {
try { startProjectLoad(path);
if (projectManager.loadProject(path)) { }
// Make sure project folders exist even for older/minimal projects
if (!fs::exists(projectManager.currentProject.assetsPath)) {
fs::create_directories(projectManager.currentProject.assetsPath);
}
if (!fs::exists(projectManager.currentProject.scenesPath)) {
fs::create_directories(projectManager.currentProject.scenesPath);
}
packageManager.setProjectRoot(projectManager.currentProject.projectPath); void Engine::startProjectLoad(const std::string& path) {
if (projectLoadInProgress) return;
projectManager.errorMessage.clear();
projectLoadInProgress = true;
projectLoadStartTime = glfwGetTime();
projectLoadPath = path;
showLauncher = true;
if (!initRenderer()) { projectLoadFuture = std::async(std::launch::async, [path]() {
addConsoleMessage("Error: Failed to initialize renderer!", ConsoleMessageType::Error); ProjectLoadResult result;
showLauncher = true; result.path = path;
return; try {
Project project;
if (project.load(path)) {
result.success = true;
result.project = std::move(project);
} else {
result.error = "Failed to load project file";
} }
} catch (const std::exception& e) {
if (!physics.isReady() && !physics.init()) { result.error = std::string("Exception opening project: ") + e.what();
addConsoleMessage("Warning: PhysX failed to initialize; physics disabled for this session", ConsoleMessageType::Warning); } catch (...) {
} result.error = "Unknown exception opening project";
loadRecentScenes();
fs::path contentRoot = projectManager.currentProject.usesNewLayout
? projectManager.currentProject.assetsPath
: projectManager.currentProject.projectPath;
fileBrowser.setProjectRoot(contentRoot);
fileBrowser.currentPath = contentRoot;
fileBrowser.needsRefresh = true;
scriptEditorWindowsDirty = true;
scriptEditorWindows.clear();
showLauncher = false;
addConsoleMessage("Opened project: " + projectManager.currentProject.name, ConsoleMessageType::Info);
} else {
addConsoleMessage("Error opening project: " + projectManager.errorMessage, ConsoleMessageType::Error);
} }
} catch (const std::exception& e) { return result;
addConsoleMessage(std::string("Exception opening project: ") + e.what(), ConsoleMessageType::Error); });
showLauncher = true; }
} catch (...) {
addConsoleMessage("Unknown exception opening project", ConsoleMessageType::Error); void Engine::pollProjectLoad() {
showLauncher = true; if (!projectLoadInProgress) return;
if (!projectLoadFuture.valid()) {
projectLoadInProgress = false;
return;
} }
auto state = projectLoadFuture.wait_for(std::chrono::milliseconds(0));
if (state == std::future_status::ready) {
ProjectLoadResult result = projectLoadFuture.get();
projectLoadInProgress = false;
finishProjectLoad(result);
}
}
void Engine::finishProjectLoad(ProjectLoadResult& result) {
if (!result.success) {
projectManager.errorMessage = result.error.empty() ? "Failed to load project file" : result.error;
addConsoleMessage("Error opening project: " + projectManager.errorMessage, ConsoleMessageType::Error);
showLauncher = true;
return;
}
projectManager.currentProject = std::move(result.project);
projectManager.addToRecentProjects(projectManager.currentProject.name, result.path);
// Make sure project folders exist even for older/minimal projects
if (!fs::exists(projectManager.currentProject.assetsPath)) {
fs::create_directories(projectManager.currentProject.assetsPath);
}
if (!fs::exists(projectManager.currentProject.scenesPath)) {
fs::create_directories(projectManager.currentProject.scenesPath);
}
packageManager.setProjectRoot(projectManager.currentProject.projectPath);
if (!initRenderer()) {
addConsoleMessage("Error: Failed to initialize renderer!", ConsoleMessageType::Error);
showLauncher = true;
return;
}
if (!physics.isReady() && !physics.init()) {
addConsoleMessage("Warning: PhysX failed to initialize; physics disabled for this session", ConsoleMessageType::Warning);
}
loadRecentScenes();
fs::path contentRoot = projectManager.currentProject.usesNewLayout
? projectManager.currentProject.assetsPath
: projectManager.currentProject.projectPath;
fileBrowser.setProjectRoot(contentRoot);
fileBrowser.currentPath = contentRoot;
fileBrowser.needsRefresh = true;
scriptEditorWindowsDirty = true;
scriptEditorWindows.clear();
scriptLastAutoCompileTime.clear();
autoCompileQueue.clear();
autoCompileQueued.clear();
scriptAutoCompileLastCheck = 0.0;
showLauncher = false;
addConsoleMessage("Opened project: " + projectManager.currentProject.name, ConsoleMessageType::Info);
} }
void Engine::createNewProject(const char* name, const char* location) { void Engine::createNewProject(const char* name, const char* location) {
@@ -1407,6 +1572,10 @@ void Engine::createNewProject(const char* name, const char* location) {
fileBrowser.needsRefresh = true; fileBrowser.needsRefresh = true;
scriptEditorWindowsDirty = true; scriptEditorWindowsDirty = true;
scriptEditorWindows.clear(); scriptEditorWindows.clear();
scriptLastAutoCompileTime.clear();
autoCompileQueue.clear();
autoCompileQueued.clear();
scriptAutoCompileLastCheck = 0.0;
showLauncher = false; showLauncher = false;
firstFrame = true; firstFrame = true;
@@ -1882,70 +2051,120 @@ void Engine::compileScriptFile(const fs::path& scriptPath) {
return; return;
} }
if (compileInProgress) {
showCompilePopup = true;
lastCompileStatus = "Compile already in progress";
return;
}
if (compileWorker.joinable()) {
compileWorker.join();
}
showCompilePopup = true; showCompilePopup = true;
compilePopupHideTime = 0.0;
lastCompileLog.clear(); lastCompileLog.clear();
lastCompileStatus = "Compiling " + scriptPath.filename().string(); lastCompileStatus = "Compiling " + scriptPath.filename().string();
lastCompileSuccess = false;
fs::path configPath = projectManager.currentProject.scriptsConfigPath; fs::path configPath = projectManager.currentProject.scriptsConfigPath;
if (configPath.empty()) { if (configPath.empty()) {
configPath = projectManager.currentProject.projectPath / "Scripts.modu"; configPath = projectManager.currentProject.projectPath / "Scripts.modu";
} }
ScriptBuildConfig config; compileInProgress = true;
std::string error; compileResultReady = false;
if (!scriptCompiler.loadConfig(configPath, config, error)) { compileWorker = std::thread([this, scriptPath, configPath]() {
lastCompileSuccess = false; ScriptCompileJobResult result;
lastCompileLog = error; result.scriptPath = scriptPath;
addConsoleMessage("Script config error: " + error, ConsoleMessageType::Error); std::string error;
return; ScriptBuildConfig config;
} if (!scriptCompiler.loadConfig(configPath, config, error)) {
result.error = error;
packageManager.applyToBuildConfig(config); } else {
packageManager.applyToBuildConfig(config);
ScriptBuildCommands commands; ScriptBuildCommands commands;
if (!scriptCompiler.makeCommands(config, scriptPath, commands, error)) { if (!scriptCompiler.makeCommands(config, scriptPath, commands, error)) {
lastCompileSuccess = false; result.error = error;
lastCompileLog = error; } else {
addConsoleMessage("Script build error: " + error, ConsoleMessageType::Error); ScriptCompileOutput output;
return; if (!scriptCompiler.compile(commands, output, error)) {
} result.compileLog = output.compileLog;
result.linkLog = output.linkLog;
ScriptCompileOutput output; result.error = error;
if (!scriptCompiler.compile(commands, output, error)) { } else {
lastCompileSuccess = false; result.success = true;
lastCompileStatus = "Compile failed"; result.compileLog = output.compileLog;
lastCompileLog = output.compileLog + output.linkLog + error; result.linkLog = output.linkLog;
addConsoleMessage("Compile failed: " + error, ConsoleMessageType::Error); result.binaryPath = commands.binaryPath;
if (!output.compileLog.empty()) addConsoleMessage(output.compileLog, ConsoleMessageType::Info); result.compiledSource = fs::absolute(scriptPath).lexically_normal().string();
if (!output.linkLog.empty()) addConsoleMessage(output.linkLog, ConsoleMessageType::Info); }
return;
}
scriptRuntime.unloadAll();
lastCompileSuccess = true;
lastCompileStatus = "Reloading EngineRoot";
lastCompileLog = output.compileLog + output.linkLog;
addConsoleMessage("Compiled script -> " + commands.binaryPath.string(), ConsoleMessageType::Success);
if (!output.compileLog.empty()) addConsoleMessage(output.compileLog, ConsoleMessageType::Info);
if (!output.linkLog.empty()) addConsoleMessage(output.linkLog, ConsoleMessageType::Info);
// Update any ScriptComponents that point to this source with the freshly built binary path.
std::string compiledSource = fs::absolute(scriptPath).lexically_normal().string();
for (auto& obj : sceneObjects) {
for (auto& sc : obj.scripts) {
std::error_code ec;
fs::path scAbs = fs::absolute(sc.path, ec);
std::string scPathNorm = (ec ? fs::path(sc.path) : scAbs).lexically_normal().string();
if (scPathNorm == compiledSource) {
sc.lastBinaryPath = commands.binaryPath.string();
} }
} }
std::lock_guard<std::mutex> lock(compileMutex);
compileResult = std::move(result);
compileResultReady = true;
compileInProgress = false;
});
}
void Engine::updateCompileJob() {
if (compileResultReady) {
if (compileWorker.joinable()) {
compileWorker.join();
}
ScriptCompileJobResult result;
{
std::lock_guard<std::mutex> lock(compileMutex);
result = compileResult;
compileResultReady = false;
}
if (!result.success) {
lastCompileSuccess = false;
lastCompileStatus = "Compile failed";
lastCompileLog = result.compileLog + result.linkLog + result.error;
if (!result.error.empty()) {
addConsoleMessage("Compile failed: " + result.error, ConsoleMessageType::Error);
} else {
addConsoleMessage("Compile failed", ConsoleMessageType::Error);
}
if (!result.compileLog.empty()) addConsoleMessage(result.compileLog, ConsoleMessageType::Info);
if (!result.linkLog.empty()) addConsoleMessage(result.linkLog, ConsoleMessageType::Info);
} else {
scriptRuntime.unloadAll();
lastCompileSuccess = true;
lastCompileStatus = "Reloading ModuCore";
lastCompileLog = result.compileLog + result.linkLog;
addConsoleMessage("Compiled script -> " + result.binaryPath.string(), ConsoleMessageType::Success);
if (!result.compileLog.empty()) addConsoleMessage(result.compileLog, ConsoleMessageType::Info);
if (!result.linkLog.empty()) addConsoleMessage(result.linkLog, ConsoleMessageType::Info);
for (auto& obj : sceneObjects) {
for (auto& sc : obj.scripts) {
std::error_code ec;
fs::path scAbs = fs::absolute(sc.path, ec);
std::string scPathNorm = (ec ? fs::path(sc.path) : scAbs).lexically_normal().string();
if (scPathNorm == result.compiledSource) {
sc.lastBinaryPath = result.binaryPath.string();
}
}
}
scriptEditorWindowsDirty = true;
refreshScriptEditorWindows();
}
compilePopupHideTime = glfwGetTime() + 1.0;
showCompilePopup = true;
} }
// Refresh scripted editor window registry in case new tabs were added by this build. if (!compileInProgress && showCompilePopup && compilePopupHideTime > 0.0 &&
scriptEditorWindowsDirty = true; glfwGetTime() >= compilePopupHideTime) {
refreshScriptEditorWindows(); showCompilePopup = false;
compilePopupOpened = false;
compilePopupHideTime = 0.0;
}
} }
void Engine::refreshScriptEditorWindows() { void Engine::refreshScriptEditorWindows() {

View File

@@ -14,6 +14,12 @@
#include "PackageManager.h" #include "PackageManager.h"
#include "../include/Window/Window.h" #include "../include/Window/Window.h"
#include <unordered_map> #include <unordered_map>
#include <unordered_set>
#include <atomic>
#include <deque>
#include <future>
#include <mutex>
#include <thread>
void window_size_callback(GLFWwindow* window, int width, int height); void window_size_callback(GLFWwindow* window, int width, int height);
@@ -113,8 +119,13 @@ private:
int previewCameraId = -1; int previewCameraId = -1;
bool gameViewCursorLocked = false; bool gameViewCursorLocked = false;
bool gameViewportFocused = false; bool gameViewportFocused = false;
bool showUITextOverlay = false; bool showGameProfiler = true;
bool showCanvasOverlay = false; bool showCanvasOverlay = false;
int gameViewportResolutionIndex = 0;
int gameViewportCustomWidth = 1920;
int gameViewportCustomHeight = 1080;
float gameViewportZoom = 1.0f;
bool gameViewportAutoFit = true;
int activePlayerId = -1; int activePlayerId = -1;
MeshBuilder meshBuilder; MeshBuilder meshBuilder;
char meshBuilderPath[260] = ""; char meshBuilderPath[260] = "";
@@ -135,9 +146,40 @@ private:
PhysicsSystem physics; PhysicsSystem physics;
AudioSystem audio; AudioSystem audio;
bool showCompilePopup = false; bool showCompilePopup = false;
bool compilePopupOpened = false;
double compilePopupHideTime = 0.0;
bool lastCompileSuccess = false; bool lastCompileSuccess = false;
std::string lastCompileStatus; std::string lastCompileStatus;
std::string lastCompileLog; std::string lastCompileLog;
struct ScriptCompileJobResult {
bool success = false;
fs::path scriptPath;
fs::path binaryPath;
std::string compiledSource;
std::string compileLog;
std::string linkLog;
std::string error;
};
std::atomic<bool> compileInProgress = false;
std::atomic<bool> compileResultReady = false;
std::thread compileWorker;
std::mutex compileMutex;
ScriptCompileJobResult compileResult;
std::unordered_map<std::string, fs::file_time_type> scriptLastAutoCompileTime;
std::deque<fs::path> autoCompileQueue;
std::unordered_set<std::string> autoCompileQueued;
double scriptAutoCompileLastCheck = 0.0;
double scriptAutoCompileInterval = 0.5;
struct ProjectLoadResult {
bool success = false;
Project project;
std::string error;
std::string path;
};
bool projectLoadInProgress = false;
double projectLoadStartTime = 0.0;
std::string projectLoadPath;
std::future<ProjectLoadResult> projectLoadFuture;
bool specMode = false; bool specMode = false;
bool testMode = false; bool testMode = false;
bool collisionWireframe = false; bool collisionWireframe = false;
@@ -190,11 +232,18 @@ private:
void renderViewport(); void renderViewport();
void renderGameViewportWindow(); void renderGameViewportWindow();
void renderDialogs(); void renderDialogs();
void updateCompileJob();
void renderProjectBrowserPanel(); void renderProjectBrowserPanel();
void renderScriptEditorWindows(); void renderScriptEditorWindows();
void refreshScriptEditorWindows(); void refreshScriptEditorWindows();
Camera makeCameraFromObject(const SceneObject& obj) const; Camera makeCameraFromObject(const SceneObject& obj) const;
void compileScriptFile(const fs::path& scriptPath); void compileScriptFile(const fs::path& scriptPath);
void updateAutoCompileScripts();
void processAutoCompileQueue();
void queueAutoCompile(const fs::path& scriptPath, const fs::file_time_type& sourceTime);
void startProjectLoad(const std::string& path);
void pollProjectLoad();
void finishProjectLoad(ProjectLoadResult& result);
void updateScripts(float delta); void updateScripts(float delta);
void updatePlayerController(float delta); void updatePlayerController(float delta);
void updateRigidbody2D(float delta); void updateRigidbody2D(float delta);

View File

@@ -898,12 +898,36 @@ void Renderer::ensureQuad() {
} }
void Renderer::drawFullscreenQuad() { void Renderer::drawFullscreenQuad() {
recordFullscreenDraw();
if (quadVAO == 0) ensureQuad(); if (quadVAO == 0) ensureQuad();
glBindVertexArray(quadVAO); glBindVertexArray(quadVAO);
glDrawArrays(GL_TRIANGLES, 0, 6); glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0); glBindVertexArray(0);
} }
void Renderer::resetStats(RenderStats& stats) {
stats.drawCalls = 0;
stats.meshDraws = 0;
stats.fullscreenDraws = 0;
}
void Renderer::recordDrawCall() {
if (!activeStats) return;
activeStats->drawCalls += 1;
}
void Renderer::recordMeshDraw() {
if (!activeStats) return;
activeStats->drawCalls += 1;
activeStats->meshDraws += 1;
}
void Renderer::recordFullscreenDraw() {
if (!activeStats) return;
activeStats->drawCalls += 1;
activeStats->fullscreenDraws += 1;
}
void Renderer::clearHistory() { void Renderer::clearHistory() {
historyValid = false; historyValid = false;
if (historyTarget.fbo != 0 && historyTarget.width > 0 && historyTarget.height > 0) { if (historyTarget.fbo != 0 && historyTarget.width > 0 && historyTarget.height > 0) {
@@ -1303,6 +1327,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
} }
if (meshToDraw) { if (meshToDraw) {
recordMeshDraw();
meshToDraw->draw(); meshToDraw->draw();
} }
} }
@@ -1313,6 +1338,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
(float)width / height, (float)width / height,
nearPlane, farPlane); nearPlane, farPlane);
recordDrawCall();
skybox->draw(glm::value_ptr(view), glm::value_ptr(proj)); skybox->draw(glm::value_ptr(view), glm::value_ptr(proj));
} }
@@ -1474,6 +1500,8 @@ unsigned int Renderer::applyPostProcessing(const std::vector<SceneObject>& scene
} }
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) {
resetStats(viewportStats);
activeStats = &viewportStats;
updateMirrorTargets(camera, sceneObjects, currentWidth, currentHeight, fovDeg, nearPlane, farPlane); updateMirrorTargets(camera, sceneObjects, currentWidth, currentHeight, fovDeg, nearPlane, farPlane);
renderSceneInternal(camera, sceneObjects, currentWidth, currentHeight, true, fovDeg, nearPlane, farPlane, true); renderSceneInternal(camera, sceneObjects, currentWidth, currentHeight, true, fovDeg, nearPlane, farPlane, true);
if (drawColliders) { if (drawColliders) {
@@ -1485,11 +1513,17 @@ void Renderer::renderScene(const Camera& camera, const std::vector<SceneObject>&
renderSelectionOutline(camera, sceneObjects, selectedId, fovDeg, nearPlane, farPlane); renderSelectionOutline(camera, sceneObjects, selectedId, fovDeg, nearPlane, farPlane);
unsigned int result = applyPostProcessing(sceneObjects, viewportTexture, currentWidth, currentHeight, true); unsigned int result = applyPostProcessing(sceneObjects, viewportTexture, currentWidth, currentHeight, true);
displayTexture = result ? result : viewportTexture; displayTexture = result ? result : viewportTexture;
activeStats = nullptr;
} }
unsigned int Renderer::renderScenePreview(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane, bool applyPostFX) { unsigned int Renderer::renderScenePreview(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane, bool applyPostFX) {
resetStats(previewStats);
activeStats = &previewStats;
ensureRenderTarget(previewTarget, width, height); ensureRenderTarget(previewTarget, width, height);
if (previewTarget.fbo == 0) return 0; if (previewTarget.fbo == 0) {
activeStats = nullptr;
return 0;
}
glBindFramebuffer(GL_FRAMEBUFFER, previewTarget.fbo); glBindFramebuffer(GL_FRAMEBUFFER, previewTarget.fbo);
glViewport(0, 0, width, height); glViewport(0, 0, width, height);
@@ -1499,9 +1533,11 @@ unsigned int Renderer::renderScenePreview(const Camera& camera, const std::vecto
updateMirrorTargets(camera, sceneObjects, width, height, fovDeg, nearPlane, farPlane); updateMirrorTargets(camera, sceneObjects, width, height, fovDeg, nearPlane, farPlane);
renderSceneInternal(camera, sceneObjects, width, height, true, fovDeg, nearPlane, farPlane, true); renderSceneInternal(camera, sceneObjects, width, height, true, fovDeg, nearPlane, farPlane, true);
if (!applyPostFX) { if (!applyPostFX) {
activeStats = nullptr;
return previewTarget.texture; return previewTarget.texture;
} }
unsigned int processed = applyPostProcessing(sceneObjects, previewTarget.texture, width, height, false); unsigned int processed = applyPostProcessing(sceneObjects, previewTarget.texture, width, height, false);
activeStats = nullptr;
return processed ? processed : previewTarget.texture; return processed ? processed : previewTarget.texture;
} }

View File

@@ -61,6 +61,13 @@ public:
class Camera; class Camera;
class Renderer { class Renderer {
public:
struct RenderStats {
int drawCalls = 0;
int meshDraws = 0;
int fullscreenDraws = 0;
};
private: private:
unsigned int framebuffer = 0, viewportTexture = 0, rbo = 0; unsigned int framebuffer = 0, viewportTexture = 0, rbo = 0;
int currentWidth = 800, currentHeight = 600; int currentWidth = 800, currentHeight = 600;
@@ -115,6 +122,9 @@ private:
unsigned int displayTexture = 0; unsigned int displayTexture = 0;
bool historyValid = false; bool historyValid = false;
std::unordered_map<int, RenderTarget> mirrorTargets; std::unordered_map<int, RenderTarget> mirrorTargets;
RenderStats viewportStats;
RenderStats previewStats;
RenderStats* activeStats = nullptr;
void setupFBO(); void setupFBO();
void ensureRenderTarget(RenderTarget& target, int w, int h); void ensureRenderTarget(RenderTarget& target, int w, int h);
@@ -122,6 +132,10 @@ private:
void updateMirrorTargets(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane); void updateMirrorTargets(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane);
void ensureQuad(); void ensureQuad();
void drawFullscreenQuad(); void drawFullscreenQuad();
void resetStats(RenderStats& stats);
void recordDrawCall();
void recordMeshDraw();
void recordFullscreenDraw();
void clearHistory(); void clearHistory();
void clearTarget(RenderTarget& target); void clearTarget(RenderTarget& target);
void renderSceneInternal(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, bool unbindFramebuffer, float fovDeg, float nearPlane, float farPlane, bool drawMirrorObjects); void renderSceneInternal(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, bool unbindFramebuffer, float fovDeg, float nearPlane, float farPlane, bool drawMirrorObjects);
@@ -154,4 +168,6 @@ public:
Skybox* getSkybox() { return skybox; } Skybox* getSkybox() { return skybox; }
unsigned int getViewportTexture() const { return displayTexture ? displayTexture : viewportTexture; } unsigned int getViewportTexture() const { return displayTexture ? displayTexture : viewportTexture; }
const RenderStats& getLastViewportStats() const { return viewportStats; }
const RenderStats& getLastPreviewStats() const { return previewStats; }
}; };

View File

@@ -1,6 +1,7 @@
#include "ScriptRuntime.h" #include "ScriptRuntime.h"
#include "Engine.h" #include "Engine.h"
#include "SceneObject.h" #include "SceneObject.h"
#include "ThirdParty/imgui/imgui.h"
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cctype> #include <cctype>
@@ -195,6 +196,154 @@ void ScriptContext::GetPlanarYawPitchVectors(float pitchDeg, float yawDeg,
} }
} }
glm::vec3 ScriptContext::GetMoveInputWASD(float pitchDeg, float yawDeg) const {
glm::vec3 forward(0.0f);
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 (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);
float len = glm::length(delta);
if (len > maxDelta) delta *= (maxDelta / len);
yawDeg -= delta.x * 50.0f * sensitivity * deltaTime;
pitchDeg -= delta.y * 50.0f * sensitivity * deltaTime;
pitchDeg = std::clamp(pitchDeg, -89.0f, 89.0f);
return true;
}
bool ScriptContext::IsSprintDown() const {
return ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift);
}
bool ScriptContext::IsJumpDown() const {
return ImGui::IsKeyDown(ImGuiKey_Space);
}
bool ScriptContext::ResolveGround(float capsuleHalf, float probeExtra, float groundSnap, float verticalVelocity,
glm::vec3* outHitPos, bool* outHitGround) const {
if (!object) return false;
glm::vec3 hitPos(0.0f);
glm::vec3 hitNormal(0.0f, 1.0f, 0.0f);
float hitDist = 0.0f;
float probeDist = capsuleHalf + probeExtra;
glm::vec3 rayStart = object->position + glm::vec3(0.0f, 0.1f, 0.0f);
bool hitGround = RaycastClosest(rayStart, glm::vec3(0.0f, -1.0f, 0.0f), probeDist,
&hitPos, &hitNormal, &hitDist);
bool grounded = hitGround && hitNormal.y > 0.25f &&
hitDist <= capsuleHalf + groundSnap &&
verticalVelocity <= 0.35f;
if (!hitGround) {
grounded = object->position.y <= capsuleHalf + 0.12f && verticalVelocity <= 0.35f;
}
if (outHitPos) *outHitPos = hitPos;
if (outHitGround) *outHitGround = hitGround;
return grounded;
}
void ScriptContext::ApplyVelocity(const glm::vec3& velocity, float deltaTime) {
if (!object) return;
if (!SetRigidbodyVelocity(velocity)) {
object->position += velocity * deltaTime;
}
}
void ScriptContext::BindStandaloneMovementSettings(StandaloneMovementSettings& settings) {
AutoSetting("moveTuning", settings.moveTuning);
AutoSetting("lookTuning", settings.lookTuning);
AutoSetting("capsuleTuning", settings.capsuleTuning);
AutoSetting("gravityTuning", settings.gravityTuning);
AutoSetting("enableMouseLook", settings.enableMouseLook);
AutoSetting("requireMouseButton", settings.requireMouseButton);
AutoSetting("enforceCollider", settings.enforceCollider);
AutoSetting("enforceRigidbody", settings.enforceRigidbody);
}
void ScriptContext::DrawStandaloneMovementInspector(StandaloneMovementSettings& settings, bool* showDebug) {
BindStandaloneMovementSettings(settings);
ImGui::TextUnformatted("Standalone Movement Controller");
ImGui::Separator();
ImGui::DragFloat3("Walk/Run/Jump", &settings.moveTuning.x, 0.05f, 0.0f, 25.0f, "%.2f");
ImGui::DragFloat2("Look Sens/Clamp", &settings.lookTuning.x, 0.01f, 0.0f, 500.0f, "%.2f");
ImGui::DragFloat3("Height/Radius/Snap", &settings.capsuleTuning.x, 0.02f, 0.0f, 5.0f, "%.2f");
ImGui::DragFloat3("Gravity/Probe/MaxFall", &settings.gravityTuning.x, 0.05f, -50.0f, 50.0f, "%.2f");
ImGui::Checkbox("Enable Mouse Look", &settings.enableMouseLook);
ImGui::Checkbox("Hold RMB to Look", &settings.requireMouseButton);
ImGui::Checkbox("Force Collider", &settings.enforceCollider);
ImGui::Checkbox("Force Rigidbody", &settings.enforceRigidbody);
if (showDebug) {
AutoSetting("showDebug", *showDebug);
ImGui::Checkbox("Show Debug", showDebug);
}
}
void ScriptContext::TickStandaloneMovement(StandaloneMovementState& state, StandaloneMovementSettings& settings,
float deltaTime, StandaloneMovementDebug* debug) {
if (!object) return;
BindStandaloneMovementSettings(settings);
if (settings.enforceCollider) EnsureCapsuleCollider(settings.capsuleTuning.x, settings.capsuleTuning.y);
if (settings.enforceRigidbody) EnsureRigidbody(true, false);
const float walkSpeed = settings.moveTuning.x;
const float runSpeed = settings.moveTuning.y;
const float jumpStrength = settings.moveTuning.z;
const float lookSensitivity = settings.lookTuning.x;
const float maxMouseDelta = glm::max(5.0f, settings.lookTuning.y);
const float height = settings.capsuleTuning.x;
const float groundSnap = settings.capsuleTuning.z;
const float gravity = settings.gravityTuning.x;
const float probeExtra = settings.gravityTuning.y;
const float maxFall = glm::max(1.0f, settings.gravityTuning.z);
if (settings.enableMouseLook) {
ApplyMouseLook(state.pitch, state.yaw, lookSensitivity, maxMouseDelta, deltaTime, settings.requireMouseButton);
}
glm::vec3 move = GetMoveInputWASD(state.pitch, state.yaw);
float targetSpeed = IsSprintDown() ? runSpeed : walkSpeed;
glm::vec3 velocity = move * targetSpeed;
float capsuleHalf = std::max(0.1f, height * 0.5f);
glm::vec3 physVel;
bool havePhysVel = GetRigidbodyVelocity(physVel);
if (havePhysVel) state.verticalVelocity = physVel.y;
glm::vec3 hitPos(0.0f);
bool hitGround = false;
bool grounded = ResolveGround(capsuleHalf, probeExtra, groundSnap, state.verticalVelocity, &hitPos, &hitGround);
if (grounded) {
state.verticalVelocity = 0.0f;
if (!havePhysVel) {
object->position.y = hitGround ? std::max(object->position.y, hitPos.y + capsuleHalf) : capsuleHalf;
}
if (IsJumpDown()) state.verticalVelocity = jumpStrength;
} else {
state.verticalVelocity += gravity * deltaTime;
}
state.verticalVelocity = std::clamp(state.verticalVelocity, -maxFall, maxFall);
velocity.y = state.verticalVelocity;
glm::vec3 rotation(state.pitch, state.yaw, 0.0f);
SetRotation(rotation);
SetRigidbodyRotation(rotation);
ApplyVelocity(velocity, deltaTime);
if (debug) {
debug->velocity = velocity;
debug->grounded = grounded;
}
}
bool ScriptContext::IsUIButtonPressed() const { bool ScriptContext::IsUIButtonPressed() const {
return object && object->type == ObjectType::UIButton && object->ui.buttonPressed; return object && object->type == ObjectType::UIButton && object->ui.buttonPressed;
} }

View File

@@ -40,6 +40,37 @@ struct ScriptContext {
void SetRotation(const glm::vec3& rot); void SetRotation(const glm::vec3& rot);
void SetScale(const glm::vec3& scl); void SetScale(const glm::vec3& scl);
void GetPlanarYawPitchVectors(float pitchDeg, float yawDeg, glm::vec3& outForward, glm::vec3& outRight) const; void GetPlanarYawPitchVectors(float pitchDeg, float yawDeg, glm::vec3& outForward, glm::vec3& outRight) const;
glm::vec3 GetMoveInputWASD(float pitchDeg, float yawDeg) const;
bool ApplyMouseLook(float& pitchDeg, float& yawDeg, float sensitivity, float maxDelta, float deltaTime,
bool requireMouseButton) const;
bool IsSprintDown() const;
bool IsJumpDown() const;
bool ResolveGround(float capsuleHalf, float probeExtra, float groundSnap, float verticalVelocity,
glm::vec3* outHitPos = nullptr, bool* outHitGround = nullptr) const;
void ApplyVelocity(const glm::vec3& velocity, float deltaTime);
struct StandaloneMovementSettings {
glm::vec3 moveTuning = glm::vec3(4.5f, 7.5f, 6.5f);
glm::vec3 lookTuning = glm::vec3(0.12f, 200.0f, 0.0f);
glm::vec3 capsuleTuning = glm::vec3(1.8f, 0.4f, 0.2f);
glm::vec3 gravityTuning = glm::vec3(-9.81f, 0.4f, 30.0f);
bool enableMouseLook = true;
bool requireMouseButton = false;
bool enforceCollider = true;
bool enforceRigidbody = true;
};
struct StandaloneMovementState {
float pitch = 0.0f;
float yaw = 0.0f;
float verticalVelocity = 0.0f;
};
struct StandaloneMovementDebug {
glm::vec3 velocity = glm::vec3(0.0f);
bool grounded = false;
};
void BindStandaloneMovementSettings(StandaloneMovementSettings& settings);
void DrawStandaloneMovementInspector(StandaloneMovementSettings& settings, bool* showDebug = nullptr);
void TickStandaloneMovement(StandaloneMovementState& state, StandaloneMovementSettings& settings,
float deltaTime, StandaloneMovementDebug* debug = nullptr);
// UI helpers // UI helpers
bool IsUIButtonPressed() const; bool IsUIButtonPressed() const;
bool IsUIInteractable() const; bool IsUIInteractable() const;