Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions include/EcoSimEngine/system/MetabolismSystem.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#pragma once

#include <algorithm>
#include <vector>

#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<EntityId> entitiesToDestroy;

for (EntityId id : mEntities)
{
auto entity = em.getEntityById(id);

if (!entity || !entity->isActive())
{
continue;
}

if (!cm.has<CEnergy>(id) || !cm.has<CHealth>(id))
{
continue;
}

auto &energy = cm.get<CEnergy>(id);
auto &health = cm.get<CHealth>(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);
}
}
}
};
21 changes: 20 additions & 1 deletion src/simulation/SimulationWorld.cpp
Original file line number Diff line number Diff line change
@@ -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<MovementSystem>();

Expand All @@ -23,12 +25,29 @@ SimulationWorld::SimulationWorld()
aiSignature.set(COMP_INDEX_CTransform);

m_systemManager.SetSignature<AISystem>(aiSignature);

// Metabolism system
m_systemManager.RegisterSystem<MetabolismSystem>();

Signature metabolismSignature;
metabolismSignature.set(COMP_INDEX_CEnergy);
metabolismSignature.set(COMP_INDEX_CHealth);

m_systemManager.SetSignature<MetabolismSystem>(metabolismSignature);
}

void SimulationWorld::update(float dt)
{
m_entityManager.update();

if (auto metabolism = m_systemManager.GetSystem<MetabolismSystem>())
{
metabolism->update(
m_entityManager,
m_componentManager,
dt);
}

if (auto ai = m_systemManager.GetSystem<AISystem>())
{
ai->update(m_entityManager, m_componentManager, dt, m_rng);
Expand Down
202 changes: 144 additions & 58 deletions tests/foundation_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<CTransform>(
entity,
Vec2f{0.0f, 0.0f});
entities.addComponent<CTransform>(
entity,
Vec2f{0.0f, 0.0f});

entities.addComponent<CBehavior>(
entity);
entities.addComponent<CBehavior>(
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<CTransform>(id);
const auto &transform =
components.get<CTransform>(id);

const auto& behavior =
components.get<CBehavior>(id);
const auto &behavior =
components.get<CBehavior>(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()
{
Expand Down Expand Up @@ -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<CEnergy>(
entity,
1.0f);

energy.consumptionRate = 1.0f;

entities.addComponent<CHealth>(
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<CEnergy>(id).current == 0.5f,
"metabolism must consume energy according to dt");

require(
components.get<CHealth>(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<CEnergy>(id).current == 0.0f,
"energy must clamp at zero");

require(
components.get<CHealth>(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<CHealth>(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<CEnergy>(id),
"destroyed entity must have its energy component removed");

require(
!components.has<CHealth>(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

Expand All @@ -408,6 +493,7 @@ int main()
testEntityDestruction();
testEventBus();
testSimulationClock();
testMetabolismSystem();
testDeterministicSimulation();
}
catch (const std::exception &error)
Expand Down