diff --git a/include/public/Core/Scene.hpp b/include/public/Core/Scene.hpp index f355a5a0..4ed4df5f 100644 --- a/include/public/Core/Scene.hpp +++ b/include/public/Core/Scene.hpp @@ -4,84 +4,85 @@ #include "SceneBase.hpp" namespace Sleak { - /// Concrete scene type games subclass. Adds an opt-out switch for the - /// fixed-timestep update on top of SceneBase's lifecycle. - /// - /// This is the class your levels, menus, and screens derive from. - /// Override the hooks you need and call the base implementation from - /// each one: OnLoad() and OnUnload() for assets, OnActivate() and - /// OnDeactivate() for entering and leaving, Begin() to build the object - /// graph, and Update() / FixedUpdate() / LateUpdate() for per-frame - /// work. - /// - /// The scene owns every object handed to AddObject(), including lights - /// and cameras, and destroys all of them on unload. It also owns a - /// LightManager and a Physics::PhysicsWorld, reachable through - /// GetLightManager() and GetPhysicsWorld(), and registers added objects - /// with both automatically. - /// - /// Call SetFixedUpdateEnabled(false) on scenes that do not simulate - /// anything, such as menus, to skip the fixed-timestep pass entirely. - /// - /// @code{.cpp} - /// class WorldScene : public Sleak::Scene { - /// public: - /// WorldScene() : Sleak::Scene("WorldScene") {} - /// - /// void Begin() override { - /// auto* cube = Sleak::GameObject::CreateCube( - /// Sleak::Math::Vector3D(0.0f, 0.0f, 0.0f)); - /// AddObject(cube); - /// - /// auto* sun = new Sleak::DirectionalLight("Sun"); - /// sun->SetDirection(Sleak::Math::Vector3D(-0.5f, -0.8f, -0.3f)); - /// sun->SetIntensity(4.0f); - /// AddObject(sun); - /// - /// auto* cam = new Sleak::Camera("MainCamera"); - /// AddObject(cam); - /// SetActiveCamera(cam); - /// - /// Sleak::Scene::Begin(); // activates everything added above - /// } - /// - /// void Update(float deltaTime) override { - /// Sleak::Scene::Update(deltaTime); - /// } - /// }; - /// @endcode - /// - /// @see SceneBase, GameBase, GameObject, LightManager - /// @ingroup core - class ENGINE_API Scene : public SceneBase { - public: - explicit Scene(const std::string& name) - : SceneBase(name), bEnableFixedUpdate(true) {} - ~Scene() override = default; +/// Concrete scene type games subclass. Adds an opt-out switch for the +/// fixed-timestep update on top of SceneBase's lifecycle. +/// +/// This is the class your levels, menus, and screens derive from. +/// Override the hooks you need and call the base implementation from +/// each one: OnLoad() and OnUnload() for assets, OnActivate() and +/// OnDeactivate() for entering and leaving, Begin() to build the object +/// graph, and Update() / FixedUpdate() / LateUpdate() for per-frame +/// work. +/// +/// The scene owns every object handed to AddObject(), including lights +/// and cameras, and destroys all of them on unload. It also owns a +/// LightManager and a Physics::PhysicsWorld, reachable through +/// GetLightManager() and GetPhysicsWorld(), and registers added objects +/// with both automatically. +/// +/// Call SetFixedUpdateEnabled(false) on scenes that do not simulate +/// anything, such as menus, to skip the fixed-timestep pass entirely. +/// That pass is also where physics steps, so it stops physics too. +/// +/// @code{.cpp} +/// class WorldScene : public Sleak::Scene { +/// public: +/// WorldScene() : Sleak::Scene("WorldScene") {} +/// +/// void Begin() override { +/// auto* cube = Sleak::GameObject::CreateCube( +/// Sleak::Math::Vector3D(0.0f, 0.0f, 0.0f)); +/// AddObject(cube); +/// +/// auto* sun = new Sleak::DirectionalLight("Sun"); +/// sun->SetDirection(Sleak::Math::Vector3D(-0.5f, -0.8f, -0.3f)); +/// sun->SetIntensity(4.0f); +/// AddObject(sun); +/// +/// auto* cam = new Sleak::Camera("MainCamera"); +/// AddObject(cam); +/// SetActiveCamera(cam); +/// +/// Sleak::Scene::Begin(); // activates everything added above +/// } +/// +/// void Update(float deltaTime) override { +/// Sleak::Scene::Update(deltaTime); +/// } +/// }; +/// @endcode +/// +/// @see SceneBase, GameBase, GameObject, LightManager +/// @ingroup core +class ENGINE_API Scene : public SceneBase { + public: + explicit Scene(const std::string& name) + : SceneBase(name), bEnableFixedUpdate(true) {} + ~Scene() override = default; - // Resource lifecycle - void OnLoad() override; - void OnUnload() override; + // Resource lifecycle + void OnLoad() override; + void OnUnload() override; - // Activation lifecycle - void OnActivate() override; - void OnDeactivate() override; + // Activation lifecycle + void OnActivate() override; + void OnDeactivate() override; - // Initialization - bool Initialize() override; - void Begin() override; + // Initialization + bool Initialize() override; + void Begin() override; - // Update loops - void Update(float deltaTime) override; - void FixedUpdate(float fixedDeltaTime) override; - void LateUpdate(float deltaTime) override; + // Update loops + void Update(float deltaTime) override; + void FixedUpdate(float fixedDeltaTime) override; + void LateUpdate(float deltaTime) override; - void SetFixedUpdateEnabled(bool enabled) { bEnableFixedUpdate = enabled; } - bool IsFixedUpdateEnabled() const { return bEnableFixedUpdate; } + void SetFixedUpdateEnabled(bool enabled) { bEnableFixedUpdate = enabled; } + bool IsFixedUpdateEnabled() const { return bEnableFixedUpdate; } - private: - bool bEnableFixedUpdate; - }; + private: + bool bEnableFixedUpdate; +}; } #endif // _SCENE_HPP_ diff --git a/include/public/Core/SceneBase.hpp b/include/public/Core/SceneBase.hpp index f8cc161f..0842f366 100644 --- a/include/public/Core/SceneBase.hpp +++ b/include/public/Core/SceneBase.hpp @@ -94,9 +94,11 @@ namespace Sleak { virtual void Begin() = 0; // Update loops - /// Advances all active, root-level objects by deltaTime, then steps lighting and physics. + /// Advances all active, root-level objects by deltaTime, then runs + /// as many FixedUpdate() steps as the accumulated time allows. virtual void Update(float deltaTime) = 0; - /// Advances all active, root-level objects on the fixed timestep. + /// Advances all active, root-level objects on the fixed timestep, then + /// steps physics. virtual void FixedUpdate(float fixedDeltaTime); /// Advances all active, root-level objects after the main Update pass. virtual void LateUpdate(float deltaTime); @@ -140,6 +142,21 @@ namespace Sleak { List FindObjectsByTag(const std::string& tag); size_t GetObjectCount() const { return Objects.GetSize(); } + /// Length of one FixedUpdate() step in seconds; defaults to 1/60. + float GetFixedTimestep() const { return m_fixedTimestep; } + void SetFixedTimestep(float seconds); + /// Most FixedUpdate() steps one frame may run; leftover time is + /// dropped. + int GetMaxFixedSteps() const { return m_maxFixedSteps; } + void SetMaxFixedSteps(int steps) { + m_maxFixedSteps = steps > 0 ? steps : 1; + } + /// How far the current frame sits between the last fixed step and + /// the next one, in [0, 1). Use it to interpolate rendered poses. + float GetFixedStepAlpha() const { + return static_cast(m_fixedAccumulator / m_fixedTimestep); + } + Camera* GetActiveCamera() const { return m_activeCamera; } void SetActiveCamera(Camera* cam) { m_activeCamera = cam; } @@ -170,6 +187,14 @@ namespace Sleak { Physics::PhysicsWorld* m_physicsWorld = nullptr; Skybox* m_skybox = nullptr; + double m_fixedAccumulator = 0.0; + float m_fixedTimestep = 1.0f / 60.0f; + int m_maxFixedSteps = 5; + + /// Adds deltaTime to the accumulator and runs the FixedUpdate() steps + /// it covers. + void RunFixedSteps(float deltaTime); + /// Actually deletes objects queued by DestroyObject(). void ProcessPendingDestroy(); /// Destroys every object still owned by the scene, e.g. during Unload(). diff --git a/include/public/Core/Timer.hpp b/include/public/Core/Timer.hpp index 83a3ecdb..3ea571c6 100644 --- a/include/public/Core/Timer.hpp +++ b/include/public/Core/Timer.hpp @@ -17,9 +17,13 @@ namespace Sleak { /// Seconds since construction or the last Reset(). float Elapsed() const; - private: - std::chrono::time_point startPoint; + /// Same as Elapsed() in double precision, for subtracting two + /// readings that are hours into a session. + double ElapsedSeconds() const; + + private: + std::chrono::time_point startPoint; }; }; -#endif \ No newline at end of file +#endif diff --git a/include/public/Physics/PhysicsWorld.hpp b/include/public/Physics/PhysicsWorld.hpp index 4a1e3d3d..fe4a15c8 100644 --- a/include/public/Physics/PhysicsWorld.hpp +++ b/include/public/Physics/PhysicsWorld.hpp @@ -41,14 +41,15 @@ namespace Sleak { float distance = 0.0f; }; - /// Owns the broadphase tree and every registered collider; drives collision detection and resolution each step. + /// Owns the broadphase tree and every registered collider; drives + /// collision detection and resolution each step. /// /// Each Scene creates one PhysicsWorld and steps it from its - /// update, so you normally reach it with - /// SceneBase::GetPhysicsWorld() rather than constructing one. You - /// also rarely call RegisterCollider() by hand: adding a - /// GameObject that carries a ColliderComponent registers it (and - /// its children) automatically. + /// FixedUpdate() on the scene's fixed timestep, so you normally reach + /// it with SceneBase::GetPhysicsWorld() rather than constructing one. + /// You also rarely call RegisterCollider() by hand: adding a GameObject + /// that carries a ColliderComponent registers it (and its children) + /// automatically. /// /// Step() integrates every dynamic RigidbodyComponent using that /// body's own gravity setting, refreshes the DynamicAABBTree @@ -88,9 +89,10 @@ namespace Sleak { PhysicsWorld() = default; ~PhysicsWorld() = default; - /// Advances the simulation by dt: integrates each dynamic rigidbody's - /// velocity, including its own gravity setting, into position, then - /// updates the broadphase and resolves collisions. + /// Advances the simulation by dt: integrates each dynamic + /// rigidbody's velocity, including its own gravity setting, into + /// position, then updates the broadphase and resolves collisions. + /// Ignores dt <= 0. void Step(float dt); void RegisterCollider(ColliderComponent* collider); diff --git a/src/Core/Application.cpp b/src/Core/Application.cpp index 83c0eae5..3927697d 100644 --- a/src/Core/Application.cpp +++ b/src/Core/Application.cpp @@ -126,8 +126,6 @@ namespace Sleak { int Application::Run(GameBase* game) { Game = game; - float lastTime = FrameTimer.Elapsed(); - if(CoreWindow && CoreWindow->InitializeWindow()) { if(renderer && renderer->Initialize()) { @@ -150,9 +148,7 @@ namespace Sleak { auto context = renderer->GetContext(); auto queue = RenderEngine::RenderCommandQueue::GetInstance(); - float lastTime = FrameTimer.Elapsed(); - float accumulator = 0.0f; - const float fixedTimestep = 1.0f / 60.0f; + constexpr double kMaxDeltaTime = 0.25; // Initialize and begin the game (and scene) if (Game) { @@ -184,10 +180,13 @@ namespace Sleak { m_benchmark->ToggleRecording(); } + // Start after init so load time is not the first frame delta + double lastTime = FrameTimer.ElapsedSeconds(); + while(!CoreWindow->ShouldClose()) { - - float currentTime = FrameTimer.Elapsed(); - DeltaTime = currentTime - lastTime; + double currentTime = FrameTimer.ElapsedSeconds(); + DeltaTime = static_cast( + std::clamp(currentTime - lastTime, 0.0, kMaxDeltaTime)); lastTime = currentTime; #if defined(_DEBUG) && defined(COUNT_FRAME) @@ -218,14 +217,7 @@ namespace Sleak { if (Game && Game->GetActiveScene()) { auto* activeScene = Game->GetActiveScene(); - // Fixed timestep updates (physics, etc.) - accumulator += DeltaTime; - while (accumulator >= fixedTimestep) { - activeScene->FixedUpdate(fixedTimestep); - accumulator -= fixedTimestep; - } - - // Per-frame update + // Per-frame update; the scene runs its fixed steps inside activeScene->Update(DeltaTime); // Late update (after all updates, e.g. camera follow) diff --git a/src/Core/Timer.cpp b/src/Core/Timer.cpp index f20aa707..e3fef64c 100644 --- a/src/Core/Timer.cpp +++ b/src/Core/Timer.cpp @@ -5,14 +5,15 @@ namespace Sleak { Reset(); } - void Timer::Reset() { - startPoint = std::chrono::high_resolution_clock::now(); - } + void Timer::Reset() { startPoint = std::chrono::steady_clock::now(); } - // Outputs elaosed time as seconds float Timer::Elapsed() const { - auto now = std::chrono::high_resolution_clock::now(); - std::chrono::duration elapsed = now - startPoint; + return static_cast(ElapsedSeconds()); + } + + double Timer::ElapsedSeconds() const { + std::chrono::duration elapsed = + std::chrono::steady_clock::now() - startPoint; return elapsed.count(); } -}; \ No newline at end of file + }; // namespace Sleak diff --git a/src/Physics/PhysicsWorld.cpp b/src/Physics/PhysicsWorld.cpp index d4d6783e..f8ea165f 100644 --- a/src/Physics/PhysicsWorld.cpp +++ b/src/Physics/PhysicsWorld.cpp @@ -1,11 +1,12 @@ -#include -#include -#include -#include #include -#include +#include #include +#include +#include +#include +#include #include +#include namespace Sleak { namespace Physics { @@ -39,6 +40,8 @@ void PhysicsWorld::UnregisterCollider(ColliderComponent* collider) { } void PhysicsWorld::Step(float dt) { + if (!(dt > 0.0f) || !std::isfinite(dt)) return; + // We need wasGrounded BEFORE clearing, so gravity doesn't apply while standing for (auto* collider : m_colliders) { if (auto* owner = collider->GetOwner()) { diff --git a/src/Scene/SceneBase.cpp b/src/Scene/SceneBase.cpp index 8ab146a4..80b9a279 100644 --- a/src/Scene/SceneBase.cpp +++ b/src/Scene/SceneBase.cpp @@ -1,14 +1,17 @@ #include "../../include/public/Core/SceneBase.hpp" + +#include #include #include -#include +#include #include #include #include -#include -#include #include -#include +#include +#include +#include + #include "../../include/private/Graphics/Common/RenderCommandQueue.hpp" namespace Sleak { @@ -170,8 +173,7 @@ void SceneBase::Update(float deltaTime) { if (m_skybox) m_skybox->Render(); - if (m_physicsWorld) - m_physicsWorld->Step(deltaTime); + RunFixedSteps(deltaTime); if (DebugLineRenderer::IsEnabled()) { auto drawColliderShape = [](ColliderComponent* collider, const Math::Vector3D& worldPos, const Math::Vector3D& worldScale) { @@ -227,6 +229,31 @@ void SceneBase::FixedUpdate(float fixedDeltaTime) { Objects[i]->FixedUpdate(fixedDeltaTime); } } + + if (m_physicsWorld) m_physicsWorld->Step(fixedDeltaTime); +} + +void SceneBase::SetFixedTimestep(float seconds) { + if (!(seconds > 0.0f) || !std::isfinite(seconds)) return; + m_fixedTimestep = seconds; + m_fixedAccumulator = std::fmod(m_fixedAccumulator, m_fixedTimestep); +} + +void SceneBase::RunFixedSteps(float deltaTime) { + if (!(deltaTime > 0.0f) || !std::isfinite(deltaTime)) return; + + m_fixedAccumulator += deltaTime; + + int steps = 0; + while (m_fixedAccumulator >= m_fixedTimestep && steps < m_maxFixedSteps) { + FixedUpdate(m_fixedTimestep); + m_fixedAccumulator -= m_fixedTimestep; + ++steps; + } + + // Behind by more than the cap: drop the backlog instead of spiraling + if (m_fixedAccumulator >= m_fixedTimestep) + m_fixedAccumulator = std::fmod(m_fixedAccumulator, m_fixedTimestep); } void SceneBase::LateUpdate(float deltaTime) {