Do everything through ECS? Or implement other systems?

Started by
9 comments, last by Oberon_Command 9 years, 2 months ago

I'm running into trouble trying to implement certain features in a clean way. For example, modifiers to an entity (say a speed boost, or +attack power, or damage-over-time). My first idea was to add ComponentModifiers, which could be attached to an entity, grab components from the entity and modify them, and then undo those changes when they're removed. However I'm not sure if building these other systems that don't follow the ECS paradigm will overly complicate things if they were doable within the ECS. For those of you who have implemented ECS in your games, how much of the game logic is handled by ECS, and how much exists alongside it?

Advertisement

Disclaimer: I have not implemented any ECS system myself - I've read alot about them, but I've never used them.

While I can't speak too specifically about ECS systems, paradigms are a way of looking at and laying out your code to make it easier to understand complex programs. Paradigms (and design patterns) are not meant to get you to cram/force all your code into the paradigm's preferred shape.

It is not uncommon that different parts of a large program use different paradigms - whatever paradigm is most appropriate to the problem being solved. For example, some people have come to the conclusion that ECS don't really work for game GUIs (though you can cram a game GUI into an ECS system), so they use a different paradigm for the GUI that more closely fits the nature of GUIs.

This isn't a sports tournament where you favor one team exclusively over another team. Favoritism blinds pragmatism. ECS isn't better than other architectures - it's better than other architectures at some problems. A game is a complex program, that has many different problems. So you use multiple different designs to solve those problems - or rather, you solve those problems and use different designs to describe and guide, but not dictate, your solution.

In addition to that, programs are built up of layers, and different paradigms are used at different levels of abstractions, depending on the architecture. What level of abstraction is your ECS system? Usually, it's pretty high level.

So, what level of abstraction is your entity's stats that you are wanting to modify? Attack power, speed, health, etc... sounds like they are all a part of one or two components, depending on how your game uses them. So then, your current problem (changing these variables over time) is perhaps not actually at the ECS level of abstraction, but a lower level below it. So then, the question is, what architecture should be used for this specific component-system that contains these variables? Different component-system can be implemented differently, using different paradigms. When you are implementing a specific component's system, you have to think less of it at the ECS level, and think more of it as its own sub-architecture. It lives mostly in isolation from the other components (unless you have them too tightly tied together).

Sometimes it helps to think of the simplest way of doing something, and then decide whether you actually need it more complex than that.
Where are these modifications coming from? Equipment, magic buffs and debuffs?

Maybe it makes sense to do something straightforward like:


//Fast and sloppy solution:

struct Stats
{
    HealthPerSec; //For regen or DPS equipment or spells.
    MaxHealth;
    Attack;
    Defense;
    Speed;
};
 
struct Equipment
{
     Stats modifications;
     //...other stuff...
};
 
struct Magic
{
     Stats modifications;
     float duration;
     //...other stuff...
};
 
Stats basePlayerStats;
std::vector<Equipment> equippedItems;
std::vector<Magic> activeMagic;
 
Stats currentStats; //Updated once a second by crawling 'equippedItems' and 'activeMagic'

Or maybe some slightly more intricate is needed, but the point is it can exist apart from, and unaware of, your ECS system, and your ECS system can exist apart from it. They can interact without being too intertwined.

Or you could have a AttributeComponent that holds all the stats (used by other components) and also who's AttributeComponentSystem has the logic for varying them overtime - perhaps by handing out temporary IDs to equipped items and cast spells that can then be used to remove the attribute changes later.

Note: This is partially dependant on the nature of the game you are making. Some games may have each attribute being a component (i.e. HealthComponent, AttackComponent, etc...), but I think that's putting the ECS at too low of a level.

I've shipped one game with an ECS, and built part of a much larger project with an ECS.

In the first case, I'd say roughly half the gameplay mechanics were part of systems (which operate over components or sets of components), and half were part of one-off scripts attached to an entity. Attaching scripts to an entity was kind of an "escape valve" for stuff that doesn't fit nicely within the ECS... the scripts are kind of a black box that is just told to update.

As for the buffs and modifiers you talk about, I agree with SotL that this isn't really relevant to the ECS. I don't think it gets any easier or harder or much different whether or not you're using an ECS.

