Вопрос

I have been reading a lot about different design patterns, their pros, cons, main usage etc. However, I like to experiment and try to invent my own ways of the implementations (even if they are not the best). Now I am facing a problem with my Entity component system implementation.

Despite of the pros of this pattern I want to do some of the things in my own way, for example:

1.) Components are wrapped into bigger interfaces for data manipulation. In most of the cases i read that ECS components must contain only data and the systems are their main manipulators. In my system I intend to use a wrapper classes for the components

Little example: Lets say we want to have a Sprite component, that represents the visual component of each entity in game. I wrapped it inside the RenderableEntity class that can be a component of an entity and has additional functions and variables.

2.) I don´t use a system for every type of component.

Again an example from game engine : We have a CollidableEntity, that is put in some spatial data structure and doing its collision checks, but for RenderableEntity there is no system that manages it (I assume that we are in 2D now). Reason? I want the user to have the power of rendering the sprites of each entities, because of specific order or using different batches.

3.) Entities can have more than 1 component of the same type and no bitsets are used for checking if is the system interested in given entity. The components are assigned to systems at their creation via the data type.

Little code:

template<class DataType>
DataType* createComponent(GameEntity* mainEntity)
{
    //Create it via default constructor
    DataType* component = new DataType();

    //Add it to the back of the component vector
    mainEntity->_entityComponents.emplace_back(component);

    //Assign it to system
    ECS::assignToSystem<T>(component);

    //Return it to access its functions
    return component;
}

The assignToSystem function does following : checks if is the given data type in our map and if not, it adds the component to the system and from now is the system the maintainer of the component. Also the gamesystems are attached to given data types via a function and it looks like this :

class ColliderSystem:public ECS::System
{
    void assignSystem() override
    {
        ECS::assignSystemToComponent<CollidableEntity>(this);
    }
};

Can you tell me if its a good idea to experiment with given patterns and try to do things your own way? Especially I care about this because want to use this approach in my graduation work and wonder if it will be a problem if I "break rules" of some patterns. Also , what do you think about my "changes", are there any cons? Thanks.

Это было полезно?

Решение

People have experimented with different mechanics of implementing a design pattern in the past with differing results. For example, the definition of a singleton is that there is only one instance of a class for your entire application. There is nothing in that definition that requires you to implement it with a static accessor, even though that's how the example in the GoF book is written. Folks that use components like the Spring framework let the component container guarantee that there is only one instance and a reference to that instance is injected everywhere you need access.

That example leads to a few points:

  • If what you are doing is fundamentally different from the pattern, call it something different. There is nothing worse than two people using the same words that are talking about different concepts. Nothing but confusion happens after that.
  • If what you are doing is following the pattern, make sure your differences are actually solving a problem. Being different just to be different is not helpful 9 times out of 10.
  • "playing" with implementation details can help you understand the pattern better, and why the example code looks the way it does.
  • Mixing all the patterns into one project leads to an incomprehensible mess. Just use what you need to use.

At the end of the day, no user is going to praise your judicious use of the flyweight pattern or even know that you used a decorator pattern in there somewhere. Patterns are called that because they are common solutions to common problems that happen to have very similar implementations. Many times the details of the implementation are less important than the concept that inspired it. Just make sure that your code is comprehensible to someone else, or even your future self 3 months down the road.

I'm not going to get preachy, but I will encourage you to look at the impact of what you are doing as objectively as you can. Users only care that the application/game/tool/etc. works. Developers on your team care about whether they can understand it and fix it when they find things that are broken. Every time you deviate from the norm, it increases the amount of time it takes to understand the code. Make it annoying enough and your development team will rewrite everything so that it is more familiar.

Другие советы

No.

Design patterns are not building blocks. If you are trying to 'experiment' with them, you are most likely trying to apply them in situations that don't make any sense.

Instead, you should experiment by trying to build a wide variety of programs that solve a wide variety of problems. In doing so, you'll find various techniques you used in some programs have similarities to other programs, and can be applicable to both. Those similarities are where the concept of design patterns arose, but not as building blocks, but as terminology.

Unless you know the problems the named patterns were recognized to be useful in solving, you won't be able to recognize them and know what pattern is useful. The only way to learn those problems is to tackle a wide variety of problem domains.

I'll Put it another way

Design patterns are like the software equivalent of joist hangers. Joist hangars are forged metal pieces you use to attach framing members of structures together. Attaching joists to framing is a commonplace activity, and for years everyone would just do it however, until some enterprising individual realized it was a common activity and built hardware to quickly and effectively do the job.

If a carpenter on the jobsite asked "is it a good idea to experiment with joist hangers?" they'll get hit over the head with a 2x4. Like design patterns, joist hangers are a means to an end, and it is important to understand what their purpose is. You don't create a design so that you use joist hangers, you instead use the hangers because they're useful in creating strong structural joints.

A complete knowledge of the Simpson Strong-Tie catalog would be useless to someone unless they can already visualize most of how a wooden structure comes together in the first place. Likewise with software design patterns, you must first already be capable of envisioning how to build a complete solution first.

Patterns are a help to reach your goal by applying a design approach that proved to be suitable for achieving a specific intent .

Of course, you are free to experiment:

  • Very often, looking closer at the new design, you may find out that it's a combination of several existing patterns.
  • Sometimes, though, you might discover the hard way that your new invention has some hidden flaws. Typically, it's additional, undesired dependencies.
  • But, sometimes you'll really have something new and suitable.

