Jump to content
  • Advertisement
Sign in to follow this  
Midori Ryuu

Composition heavy OOP vs pure entity component systems?

This topic is 2203 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Advertisement

[quote name='Midori Ryuu' timestamp='1345442448' post='4971354']
I would have a small tree for vehicles. The root vehicle class would have a rendering component, a collision component, position component etc..

Then a tank, a subclass of vehicle would inherit those components from it, and be given it's own "cannon" component.
This is still an abuse of the OO concept of inheritance. You could compose your tank out of a vehicle and a cannon -- e.g. struct Tank {
Vehicle vehicle;
Cannon cannon;
};

[/quote]

How is this different from adding the vehicle's components inside the tank through inheritance and then adding a cannon component? They really seem the same to me. Will it cause the same problem as the one in your second example?


The same goes for the Characters. A character would have it's own components, then the Player Class would inherit it, and be given an Input controller, while other enemy classes would inherit from the Character class and be given an AI controller.
As an example of how inheritance is causing unnecessary restrictions here -- what if you want the ability for the player to take over an AI character, and let the AI take control of their previous character? Decoupled composition allows for this:struct BaseController
{
Character* controlled;
}
struct PlayerController : BaseController
{
InputDevice inputs;
void ReadInputsAndControlCharacter();
};
struct AiController : BaseController
{
CharacterAi brains;
void UpdateBrainsAndControlCharacter();
};
void SwapControl( BaseController& a, BaseController& b ) { std::swap( a.controlled, b.controlled ); }
N.B. in the above, CharacterAi, InputDevice and Character are completely decoupled - they don't know about each other at all. The player and AI controller classes form a 'bridge' between these decoupled components -- reading intentions from inputs or AI and passing them on to the character interface (and reading character state ans passing it to the 'brain').
[/quote]

I was actually thinking about similar problems when I posted. I thought of doing some sort of duck-typing in a scripting language to solve this, but I didn't have an actual idea how to. The way you implemented the controllers is much more elegant. I assume this removes the need for a player and an enemy class and the distinction will be made at runtime when the components are assigned?

If so, then the will the character class have a polymorphic reference to the BaseController which can be either an instance of PlayerController or AIController?


[edit]Sorry for the C++ examples
[/quote]
It's perfectly fine as long as you excuse my inexperience with C++, as I may have understood the example you gave a bit wrong.. >_< Edited by Midori Ryuu

Share this post


Link to post
Share on other sites

[quote name='Midori Ryuu' timestamp='1345448868' post='4971370']
How is this different from adding the vehicle's components inside the tank through inheritance and then adding a cannon component? They really seem the same to me. Will it cause the same problem as the one in your second example?
In practice, in effect, there's no apparent difference... but you need to distinguish between OOP and OOD.
Lots of people use inheritance when writing OOP code out of convenience -- "I want all this stuff in my new class, so I'll inherit it" -- n.b. this same goal can be achieved via composition or inheritance.
However, when designing in OOD, the concepts implemented in OOP languages have very specific meanings and using these concepts implies that you're promising to follow certain rules -- e.g. making a class implies you'll follow the SRP, and inheriting a class implies you follow the LSP.

All these inflexible, monolithic, inheritance-based designs of the past have be born out of teams who understood OOP, but not OOD. That's fine -- the majority of code written in OOP languages probably isn't following OO rules at all, and at the same time, OO designs can be implemented in non-OOP languages, such as C... but it's important to note the destiction between the OO theory of design, and the OOP features in certain languages. They're meant to go together, but don't have to.

Back to the choice of inheritance vs composition here though --
1) The main difference in this case is that with composition, Tank only gains access to Vehicle's public interface, whereas with inheritance, Tank gains access to it's protected interface. This is probably bad as it increases the potential to extra unnecessary coupling.
2) If using inheritance, it would also be valid for Tank to inherit from Cannon, and then compose in a Vehicle... so how do you choose which one is right? If it's an arbitrary choice between the two, then it seem that inheritance is unnecessary, and should probably be avoided. If the answer is to inherit from both, then what happens when you want a tank with 2 cannons, or if you make a transforming super-tank made up of 2 vehicles?

Common wisdom is that you should default to composition for code re-use, and only use inheritance where necessary.
[/quote]

The problem is I am trying to keep the layout of my code intuitive (to me at least). As in, a tank "Is a" vehicle but "has a" cannon. So that is why I opted to go with limited inheritance in some places, despite composition being a better solution, since the damage that it might cause will be rather small in comparison to large inheritance chains.

