Guidelines for determining what should be a component

Started by
38 comments, last by Zipster 6 years, 7 months ago
On 08/09/2017 at 9:25 PM, MarcusAseth said:

Also if possible some stuff on the line below, like even only what I should be google-ing for in order to understand how that code is working



entity.GetComponent<Move>()

 

Outside of ECS circles, this is known as the service locator pattern (each entity is a "service registry", to use the standard jargon). A search for that name will bring up lots of articles. It's widely regarded as an anti-pattern though, as it obfuscates dependencies and communication flow... 

It's somewhat useful in high level gameplay code as a mechanism for  sending abstract messages - e.g. "you've been hit by poison, if any part of you has a response to poison (implements IPoisonable?) trigger now"... Otherwise it's got similar restrictions to naming all of your member variables the same as their class name, which is dumb.

Advertisement

Thanks @Hodgman, I'll look that up :P

I assume anti-pattern means "avoid using this", no worries I don't plan too, I just want to understand what the unreal engine is doing from the code side when I see stuff like that :P

 

5 hours ago, MarcusAseth said:

I assume anti-pattern means "avoid using this"

It normally means something like "this is often over/mis-used". You are probably better off avoiding things that are considered anti-patterns, but most of them do have valid uses if you understand them properly and approach with caution.

If, as a beginner, you don't understand the reasoning against an anti-pattern after reading a few explanations, and you think they're the right approach to your problem, I normally recommend just going ahead - sometimes you just need to experience the issues yourself to really understand. (Assuming of course you aren't working on something important or for which an employer or teacher has forbidden the pattern.)

- Jason Astle-Adams

15 hours ago, Zipster said:

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.

You misunderstand. I'm not talking about accessing any entity from anywhere, I'm talking about accessing any component from anywhere, given an entity. In your last post you posted code that does this:


entity.component<Position>()

My argument is specifically against having any code like this. I'm saying that making arbitrary components accessible through the entity is "like" having global state, because there isn't any access control at the component level if you have the component's entity. By all means, write code that doesn't touch the entity itself. Even if you do your best to write code that doesn't touch the entity yourself, someone else working on your code will abuse this mechanism. Forcing them to declare their dependencies up front (or declare that they can depend on everything) discourages that kind of abuse. I speak from experience of what codebases turn into ten years of crunch later when given an object identifier you can access any part of its state.

I am generally against the service locator pattern on similar grounds.

15 hours ago, Zipster said:

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.

You're right, I don't care that I have the component's type (although not having it can help discourage bad behaviour), I care that I can query any component state given an entity ID. With the code as you've written I can access any kind of component from anywhere that can access an entity. So then when I'm trying to follow code that takes an entity, I have to assume that it could access anything, even component state for which it doesn't include the header, since the code could access the component and pass it on to something else without touching it directly.

15 hours ago, Zipster said:

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.

It changes where we get the data from. With what you posted, the data is coming from one central (but obfuscated) location that's accessible to anything that has an entity - because the entity itself is the access point. With what I posted, the data is coming from the place where it is actually stored and has to be passed down explicitly. Having an entity doesn't give you access to its components unless the code calling your code says you can access the components.

15 hours ago, Zipster said:

I can include the appropriate header file and update the specification at-will to accommodate my new component dependency, and nothing can stop me.

Well... yes, exactly! That's kind of the point. You now have to stop and add the dependency explicitly, forcing you to think about the dependencies you're adding and whether your code now has too many and needs to be refactored. Plus your successors will thank you for making the dependency obvious from just looking at the function signatures...

15 hours ago, Zipster said:

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.

Yes, that's exactly what I'm saying. I did use the phrase "component types," but I only meant that in the informal sense. Perhaps I should have been more precise. :D 

15 hours ago, Zipster said:

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.

Indeed. In fact, I'm specifically advocating a approach to program structure which lets you take advantage of those things by allowing for cases where component symbols aren't even visible to code that doesn't deal with them directly. How does the Unity-style ECS where the entity is the access point for components allow you to do that?

15 hours ago, Zipster said:

Why does it matter if arbitrary components types are exposed to arbitrary code?

You seem to have gotten hung up on my use of the term "component types." The thrust of my point was that we shouldn't expose arbitrary component state to arbitrary code. The approach I suggested allows us to not expose the component types in some cases, as well, which enhances the effect but isn't the point.

On 10/09/2017 at 3:09 AM, MarcusAseth said:

Thanks @Hodgman, I'll look that up

I assume anti-pattern means "avoid using this", no worries I don't plan too, I just want to understand what the unreal engine is doing from the code side when I see stuff like that

 

The Unreal Engine is an unholy mess, so shouldn't be taken as an example of a good way to structure software.

However, it is a working mess, and a broadly understandable mess, and the UE4 component system - like the Unity component system - gives you the tools to make working, shippable games. Yes, there are big downsides to the entity.GetComponent<Whatever> approach. But it's readable and self-explanatory.

The more abstract component-oriented designs are used in very few games, and typically only by very skilled and experienced developers. If you try writing your first or second large game that way, you're guaranteed to spend all your time thinking about how to structure things and no time actually making your game. There's a reason why there are very few examples of comprehensible code in this style.

 

@Kylotan 

4 hours ago, Kylotan said:

The more abstract component-oriented designs are used in very few games, and typically only by very skilled and experienced developers. If you try writing your first or second large game that way, you're guaranteed to spend all your time thinking about how to structure things and no time actually making your game. There's a reason why there are very few examples of comprehensible code in this style.

I'm really finding this to be quite true so far. Everytime I think I've grasped a concept and implemented things well I run into another 'design decision' I have to make no to far down the line. For anyone considering implementing this architecture I will give a quick overview I've what I've gone through so far. For example, at first I had decide my basic component architecture. Since I will only have two characters on screen with a background image (for a 2D fighting game) both characters will be almost identical in their makeup. So I have components that are statically added to a 'Fighter' class like so:


class Fighter :
    public ComponentHolder<Comp::Transform>,
    public ComponentHolder<Comp::Velocity>,
    public ComponentHolder<Comp::SpriteTileSheet>,
    public ComponentHolder<Comp::Animation>,
    public ComponentHolder<Comp::Input>
{
public:
    Fighter() = default;
    ~Fighter() = default;

    template<class T>
    const T GetComponent() const;

    template<class T>
    void Insert(T comp);
};

template<class T>
inline const T Fighter::GetComponent() const
{
    return this->ComponentHolder<T>::component;
}

template<class T>
inline void Fighter::Insert(T comp)
{
    this->ComponentHolder<T>::component = comp;
}

My Component holder class is just a class which contains a single protected templated 'T component' member which I can access via 'Fighter' class. This provides a quick way to get and insert components. Okay that part done. Then, not too far down the line I discovered the issue I wrote about for this thread, which was how I'm suppose to decide what should be a component and what shouldn't? How granular do I need to be? After coming to the conclusion that I shouldn't be making things to granular and should think of broader components such as animation component and Input component, I ran into another issue of how components are suppose to talk to and use one another. This is where some suggestions came in of creating one or two explicit dependecies inside components if you need to or just parameterize components within component systems (which are just functions) which will help provide the glue to combining components, or both I'm sure in some cases as some components will probably need information provided by others.

Even after all that there are still many design issues that come up almost every day on how to implement this custom ECS like architecture. Obviously design is important in software, but sometimes the design might just be way to complicated and unnecessary for a lot of non-triple A style games. So, hearkening back to what @Kylotan , as well as a few others throughout this post, has said I wouldn't recommend trying to implement this sort of architecture in any serious game you want shipped without already having a lot of experience with ECS  in the past and having mentally resolved all of the issues mentioned above. Obviously if you're just trying to learn to develop an ECS like architecture and see how exactly it can be used in games then definitely go for it as you will learn a lot about the backbone of some of the major game engines such as Unity and Unreal. 

3 hours ago, boagz57 said:

So I have components that are statically added to a 'Fighter' class like so:



class Fighter :
    public ComponentHolder<Comp::Transform>,
    public ComponentHolder<Comp::Velocity>,
    public ComponentHolder<Comp::SpriteTileSheet>,
    public ComponentHolder<Comp::Animation>,
    public ComponentHolder<Comp::Input>
{
public:
    Fighter() = default;
    ~Fighter() = default;

    template<class T>
    const T GetComponent() const;

    template<class T>
    void Insert(T comp);
};

template<class T>
inline const T Fighter::GetComponent() const
{
    return this->ComponentHolder<T>::component;
}

template<class T>
inline void Fighter::Insert(T comp)
{
    this->ComponentHolder<T>::component = comp;
}

My Component holder class is just a class which contains a single protected templated 'T component' member which I can access via 'Fighter' class. This provides a quick way to get and insert components. Okay that part done. Then, not too far down the line I discovered the issue I wrote about for this thread, which was how I'm suppose to decide what should be a component and what shouldn't? How granular do I need to be? After coming to the conclusion that I shouldn't be making things to granular and should think of broader components such as animation component and Input component, I ran into another issue of how components are suppose to talk to and use one another. This is where some suggestions came in of creating one or two explicit dependecies inside components if you need to or just parameterize components within component systems (which are just functions) which will help provide the glue to combining components, or both I'm sure in some cases as some components will probably need information provided by others.

That's... not ECS. That's an over-complicated way to do regular compile-time composition. I mean, what you're doing is pretty much exactly equivalent to this, albeit with some extra template boilerplate to access components by type (which you don't actually need if you're using static composition, you can just refer to the member directly):


class Fighter
{
public:
    Fighter() = default;
    ~Fighter() = default;

    Comp::Transform transform;
    Comp::Velocity velocity;
    Comp::SpriteTileSheet spriteTileSheet;
    Comp::Animation animation;
    Comp::Input input;
};

You still need to decide on component granularity, but if all the components are part of the same object (ie. you aren't using an architecture that supports "discontinuous composition", like ECS) certain design decisions become easier.

13 hours ago, boagz57 said:

@Kylotan 

 

I agree with @Oberon_Command, the code you have there is over-engineered for no apparent reason, except maybe to avoid the composition of just having the components as explicit member variables.  The code he gives is pretty much functionally the same, but a much simpler implementation without all the templatized nastiness.

My recommendation when approaching this is to write the code as simply as you can and not worrying all that much about all the things that you might do in the future, or all the horribles that are being presented to you (if you dont fully understand them).  Just think of what your needs are, then think what the most obvious and clearest way you can come up with to implement it, then implement it.  As you progress you will find new things you need, or roadblocks that require architectural changes... and when that happens you can stop and do the re-architecting.  You will learn by doing that, and having a simple implementation to begin with will make all that easier... rather than having to undo a lot of over-engineered stuff that you put in just because you thought you'd need it.  Simple mistakes are way easier to correct later on than complex ones, I guarantee you.

I see two easy approaches for you here, 1) do like Oberon_Command suggests and just use simple composition, or if you need to create components in some editor and you cant hard-code the components, then 2) just have an abstract base class for the components and then have the entity keep a list of those.  Of course then to get pointers to the components you'll need to go through the list, but that should be fine for you.

@Oberon_Command

You're getting caught up on a specific detail of my example code that's really besides the point I was trying to make. Appropriate scoping and visibility of code and data is important, but an unrelated issue.

I'm warning against baking too much behavior into the components such that they become difficult to re-use. This is from my own experience working on codebases with large ECS frameworks -- there's nothing more frustrating than having to add a flag to a large component that mutates parts of its behavior because someone thought no one would ever use it for any other reason or purpose beyond what they originally envisioned. Or having to choose between writing a one-off throwaway component with duplicate functionality, versus trying to refactor out the needed functionality from another component into a third, shared component. And on top of it all, dealing with the code coupling issues when it's time to writes tests or split apart components into separate libraries. This is the real, in-the-trenches work you inherit when using an ECS. Ultimately it can't always be avoided, since behaviors change with requirements, but the better the choices you make up front, the less headaches you'll have later.

But to be perfectly honest, I have my own radical views on what an ECS abstraction would look like (who doesn't, right?) that don't really align with any of the articles or discussions I've seen, so perhaps that taints my opinions on the subject...

This topic is closed to new replies.

Advertisement