Be careful however : graduation work requires discipline and precision. So don't call your new pattern like an existing pattern : reviewers might interpret this as insufficient knowledge rather than creativity.

So if you've something new, give it a new name and expose it in your graduation report using a proven design pattern template such as:

I'll tackle the ECS-specific parts of your question. There are already great answers for the more generalized question.

Components are wrapped into bigger interfaces for data manipulation. In most of the cases i read that ECS components must contain only data and the systems are their main manipulators. In my system I intend to use a wrapper classes for the components

I did the same thing originally when I started because the most alien aspect to me of ECS which challenged all my notions of OOP like information hiding and encapsulation was the idea that components were raw data. I even started off doing things like having some components inherit from an abstract interface and still trying to apply things like polymorphism at the component level.

As my system evolved, I actually found that to be increasingly of a hindrance. My systems wanted access to component internals in low-level ways to be able to plow through them and transform them in parallel and likewise transform them individually with scalar logic. I also increasingly realized I didn't need polymorphism through inheritance. An ECS basically works through duck typing of the same level of flexibility you have with C++ templates, not requiring any formal inheritance hierarchy to be defined. If something has a motion component, then it can move.

You can reduce the scope of component fields visible to systems and still achieve information hiding that way by limiting the number of systems that can treat a component as anything more than opaque in order to maintain invariants. For some components it might only make sense for a single system to be able to modify their state. You can enforce that policy in ways besides turning your components into full-blown classes with separate public and private interfaces and fields.

A lot of people don't talk about this aspect of ECS but the biggest benefit of ECS to me personally was actually in simplifying my system. It's so much easier to reason about an engine with 20 systems, even if they have somewhat complex implementations behind them, than a system with a hundred types of smaller things with functions inside of them interacting with each other, even if those functions are simpler. If you plot out the dependencies in an ECS, it's laughably simple with just arrows pointing from systems to components, not components to other components and components to systems. You lose a lot of those benefits if your systems aren't the only ones with the functionality.

2.) I don´t use a system for every type of component.

That's a natural tendency if you are tempted to provide functionality in your components. Now they are mixing up the responsibilities of systems to some degree. In your normal ECS, the systems are the only ones that provide functions and for reasons you can come to appreciate if you end up following the same path I did described above.

3.) Entities can have more than 1 component of the same type [...]

Again I had a similar temptation. However, that makes it so you can no longer effectively express your operations so clearly based on component type. I can no longer say, "Give me all the entities in the system that have a bone and motion component." At the very least I'd have to change the mindset more to like, "*Give me all the entities in the system that have at least one bone and motion component."*

While this might not seem like a big deal, it multiplies complexity in a way where you have to separate the idea of a bone component instance from bone component type in terms of entity-related operations. All systems then have to work with the assumption that there could be a variable number of components of every single possible component type out there attached to any given entity. I found this complexity to be more trouble than it is worth because the design complexity spreads to every corner of each system with endless questions being raised like, "What should a physics system do with entities that have 2 or more motion components?". Even if you can come up with a simple answer to these questions, the fact that such a design raises so many questions (both user-end and technical) with every single system is a sign to me that it is a can of worms.

Instead you can actually make specific component types themselves into aggregates, like a skeleton component which consists of one or more bones as opposed to a bone component. Instead of generalizing your system to allow as many component instances of the same type to be attached to any single entity, you can have component types which are aggregates themselves.

Anyway, you might be able to come up with something nice for your purposes by deviating like this, but know that these patterns did not arise arbitrarily. People have likely explored the same alternative ideas you've had only to settle on the ECS approach as it is widely applied now, and while I also had a difficult time appreciating it in the beginning and had similar temptations you had, I now, in hindsight, would not do things any other way.

[Is it] a good idea to experiment with given patterns and try to do things your own way?

Patterns exist for two reasons: (1) they make you solve a specific problem, (2) in a way that makes it easy for others to understand what is happening in your code. By changing an existent pattern, you go against those original reasons.

Let's take an example. I need to be able to run a piece of code if a condition is true, or another piece of code if the same condition is false, unless I'm in a specific case, in which case I want the third piece of code to run. Those pieces of code perform different things, but their interface is identical.

I can use a strategy pattern for that. I can crate three classes with the pieces of code, and I can name them *Strategy. The next time another person is reading my code, he immediately understands that I'm using a strategy pattern.

Now, imagine that those pieces of code don't have identical interfaces. I can:

  1. Avoid using anything which looks like the strategy pattern.

  2. Make the code look like using the strategy pattern, but choosing a different name.

  3. Make the code look like using the strategy pattern and still use *Strategy suffix.

The last case is the worst. It makes my code error prone and particularly unclear. Never do that. The second case is slightly better, but still can induce a reader in error of thinking that it's a strategy pattern. Proper naming convention could help preventing this sort of mistakes. The first case is still the best; the reader knows that this is custom code, and doesn't expect to find any specific pattern.

So, if you modify a pattern:

  • Do never use the name of the original pattern.

  • Make sure it doesn't look like the original pattern, i.e. that your modifications are substantial.

Especially I care about this because want to use this approach in my graduation work

If your teacher expects you to use existent patterns, modifying them would only give an impression that you don't understand or haven't learned the patterns in the first place. So, here again, you'd better avoid changing the patterns.

Лицензировано под: CC-BY-SA с атрибуция
scroll top