(When most people are talking about entity-component systems with separate components and systems, they have all the component data exposed and systems access whatever they need to - eg. http://www.gamedev.net/page/resources/_/technical/game-programming/understanding-component-entity-systems-r3013)
I didn't think that way was all that common. Now that I read some of the other ECS-threads you've replied in, I can kind of see where you're coming from. Those threads don't portray ECS as I understand it, at all.
The ECS I'm describing is just what I thought a "proper" ECS was from the start. There was a thread here on GDNet a few years back advocating exactly putting state + behaviour in systems. A lot of the "data-oriented design" blogs tend to advocate something similar, as well. I might be venturing into "no true Scotsman" territory here, but I suggest that folks who explain ECS as completely separating the components and their systems misunderstood the original idea, as all the early references to ECS I can recall portray ECS as both component state and behaviour living together in cohesive systems. Of course, even technical language evolves - Alan Kay has complained that object-orientation is really about messaging and not all that other stuff, so maybe this suggestion doesn't matter much.
This is why I don't actually use the term "ECS" much anymore, and why I don't describe my implementation as ECS even though it looks a lot like it. It's too easily misunderstood, and taken dogmatically is a bit rigid.
I might have a LifeCycle component which checks Inventory for food and decreases the health part of CharacterStats accordingly.
This is likely how I would first approach this, as well, with the difference that what you call a "LifeCycle component" is where I would actually store health, as well. There wouldn't be a "CharacterStats" component in the first place. A life cycle system might look like this:
struct LifecycleSystem
{
// ...
void ApplyInventoryEffects(InventorySystem& inventory);
void CheckForDeath(std::vector<CharacterHandle>& outDeadCharacters);
// ...
private:
struct LifecycleDatum
{
float baseHealth; // initialized when an entity is created
float currentHealth; // updated as gameplay progresses
float poisonTimeRemaining; // for example...
};
// typed pool is an object pool that enforces that handles to data in the pool are marked with a specific type ID and use count
TypedPool<ObjectType::Character, LifeCycleDatum> m_data;
// active data list so we don't have to iterate through the entire data pool;
// would probably store this externally, at least initially, but putting it here gives us a new option:
// not all characters actually need to have have health!
// implementing TypedPool<> as a sparse array would make this unnecessary, of course
std::vector<TypedHandle> m_charactersWithHealth;
};
// one way
void LifecycleSystem::ApplyInventoryEffects(float dt, InventorySystem& inventory)
{
for (const TypedHandle& character : m_charactersWithHealth)
{
if (!inventory.HasFood(character))
{
character.GetMutableFrom(m_data).currentHealth -= k_starvationDepletionPerFrame * dt;
}
}
}
// another way - more efficient, but splits concerns up differently
void LifecycleSystem::ApplyInventoryEffects(float dt, InventorySystem& inventory)
{
// std::vector<TypedHandle> InventorySystem::charactersWithNoFood;
for (const TypedHandle& character : inventory.charactersWithNoFood)
{
if (auto characterInstance = character.TryAndGetMutableFrom(m_data))
{
characterInstance->currentHealth -= k_starvationDepletionPerFrame * dt;
}
}
}
// still another way - likely less efficient, but better separation of concerns
void LifecycleSystem::ApplyInventoryEffects(float dt, InventorySystem& inventory)
{
// a handle to a character is in this list if the character's inventory was updated this frame
// std::vector<TypedHandle> InventorySystem::recentlyUpdatedCharacters;
for (const TypedHandle& character : inventory.recentlyUpdatedCharacters)
{
if (!inventory.HasFood(character))
{
if (auto characterInstance = character.TryAndGetMutableFrom(m_data))
{
characterInstance->currentHealth -= k_starvationDepletionPerFrame * dt;
}
}
}
}
edit: grammar and code example, haven't had coffee yet.