To start with, I would just have a "Stats" component that includes: base stats, modifiers, and possibly "final stats". e.g.


struct Stats
{
    int BaseSpeed;
    int BaseAttack;
    int SpeedModifier;
    int AttackModifier;
    int Speed;
    int Attack;
};

A stats system would iterate over these things and ensure that Speed and Attack are kept up-to-date with the result of whatever formula calculates them from base and mod. Other code (such as the combat logic) would get the Stats component for an entity and just look at Speed and Attack. That way the combat code knows nothing about modifiers. And the stat calculation code has nothing to do with combat.

Another alternative is maybe the "FinalStats" could be a separate component, and the combat code only cares about that. The stats system would then be responsible for transforming the data from the "Stats" component to the "FinalStats" component for an entity. And if some enemies are very simple and don't have such a thing as base stats and mods, then they only need a "FinalStats" component. The combat logic continues as before (since it only cares about FinalStats), and the stats system would just ignore anything without a "Stats" component.

The ECS "pattern" makes it easy to think about decoupling code like this, but you could do essentially the same thing in a more traditional OOP way too.

Servant has a good post on starting with. ECS is typically for game-related logic. You don't really want it in your rendering code on a low level, or user interfaces. It was intended for use with interaction of elements in the game's world.

In your case with modifying stats, I would make items, equipment and spells have their own Stats component and then simply add the equipment or spell stats to the player's own and them subtract them when they get unequipped. It doesn't get simpler than that to me. Spells and temporary enhancement items can be treated as the active magic that may or may not have a limited lifespan, and consumable items like health potions will not get added to your equipped items list- they simply change the stats and get removed.

New game in progress: Project SeedWorld

My development blog: Electronic Meteor

This is what I've learned reading ECS by reading it (I've tried to implement about 2 years ago):

Is at the highest-level module of your engine;

Leads the programmer to generalize the component architecture (in this case the component being a game-component) by saying that a entity is just an ID (a number) or a pointer that has components or gets mapped to components by its ID;

It is usefull for a well defined engine that supports scripting systems, uses the concept of message handlers (known as brute-force event systems tough games are not event-based), fast components table look-up, and many others optimizations;

Sometimes could be replaced with hierarchy of game entities such Entity->GameObject->Vehicle by the fact that it just abstracts game-components and its systems forcing you to relate each component with a system or use the component term as just an object composition (you get the idea);

Sometimes solves some problems with hierarchy conflicts of game-objects, but most of the time this can be solved with classes duplication.

That's for sure:

For doing a correctly game-object component or ECS it requires a lot of optimization and development effort in order to get something more than the object-oriented approach (such the entity-hierarchy above). Look at Unity documentation and you'll find that is not the simple thing of giving an entity an ID, game-object components, and make a soup of that automating everything.

This is when you actually need to stop and think for a moment if you really want to implement and generalize game object components. The probability of mixing the low-level modules of the game engine with the high-level game-object architeture IT IS OVER 9000!

Sometimes people tend to ask if they "RenderSystem" that manages and render a "RenderComponent" (what is a "RenderComponent"? Anything that can be rendered?) is well implemented, and when you check you see that he's mixing vertex buffers with a character that even know what type of shirt he is using (poor game entity that knows everything, he must be on god mode in this type of game); then you see that mixing high-level systems without having the low-level ones already done mess up everything, and in order to use a vertex buffer, the user includes the whole game module on its project by the fact that a "RenderSystem" is supposed to be a component system that is from the game-module/engine-module/highest-level module in the engine.

I'm actually currently working on one myself. The ECS so far from my experience is incredibly tricky to get the hang of. You do not make a system for everything. Other wise your code would best be suited for regular Object oriented. It's better to keep it standard for low level things. You don't want your engine to become infected with game based code. Trust me when I say it gets hard to read after a while.

Oh... and also, if this is an RPG. Make sure that you don't have to make EVERYTHING a separate entity instance. Or separate them. The reality is that you WILL eventually run out of digits if you have to constantly iterate. And for things that uses bullets, particles, ect. It gets pretty jaded.

For stats. You can make it a component if you want, your system should be intelligent enough to filter components out anyways. But you should NOT make a system for the stats component. The stat's component is simply a book keeping object. You do not want to waste engine time trying to iterate this if you can avoid it.