I do agree with you that it's better to compose here, but I kind of feel that as long as I don't allow the inheritance to expand more than 2-3 levels deep it's an acceptable tradeoff to the advantages given by composition. Then again, it is the (bad) OOP thinking that's immersed in my head thinking that, and the examples you keep giving are really helping to dispel that kind of thinking (Transforming super-tank made up of two vehicles! tongue.png)

It will be a pain, and probably cost me more development, but I will try to do things the correct way this time, since I know that once I do things the right way, it will be much easier doing it the right way the second time, and third time and so on.



If so, then the will the character class have a polymorphic reference to the BaseController which can be either an instance of PlayerController or AIController?
I had it the other way around, where Character is unaware of the controller classes at all, and the controllers have a polymorphic reference to the character.

You should design your code in layers, where each layer only uses classes in layer below it, or the same layer. You should also generally avoid designs where two classes both know about each other, i.e. dependencies should almost always be 1-way, not cyclic.
For example, C is in the top layer and knows about the public interfaces of A and B. A+B are in the layer below, and each have some complex private details that are wrapped up in a simpler public interface. Also, A and B don't know about each other at all - they're decoupled, which is good; they rely on the next layer of code to glue them together in some useful way. See also the Law of Demeter on principles about loose coupling - you generally want each class to have knowledge of as few other classes as possible, and use sensible layering to glue different bits together into a useful whole.
In this case, the controller is in the top layer ©, and AI and characters are in the bottom layer (A & B). C makes use of A&B's public interfaces, inside it's private details, and then wraps this behaviour up in a new public interface (which might be used by another layer above this).
[source] +-----------+
| public |
| interface |
|-----------|
|Component C|
|-----------|
| private |
| details |
+-----------+
| |
| |
\|/ \|/
+-----------+ +-----------+
| public | | public |
| interface | | interface |
|-----------| |-----------|
|Component A| |Component B|
|-----------| |-----------|
| private | | private |
| details | | details |
+-----------+ +-----------+ [/source]
[/quote]

Ah yes, that makes a lot more sense, since the player might be in control of many characters, and it also enables the AI to reason in term of groups (cooperation, etc..) instead of just simple characters. That and once the lower layers are taken care of, I assume programming the game and scripting will be a breeze!

On another forum where I asked this question it was also pointed how I the setup I made would have caused, for example, the coupling of the characters and rendering to be coupled, which will come in the way in case of a server-client architecture is done for multiplayer.

By following your way the rendering will be decoupled from the characters and allows for client side rendering only.

Thank you for the time you're taking to answer my questions and all the free tips you're giving us!



Just a small question though, (I know it's example and implementation specific and doesn't have much to do with the thread but) why is the AI in the layer below the controller? Is it because it will ask C to provide the public interface of B?
You say that I should talk to the below layers, and the dependency should be 1 way, but won't this be better achieved if the A.I is above the controller for it to be able to control the characters, tanks,etc.. through the Controller which provides a public interface wrapping the lower layer?

Thank you again for your patience to my noobiness! Edited by Midori Ryuu

Share this post


Link to post
Share on other sites

Thank you again for your patience to my noobiness!


Just to intrude for one moment: your posts are well formed and ask good questions. Frankly, most professional programmers I've worked with (albeit in non-gamedev) wouldn't even think to look into how to design things better; let alone speak intelligently about how/why they did their design a certain way.

Being humble is good, but make sure your self-evaluation is accurate too.

Share this post


Link to post
Share on other sites

I do agree with you that it's better to compose here, but I kind of feel that as long as I don't allow the inheritance to expand more than 2-3 levels deep it's an acceptable tradeoff to the advantages given by composition.

I tend to think that more than 1-2 levels is questionable. Where 1 level obviously means you're extending an interface or abstract base class and 2 levels typically means you've introduced an abstract helper class that implements some or all of an interface and maybe applies the Template Pattern to simplify things for subclasses. Any deeper and it starts to smell and I begin to wonder whether something has gone wrong.

Obviously "is-a" is itself a multi-faceted concept and I have long since decided that using inheritance to model the structural or logical is-a relationships is not the way to go. These days things like LSP teach us that you should really just use it for just the behavioural is-a relationships, in other words whenever you need polymorphism.

Some concrete examples:

Tank is-a Vehicle (logical)
Duck is-a Bird is-a Animal (logical)

Square is-a Rectangle (structural)
RoomWithTwoDoors is-a RoomWithOneDoor is-a Door (structural)

Tank is-a GameEntityEventListener (behavioural - tank will polymorphically respond to game events such as onUpdate)
XMLParser is-a TextParser (behavioural - subclass implements parse(String text))

(Note: Please don't think that my distinction between structural and logical is-a relationships is a formal one, it's more intuitive than anything).


So I would say there are two okay reasons to use inheritance, first and foremost is this:

