18 hours ago, 0r0d said:
There's really nothing wrong with accessing components directly. It all just depends on what your architecture is trying to accomplish and how it's doing it. In fact, the entire point of an ECS is that all (or most of) your functionality will be components. So, for code to do anything useful it has to deal with components. What's the alternative? To put functionality into the entity and have that redirect calls to the appropriate component? That's just going to give you unneeded complexity and problems. Usually, code shouldnt even need to deal with an entity, since what it should be doing is dealing with the appropriate components. The entities should mostly only be there as a way to get associated components.
The ECS pattern enables you to express the behavior of entities as a combination of modular components. It doesn't mean the business logic driving this expression is actually contained within the components. If anything, such an approach immediately breaks the modularity and encapsulation you're trying to achieve and can quickly lead to unmaintainable spaghetti code.
Take your AI component and render component as an example. There is now a specific behavioral and component dependency built into the AI component. It's impossible for higher-level code to use the AI component without inheriting this internal behavioral interaction with the render component. The fact that internal ECS behavior can't be decoupled from external game behavior should raise a red flag.
Let's also consider unit testing for a moment. How would one unit test the AI component independently of the render component? The fact that it's impossible should immediately raise another red flag.
There's also the issue of the code itself being coupled at the build level. Your AI component code must now be linked with the render component code. It doesn't matter if the final entity actually has both components. You've created a permanent symbol dependency that must always be resolved either at build-time or load-time (i.e. by the dynamic linker). It might not be a big deal now, but good luck trying to split your components into modular, reusable, domain-specific type libraries.
21 hours ago, 0r0d said:
The problem with the code above is not that you're exposing functionality, it's that you have 2 different components that have a position. Why is that? Of course if your components are not orthogonal you will have problems. There should be no need to set a position on the Model component because it shouldnt have a position in the first place. If it wants to render, then it should get the Position component by going through the Entity.
Data duplication was a bad example on my part. This is more appropriate to the discussion:
entity.component<Position>().set(10, 10);
if (entity.has<Networking>())
entity.component<Networking>().markDirty<Position>();
Two completely unrelated components that share only a behavioral relationship (notify networking when position changes)
20 hours ago, Oberon_Command said:
I'm not at all a fan of this approach, or any approach where anything can query any entity for any kind of component from anywhere. It's not far removed from just having global state all over the place. If it's done poorly, it makes for shitty build times, too (if you're using C++).
There's nothing in the code to suggest that any entity can be accessed from anywhere. It's precisely the opposite, where the "move" method accepts only the bare minimum data it needs, and doesn't access any global state. If code shouldn't have access to a particular piece of data (such as an entity ID), then hide it. Data hiding is a separate issue entirely that can be solved using other well-known methods.
The fact that you can query any component type is neither here nor there. If code doesn't have an actual entity ID or other relevant data to work with (because you appropriate hid it), then the knowledge of those types does nothing for you.
20 hours ago, Oberon_Command said:
An advantage to having "entities" be only simple identifiers - handles, really - is that you can make some component state inaccessible to other components and when you do need that access, the dependency is obvious because you have to tell the code that wants access where to find the components. That makes the code more verbose, but that's a feature, not a bug. Requiring explicit dependency specification is a feature, not a bug. Without that, it can become difficult to reason about what depend on which components. An inability to reason about dependencies can lead to a maintenance nightmare.
With a system where entities are only handles, I can make component types that only the code that cares about them needs to care about and as long as I ensure that some kind of mapping from entity to component state exists, I can arrange both the components AND the entities (eg. lists of entities by value!) in whatever the most useful way happens to be for what I'm going to do with them. I don't even need to allow anything outside whatever system deals with that component know what that component is - translation unit-local components can be a thing! For that matter, "components" don't even need to be monolithic objects, they can be "structures of arrays" or even more exotic things as long as there's a consistent interface presented to them that uses the entity handle.
Having entities be identifiers versus objects has nothing to do with component accessibility or dependency specifications. It's also not relevant to the discussion. We could just as easily assume that the entity object in my examples was actually a thin handle/wrapper about a functional interface that stores components in "structures of arrays" (or any layout of your choosing). It doesn't change anything, as it's functionally equivalent.
The only way to truly "hide" a component (or any code for that matter) is to make its symbols inaccessible. This is ultimately determined by how you encapsulate and layer your software into "black boxes", not by any particular implementation. Explicit declaration of dependencies is a nice touch that helps code be more self-documenting and enables certain optimizations in the ESC implementation, sure, but it's useless as a mechanism for controlling type access. I can include the appropriate header file and update the specification at-will to accommodate my new component dependency, and nothing can stop me.
Trying to arbitrarily limit or control type access from code pointless. All you can do is control data, but that's entirely sufficient for all intents and purposes.
21 hours ago, Oberon_Command said:
Whereas in order to make this Unity-style ECS work, you're likely going to need some kind of mechanism to expose arbitrary component types to arbitrary code, which adds more complexity and probably global(ish) state (that can then be abused). Then to make has<T> and component<T> work you probably ALSO need some kind of RTTI mechanism for your components, which extra complexity that isn't always actually necessary for anything other than the code that glues the components together.
These mechanisms already exist, and you use them all the time. Code access and visibility is controlled by its structure and layout, the use of public vs private header files, visible vs hidden symbols in shared libraries, etc. Splitting software components into black boxes that can communicate through public interfaces is nothing new. However that's besides the point. Why does it matter if arbitrary components types are exposed to arbitrary code? Going back to what I said previously, without real data it's a moot point.