You want to let scripting languages handle stat changes. Scripting languages will also be driving your movement anyways, so it could be as trivial as placing a modifier on the speed function for a certain time.

The way I am doing it currently, is that I will be using LUA. And lua will be modifying data inside entity PODs. Now... this might be a little inefficient... but it's better than having multiple virtual machines at run time.

Also... do not try to make a Unity3D. The likely hood of you ever needing half those components in a game... even for something like Skyrim, is on the verge of NEVER. It makes the system more complex, and makes more work for you. The only programmer. Unity3D also made things entirely impracticable by requiring simple scripts to be in the physical world. Try to make sense of that...

Sometimes people tend to ask if they "RenderSystem" that manages and render a "RenderComponent" (what is a "RenderComponent"? Anything that can be rendered?) is well implemented, and when you check you see that he's mixing vertex buffers with a character that even know what type of shirt he is using (poor game entity that knows everything, he must be on god mode in this type of game); then you see that mixing high-level systems without having the low-level ones already done mess up everything, and in order to use a vertex buffer, the user includes the whole game module on its project by the fact that a "RenderSystem" is supposed to be a component system that is from the game-module/engine-module/highest-level module in the engine.

I think that systems dealing with graphics and physics should be more of a wrapper and the actual rendering and physics code is implemented in a black box manner. This means that these systems are just responsible for stripping the raw data from the components and passing it directly to the rendering and physics engines. The engines do the dirty lower-level work, and are objects inside of a system, but aren't aware that they are in a system.

Unity3D also made things entirely impracticable by requiring simple scripts to be in the physical world. Try to make sense of that...

Admittedly, this is a side effect of Unity requiring a Transform component for everything, even scripts. So even things that shouldn't need a physical location get one anyways. It's the only thing that sticks out to me as a big overthought in their ECS.

New game in progress: Project SeedWorld

My development blog: Electronic Meteor


I think that systems dealing with graphics and physics should be more of a wrapper and the actual rendering and physics code is implemented in a black box manner. This means that these systems are just responsible for stripping the raw data from the components and passing it directly to the rendering and physics engines. The engines do the dirty lower-level work, and are objects inside of a system, but aren't aware that they are in a system.

Yeah. I was just saying that those type of separations becomes a mess with component systems if the user do not have already-done low-level engine implementation.

The plug-in approach doesn't work on most of the time. Plugging vertex buffers to render components is not the way to go either. Pick a model for instance; such thing needs a completely converter, manager, class, module, parsing, etc.; it doesn't make sense to call it a render component; but I see no problem in plugging in a model to a render component. Using something such CRenderableModelComponent that has a pointer to a renderable model could probably more correct.

Since I'm now trying out coding with ECS (again), with a simple game, some thoughts came to mind. Is programming a game from the ground up with custom ECS code premature optimization? Let's ignore the cache and other data-driven tricks for now, and just think about the ECS architecture.

Last time I attempted ECS it was for a 2D shooter game that was still too complex at the moment to completely understand how I'd put the parts together. Phil_T's article on ECS inspired me to make my own Bomberman clone but writing my own code, and is a simpler game to do. I am making more progress this time. One thing I quickly found out is, when you are doing ECS startup/setup is initially very slow and you will not see immediate results, but once you have most of the core ECS code done things pick up a lot faster and you can get to test various things more easily.

New game in progress: Project SeedWorld

My development blog: Electronic Meteor

@CCRicers: No, it's not premature optimization. It's an architecture choice that happens to be good for optimizations, but even if ECS wasn't too optimized, it'd still be a valid architecture to use for other benefits it provides (mainly gameplay-implementation related).

It is very likely to be overkill for a small 2D game, but that "overkill" isn't necessarily wasted if you are using the small game as a learning experience for how to code better game architecture.

About the slow initial development, that's a downside of many larger architectures: they depend on you implementing alot of code at once before it starts working as intended. Sometimes mock objects help get things up and running quicker, as replacement dummy code or as a temporary rough-and-dirty but real implementation for chunks of the architecture that's not yet actually written. Slightly related: Tracer bullets (and the difference between Prototypes and Tracer bullets).

This topic is closed to new replies.

Advertisement