1) Use inheritance to gain polymorphic behaviour (but if you need polymorphic behaviour, otherwise you're over-engineering).

The second I permit for pragmatic reasons in languages that don't support mixins as a first-class concept:

2) May use inheritance as a way to mix-in or reuse an implementation of a public class interface.

The key part to point 2 is the word "public", if you're using inheritance just to gain reuse of private code then it's likely you've modelled a structural is-a relationship (which is bad) and is also subject to Hodgman's explanation given earlier about whether Tank should inherit or compose Vehicle.
Point 2 is easily abused though, think of it simply as a get-out-clause for when it's pragmatic :-)

The logical relationships in particular may well fall out naturally anyway - you could imagine renaming GameEntityEventListener into simply Entity and then you have achieved both logical and behavioural subclassing, in this case it is okay since it was the behavioural aspect that you designed for and the logical aspect is purely cosmetic but aids in your comprehension.

The real point is that inheritance is not being used to model real-life, or mimic some scientific classification of types - in software it is merely a tool that allows us to separate specialised implementations from generalised implementations from interfaces. Edited by dmatter

Share this post


Link to post
Share on other sites
Square is-a Rectangle
Sorry to use this as a good example of how easy it is to violate the LSP ph34r.png
If the Rectangle interface is as below, the it's safe to say that "Square is-a Rectangle":
class Rectangle {
virtual int Width();
virtual int Height();
};
...but, if the Rectangle interface looks like this:
class Rectangle {
virtual int GetWidth();
virtual int GetHeight();
virtual void SetWidth(int w);
virtual void SetHeight(int h);
};
...then now "Square is-a Rectangle" is no longer valid! This is unintuitive, because logically/mathematically "a square is a rectangle" is true!
To explain: LSP basically says that any operation that works on the base class, must continue to work as expected if you substitute in a derived class -- and the following function can be expect to work on Rectangles, but will fail for Squares:
void TestRectangle( Rectangle& r )
{
r.SetWidth(5);
r.SetHeight(10);
int area = r.GetWidth() * r.GetHeight();
ASSERT( area == 5*10 );
}


Just a small question though, (I know it's example and implementation specific and doesn't have much to do with the thread but) why is the AI in the layer below the controller? Is it because it will ask C to provide the public interface of B?
Yeah any specific example is going to have to come with specific requirements, etc... In my example, I was assuming that:
* the AI code can all be self-contained, with it's user/owner passing it information about where it is, etc, and retrieving information such as what it wants to do.
* the character code can all be self-contained, with it's user/owner passing it information about how it should move, etc, and retrieving infomation such as where it currently is.
* The controller is then a higher-level 'glue' class that plugs these two components into each other (N.B. the inputs/outputs of each class are a close match).
* A different controller could replace the AI class with an InputDevice class, to instead glue together an InputDevice to a Character.

In this design, the InputDevice, AI and Character are all completely decoupled -- they don't directly depend on each other.
However, yes, it would also be valid for the Character to not be aware of the AI, but have the AI directly use the Character interface (without the need for a 'glue' controller in the middle).

Share this post


Link to post
Share on other sites
I'm not much of a proponent of OO in general anymore, so take what I say in light of that fact.

I wrote this little series on component based entities systems a few years ago. Since then, I've extensively toyed with the idea, cleaned it up, and taken it much further.


A pure component system seemed either overkill, or rather, unintuitive, which is probably the strength of OOP.


Why does a pure component system seem like overkill? I understand that the idea can be repulsive at first, but only given a prior history in adherence to strict OOP principles. It's a perfect fit for the inherently dynamic nature of video games.


Then a tank, a subclass of vehicle would inherit those components from it, and be given it's own "cannon" component.


Isn't the definition of tank rather arbitrary though? What if, without recompiling, you could compose a completely new sort of vehicle at runtime? Is your base vehicle class compatible with a flying vehicle? Or do you have to derive a flying and non-flying vehicle from it? And then, what if you have something that occupies a middle ground...like a "hover tank?" As you can see, a class hierarchy still causes a combinatoric explosion of rigid classes.

I'm retreading ground that other posters have covered, but my main point is to cause you to rethink your attachment to OOP with regard to entity systems. It's just not a good fit.

Share this post


Link to post
Share on other sites

I'm retreading ground that other posters have covered, but my main point is to cause you to rethink your attachment to OOP with regard to entity systems. It's just not a good fit.
Note that any good "component system" is basically just a DDL or scripting-language or serialization-library for OO structures.
i.e. good OO code is component based -- you don't need a "component framework" to allow you to write good component-based OO code (inheriting everything from [font=courier new,courier,monospace]Component[/font] is another abuse of inheritance, IMHO). If you want the game to be data-driven, you just need a good serialization library, or a scripting language.

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!