From c85c0d790dda5bb51d25f8f1c68d78b1526cd6a5 Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Mon, 22 Dec 2025 00:19:39 +0900 Subject: [PATCH] ADD: callback function --- src/CMakeLists.txt | 7 ++ src/core/engine.cpp | 58 +-------- src/core/engine_ui.cpp | 223 +++++++++++++++++++++++---------- src/main.cpp | 86 +++++++++++-- src/runtime/game_runtime.cpp | 211 +++++++++++++++++++++++++++++++ src/runtime/game_runtime.h | 145 +++++++++++++++++++++ src/runtime/i_game_callbacks.h | 35 ++++++ src/runtime/time_manager.cpp | 52 ++++++++ src/runtime/time_manager.h | 71 +++++++++++ 9 files changed, 760 insertions(+), 128 deletions(-) create mode 100644 src/runtime/game_runtime.cpp create mode 100644 src/runtime/game_runtime.h create mode 100644 src/runtime/i_game_callbacks.h create mode 100644 src/runtime/time_manager.cpp create mode 100644 src/runtime/time_manager.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b3007b6..f02916e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -114,6 +114,12 @@ add_executable (vulkan_engine # compute compute/vk_compute.h compute/vk_compute.cpp + # runtime + runtime/i_game_callbacks.h + runtime/time_manager.h + runtime/time_manager.cpp + runtime/game_runtime.h + runtime/game_runtime.cpp ) set_property(TARGET vulkan_engine PROPERTY CXX_STANDARD 20) @@ -270,6 +276,7 @@ target_include_directories(vulkan_engine PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/render/graph" "${CMAKE_CURRENT_SOURCE_DIR}/scene" "${CMAKE_CURRENT_SOURCE_DIR}/compute" + "${CMAKE_CURRENT_SOURCE_DIR}/runtime" ) option(ENABLE_MIKKTS "Use MikkTSpace for tangent generation" ON) diff --git a/src/core/engine.cpp b/src/core/engine.cpp index 3901642..d587637 100644 --- a/src/core/engine.cpp +++ b/src/core/engine.cpp @@ -594,61 +594,9 @@ void VulkanEngine::init_default_data() sphereMesh = _assetManager->createMesh(ci); } - // Register default primitives as dynamic scene instances - if (_sceneManager) - { - _sceneManager->addMeshInstance("default.cube", cubeMesh, - glm::translate(glm::mat4(1.f), glm::vec3(-2.f, 0.f, -2.f))); - _sceneManager->addMeshInstance("default.sphere", sphereMesh, - glm::translate(glm::mat4(1.f), glm::vec3(2.f, 0.f, -2.f)), - BoundsType::Sphere); - } - - // Test textured primitives - { - AssetManager::MeshMaterialDesc matDesc; - matDesc.kind = AssetManager::MeshMaterialDesc::Kind::Textured; - matDesc.options.albedoPath = "textures/grass_albedo.png"; - matDesc.options.normalPath = "textures/grass_normal.png"; - matDesc.options.metalRoughPath = "textures/grass_mro.png"; - matDesc.options.occlusionPath = "textures/grass_ao.png"; - - addPrimitiveInstance("textured.cube", - AssetManager::MeshGeometryDesc::Type::Cube, - glm::translate(glm::mat4(1.f), glm::vec3(0.f, 1.f, -4.f)), - matDesc); - - addPrimitiveInstance("textured.sphere", - AssetManager::MeshGeometryDesc::Type::Sphere, - glm::translate(glm::mat4(1.f), glm::vec3(3.f, 1.f, -4.f)), - matDesc); - - addPrimitiveInstance("textured.plane", - AssetManager::MeshGeometryDesc::Type::Plane, - glm::scale(glm::translate(glm::mat4(1.f), glm::vec3(0.f, 0.f, -6.f)), glm::vec3(4.f)), - matDesc); - } - - if (addGLTFInstance("mirage", "mirage2000/scene.gltf", glm::mat4(1.0f))) - { - preloadInstanceTextures("mirage"); - } - - // Windmill animation test - { - glm::mat4 windmillTransform = glm::translate(glm::mat4(1.0f), glm::vec3(10.0f, 0.0f, 0.0f)); - windmillTransform = glm::scale(windmillTransform, glm::vec3(0.5f)); - if (addGLTFInstance("windmill", "windmill/scene.gltf", windmillTransform)) - { - preloadInstanceTextures("windmill"); - // Enable first animation (index 0) with looping - if (_sceneManager) - { - _sceneManager->setGLTFInstanceAnimation("windmill", 0, true); - _sceneManager->setGLTFInstanceAnimationLoop("windmill", true); - } - } - } + // Note: Default primitive meshes (cubeMesh, sphereMesh) are created above + // but no longer spawned as scene instances. Use GameRuntime callbacks + // or GameAPI to add objects to the scene at runtime. _mainDeletionQueue.push_function([&]() { _resourceManager->destroy_image(_whiteImage); diff --git a/src/core/engine_ui.cpp b/src/core/engine_ui.cpp index f15dc65..100b1a9 100644 --- a/src/core/engine_ui.cpp +++ b/src/core/engine_ui.cpp @@ -1840,80 +1840,171 @@ namespace } } // namespace +// Window visibility states for menu-bar toggles +namespace +{ + struct DebugWindowStates + { + bool show_overview{false}; + bool show_window{false}; + bool show_background{false}; + bool show_particles{false}; + bool show_shadows{false}; + bool show_render_graph{false}; + bool show_pipelines{false}; + bool show_ibl{false}; + bool show_postfx{false}; + bool show_scene{false}; + bool show_async_assets{false}; + bool show_textures{false}; + }; + static DebugWindowStates g_debug_windows; +} // namespace + void vk_engine_draw_debug_ui(VulkanEngine *eng) { if (!eng) return; ImGuizmo::BeginFrame(); - // Consolidated debug window with tabs - if (ImGui::Begin("Debug")) + // Main menu bar at the top + if (ImGui::BeginMainMenuBar()) { - const ImGuiTabBarFlags tf = - ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_AutoSelectNewTabs; - if (ImGui::BeginTabBar("DebugTabs", tf)) + if (ImGui::BeginMenu("View")) { - if (ImGui::BeginTabItem("Overview")) - { - ui_overview(eng); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Window")) - { - ui_window(eng); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Background")) - { - ui_background(eng); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Particles")) - { - ui_particles(eng); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Shadows")) - { - ui_shadows(eng); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Render Graph")) - { - ui_render_graph(eng); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Pipelines")) - { - ui_pipelines(eng); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("IBL")) - { - ui_ibl(eng); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("PostFX")) - { - ui_postfx(eng); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Scene")) - { - ui_scene(eng); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Async Assets")) - { - ui_async_assets(eng); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Textures")) - { - ui_textures(eng); - ImGui::EndTabItem(); - } - ImGui::EndTabBar(); + ImGui::MenuItem("Overview", nullptr, &g_debug_windows.show_overview); + ImGui::MenuItem("Window", nullptr, &g_debug_windows.show_window); + ImGui::Separator(); + ImGui::MenuItem("Scene", nullptr, &g_debug_windows.show_scene); + ImGui::MenuItem("Render Graph", nullptr, &g_debug_windows.show_render_graph); + ImGui::MenuItem("Pipelines", nullptr, &g_debug_windows.show_pipelines); + ImGui::Separator(); + ImGui::MenuItem("Shadows", nullptr, &g_debug_windows.show_shadows); + ImGui::MenuItem("IBL", nullptr, &g_debug_windows.show_ibl); + ImGui::MenuItem("PostFX", nullptr, &g_debug_windows.show_postfx); + ImGui::MenuItem("Background", nullptr, &g_debug_windows.show_background); + ImGui::Separator(); + ImGui::MenuItem("Particles", nullptr, &g_debug_windows.show_particles); + ImGui::MenuItem("Textures", nullptr, &g_debug_windows.show_textures); + ImGui::MenuItem("Async Assets", nullptr, &g_debug_windows.show_async_assets); + ImGui::EndMenu(); + } + + // Quick stats in menu bar + ImGui::Separator(); + ImGui::Text("%.1f ms | %d tris | %d draws", + eng->stats.frametime, + eng->stats.triangle_count, + eng->stats.drawcall_count); + + ImGui::EndMainMenuBar(); + } + + // Individual debug windows (only shown when toggled) + if (g_debug_windows.show_overview) + { + if (ImGui::Begin("Overview", &g_debug_windows.show_overview)) + { + ui_overview(eng); + } + ImGui::End(); + } + + if (g_debug_windows.show_window) + { + if (ImGui::Begin("Window Settings", &g_debug_windows.show_window)) + { + ui_window(eng); + } + ImGui::End(); + } + + if (g_debug_windows.show_background) + { + if (ImGui::Begin("Background", &g_debug_windows.show_background)) + { + ui_background(eng); + } + ImGui::End(); + } + + if (g_debug_windows.show_particles) + { + if (ImGui::Begin("Particles", &g_debug_windows.show_particles)) + { + ui_particles(eng); + } + ImGui::End(); + } + + if (g_debug_windows.show_shadows) + { + if (ImGui::Begin("Shadows", &g_debug_windows.show_shadows)) + { + ui_shadows(eng); + } + ImGui::End(); + } + + if (g_debug_windows.show_render_graph) + { + if (ImGui::Begin("Render Graph", &g_debug_windows.show_render_graph)) + { + ui_render_graph(eng); + } + ImGui::End(); + } + + if (g_debug_windows.show_pipelines) + { + if (ImGui::Begin("Pipelines", &g_debug_windows.show_pipelines)) + { + ui_pipelines(eng); + } + ImGui::End(); + } + + if (g_debug_windows.show_ibl) + { + if (ImGui::Begin("IBL", &g_debug_windows.show_ibl)) + { + ui_ibl(eng); + } + ImGui::End(); + } + + if (g_debug_windows.show_postfx) + { + if (ImGui::Begin("PostFX", &g_debug_windows.show_postfx)) + { + ui_postfx(eng); + } + ImGui::End(); + } + + if (g_debug_windows.show_scene) + { + if (ImGui::Begin("Scene", &g_debug_windows.show_scene)) + { + ui_scene(eng); + } + ImGui::End(); + } + + if (g_debug_windows.show_async_assets) + { + if (ImGui::Begin("Async Assets", &g_debug_windows.show_async_assets)) + { + ui_async_assets(eng); + } + ImGui::End(); + } + + if (g_debug_windows.show_textures) + { + if (ImGui::Begin("Textures", &g_debug_windows.show_textures)) + { + ui_textures(eng); } ImGui::End(); } diff --git a/src/main.cpp b/src/main.cpp index ca04d95..9cb6405 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,14 +1,86 @@ +// Main entry point for Vulkan Engine +// +// Two modes are available: +// 1. Legacy mode: Uses VulkanEngine::run() directly (simple, no game separation) +// 2. GameRuntime mode: Uses GameRuntime for clean game/engine separation +// +// Set USE_GAME_RUNTIME to 1 to enable GameRuntime with example callbacks. + +#define USE_GAME_RUNTIME 1 + #include "core/engine.h" +#if USE_GAME_RUNTIME +#include "runtime/game_runtime.h" +#include + +// Example game implementation using IGameCallbacks +class ExampleGame : public GameRuntime::IGameCallbacks +{ +public: + void on_init(GameRuntime::Runtime& runtime) override + { + // Example: Set up initial scene + auto& api = runtime.api(); + + // Load a glTF model asynchronously + // api.load_gltf_async("example_model", "models/example.gltf", + // GameAPI::Transform{}.with_position({0, 0, 0})); + + // Spawn a primitive + // api.spawn_mesh_instance("test_cube", api.primitive_cube(), + // GameAPI::Transform{}.with_position({2, 0, 0})); + + // Set up camera + // api.set_camera_position({0, 5, -10}); + // api.set_camera_rotation({-20, 0, 0}); + } + + void on_update(float dt) override + { + // Called every frame with variable delta time + // Use for rendering-dependent logic, input handling, camera control + _elapsed += dt; + } + + void on_fixed_update(float fixed_dt) override + { + // Called at fixed intervals (default: 60Hz) + // Use for physics updates, AI tick, game state simulation + // Example: Apply physics forces, update AI state machines + } + + void on_shutdown() override + { + // Called before engine shutdown + // Use for cleanup, saving game state, etc. + } + +private: + float _elapsed{0.0f}; +}; +#endif // USE_GAME_RUNTIME + int main(int argc, char* argv[]) { - VulkanEngine engine; + (void)argc; + (void)argv; - engine.init(); - - engine.run(); + VulkanEngine engine; + engine.init(); - engine.cleanup(); +#if USE_GAME_RUNTIME + // GameRuntime mode: clean separation between engine and game logic + { + GameRuntime::Runtime runtime(&engine); + ExampleGame game; + runtime.run(&game); + } +#else + // Legacy mode: simple direct engine loop + engine.run(); +#endif - return 0; -} + engine.cleanup(); + return 0; +} \ No newline at end of file diff --git a/src/runtime/game_runtime.cpp b/src/runtime/game_runtime.cpp new file mode 100644 index 0000000..6ae4521 --- /dev/null +++ b/src/runtime/game_runtime.cpp @@ -0,0 +1,211 @@ +#include "game_runtime.h" +#include "core/engine.h" +#include "core/input/input_system.h" +#include "core/ui/imgui_system.h" +#include "scene/vk_scene.h" + +#include "SDL2/SDL.h" + +#include +#include + +namespace GameRuntime +{ + Runtime::Runtime(VulkanEngine *renderer) + : _renderer(renderer) + { + _api = std::make_unique(renderer); + } + + Runtime::~Runtime() = default; + + void Runtime::set_physics_world(IPhysicsWorld *physics) + { + _physics = physics; + } + + void Runtime::set_audio_system(IAudioSystem *audio) + { + _audio = audio; + } + + void Runtime::sync_physics_to_render() + { + // TODO: When physics integration is added, sync physics body transforms + // to their corresponding render instances here. + // For each physics body with a render instance: + // glm::mat4 transform; + // _physics->get_body_transform(bodyId, transform); + // _api->set_mesh_instance_transform(instanceName, GameAPI::Transform::from_matrix(transform)); + } + + void Runtime::update_audio_listener() + { + if (!_audio || !_renderer || !_renderer->_sceneManager) + { + return; + } + + auto &cam = _renderer->_sceneManager->getMainCamera(); + glm::vec3 pos = _renderer->_sceneManager->get_camera_local_position(); + + // Calculate forward and up from camera orientation quaternion + glm::vec3 forward = glm::normalize(cam.orientation * glm::vec3(0.0f, 0.0f, -1.0f)); + glm::vec3 up = glm::normalize(cam.orientation * glm::vec3(0.0f, 1.0f, 0.0f)); + + _audio->set_listener(pos, forward, up); + } + + void Runtime::run(IGameCallbacks *game) + { + if (!game || !_renderer) + { + return; + } + + _quit_requested = false; + + // Call game initialization + game->on_init(*this); + + // Main game loop + while (!_quit_requested) + { + // --- Begin frame: time, input --- + _time.begin_frame(); + + InputSystem *input = _renderer->input(); + if (input) + { + input->begin_frame(); + input->pump_events(); + + if (input->quit_requested()) + { + _quit_requested = true; + } + + _renderer->freeze_rendering = input->window_minimized(); + + if (input->resize_requested()) + { + _renderer->resize_requested = true; + input->clear_resize_request(); + } + } + + // --- Process UI and input capture --- + const bool ui_capture_mouse = _renderer->ui() && _renderer->ui()->want_capture_mouse(); + const bool ui_capture_keyboard = _renderer->ui() && _renderer->ui()->want_capture_keyboard(); + + // Dispatch native events to UI and picking + if (input) + { + struct DispatchCtx + { + VulkanEngine *engine; + bool ui_capture_mouse; + } ctx{_renderer, ui_capture_mouse}; + + input->for_each_native_event([](void *user, InputSystem::NativeEventView view) { + auto *c = static_cast(user); + if (!c || !c->engine || view.backend != InputSystem::NativeBackend::SDL2 || view.data == nullptr) + { + return; + } + const SDL_Event &e = *static_cast(view.data); + if (c->engine->ui()) + { + c->engine->ui()->process_event(e); + } + if (c->engine->picking()) + { + c->engine->picking()->process_event(e, c->ui_capture_mouse); + } + }, &ctx); + } + + // --- Camera input (if not captured by UI) --- + if (_renderer->_sceneManager && input) + { + _renderer->_sceneManager->getMainCamera().process_input(*input, ui_capture_keyboard, ui_capture_mouse); + } + + // --- Throttle when minimized --- + if (_renderer->freeze_rendering) + { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + continue; + } + + // --- Handle resize --- + if (_renderer->resize_requested) + { + if (_renderer->_swapchainManager) + { + _renderer->_swapchainManager->resize_swapchain(_renderer->_window); + if (_renderer->ui()) + { + _renderer->ui()->on_swapchain_recreated(); + } + _renderer->resize_requested = false; + } + } + + // --- Fixed update loop (physics) --- + while (_time.consume_fixed_step()) + { + game->on_fixed_update(_time.fixed_delta_time()); + + if (_physics) + { + _physics->step(_time.fixed_delta_time()); + } + } + + // --- Sync physics transforms to render --- + sync_physics_to_render(); + + // --- Variable update --- + game->on_update(_time.delta_time()); + + // --- Audio listener update --- + update_audio_listener(); + if (_audio) + { + _audio->update(); + } + + // --- Wait for GPU and prepare frame --- + VK_CHECK(vkWaitForFences(_renderer->_deviceManager->device(), 1, + &_renderer->get_current_frame()._renderFence, true, 1000000000)); + + if (_renderer->_rayManager) + { + _renderer->_rayManager->flushPendingDeletes(); + _renderer->_rayManager->pump_blas_builds(1); + } + + // --- Flush per-frame resources --- + _renderer->get_current_frame()._deletionQueue.flush(); + if (_renderer->_renderGraph) + { + _renderer->_renderGraph->resolve_timings(); + } + _renderer->get_current_frame()._frameDescriptors.clear_pools(_renderer->_deviceManager->device()); + + // --- ImGui --- + if (_renderer->ui()) + { + _renderer->ui()->begin_frame(); + _renderer->ui()->end_frame(); + } + + // --- Draw --- + _renderer->draw(); + } + + // Call game shutdown + game->on_shutdown(); + } +} // namespace GameRuntime diff --git a/src/runtime/game_runtime.h b/src/runtime/game_runtime.h new file mode 100644 index 0000000..e86afd5 --- /dev/null +++ b/src/runtime/game_runtime.h @@ -0,0 +1,145 @@ +#pragma once + +// GameRuntime: High-level game loop manager +// Provides a clean separation between engine and game logic with proper +// time management, fixed timestep for physics, and game callbacks. + +#include "i_game_callbacks.h" +#include "time_manager.h" +#include "core/game_api.h" + +#include + +class VulkanEngine; + +namespace GameRuntime +{ + +// Forward declarations for future integrations +class IPhysicsWorld; +class IAudioSystem; + +class Runtime +{ +public: + explicit Runtime(VulkanEngine* renderer); + ~Runtime(); + + // Non-copyable + Runtime(const Runtime&) = delete; + Runtime& operator=(const Runtime&) = delete; + + // ------------------------------------------------------------------------ + // External System Integration (optional) + // ------------------------------------------------------------------------ + + // Set physics world (e.g., Jolt, Bullet, PhysX wrapper) + void set_physics_world(IPhysicsWorld* physics); + IPhysicsWorld* physics() const { return _physics; } + + // Set audio system (e.g., FMOD, OpenAL wrapper) + void set_audio_system(IAudioSystem* audio); + IAudioSystem* audio() const { return _audio; } + + // ------------------------------------------------------------------------ + // Time Management + // ------------------------------------------------------------------------ + + // Get time manager for direct access + TimeManager& time() { return _time; } + const TimeManager& time() const { return _time; } + + // Convenience accessors + float delta_time() const { return _time.delta_time(); } + float fixed_delta_time() const { return _time.fixed_delta_time(); } + float time_scale() const { return _time.time_scale(); } + void set_time_scale(float scale) { _time.set_time_scale(scale); } + void set_fixed_delta_time(float dt) { _time.set_fixed_delta_time(dt); } + + // ------------------------------------------------------------------------ + // Game API Access + // ------------------------------------------------------------------------ + + // Get the high-level game API for engine interaction + GameAPI::Engine& api() { return *_api; } + const GameAPI::Engine& api() const { return *_api; } + + // Get the underlying Vulkan engine (for advanced use) + VulkanEngine* renderer() const { return _renderer; } + + // ------------------------------------------------------------------------ + // Main Loop + // ------------------------------------------------------------------------ + + // Run the game loop with the given callback handler. + // Blocks until the game exits. + void run(IGameCallbacks* game); + + // Request quit (sets quit flag, loop will exit next frame) + void request_quit() { _quit_requested = true; } + + // Check if quit was requested + bool quit_requested() const { return _quit_requested; } + +private: + VulkanEngine* _renderer{nullptr}; + std::unique_ptr _api; + TimeManager _time; + + IPhysicsWorld* _physics{nullptr}; + IAudioSystem* _audio{nullptr}; + + bool _quit_requested{false}; + + // Internal helpers + void sync_physics_to_render(); + void update_audio_listener(); +}; + +// ============================================================================ +// Physics Interface (for future integration) +// ============================================================================ + +class IPhysicsWorld +{ +public: + virtual ~IPhysicsWorld() = default; + + // Step the physics simulation by dt seconds + virtual void step(float dt) = 0; + + // Get transform of a physics body by ID + virtual void get_body_transform(uint32_t id, glm::mat4& out) = 0; + + // Raycast into the physics world + struct RayHit + { + bool hit{false}; + glm::vec3 position{0.0f}; + glm::vec3 normal{0.0f}; + float distance{0.0f}; + uint32_t bodyId{0}; + }; + virtual RayHit raycast(const glm::vec3& origin, const glm::vec3& direction, float maxDistance) = 0; +}; + +// ============================================================================ +// Audio Interface (for future integration) +// ============================================================================ + +class IAudioSystem +{ +public: + virtual ~IAudioSystem() = default; + + // Set listener position and orientation + virtual void set_listener(const glm::vec3& position, const glm::vec3& forward, const glm::vec3& up) = 0; + + // Play a 3D sound at a position + virtual void play_3d(const std::string& event, const glm::vec3& position) = 0; + + // Update audio system (call once per frame) + virtual void update() = 0; +}; + +} // namespace GameRuntime \ No newline at end of file diff --git a/src/runtime/i_game_callbacks.h b/src/runtime/i_game_callbacks.h new file mode 100644 index 0000000..a7d9aef --- /dev/null +++ b/src/runtime/i_game_callbacks.h @@ -0,0 +1,35 @@ +#pragma once + +// IGameCallbacks: Interface for game logic callbacks +// Implement this interface and pass to GameRuntime::run() to receive game loop events. + +namespace GameRuntime +{ + +class Runtime; + +class IGameCallbacks +{ +public: + virtual ~IGameCallbacks() = default; + + // Called once after runtime initialization, before the first update. + // Use this to load initial assets, spawn entities, set up the camera, etc. + virtual void on_init(Runtime& runtime) = 0; + + // Called every frame with variable delta time. + // Use for rendering-dependent logic, input handling, camera control, etc. + // @param dt: Frame delta time in seconds (clamped to 0.0-0.1) + virtual void on_update(float dt) = 0; + + // Called at fixed intervals for physics/simulation. + // Use for physics updates, AI tick, game state simulation, etc. + // @param fixed_dt: Fixed delta time in seconds (typically 1/60) + virtual void on_fixed_update(float fixed_dt) = 0; + + // Called once before shutdown. + // Use for cleanup, saving state, etc. + virtual void on_shutdown() = 0; +}; + +} // namespace GameRuntime \ No newline at end of file diff --git a/src/runtime/time_manager.cpp b/src/runtime/time_manager.cpp new file mode 100644 index 0000000..4ddbf1f --- /dev/null +++ b/src/runtime/time_manager.cpp @@ -0,0 +1,52 @@ +#include "time_manager.h" + +#include + +namespace GameRuntime +{ + TimeManager::TimeManager() + { + _start_time = Clock::now(); + _last_time = _start_time; + } + + void TimeManager::begin_frame() + { + TimePoint now = Clock::now(); + auto elapsed = std::chrono::duration(now - _last_time); + _last_time = now; + + // Clamp delta time to avoid spiral of death + _unscaled_delta_time = std::min(elapsed.count(), k_max_delta_time); + _delta_time = _unscaled_delta_time * _time_scale; + + // Update total times + _total_time += _delta_time; + _unscaled_total_time += _unscaled_delta_time; + + // Accumulate for fixed timestep + _fixed_accumulator += _delta_time; + + ++_frame_count; + } + + void TimeManager::set_fixed_delta_time(float dt) + { + _fixed_delta_time = std::clamp(dt, 1.0f / 240.0f, 1.0f / 10.0f); + } + + void TimeManager::set_time_scale(float scale) + { + _time_scale = std::max(0.0f, scale); + } + + bool TimeManager::consume_fixed_step() + { + if (_fixed_accumulator >= _fixed_delta_time) + { + _fixed_accumulator -= _fixed_delta_time; + return true; + } + return false; + } +} // namespace GameRuntime diff --git a/src/runtime/time_manager.h b/src/runtime/time_manager.h new file mode 100644 index 0000000..0dab25d --- /dev/null +++ b/src/runtime/time_manager.h @@ -0,0 +1,71 @@ +#pragma once + +// TimeManager: Manages game time, time scale, and fixed timestep accumulation. + +#include + +namespace GameRuntime +{ + +class TimeManager +{ +public: + TimeManager(); + + // Begin a new frame. Call at the start of each frame. + void begin_frame(); + + // Get delta time in seconds since last frame (scaled by time_scale, clamped). + float delta_time() const { return _delta_time; } + + // Get unscaled delta time in seconds since last frame (clamped). + float unscaled_delta_time() const { return _unscaled_delta_time; } + + // Get fixed delta time for physics updates. + float fixed_delta_time() const { return _fixed_delta_time; } + + // Set fixed delta time for physics updates (default: 1/60). + void set_fixed_delta_time(float dt); + + // Get time scale multiplier (default: 1.0). + float time_scale() const { return _time_scale; } + + // Set time scale multiplier (0 = paused, 0.5 = half speed, 2 = double speed). + void set_time_scale(float scale); + + // Get total elapsed time in seconds since start. + float total_time() const { return _total_time; } + + // Get total unscaled elapsed time in seconds since start. + float unscaled_total_time() const { return _unscaled_total_time; } + + // Get accumulated fixed time (for physics step loop). + float fixed_accumulator() const { return _fixed_accumulator; } + + // Consume fixed timestep from accumulator. Returns true if step should run. + bool consume_fixed_step(); + + // Get frame count since start. + uint64_t frame_count() const { return _frame_count; } + +private: + using Clock = std::chrono::high_resolution_clock; + using TimePoint = std::chrono::time_point; + + TimePoint _last_time; + TimePoint _start_time; + + float _delta_time{0.0f}; + float _unscaled_delta_time{0.0f}; + float _fixed_delta_time{1.0f / 60.0f}; + float _time_scale{1.0f}; + float _total_time{0.0f}; + float _unscaled_total_time{0.0f}; + float _fixed_accumulator{0.0f}; + + uint64_t _frame_count{0}; + + static constexpr float k_max_delta_time = 0.1f; // 100ms cap +}; + +} // namespace GameRuntime \ No newline at end of file