diff --git a/ChangeLog.md b/ChangeLog.md index da2a0c98e..c87390157 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -12,6 +12,9 @@ Miscellaneous: - FirstScene now uses the EventHandler (#151) - ⚠️ Rename Prerequesites.hpp to Prerequisites.hpp (#153) - Updated premake5-linux64 with a nightly to fix a build error when a previous version of Nazara was installed on the system. +- Fix compilation with some MinGW distributions +- Add Lua unit tests +- NDEBUG is now defined in Release Nazara Engine: - VertexMapper:GetComponentPtr no longer throw an error if component is disabled or incompatible with template type, instead a null pointer is returned. @@ -59,6 +62,10 @@ Nazara Engine: - Fix LuaClass not working correctly when Lua stack wasn't empty - Add RigidBody2D simulation control (via EnableSimulation and IsSimulationEnabled), which allows to disable physics and collisions at will. - ⚠️ LuaInstance no longer load all lua libraries on construction, this is done in the new LoadLibraries method which allows you to excludes some libraries +- Clock::Restart now returns the elapsed microseconds since construction or last Restart call +- Add PhysWorld2D::[Get|Set]IterationCount to control how many iterations chipmunk will perform per step. +- Add PhysWorld2D::UseSpatialHash to use spatial hashing instead of bounding box trees, which may speedup simulation in some cases. +- Add PhysWorld[2D|3D] max step count per Step call (default: 50), to avoid spirals of death when the physics engine simulation time is over step size. Nazara Development Kit: - Added ImageWidget (#139) @@ -91,6 +98,9 @@ Nazara Development Kit: - Fix TextAreaWidget::Clear crash - Add ConstraintComponent2D class - Fix CollisionComponent3D initialization (teleportation to their real coordinates) which could sometimes mess up the physics scene. +- ⚠️ Renamed World::Update() to World::Refresh() for more clarity and to differentiate it from World::Update(elapsedTime) +- World entity ids are now reused from lowest to highest (they were previously reused in reverse order of death) +- World now has an internal profiler, allowing to measure the refresh and system update time # 0.4: diff --git a/SDK/include/NDK/BaseSystem.inl b/SDK/include/NDK/BaseSystem.inl index b2faa10b8..843448988 100644 --- a/SDK/include/NDK/BaseSystem.inl +++ b/SDK/include/NDK/BaseSystem.inl @@ -164,8 +164,10 @@ namespace Ndk } else { - OnUpdate(m_maxUpdateRate); - m_updateCounter -= m_maxUpdateRate; + float updateRate = std::max(elapsedTime, m_maxUpdateRate); + + OnUpdate(updateRate); + m_updateCounter -= updateRate; } } } diff --git a/SDK/include/NDK/World.hpp b/SDK/include/NDK/World.hpp index ee7d4bbb2..8a0fe2648 100644 --- a/SDK/include/NDK/World.hpp +++ b/SDK/include/NDK/World.hpp @@ -29,6 +29,7 @@ namespace Ndk public: using EntityVector = std::vector; + struct ProfilerData; inline World(bool addDefaultSystems = true); World(const World&) = delete; @@ -46,8 +47,12 @@ namespace Ndk void Clear() noexcept; const EntityHandle& CloneEntity(EntityId id); + inline void DisableProfiler(); + inline void EnableProfiler(bool enable = true); + inline const EntityHandle& GetEntity(EntityId id); inline const EntityList& GetEntities() const; + inline const ProfilerData& GetProfilerData() const; inline BaseSystem& GetSystem(SystemIndex index); template SystemType& GetSystem(); @@ -59,17 +64,27 @@ namespace Ndk inline bool IsEntityValid(const Entity* entity) const; inline bool IsEntityIdValid(EntityId id) const; + inline bool IsProfilerEnabled() const; + + void Refresh(); inline void RemoveAllSystems(); inline void RemoveSystem(SystemIndex index); template void RemoveSystem(); + inline void ResetProfiler(); - void Update(); - inline void Update(float elapsedTime); + void Update(float elapsedTime); World& operator=(const World&) = delete; inline World& operator=(World&& world) noexcept; + struct ProfilerData + { + Nz::UInt64 refreshTime = 0; + std::vector updateTime; + std::size_t updateCount = 0; + }; + private: inline void Invalidate(); inline void Invalidate(EntityId id); @@ -95,11 +110,13 @@ namespace Ndk std::vector m_entities; std::vector m_entityBlocks; std::vector> m_waitingEntities; - std::vector m_freeIdList; EntityList m_aliveEntities; + ProfilerData m_profilerData; Nz::Bitset m_dirtyEntities; + Nz::Bitset m_freeEntityIds; Nz::Bitset m_killedEntities; bool m_orderedSystemsUpdated; + bool m_isProfilerEnabled; }; } diff --git a/SDK/include/NDK/World.inl b/SDK/include/NDK/World.inl index d2be8ac40..32903502d 100644 --- a/SDK/include/NDK/World.inl +++ b/SDK/include/NDK/World.inl @@ -2,6 +2,7 @@ // This file is part of the "Nazara Development Kit" // For conditions of distribution and use, see copyright notice in Prerequisites.hpp +#include #include #include @@ -14,7 +15,8 @@ namespace Ndk */ inline World::World(bool addDefaultSystems) : - m_orderedSystemsUpdated(false) + m_orderedSystemsUpdated(false), + m_isProfilerEnabled(false) { if (addDefaultSystems) AddDefaultSystems(); @@ -47,7 +49,10 @@ namespace Ndk // We must ensure that the vector is big enough to hold the new system if (index >= m_systems.size()) + { m_systems.resize(index + 1); + m_profilerData.updateTime.resize(index + 1, 0); + } // Affectation and return of system m_systems[index] = std::move(system); @@ -82,7 +87,6 @@ namespace Ndk * * \param count Number of entities to create */ - inline World::EntityVector World::CreateEntities(unsigned int count) { EntityVector list; @@ -94,16 +98,79 @@ namespace Ndk return list; } + /*! + * \brief Disables the profiler, clearing up results + * + * This is just a shortcut to EnableProfiler(false) + * + * \param enable Should the entity be enabled + * + * \see EnableProfiler + */ + inline void World::DisableProfiler() + { + EnableProfiler(false); + } + + /*! + * \brief Enables/Disables the internal profiler + * + * Worlds come with a built-in profiler, allowing to measure update count along with time passed in refresh and system updates. + * This is disabled by default as it adds an small overhead to the update process. + * + * \param enable Should the profiler be enabled + * + * \remark Disabling the profiler clears up results, as if ResetProfiler has been called + */ + inline void World::EnableProfiler(bool enable) + { + if (m_isProfilerEnabled != enable) + { + m_isProfilerEnabled = enable; + + if (enable) + ResetProfiler(); + } + } + + /*! + * \brief Gets an entity + * \return A constant reference to a handle of the entity + * + * \param id Identifier of the entity + * + * \remark Handle referenced by this function can move in memory when updating the world, do not keep a handle reference from a world update to another + * \remark If an invalid identifier is provided, an error got triggered and an invalid handle is returned + */ + inline const EntityHandle& World::GetEntity(EntityId id) + { + if (IsEntityIdValid(id)) + return m_entityBlocks[id]->handle; + else + { + NazaraError("Invalid ID"); + return EntityHandle::InvalidHandle; + } + } + /*! * \brief Gets every entities in the world * \return A constant reference to the entities */ - inline const EntityList& World::GetEntities() const { return m_aliveEntities; } + /*! + * \brief Gets the latest profiler data + * \return A constant reference to the profiler data + */ + inline const World::ProfilerData& World::GetProfilerData() const + { + return m_profilerData; + } + /*! * \brief Gets a system in the world by index * \return A reference to the system @@ -192,26 +259,6 @@ namespace Ndk KillEntity(entity); } - /*! - * \brief Gets an entity - * \return A constant reference to a handle of the entity - * - * \param id Identifier of the entity - * - * \remark Handle referenced by this function can move in memory when updating the world, do not keep a handle reference from a world update to another - * \remark If an invalid identifier is provided, an error got triggered and an invalid handle is returned - */ - inline const EntityHandle& World::GetEntity(EntityId id) - { - if (IsEntityIdValid(id)) - return m_entityBlocks[id]->handle; - else - { - NazaraError("Invalid ID"); - return EntityHandle::InvalidHandle; - } - } - /*! * \brief Checks whether or not an entity is valid * \return true If it is the case @@ -230,12 +277,22 @@ namespace Ndk * * \param id Identifier of the entity */ - inline bool World::IsEntityIdValid(EntityId id) const { return id < m_entityBlocks.size() && m_entityBlocks[id]->entity.IsValid(); } + /*! + * \brief Checks whether or not the profiler is enabled + * \return true If it is the case + * + * \see EnableProfiler + */ + inline bool World::IsProfilerEnabled() const + { + return m_isProfilerEnabled; + } + /*! * \brief Removes each system from the world */ @@ -265,10 +322,21 @@ namespace Ndk } } + /*! + * \brief Clear profiler results + * + * This reset the profiler results, filling all counters with zero + */ + inline void World::ResetProfiler() + { + m_profilerData.refreshTime = 0; + m_profilerData.updateCount = 0; + std::fill(m_profilerData.updateTime.begin(), m_profilerData.updateTime.end(), 0); + } + /*! * \brief Removes a system from the world by type */ - template void World::RemoveSystem() { @@ -278,21 +346,6 @@ namespace Ndk RemoveSystem(index); } - /*! - * \brief Updates the world - * - * \param elapsedTime Delta time used for the update - */ - - inline void World::Update(float elapsedTime) - { - Update(); //< Update entities - - // And then update systems - for (auto& systemPtr : m_orderedSystems) - systemPtr->Update(elapsedTime); - } - /*! * \brief Moves a world into another world object * \return A reference to the object @@ -303,10 +356,12 @@ namespace Ndk m_aliveEntities = std::move(world.m_aliveEntities); m_dirtyEntities = std::move(world.m_dirtyEntities); m_entityBlocks = std::move(world.m_entityBlocks); - m_freeIdList = std::move(world.m_freeIdList); + m_freeEntityIds = std::move(world.m_freeEntityIds); m_killedEntities = std::move(world.m_killedEntities); m_orderedSystems = std::move(world.m_orderedSystems); m_orderedSystemsUpdated = world.m_orderedSystemsUpdated; + m_profilerData = std::move(world.m_profilerData); + m_isProfilerEnabled = m_isProfilerEnabled; m_entities = std::move(world.m_entities); for (EntityBlock& block : m_entities) diff --git a/SDK/src/NDK/Application.cpp b/SDK/src/NDK/Application.cpp index 5e0f07d00..a4fca055a 100644 --- a/SDK/src/NDK/Application.cpp +++ b/SDK/src/NDK/Application.cpp @@ -107,8 +107,7 @@ namespace Ndk if (m_shouldQuit) return false; - m_updateTime = m_updateClock.GetSeconds(); - m_updateClock.Restart(); + m_updateTime = m_updateClock.Restart() / 1'000'000.f; for (World& world : m_worlds) world.Update(m_updateTime); diff --git a/SDK/src/NDK/Lua/LuaBinding_SDK.cpp b/SDK/src/NDK/Lua/LuaBinding_SDK.cpp index 36cccfb1a..36c28d973 100644 --- a/SDK/src/NDK/Lua/LuaBinding_SDK.cpp +++ b/SDK/src/NDK/Lua/LuaBinding_SDK.cpp @@ -179,6 +179,12 @@ namespace Ndk world.BindMethod("CreateEntity", &World::CreateEntity); world.BindMethod("CreateEntities", &World::CreateEntities); world.BindMethod("Clear", &World::Clear); + world.BindMethod("DisableProfiler", &World::DisableProfiler); + world.BindMethod("EnableProfiler", &World::EnableProfiler); + world.BindMethod("IsProfilerEnabled", &World::IsProfilerEnabled); + world.BindMethod("Refresh", &World::Refresh); + world.BindMethod("ResetProfiler", &World::ResetProfiler); + world.BindMethod("Update", &World::Update); world.BindMethod("IsValidHandle", &WorldHandle::IsValid); } diff --git a/SDK/src/NDK/World.cpp b/SDK/src/NDK/World.cpp index 3a7427ecc..7582461b9 100644 --- a/SDK/src/NDK/World.cpp +++ b/SDK/src/NDK/World.cpp @@ -3,6 +3,7 @@ // For conditions of distribution and use, see copyright notice in Prerequisites.hpp #include +#include #include #include #include @@ -61,11 +62,14 @@ namespace Ndk { EntityId id; EntityBlock* entBlock; - if (!m_freeIdList.empty()) + + std::size_t freeEntityId = m_freeEntityIds.FindFirst(); + if (freeEntityId != m_freeEntityIds.npos) { // We get an identifier - id = m_freeIdList.back(); - m_freeIdList.pop_back(); + m_freeEntityIds.Reset(freeEntityId); //< Remove id from free entity id + + id = static_cast(freeEntityId); entBlock = &m_entities[id]; entBlock->handle.Reset(&entBlock->entity); //< Reset handle (as it was reset when entity got destroyed) @@ -81,7 +85,7 @@ namespace Ndk { NazaraAssert(m_waitingEntities.empty(), "There should be no waiting entities if space is available in main container"); - m_entities.push_back(Entity(this, id)); //< We can't use emplace_back due to the scope + m_entities.emplace_back(Entity(this, id)); //< We can't make our vector create the entity due to the scope entBlock = &m_entities.back(); } else @@ -124,11 +128,11 @@ namespace Ndk m_entityBlocks.clear(); m_entities.clear(); - m_freeIdList.clear(); m_waitingEntities.clear(); m_aliveEntities.Clear(); m_dirtyEntities.Clear(); + m_freeEntityIds.Clear(); m_killedEntities.Clear(); } @@ -166,12 +170,21 @@ namespace Ndk } /*! - * \brief Updates the world + * \brief Refreshes the world * - * \remark Produces a NazaraAssert if an entity is invalid + * This function will perform all pending operations in the following order: + * - Reorder systems according to their update order if needed + * - Moving newly created entities (whose which allocate never-used id) data and handles to normal entity list, this will invalidate references to world EntityHandle + * - Destroying dead entities and allowing their ids to be used by newly created entities + * - Update dirty entities, destroying their removed components and filtering them along systems + * + * \remark This is called automatically by Update and you most likely won't need to call it yourself + * \remark Calling this outside of Update will not increase the profiler values + * + * \see GetProfilerData + * \see Update */ - - void World::Update() + void World::Refresh() { if (!m_orderedSystemsUpdated) ReorderSystems(); @@ -203,7 +216,7 @@ namespace Ndk entity->Destroy(); // Send back the identifier of the entity to the free queue - m_freeIdList.push_back(entity->GetId()); + m_freeEntityIds.UnboundedSet(entity->GetId()); } m_killedEntities.Reset(); @@ -248,6 +261,43 @@ namespace Ndk m_dirtyEntities.Reset(); } + /*! + * \brief Updates the world + * \param elapsedTime Delta time used for the update + * + * This function Refreshes the world and calls the Update function of every active system part of it with the elapsedTime value. + * It also increase the profiler data with the elapsed time passed in Refresh and every system update. + */ + void World::Update(float elapsedTime) + { + if (m_isProfilerEnabled) + { + Nz::UInt64 t1 = Nz::GetElapsedMicroseconds(); + Refresh(); + Nz::UInt64 t2 = Nz::GetElapsedMicroseconds(); + + m_profilerData.refreshTime += t2 - t1; + + for (auto& systemPtr : m_orderedSystems) + { + systemPtr->Update(elapsedTime); + + Nz::UInt64 t3 = Nz::GetElapsedMicroseconds(); + m_profilerData.updateTime[systemPtr->GetIndex()] += t3 - t2; + t2 = t3; + } + + m_profilerData.updateCount++; + } + else + { + Refresh(); + + for (auto& systemPtr : m_orderedSystems) + systemPtr->Update(elapsedTime); + } + } + void World::ReorderSystems() { m_orderedSystems.clear(); diff --git a/build/scripts/common.lua b/build/scripts/common.lua index 28243e888..f4572322e 100644 --- a/build/scripts/common.lua +++ b/build/scripts/common.lua @@ -821,6 +821,7 @@ function NazaraBuild:PrepareGeneric() -- Setup some optimizations for release filter("configurations:Release*") + defines("NDEBUG") optimize("Speed") vectorextensions("SSE2") diff --git a/examples/FirstScene/main.cpp b/examples/FirstScene/main.cpp index 00fbebae1..2aec4861e 100644 --- a/examples/FirstScene/main.cpp +++ b/examples/FirstScene/main.cpp @@ -325,9 +325,7 @@ int main() while (application.Run()) { - Nz::UInt64 elapsedUS = updateClock.GetMicroseconds(); - // On relance l'horloge - updateClock.Restart(); + Nz::UInt64 elapsedUS = updateClock.Restart() / 1'000'000; // Mise à jour (Caméra) const Nz::UInt64 updateRate = 1000000 / 60; // 60 fois par seconde diff --git a/include/Nazara/Core/Clock.hpp b/include/Nazara/Core/Clock.hpp index 699f1a78c..a6b464db6 100644 --- a/include/Nazara/Core/Clock.hpp +++ b/include/Nazara/Core/Clock.hpp @@ -32,7 +32,7 @@ namespace Nz bool IsPaused() const; void Pause(); - void Restart(); + UInt64 Restart(); void Unpause(); Clock& operator=(const Clock& clock) = default; @@ -46,7 +46,7 @@ namespace Nz bool m_paused; }; - typedef UInt64 (*ClockFunction)(); + using ClockFunction = UInt64 (*)(); extern NAZARA_CORE_API ClockFunction GetElapsedMicroseconds; extern NAZARA_CORE_API ClockFunction GetElapsedMilliseconds; diff --git a/include/Nazara/Physics2D/PhysWorld2D.hpp b/include/Nazara/Physics2D/PhysWorld2D.hpp index f2b6195e7..e2ec3b042 100644 --- a/include/Nazara/Physics2D/PhysWorld2D.hpp +++ b/include/Nazara/Physics2D/PhysWorld2D.hpp @@ -54,6 +54,8 @@ namespace Nz float GetDamping() const; Vector2f GetGravity() const; cpSpace* GetHandle() const; + std::size_t GetIterationCount() const; + std::size_t GetMaxStepCount() const; float GetStepSize() const; bool NearestBodyQuery(const Vector2f& from, float maxDistance, Nz::UInt32 collisionGroup, Nz::UInt32 categoryMask, Nz::UInt32 collisionMask, RigidBody2D** nearestBody = nullptr); @@ -69,10 +71,14 @@ namespace Nz void SetDamping(float dampingValue); void SetGravity(const Vector2f& gravity); + void SetIterationCount(std::size_t iterationCount); + void SetMaxStepCount(std::size_t maxStepCount); void SetStepSize(float stepSize); void Step(float timestep); + void UseSpatialHash(float cellSize, std::size_t entityCount); + PhysWorld2D& operator=(const PhysWorld2D&) = delete; PhysWorld2D& operator=(PhysWorld2D&&) = delete; ///TODO @@ -140,6 +146,7 @@ namespace Nz static_assert(std::is_nothrow_move_constructible::value, "PostStepContainer should be noexcept MoveConstructible"); + std::size_t m_maxStepCount; std::unordered_map> m_callbacks; std::unordered_map m_rigidPostSteps; cpSpace* m_handle; diff --git a/include/Nazara/Physics3D/PhysWorld3D.hpp b/include/Nazara/Physics3D/PhysWorld3D.hpp index 428685b3f..76d4c09b4 100644 --- a/include/Nazara/Physics3D/PhysWorld3D.hpp +++ b/include/Nazara/Physics3D/PhysWorld3D.hpp @@ -42,9 +42,11 @@ namespace Nz Vector3f GetGravity() const; NewtonWorld* GetHandle() const; int GetMaterial(const Nz::String& name); + std::size_t GetMaxStepCount() const; float GetStepSize() const; void SetGravity(const Vector3f& gravity); + void SetMaxStepCount(std::size_t maxStepCount); void SetSolverModel(unsigned int model); void SetStepSize(float stepSize); @@ -72,6 +74,7 @@ namespace Nz std::unordered_map> m_callbacks; std::unordered_map m_materialIds; + std::size_t m_maxStepCount; Vector3f m_gravity; NewtonWorld* m_world; float m_stepSize; diff --git a/src/Nazara/Core/Clock.cpp b/src/Nazara/Core/Clock.cpp index 92183674c..13554a1fa 100644 --- a/src/Nazara/Core/Clock.cpp +++ b/src/Nazara/Core/Clock.cpp @@ -67,7 +67,7 @@ namespace Nz */ float Clock::GetSeconds() const { - return GetMicroseconds()/1000000.f; + return GetMicroseconds()/1'000'000.f; } /*! @@ -132,15 +132,26 @@ namespace Nz /*! * \brief Restart the clock + * \return Microseconds elapsed + * * Restarts the clock, putting it's time counter back to zero (as if the clock got constructed). + * It also compute the elapsed microseconds since the last Restart() call without any time loss (a problem that the combination of GetElapsedMicroseconds and Restart have). */ - void Clock::Restart() + UInt64 Clock::Restart() { NazaraLock(m_mutex); + Nz::UInt64 now = GetElapsedMicroseconds(); + + Nz::UInt64 elapsedTime = m_elapsedTime; + if (!m_paused) + elapsedTime += (now - m_refTime); + m_elapsedTime = 0; - m_refTime = GetElapsedMicroseconds(); + m_refTime = now; m_paused = false; + + return elapsedTime; } /*! diff --git a/src/Nazara/Physics2D/PhysWorld2D.cpp b/src/Nazara/Physics2D/PhysWorld2D.cpp index c0db3749f..807c0e7cc 100644 --- a/src/Nazara/Physics2D/PhysWorld2D.cpp +++ b/src/Nazara/Physics2D/PhysWorld2D.cpp @@ -80,6 +80,7 @@ namespace Nz } PhysWorld2D::PhysWorld2D() : + m_maxStepCount(50), m_stepSize(0.005f), m_timestepAccumulator(0.f) { @@ -144,6 +145,16 @@ namespace Nz return m_handle; } + std::size_t PhysWorld2D::GetIterationCount() const + { + return cpSpaceGetIterations(m_handle); + } + + std::size_t PhysWorld2D::GetMaxStepCount() const + { + return m_maxStepCount; + } + float PhysWorld2D::GetStepSize() const { return m_stepSize; @@ -281,6 +292,16 @@ namespace Nz cpSpaceSetGravity(m_handle, cpv(gravity.x, gravity.y)); } + void PhysWorld2D::SetIterationCount(std::size_t iterationCount) + { + cpSpaceSetIterations(m_handle, int(iterationCount)); + } + + void PhysWorld2D::SetMaxStepCount(std::size_t maxStepCount) + { + m_maxStepCount = maxStepCount; + } + void PhysWorld2D::SetStepSize(float stepSize) { m_stepSize = stepSize; @@ -290,7 +311,8 @@ namespace Nz { m_timestepAccumulator += timestep; - while (m_timestepAccumulator >= m_stepSize) + std::size_t stepCount = 0; + while (m_timestepAccumulator >= m_stepSize && stepCount < m_maxStepCount) { OnPhysWorld2DPreStep(this); @@ -309,9 +331,15 @@ namespace Nz } m_timestepAccumulator -= m_stepSize; + stepCount++; } } + void PhysWorld2D::UseSpatialHash(float cellSize, std::size_t entityCount) + { + cpSpaceUseSpatialHash(m_handle, cpFloat(cellSize), int(entityCount)); + } + void PhysWorld2D::InitCallbacks(cpCollisionHandler* handler, const Callback& callbacks) { auto it = m_callbacks.emplace(handler, std::make_unique(callbacks)).first; diff --git a/src/Nazara/Physics3D/PhysWorld3D.cpp b/src/Nazara/Physics3D/PhysWorld3D.cpp index 84264112b..2a01a27e0 100644 --- a/src/Nazara/Physics3D/PhysWorld3D.cpp +++ b/src/Nazara/Physics3D/PhysWorld3D.cpp @@ -12,6 +12,7 @@ namespace Nz { PhysWorld3D::PhysWorld3D() : m_gravity(Vector3f::Zero()), + m_maxStepCount(50), m_stepSize(0.005f), m_timestepAccumulator(0.f) { @@ -66,6 +67,11 @@ namespace Nz return it->second; } + std::size_t PhysWorld3D::GetMaxStepCount() const + { + return m_maxStepCount; + } + float PhysWorld3D::GetStepSize() const { return m_stepSize; @@ -76,6 +82,11 @@ namespace Nz m_gravity = gravity; } + void PhysWorld3D::SetMaxStepCount(std::size_t maxStepCount) + { + m_maxStepCount = maxStepCount; + } + void PhysWorld3D::SetSolverModel(unsigned int model) { NewtonSetSolverModel(m_world, model); @@ -132,10 +143,12 @@ namespace Nz { m_timestepAccumulator += timestep; - while (m_timestepAccumulator >= m_stepSize) + std::size_t stepCount = 0; + while (m_timestepAccumulator >= m_stepSize && stepCount < m_maxStepCount) { NewtonUpdate(m_world, m_stepSize); m_timestepAccumulator -= m_stepSize; + stepCount++; } } diff --git a/tests/Engine/Platform/EventHandler.cpp b/tests/Engine/Platform/EventHandler.cpp index b143b949a..a2f64e9f4 100644 --- a/tests/Engine/Platform/EventHandler.cpp +++ b/tests/Engine/Platform/EventHandler.cpp @@ -51,9 +51,8 @@ SCENARIO("EventHandler", "[PLATFORM][EVENTHANDLER][INTERACTIVE][.]") while (app.Run()) { window.Display(); - float elapsedTime = elapsedTimeClock.GetSeconds(); - elapsedTimeClock.Restart(); + float elapsedTime = elapsedTimeClock.Restart() / 1'000'000; if (!fsm.Update(elapsedTime)) { NazaraError("Failed to update state machine."); diff --git a/tests/SDK/NDK/EntityOwner.cpp b/tests/SDK/NDK/EntityOwner.cpp index 9ff8e1396..2d240a6d0 100644 --- a/tests/SDK/NDK/EntityOwner.cpp +++ b/tests/SDK/NDK/EntityOwner.cpp @@ -17,7 +17,7 @@ SCENARIO("EntityOwner", "[NDK][ENTITYOWNER]") Ndk::EntityOwner entityOwner(entity); - world.Update(); + world.Refresh(); CHECK(entity.IsValid()); } @@ -29,12 +29,12 @@ SCENARIO("EntityOwner", "[NDK][ENTITYOWNER]") Ndk::EntityOwner entityOwner2(std::move(entityOwner)); entityOwner.Reset(); - world.Update(); + world.Refresh(); CHECK(entity.IsValid()); entityOwner2.Reset(); - world.Update(); + world.Refresh(); CHECK(!entity.IsValid()); } @@ -47,12 +47,12 @@ SCENARIO("EntityOwner", "[NDK][ENTITYOWNER]") entityOwner2 = std::move(entityOwner); entityOwner.Reset(); - world.Update(); + world.Refresh(); CHECK(entity.IsValid()); entityOwner2.Reset(); - world.Update(); + world.Refresh(); CHECK(!entity.IsValid()); } @@ -64,7 +64,7 @@ SCENARIO("EntityOwner", "[NDK][ENTITYOWNER]") Ndk::EntityOwner entityOwner(entity); } - world.Update(); + world.Refresh(); CHECK(!entity.IsValid()); } @@ -76,7 +76,7 @@ SCENARIO("EntityOwner", "[NDK][ENTITYOWNER]") Ndk::EntityOwner entityOwner(entity); entityOwner.Reset(); - world.Update(); + world.Refresh(); CHECK(!entity.IsValid()); } @@ -87,7 +87,7 @@ SCENARIO("EntityOwner", "[NDK][ENTITYOWNER]") Ndk::EntityOwner entityOwner(entity); entityOwner = world.CreateEntity(); - world.Update(); + world.Refresh(); CHECK(!entity.IsValid()); } @@ -100,7 +100,7 @@ SCENARIO("EntityOwner", "[NDK][ENTITYOWNER]") entityOwner = std::move(entity2); - world.Update(); + world.Refresh(); CHECK(!entity.IsValid()); } } diff --git a/tests/SDK/NDK/System.cpp b/tests/SDK/NDK/System.cpp index ba7e9f4aa..5f6d74284 100644 --- a/tests/SDK/NDK/System.cpp +++ b/tests/SDK/NDK/System.cpp @@ -1,30 +1,40 @@ #include #include -/* + namespace { class TestSystem : public Ndk::System { public: - TestSystem(int value) : - m_value(value) + TestSystem() : + m_updateCounter(0), + m_elapsedTime(0.f) { } - int GetValue() const - { - return m_value; - } - ~TestSystem() = default; + float GetElapsedTime() const + { + return m_elapsedTime; + } + + std::size_t GetLoopCount() const + { + return m_updateCounter; + } + static Ndk::SystemIndex systemIndex; private: - int m_value; + std::size_t m_updateCounter; + float m_elapsedTime; void OnUpdate(float elapsedTime) override { + ++m_updateCounter; + + m_elapsedTime += elapsedTime; } }; @@ -35,16 +45,63 @@ SCENARIO("System", "[NDK][SYSTEM]") { GIVEN("Our TestSystem") { - TestSystem testSystem(666); + TestSystem testSystem; + testSystem.SetMaximumUpdateRate(30.f); - WHEN("We clone it") + float maxTimePerFrame = 1 / 30.f; + + WHEN("We update it with a higher framerate") { - std::unique_ptr clone = testSystem.Clone(); + float timePerFrame = maxTimePerFrame / 2.f; + float elapsedTime = 2.f; - THEN("We should get a copy") + std::size_t loopCount = static_cast(std::round(elapsedTime / timePerFrame)); + + for (std::size_t i = 0; i < loopCount; ++i) + testSystem.Update(timePerFrame); + + CHECK(testSystem.GetLoopCount() == loopCount / 2); + CHECK(testSystem.GetElapsedTime() == Approx(elapsedTime).epsilon(timePerFrame)); + } + + WHEN("We update it with a lower framerate") + { + float timePerFrame = maxTimePerFrame * 2.f; + float elapsedTime = 10.f; + + std::size_t loopCount = static_cast(std::round(elapsedTime / timePerFrame)); + + for (std::size_t i = 0; i < loopCount; ++i) + testSystem.Update(timePerFrame); + + CHECK(testSystem.GetLoopCount() == loopCount); + CHECK(testSystem.GetElapsedTime() == Approx(elapsedTime).epsilon(timePerFrame)); + + AND_WHEN("We suddenly increase framerate") { - REQUIRE(static_cast(clone.get())->GetValue() == 666); + float newTimePerFrame = 1 / 300.f; + float newElapsedTime = 100.f; + + std::size_t newLoopCount = static_cast(std::round(newElapsedTime / newTimePerFrame)); + + for (std::size_t i = 0; i < newLoopCount; ++i) + testSystem.Update(newTimePerFrame); + + CHECK(testSystem.GetLoopCount() == loopCount + newLoopCount / 10); + CHECK(testSystem.GetElapsedTime() == Approx(elapsedTime + newElapsedTime).epsilon(newTimePerFrame)); } } + + + WHEN("We update it with a very low framerate") + { + float timePerFrame = 0.5f; + + for (std::size_t i = 0; i < 10; ++i) + testSystem.Update(timePerFrame); + + CHECK(testSystem.GetLoopCount() == 10); + CHECK(testSystem.GetElapsedTime() == Approx(5.f)); + } } -}*/ \ No newline at end of file +} diff --git a/tests/SDK/NDK/Systems/PhysicsSystem2D.cpp b/tests/SDK/NDK/Systems/PhysicsSystem2D.cpp index 6de7b2172..6090be082 100644 --- a/tests/SDK/NDK/Systems/PhysicsSystem2D.cpp +++ b/tests/SDK/NDK/Systems/PhysicsSystem2D.cpp @@ -19,7 +19,7 @@ SCENARIO("PhysicsSystem2D", "[NDK][PHYSICSSYSTEM2D]") Ndk::NodeComponent& nodeComponent = movingEntity->GetComponent(); Ndk::PhysicsComponent2D& physicsComponent2D = movingEntity->AddComponent(); - world.GetSystem().SetFixedUpdateRate(30.f); + world.GetSystem().SetMaximumUpdateRate(0.f); WHEN("We update the world") { @@ -43,7 +43,7 @@ SCENARIO("PhysicsSystem2D", "[NDK][PHYSICSSYSTEM2D]") world.Update(1.f); - THEN("It should moved freely") + THEN("It should move freely") { REQUIRE(nodeComponent.GetPosition() == position); movingAABB.Translate(position);