diff --git a/Scripts/StandaloneMovementController.cpp b/Scripts/StandaloneMovementController.cpp index 0a01844..adac0c8 100644 --- a/Scripts/StandaloneMovementController.cpp +++ b/Scripts/StandaloneMovementController.cpp @@ -2,52 +2,31 @@ #include "SceneObject.h" #include "ThirdParty/imgui/imgui.h" #include - -namespace { -struct ControllerState { - float pitch = 0.0f; - float yaw = 0.0f; - float verticalVelocity = 0.0f; - glm::vec3 debugVelocity = glm::vec3(0.0f); - bool debugGrounded = false; - bool initialized = false; -}; - -std::unordered_map g_states; - -glm::vec3 moveTuning = glm::vec3(4.5f, 7.5f, 6.5f); // walk speed, run speed, jump -glm::vec3 lookTuning = glm::vec3(0.12f, 200.0f, 0.0f); // sensitivity, max delta clamp, reserved -glm::vec3 capsuleTuning = glm::vec3(1.8f, 0.4f, 0.2f); // height, radius, ground snap -glm::vec3 gravityTuning = glm::vec3(-9.81f, 0.4f, 30.0f); // gravity, probe extra, max fall speed -bool enableMouseLook = true; -bool requireMouseButton = false; -bool enforceCollider = true; -bool enforceRigidbody = true; -bool showDebug = false; - -ControllerState& getState(int id) { - return g_states[id]; +namespace +{ + struct ControllerState + { + ScriptContext::StandaloneMovementState movement; ScriptContext::StandaloneMovementDebug debug; + bool initialized = false; + }; + std::unordered_map g_states; + ScriptContext::StandaloneMovementSettings g_settings; + ControllerState& getState(int id) {return g_states[id];} + // aliases for readability + glm::vec3& moveTuning = g_settings.moveTuning; + glm::vec3& lookTuning = g_settings.lookTuning; + glm::vec3& capsuleTuning = g_settings.capsuleTuning; + glm::vec3& gravityTuning = g_settings.gravityTuning; + bool& enableMouseLook = g_settings.enableMouseLook; + bool& requireMouseButton = g_settings.requireMouseButton; + bool& enforceCollider = g_settings.enforceCollider; + bool& enforceRigidbody = g_settings.enforceRigidbody; } - -void bindSettings(ScriptContext& ctx) { - ctx.AutoSetting("moveTuning", moveTuning); - 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); - +extern "C" void Script_OnInspector(ScriptContext& ctx) +{ + ctx.BindStandaloneMovementSettings(g_settings); ImGui::TextUnformatted("Standalone Movement Controller"); ImGui::Separator(); - ImGui::DragFloat3("Walk/Run/Jump", &moveTuning.x, 0.05f, 0.0f, 25.0f, "%.2f"); ImGui::DragFloat2("Look Sens/Clamp", &lookTuning.x, 0.01f, 0.0f, 500.0f, "%.2f"); ImGui::DragFloat3("Height/Radius/Snap", &capsuleTuning.x, 0.02f, 0.0f, 5.0f, "%.2f"); @@ -56,133 +35,22 @@ extern "C" void Script_OnInspector(ScriptContext& ctx) { ImGui::Checkbox("Hold RMB to Look", &requireMouseButton); ImGui::Checkbox("Force Collider", &enforceCollider); 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*/) { - if (!ctx.object) return; - bindSettings(ctx); - ControllerState& state = getState(ctx.object->id); - if (!state.initialized) { - state.pitch = ctx.object->rotation.x; - state.yaw = ctx.object->rotation.y; - state.verticalVelocity = 0.0f; - state.initialized = true; +void Begin(ScriptContext& ctx, float) +{ + 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; } if (enforceCollider) ctx.EnsureCapsuleCollider(capsuleTuning.x, capsuleTuning.y); if (enforceRigidbody) ctx.EnsureRigidbody(true, false); } -void TickUpdate(ScriptContext& ctx, float deltaTime) { - if (!ctx.object) return; - - ControllerState& state = getState(ctx.object->id); - 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 TickUpdate(ScriptContext& ctx, float dt) +{ + if (!ctx.object) return; ControllerState& s = getState(ctx.object->id); ctx.TickStandaloneMovement(s.movement, g_settings, dt, nullptr); } - -void Spec(ScriptContext& /*ctx*/, float /*deltaTime*/) {} -void TestEditor(ScriptContext& /*ctx*/, float /*deltaTime*/) {} diff --git a/src/EditorWindows/ProjectManagerWindow.cpp b/src/EditorWindows/ProjectManagerWindow.cpp index 2e4a532..101a463 100644 --- a/src/EditorWindows/ProjectManagerWindow.cpp +++ b/src/EditorWindows/ProjectManagerWindow.cpp @@ -364,6 +364,54 @@ void Engine::renderLauncher() { renderNewProjectDialog(); if (projectManager.showOpenProjectDialog) renderOpenProjectDialog(); + + if (projectLoadInProgress) { + float elapsed = static_cast(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 diff --git a/src/EditorWindows/SceneWindows.cpp b/src/EditorWindows/SceneWindows.cpp index ad1b798..0c82c0b 100644 --- a/src/EditorWindows/SceneWindows.cpp +++ b/src/EditorWindows/SceneWindows.cpp @@ -2624,23 +2624,41 @@ void Engine::renderDialogs() { } if (showCompilePopup) { + if (!compilePopupOpened) { + ImGui::OpenPopup("Script Compile"); + compilePopupOpened = true; + } ImGuiIO& io = ImGui::GetIO(); 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(520, 240), ImGuiCond_FirstUseEver); - ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoSavedSettings; - if (ImGui::Begin("Script Compile", &showCompilePopup, flags)) { + ImGui::SetNextWindowSize(ImVec2(520, 260), ImGuiCond_FirstUseEver); + ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoSavedSettings; + bool allowClose = !compileInProgress; + if (ImGui::BeginPopupModal("Script Compile", allowClose ? &showCompilePopup : nullptr, flags)) { ImGui::TextWrapped("%s", lastCompileStatus.c_str()); + float t = static_cast(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::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::Spacing(); - if (ImGui::Button("Close", ImVec2(80, 0))) { + if (allowClose && ImGui::Button("Close", ImVec2(80, 0))) { showCompilePopup = false; + ImGui::CloseCurrentPopup(); + compilePopupOpened = false; } + ImGui::EndPopup(); } - ImGui::End(); + } else { + compilePopupOpened = false; } if (showSaveSceneAsDialog) { diff --git a/src/EditorWindows/ViewportWindows.cpp b/src/EditorWindows/ViewportWindows.cpp index 46ac38c..f35c2b7 100644 --- a/src/EditorWindows/ViewportWindows.cpp +++ b/src/EditorWindows/ViewportWindows.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -261,9 +262,23 @@ void Engine::renderGameViewportWindow() { ImGui::Begin("Game Viewport", &showGameViewport, ImGuiWindowFlags_NoScrollbar); bool windowFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); - ImVec2 avail = ImGui::GetContentRegionAvail(); - int width = std::max(160, (int)avail.x); - int height = std::max(120, (int)avail.y); + struct GameResolutionOption { + const char* label; + int width; + int height; + bool useWindow; + bool custom; + }; + static const std::array 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; for (auto& obj : sceneObjects) { @@ -288,12 +303,69 @@ void Engine::renderGameViewportWindow() { ImGui::Checkbox("Post FX", &dummyToggle); } ImGui::SameLine(); - ImGui::Checkbox("Text", &showUITextOverlay); + ImGui::Checkbox("Profiler", &showGameProfiler); ImGui::SameLine(); ImGui::Checkbox("Canvas Guides", &showCanvasOverlay); ImGui::EndDisabled(); 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) { projectManager.currentProject.hasUnsavedChanges = true; } @@ -302,38 +374,68 @@ void Engine::renderGameViewportWindow() { gameViewCursorLocked = false; } - if (playerCam && rendererInitialized) { + if (playerCam && rendererInitialized) { unsigned int tex = renderer.renderScenePreview( makeCameraFromObject(*playerCam), sceneObjects, - width, - height, + renderWidth, + renderHeight, playerCam->camera.fov, playerCam->camera.nearClip, playerCam->camera.farClip, 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(); ImVec2 imageMin = ImGui::GetItemRectMin(); ImVec2 imageMax = ImGui::GetItemRectMax(); 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) { ImVec2 pad(8.0f, 8.0f); ImVec2 tl(imageMin.x + pad.x, imageMin.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); } - if (showUITextOverlay) { - const char* textLabel = "Text Overlay"; - ImVec2 textPos(imageMin.x + 16.0f, imageMin.y + 16.0f); - ImVec2 size = ImGui::CalcTextSize(textLabel); - ImVec2 bgPad(6.0f, 4.0f); - ImVec2 bgMin(textPos.x - bgPad.x, textPos.y - bgPad.y); - ImVec2 bgMax(textPos.x + size.x + bgPad.x, textPos.y + size.y + bgPad.y); - drawList->AddRectFilled(bgMin, bgMax, IM_COL32(20, 20, 24, 200), 4.0f); - drawList->AddText(textPos, IM_COL32(235, 235, 245, 255), textLabel); + if (showGameProfiler) { + float fps = ImGui::GetIO().Framerate; + float frameMs = (fps > 0.0f) ? (1000.0f / fps) : 0.0f; + int zoomPercent = (int)std::round(zoom * 100.0f); + const Renderer::RenderStats& stats = renderer.getLastPreviewStats(); + + char line1[128]; + char line2[128]; + 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; auto isUIType = [](ObjectType type) { @@ -401,9 +503,9 @@ void Engine::renderGameViewportWindow() { *parentMin = regionMin; *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 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); regionMin = ImVec2(pivot.x - pivotOffset.x, pivot.y - pivotOffset.y); regionMax = ImVec2(regionMin.x + size.x, regionMin.y + size.y); @@ -590,7 +692,8 @@ void Engine::renderGameViewportWindow() { ImDrawList* dl = ImGui::GetWindowDrawList(); 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 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); ImGui::PushClipRect(clippedMin, clippedMax, true); 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); (void)rot; 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 anchorPoint = anchorToPoint(selected->ui.anchor, parentMin, parentMax); - ImVec2 pivotOffset = anchorToPivot(selected->ui.anchor, newSize); - ImVec2 pivot(newMin.x + pivotOffset.x, newMin.y + pivotOffset.y); - selected->ui.position = glm::vec2(pivot.x - anchorPoint.x, pivot.y - anchorPoint.y); - selected->ui.size = glm::vec2(newSize.x, newSize.y); + ImVec2 newSize(std::max(1.0f, scl.x), std::max(1.0f, scl.y)); + ImVec2 anchorPoint = anchorToPoint(selected->ui.anchor, parentMin, parentMax); + ImVec2 pivotOffset = anchorToPivot(selected->ui.anchor, newSize); + ImVec2 pivot(newMin.x + pivotOffset.x, newMin.y + pivotOffset.y); + float invScaleX = (uiScaleX > 0.0f) ? 1.0f / uiScaleX : 1.0f; + 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; gizmoUsed = true; } @@ -656,7 +762,6 @@ void Engine::renderGameViewportWindow() { } gameViewportFocused = windowFocused && gameViewCursorLocked; - ImGui::TextDisabled(gameViewCursorLocked ? "Camera captured (ESC to release)" : "Click to capture"); } else { ImGui::TextDisabled("No player camera found (Camera Type: Player)."); gameViewportFocused = ImGui::IsWindowFocused(); diff --git a/src/Engine.cpp b/src/Engine.cpp index 269667a..bc9cd2d 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include "ThirdParty/glm/gtc/constants.hpp" @@ -439,6 +440,7 @@ void Engine::run() { deltaTime = std::min(deltaTime, 1.0f / 30.0f); glfwPollEvents(); + pollProjectLoad(); if (!showLauncher) { handleKeyboardShortcuts(); @@ -505,6 +507,10 @@ void Engine::run() { } audio.update(sceneObjects, listenerCamera, audioShouldPlay); + updateCompileJob(); + updateAutoCompileScripts(); + processAutoCompileQueue(); + if (!showLauncher && projectManager.currentProject.isLoaded && rendererInitialized) { glm::mat4 view = camera.getViewMatrix(); float aspect = static_cast(viewportWidth) / static_cast(viewportHeight); @@ -613,6 +619,10 @@ void Engine::shutdown() { saveCurrentScene(); } + if (compileWorker.joinable()) { + compileWorker.join(); + } + physics.onPlayStop(); audio.onPlayStop(); 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 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) { if (!isPlaying) return; @@ -1327,49 +1443,98 @@ void Engine::updateHierarchyWorldTransforms() { #pragma region Project Lifecycle void Engine::OpenProjectPath(const std::string& path) { - try { - 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); - } + startProjectLoad(path); +} - 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()) { - addConsoleMessage("Error: Failed to initialize renderer!", ConsoleMessageType::Error); - showLauncher = true; - return; + projectLoadFuture = std::async(std::launch::async, [path]() { + ProjectLoadResult result; + result.path = path; + try { + Project project; + if (project.load(path)) { + result.success = true; + result.project = std::move(project); + } else { + result.error = "Failed to load project file"; } - - 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(); - showLauncher = false; - addConsoleMessage("Opened project: " + projectManager.currentProject.name, ConsoleMessageType::Info); - } else { - addConsoleMessage("Error opening project: " + projectManager.errorMessage, ConsoleMessageType::Error); + } catch (const std::exception& e) { + result.error = std::string("Exception opening project: ") + e.what(); + } catch (...) { + result.error = "Unknown exception opening project"; } - } catch (const std::exception& e) { - addConsoleMessage(std::string("Exception opening project: ") + e.what(), ConsoleMessageType::Error); - showLauncher = true; - } catch (...) { - addConsoleMessage("Unknown exception opening project", ConsoleMessageType::Error); - showLauncher = true; + return result; + }); +} + +void Engine::pollProjectLoad() { + 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) { @@ -1407,6 +1572,10 @@ void Engine::createNewProject(const char* name, const char* location) { fileBrowser.needsRefresh = true; scriptEditorWindowsDirty = true; scriptEditorWindows.clear(); + scriptLastAutoCompileTime.clear(); + autoCompileQueue.clear(); + autoCompileQueued.clear(); + scriptAutoCompileLastCheck = 0.0; showLauncher = false; firstFrame = true; @@ -1882,70 +2051,120 @@ void Engine::compileScriptFile(const fs::path& scriptPath) { return; } + if (compileInProgress) { + showCompilePopup = true; + lastCompileStatus = "Compile already in progress"; + return; + } + if (compileWorker.joinable()) { + compileWorker.join(); + } + showCompilePopup = true; + compilePopupHideTime = 0.0; lastCompileLog.clear(); lastCompileStatus = "Compiling " + scriptPath.filename().string(); + lastCompileSuccess = false; 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)) { - lastCompileSuccess = false; - lastCompileLog = error; - addConsoleMessage("Script config error: " + error, ConsoleMessageType::Error); - return; - } - - packageManager.applyToBuildConfig(config); - - ScriptBuildCommands commands; - if (!scriptCompiler.makeCommands(config, scriptPath, commands, error)) { - lastCompileSuccess = false; - lastCompileLog = error; - addConsoleMessage("Script build error: " + error, ConsoleMessageType::Error); - return; - } - - ScriptCompileOutput output; - if (!scriptCompiler.compile(commands, output, error)) { - lastCompileSuccess = false; - lastCompileStatus = "Compile failed"; - lastCompileLog = output.compileLog + output.linkLog + error; - addConsoleMessage("Compile failed: " + error, ConsoleMessageType::Error); - if (!output.compileLog.empty()) addConsoleMessage(output.compileLog, ConsoleMessageType::Info); - 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(); + compileInProgress = true; + compileResultReady = false; + compileWorker = std::thread([this, scriptPath, configPath]() { + ScriptCompileJobResult result; + result.scriptPath = scriptPath; + std::string error; + ScriptBuildConfig config; + if (!scriptCompiler.loadConfig(configPath, config, error)) { + result.error = error; + } else { + packageManager.applyToBuildConfig(config); + ScriptBuildCommands commands; + if (!scriptCompiler.makeCommands(config, scriptPath, commands, error)) { + result.error = error; + } else { + ScriptCompileOutput output; + if (!scriptCompiler.compile(commands, output, error)) { + result.compileLog = output.compileLog; + result.linkLog = output.linkLog; + result.error = error; + } else { + result.success = true; + result.compileLog = output.compileLog; + result.linkLog = output.linkLog; + result.binaryPath = commands.binaryPath; + result.compiledSource = fs::absolute(scriptPath).lexically_normal().string(); + } } } + std::lock_guard 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 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. - scriptEditorWindowsDirty = true; - refreshScriptEditorWindows(); + if (!compileInProgress && showCompilePopup && compilePopupHideTime > 0.0 && + glfwGetTime() >= compilePopupHideTime) { + showCompilePopup = false; + compilePopupOpened = false; + compilePopupHideTime = 0.0; + } } void Engine::refreshScriptEditorWindows() { diff --git a/src/Engine.h b/src/Engine.h index 48a4711..9f8f28e 100644 --- a/src/Engine.h +++ b/src/Engine.h @@ -14,6 +14,12 @@ #include "PackageManager.h" #include "../include/Window/Window.h" #include +#include +#include +#include +#include +#include +#include void window_size_callback(GLFWwindow* window, int width, int height); @@ -113,8 +119,13 @@ private: int previewCameraId = -1; bool gameViewCursorLocked = false; bool gameViewportFocused = false; - bool showUITextOverlay = false; + bool showGameProfiler = true; bool showCanvasOverlay = false; + int gameViewportResolutionIndex = 0; + int gameViewportCustomWidth = 1920; + int gameViewportCustomHeight = 1080; + float gameViewportZoom = 1.0f; + bool gameViewportAutoFit = true; int activePlayerId = -1; MeshBuilder meshBuilder; char meshBuilderPath[260] = ""; @@ -135,9 +146,40 @@ private: PhysicsSystem physics; AudioSystem audio; bool showCompilePopup = false; + bool compilePopupOpened = false; + double compilePopupHideTime = 0.0; bool lastCompileSuccess = false; std::string lastCompileStatus; 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 compileInProgress = false; + std::atomic compileResultReady = false; + std::thread compileWorker; + std::mutex compileMutex; + ScriptCompileJobResult compileResult; + std::unordered_map scriptLastAutoCompileTime; + std::deque autoCompileQueue; + std::unordered_set 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 projectLoadFuture; bool specMode = false; bool testMode = false; bool collisionWireframe = false; @@ -190,11 +232,18 @@ private: void renderViewport(); void renderGameViewportWindow(); void renderDialogs(); + void updateCompileJob(); void renderProjectBrowserPanel(); void renderScriptEditorWindows(); void refreshScriptEditorWindows(); Camera makeCameraFromObject(const SceneObject& obj) const; 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 updatePlayerController(float delta); void updateRigidbody2D(float delta); diff --git a/src/Rendering.cpp b/src/Rendering.cpp index 68b622c..01966fe 100644 --- a/src/Rendering.cpp +++ b/src/Rendering.cpp @@ -898,12 +898,36 @@ void Renderer::ensureQuad() { } void Renderer::drawFullscreenQuad() { + recordFullscreenDraw(); if (quadVAO == 0) ensureQuad(); glBindVertexArray(quadVAO); glDrawArrays(GL_TRIANGLES, 0, 6); 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() { historyValid = false; if (historyTarget.fbo != 0 && historyTarget.width > 0 && historyTarget.height > 0) { @@ -1303,6 +1327,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vectordraw(); } } @@ -1313,6 +1338,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vectordraw(glm::value_ptr(view), glm::value_ptr(proj)); } @@ -1474,6 +1500,8 @@ unsigned int Renderer::applyPostProcessing(const std::vector& scene } void Renderer::renderScene(const Camera& camera, const std::vector& sceneObjects, int selectedId, float fovDeg, float nearPlane, float farPlane, bool drawColliders) { + resetStats(viewportStats); + activeStats = &viewportStats; updateMirrorTargets(camera, sceneObjects, currentWidth, currentHeight, fovDeg, nearPlane, farPlane); renderSceneInternal(camera, sceneObjects, currentWidth, currentHeight, true, fovDeg, nearPlane, farPlane, true); if (drawColliders) { @@ -1485,11 +1513,17 @@ void Renderer::renderScene(const Camera& camera, const std::vector& renderSelectionOutline(camera, sceneObjects, selectedId, fovDeg, nearPlane, farPlane); unsigned int result = applyPostProcessing(sceneObjects, viewportTexture, currentWidth, currentHeight, true); displayTexture = result ? result : viewportTexture; + activeStats = nullptr; } unsigned int Renderer::renderScenePreview(const Camera& camera, const std::vector& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane, bool applyPostFX) { + resetStats(previewStats); + activeStats = &previewStats; ensureRenderTarget(previewTarget, width, height); - if (previewTarget.fbo == 0) return 0; + if (previewTarget.fbo == 0) { + activeStats = nullptr; + return 0; + } glBindFramebuffer(GL_FRAMEBUFFER, previewTarget.fbo); 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); renderSceneInternal(camera, sceneObjects, width, height, true, fovDeg, nearPlane, farPlane, true); if (!applyPostFX) { + activeStats = nullptr; return previewTarget.texture; } unsigned int processed = applyPostProcessing(sceneObjects, previewTarget.texture, width, height, false); + activeStats = nullptr; return processed ? processed : previewTarget.texture; } diff --git a/src/Rendering.h b/src/Rendering.h index eeca16d..bf298ec 100644 --- a/src/Rendering.h +++ b/src/Rendering.h @@ -61,6 +61,13 @@ public: class Camera; class Renderer { +public: + struct RenderStats { + int drawCalls = 0; + int meshDraws = 0; + int fullscreenDraws = 0; + }; + private: unsigned int framebuffer = 0, viewportTexture = 0, rbo = 0; int currentWidth = 800, currentHeight = 600; @@ -115,6 +122,9 @@ private: unsigned int displayTexture = 0; bool historyValid = false; std::unordered_map mirrorTargets; + RenderStats viewportStats; + RenderStats previewStats; + RenderStats* activeStats = nullptr; void setupFBO(); void ensureRenderTarget(RenderTarget& target, int w, int h); @@ -122,6 +132,10 @@ private: void updateMirrorTargets(const Camera& camera, const std::vector& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane); void ensureQuad(); void drawFullscreenQuad(); + void resetStats(RenderStats& stats); + void recordDrawCall(); + void recordMeshDraw(); + void recordFullscreenDraw(); void clearHistory(); void clearTarget(RenderTarget& target); void renderSceneInternal(const Camera& camera, const std::vector& sceneObjects, int width, int height, bool unbindFramebuffer, float fovDeg, float nearPlane, float farPlane, bool drawMirrorObjects); @@ -154,4 +168,6 @@ public: Skybox* getSkybox() { return skybox; } unsigned int getViewportTexture() const { return displayTexture ? displayTexture : viewportTexture; } + const RenderStats& getLastViewportStats() const { return viewportStats; } + const RenderStats& getLastPreviewStats() const { return previewStats; } }; diff --git a/src/ScriptRuntime.cpp b/src/ScriptRuntime.cpp index ad04f6b..d16f4cf 100644 --- a/src/ScriptRuntime.cpp +++ b/src/ScriptRuntime.cpp @@ -1,6 +1,7 @@ #include "ScriptRuntime.h" #include "Engine.h" #include "SceneObject.h" +#include "ThirdParty/imgui/imgui.h" #include #include #include @@ -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 { return object && object->type == ObjectType::UIButton && object->ui.buttonPressed; } diff --git a/src/ScriptRuntime.h b/src/ScriptRuntime.h index 3c96dad..22b9914 100644 --- a/src/ScriptRuntime.h +++ b/src/ScriptRuntime.h @@ -40,6 +40,37 @@ struct ScriptContext { void SetRotation(const glm::vec3& rot); void SetScale(const glm::vec3& scl); void GetPlanarYawPitchVectors(float pitchDeg, float yawDeg, glm::vec3& outForward, glm::vec3& outRight) const; + 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 bool IsUIButtonPressed() const; bool IsUIInteractable() const;