From f4a8352d454c34511193794d1521584ece85020a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?koukaku01=20=E8=A1=8C=E5=AE=A2?= Date: Thu, 17 Sep 2026 17:46:03 +0100 Subject: [PATCH] feat(simulation): add metabolism and starvation --- .../EcoSimEngine/system/MetabolismSystem.hpp | 70 ++++++ src/simulation/SimulationWorld.cpp | 21 +- tests/foundation_tests.cpp | 202 +++++++++++++----- 3 files changed, 234 insertions(+), 59 deletions(-) create mode 100644 include/EcoSimEngine/system/MetabolismSystem.hpp diff --git a/include/EcoSimEngine/system/MetabolismSystem.hpp b/include/EcoSimEngine/system/MetabolismSystem.hpp new file mode 100644 index 0000000..f0f8c77 --- /dev/null +++ b/include/EcoSimEngine/system/MetabolismSystem.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include +#include + +#include "EcoSimEngine/ecs/EntityManager.hpp" +#include "EcoSimEngine/system/System.hpp" + +class MetabolismSystem : public System +{ +public: + static constexpr float kStarvationDamagePerSecond{10.0f}; // Damage applied to entities that are starving + + void update( + EntityManager &em, + ComponentManager &cm, + float dt) + { + if (dt <= 0.0f) + return; + + std::vector entitiesToDestroy; + + for (EntityId id : mEntities) + { + auto entity = em.getEntityById(id); + + if (!entity || !entity->isActive()) + { + continue; + } + + if (!cm.has(id) || !cm.has(id)) + { + continue; + } + + auto &energy = cm.get(id); + auto &health = cm.get(id); + + if (energy.current > 0.0f) + { + energy.current = std::max( + 0.0f, + energy.current - energy.consumptionRate * dt); + + continue; + } + + health.health = std::max( + 0.0f, + health.health - kStarvationDamagePerSecond * dt); + + if (health.health <= 0.0f) + { + entitiesToDestroy.push_back(id); + } + } + + for (EntityId id : entitiesToDestroy) + { + auto entity = em.getEntityById(id); + + if (entity) + { + em.destroyEntity(entity); + } + } + } +}; \ No newline at end of file diff --git a/src/simulation/SimulationWorld.cpp b/src/simulation/SimulationWorld.cpp index b9dc9db..31f274d 100644 --- a/src/simulation/SimulationWorld.cpp +++ b/src/simulation/SimulationWorld.cpp @@ -1,12 +1,14 @@ #include "EcoSimEngine/simulation/SimulationWorld.hpp" #include "EcoSimEngine/component/ComponentIndices.hpp" + #include "EcoSimEngine/system/AISystem.hpp" #include "EcoSimEngine/system/MovementSystem.hpp" +#include "EcoSimEngine/system/MetabolismSystem.hpp" SimulationWorld::SimulationWorld() : m_entityManager(m_systemManager, m_componentManager) -{ +{ // Movement system m_systemManager.RegisterSystem(); @@ -23,12 +25,29 @@ SimulationWorld::SimulationWorld() aiSignature.set(COMP_INDEX_CTransform); m_systemManager.SetSignature(aiSignature); + + // Metabolism system + m_systemManager.RegisterSystem(); + + Signature metabolismSignature; + metabolismSignature.set(COMP_INDEX_CEnergy); + metabolismSignature.set(COMP_INDEX_CHealth); + + m_systemManager.SetSignature(metabolismSignature); } void SimulationWorld::update(float dt) { m_entityManager.update(); + if (auto metabolism = m_systemManager.GetSystem()) + { + metabolism->update( + m_entityManager, + m_componentManager, + dt); + } + if (auto ai = m_systemManager.GetSystem()) { ai->update(m_entityManager, m_componentManager, dt, m_rng); diff --git a/tests/foundation_tests.cpp b/tests/foundation_tests.cpp index f1ea870..0a66f9b 100644 --- a/tests/foundation_tests.cpp +++ b/tests/foundation_tests.cpp @@ -46,57 +46,57 @@ namespace // helper SimulationSnapshot runDeterministicSimulation( - std::uint32_t seed) -{ - SimulationWorld world; + std::uint32_t seed) + { + SimulationWorld world; - world.reseed(seed); + world.reseed(seed); - auto& entities = - world.entityManager(); + auto &entities = + world.entityManager(); - auto& components = - world.componentManager(); + auto &components = + world.componentManager(); - auto entity = - entities.addEntity("test"); + auto entity = + entities.addEntity("test"); - entities.addComponent( - entity, - Vec2f{0.0f, 0.0f}); + entities.addComponent( + entity, + Vec2f{0.0f, 0.0f}); - entities.addComponent( - entity); + entities.addComponent( + entity); - entities.update(); + entities.update(); - constexpr float fixedStep = - 1.0f / 60.0f; + constexpr float fixedStep = + 1.0f / 60.0f; - constexpr int ticks = 300; + constexpr int ticks = 300; - for (int i = 0; i < ticks; ++i) - { - world.update(fixedStep); - } + for (int i = 0; i < ticks; ++i) + { + world.update(fixedStep); + } - const auto id = - entity->id(); + const auto id = + entity->id(); - const auto& transform = - components.get(id); + const auto &transform = + components.get(id); - const auto& behavior = - components.get(id); + const auto &behavior = + components.get(id); - return SimulationSnapshot{ - transform.pos.x, - transform.pos.y, - transform.velocity.x, - transform.velocity.y, - behavior.current, - behavior.stateTimer}; -} + return SimulationSnapshot{ + transform.pos.x, + transform.pos.y, + transform.velocity.x, + transform.velocity.y, + behavior.current, + behavior.stateTimer}; + } void testComponentManager() { @@ -368,34 +368,119 @@ namespace "finished clock must not expose another simulation step"); } + void testMetabolismSystem() + { + SimulationWorld world; + + auto &entities = world.entityManager(); + auto &components = world.componentManager(); + + auto entity = entities.addEntity("test-animal"); + + auto &energy = + entities.addComponent( + entity, + 1.0f); + + energy.consumptionRate = 1.0f; + + entities.addComponent( + entity, + 5.0f); + + entities.update(); + + const auto id = entity->id(); + + // 0.5 seconds: + // energy 1.0 -> 0.5, health unchanged. + world.update(0.5f); + + require( + components.get(id).current == 0.5f, + "metabolism must consume energy according to dt"); + + require( + components.get(id).health == 5.0f, + "health must remain unchanged while energy is available"); + + // Another 0.5 seconds: + // energy reaches zero, but starvation starts on the next tick. + world.update(0.5f); + + require( + components.get(id).current == 0.0f, + "energy must clamp at zero"); + + require( + components.get(id).health == 5.0f, + "health must not fall on the tick that energy reaches zero"); + + // 0.25 seconds starving at 10 health/second: + // health 5.0 -> 2.5. + world.update(0.25f); + + require( + components.get(id).health == 2.5f, + "starvation must reduce health according to dt"); + + require( + entity->isActive(), + "entity must remain alive while health is above zero"); + + // Another 0.25 seconds: + // health 2.5 -> 0.0, entity dies. + world.update(0.25f); + + require( + !entity->isActive(), + "entity must be destroyed when starvation reduces health to zero"); + + require( + !components.has(id), + "destroyed entity must have its energy component removed"); + + require( + !components.has(id), + "destroyed entity must have its health component removed"); + + // EntityManager removes inactive entities from its containers + // at the beginning of the following world update. + world.update(0.0f); + + require( + entities.getEntityById(id) == nullptr, + "destroyed entity must be removed from EntityManager on the next update"); + } + void testDeterministicSimulation() -{ - constexpr std::uint32_t seed = 48596; + { + constexpr std::uint32_t seed = 48596; - const SimulationSnapshot first = - runDeterministicSimulation(seed); + const SimulationSnapshot first = + runDeterministicSimulation(seed); - const SimulationSnapshot second = - runDeterministicSimulation(seed); + const SimulationSnapshot second = + runDeterministicSimulation(seed); - require( - first.positionX == second.positionX && - first.positionY == second.positionY, - "same seed and tick sequence must reproduce entity position"); + require( + first.positionX == second.positionX && + first.positionY == second.positionY, + "same seed and tick sequence must reproduce entity position"); - require( - first.velocityX == second.velocityX && - first.velocityY == second.velocityY, - "same seed and tick sequence must reproduce entity velocity"); + require( + first.velocityX == second.velocityX && + first.velocityY == second.velocityY, + "same seed and tick sequence must reproduce entity velocity"); - require( - first.behaviorState == second.behaviorState, - "same seed and tick sequence must reproduce behavior state"); + require( + first.behaviorState == second.behaviorState, + "same seed and tick sequence must reproduce behavior state"); - require( - first.stateTimer == second.stateTimer, - "same seed and tick sequence must reproduce behavior timing"); -} + require( + first.stateTimer == second.stateTimer, + "same seed and tick sequence must reproduce behavior timing"); + } } // namespace @@ -408,6 +493,7 @@ int main() testEntityDestruction(); testEventBus(); testSimulationClock(); + testMetabolismSystem(); testDeterministicSimulation(); } catch (const std::exception &error)