Entity/Component - cross communication

Started by
20 comments, last by haegarr 14 years ago
I'm building a component system at the moment (yay, bandwagon!), but I've used one from a big commercial game engine before (not naming names) and it was awful, so I'm looking for improvements. Problem #1 was that each entity could only hold 1 of each component type. What if my crosshair entity needs 3 sprite components, what if my vehicle entity needs 5 model components? bah. Also, entities themselves were a kind of component, but were a special case in that they couldn't be composed into other entities. This meant my vehicle had to be a "prefab" of 5 entities (instead of 1 entity with 5 components), and "prefab" support was very shoddy (duplicate the vehicle and the new wheels would be attached to the old chassis). Problem #2 was: How do components (within an entity) talk to each other? Their solution was basically:
Component* Entity::GetComponent( int type )
  foreach( components as c )
    if( c.type == type )
      return c
  return NULL;

void MyComponent::DoSomethingToMySibling()
  parentEntity->GetComponent( Foo )->DoSomething();
In my system, I 'solved' problem #1 by allowing multiples of each component type. My equivalent of the GetComponent function also takes an index, and there's a GetComponentCount so you can determine the valid indices. Now I'm on to trying to solve problem #2, and I'm thinking that GetComponent/GetComponentCount aren't really needed at all! I'm coming to the realisation that the whole "entity->get this type of member" paradigm is pretty flawed, and instead of trying to work around the difficulties of this model, I can just change the model. In C++ if I have a hard-coded entity:
struct MyEntity
{
  MyEntity();
  FooComponent foo;
  BarComponent bar1;
  BarComponent bar2;
};
...Then I'd never write code that would get those members by type (or by type + instance count) - I'd get them by their names: foo, bar1 and bar2. So seeing the whole point of this entity/component deal is to let me do these compositions in data instead of hard-coding them, why not use the 'named member' model that we're all familiar with? Now the reason I arrived here was because I was trying to solve the inter-component-communication problem, and I was thinking that if a BarComponent relies on reading data from a FooComponent, then in C++ I would write:
 MyEntity::MyEntity() : foo(), bar1(foo), bar2(foo) {}
...so why don't I do the same with my data format? Basically, now I'm looking at an entity file format like this:
//declare a new prototype/template/descriptor/whatever-you-want-to-call-it
entity MyEntity
{
//when creating a 'MyEntity' construct a FooComponent using it's default constructor
  FooComponent foo;
//then construct a BarComponent passing the address of the previously constructed FooComponent to it's constructor
  BarComponent bar1( foo );
//do it again
  BarComponent bar2( foo );
}
This would be equivalent to the code:
Entity* e = new Entity;
FooComponent* f = new FooComponent;
e->AddComponent( "foo", f );
e->AddComponent( "bar1", new BarComponent(f) );
e->AddComponent( "bar2", new BarComponent(f) );
Now instead of BarComponent having to query it's parent entity to somehow locate a FooComponent, it can have a constructor that simply takes (and stores) a pointer to a FooComponent. It doesn't even need to be a smart-pointer, as the lifetime of both components is the same (lifetime of their parent entity). Also, for cross-entity communication, instead of entity->GetComponent( type, index ), I can use entity->GetComponent( type, name ). [edit] I forgot to mention that the dependencies between these two components can use interfaces so they're not quite as tightly coupled, e.g.
class FooComponent : public IWidget
...
class BarComponent { 
  BarComponent( IWidget* )
...
[/edit] Thoughts? [Edited by - Hodgman on April 11, 2010 8:50:04 PM]
Advertisement
Why not a pub/sub event model for communication between the components of an entity? That way theres no coupling between components... I knocked this together quickly to check it makes sense and show you what I mean, it's in c# and is probably pretty slow but I prefer the generality. It should translate into c++ quite readily, you might need to imagine it without the use of RTTI I guess.

using System;using System.Collections.Generic;namespace entityTest{    interface IGameEvent { }    class EngineStarted : IGameEvent    {        public double ThrottleMagnitude { get; private set; }        public string Vehicle { get; private set; }        public EngineStarted( double throttleMagnitude, string vehicle )        {            ThrottleMagnitude = throttleMagnitude;            Vehicle = vehicle;        }    }    interface IEventSubscriber    {        void SubscribeEvent<T>(Action<T> handler) where T : IGameEvent;        void UnsubscribeEvent<T>(Action<T> handler) where T : IGameEvent;    }    interface IEventPublisher    {        void PublishEvent(IGameEvent gameEvent);    }    interface IEventExecuter    {        void FlushEvents();    }          class EventSystem : IEventExecuter, IEventPublisher, IEventSubscriber    {        interface IEventCollection        {            void FireEvents(IGameEvent gameEvent);        }        class EventCollection<T> : IEventCollection where T : IGameEvent        {            Action<T> m_handlers;            public void AddHandler(Action<T> handler)            {                m_handlers += handler;            }            public void RemoveHandler(Action<T> handler)            {                m_handlers -= handler;            }            public void FireEvents(IGameEvent gameEvent)            {                var theEventHandler = m_handlers;                if (theEventHandler != null)                {                    theEventHandler((T)gameEvent);                }            }        }        Dictionary<Type, IEventCollection> m_handlers;        Queue<IGameEvent> m_events;        public EventSystem()        {            m_handlers = new Dictionary<Type, IEventCollection>();            m_events = new Queue<IGameEvent>();        }        public void FlushEvents()        {            var count = m_events.Count;            while (count > 0)            {                IEventCollection handlers;                var gameEvent = m_events.Dequeue();                if (m_handlers.TryGetValue(gameEvent.GetType(), out handlers))                {                    handlers.FireEvents(gameEvent);                }                --count;            }        }        public void PublishEvent(IGameEvent gameEvent)        {            m_events.Enqueue(gameEvent);                   }        public void SubscribeEvent<T>(Action<T> handler) where T : IGameEvent        {            IEventCollection handlers;            if (!m_handlers.TryGetValue(typeof(T), out handlers))            {                handlers = new EventCollection<T>();                m_handlers[typeof(T)] = handlers;                            }            ((EventCollection<T>)handlers).AddHandler(handler);        }        public void UnsubscribeEvent<T>(Action<T> handler) where T : IGameEvent        {            IEventCollection handlers;            if (m_handlers.TryGetValue(typeof(T), out handlers))            {                ((EventCollection<T>)handlers).RemoveHandler(handler);            }        }    }    interface IComponent : IDisposable    {        void Update(TimeSpan deltaTime, IEventPublisher eventPublisher);    }    class AIComponent : IComponent    {        IEventSubscriber m_subscriptions;        public AIComponent(IEventSubscriber subscriber)        {            m_subscriptions = subscriber;            m_subscriptions.SubscribeEvent<EngineStarted>(HandleThrottle);        }        public void Dispose()        {            m_subscriptions.UnsubscribeEvent<EngineStarted>(HandleThrottle);        }        void HandleThrottle(EngineStarted throttleEvent)        {            Console.WriteLine("AI HANDLING THROTTLE OF MAGNITUDE :: " + throttleEvent.ThrottleMagnitude + " FROM VEHICLE " + throttleEvent.Vehicle);        }        public void Update(TimeSpan deltaTime, IEventPublisher eventPublisher)        {            //update ai        }    }    class AudioComponent : IComponent    {        IEventSubscriber m_subscriptions;        public AudioComponent(IEventSubscriber subscriber)        {            m_subscriptions = subscriber;            m_subscriptions.SubscribeEvent<EngineStarted>(HandleThrottle);        }        public void Dispose()        {            m_subscriptions.UnsubscribeEvent<EngineStarted>(HandleThrottle);        }        void HandleThrottle(EngineStarted throttleEvent)        {            Console.WriteLine("AUDIO PLAYING THROTTLE SOUND");        }        public void Update(TimeSpan deltaTime, IEventPublisher eventPublisher)        {            //update audio        }    }    class PhysicsComponent : IComponent    {        readonly double TimeBetweenThrottlesSeconds;        readonly double ThrottleMagnitude;        readonly string Name;        double m_timeSinceLastThrottle;        public PhysicsComponent(double timeBetweenThrottlesSeconds, double throttleMagnitude, string name, IEventSubscriber subscriber)        {            TimeBetweenThrottlesSeconds = timeBetweenThrottlesSeconds;            ThrottleMagnitude = throttleMagnitude;            Name = name;        }        public void Dispose()        {            //dispose        }        public void Update(TimeSpan deltaTime, IEventPublisher eventPublisher)        {            m_timeSinceLastThrottle += deltaTime.Milliseconds / 1000f;            if (m_timeSinceLastThrottle > TimeBetweenThrottlesSeconds)            {                m_timeSinceLastThrottle -= TimeBetweenThrottlesSeconds;                eventPublisher.PublishEvent(new EngineStarted(ThrottleMagnitude, Name));            }        }    }    class Entity : IComponent    {        List<IComponent> m_components;        EventSystem m_eventSystem;        public Entity(IEnumerable<IComponent> components, EventSystem eventSystem)        {            m_components = new List<IComponent>(components);            m_eventSystem = eventSystem;        }        public void Update(TimeSpan deltaTime, IEventPublisher eventPublisher)        {            foreach (var component in m_components)            {                component.Update(deltaTime, eventPublisher);            }            m_eventSystem.FlushEvents();        }        public void Dispose()        {            foreach (var component in m_components)            {                component.Dispose();            }        }    }    class Program    {        static void Main(string[] args)        {            var eventSystem = new EventSystem();            var physicsComponent1 = new PhysicsComponent( 1.5, 1298.323, "physics one", eventSystem);            var physicsComponent2 = new PhysicsComponent(2.5, 32, "physics two", eventSystem);            var audioComponent = new AudioComponent(eventSystem);            var aiComponent = new AIComponent(eventSystem);            var components = new List<IComponent> { physicsComponent1, physicsComponent2, aiComponent };            var entity = new Entity(components, eventSystem);            var frameTime = TimeSpan.FromSeconds(1.0 / 60.0);                      while (true)            {                entity.Update(frameTime, eventSystem);            }            entity.Dispose();        }    }}


The code prints out

AUDIO PLAYING THROTTLE SOUNDAI HANDLING THROTTLE OF MAGNITUDE :: 32 FROM VEHICLE physics twoAUDIO PLAYING THROTTLE SOUNDAI HANDLING THROTTLE OF MAGNITUDE :: 1298.323 FROM VEHICLE physics oneAUDIO PLAYING THROTTLE SOUNDAI HANDLING THROTTLE OF MAGNITUDE :: 32 FROM VEHICLE physics twoAUDIO PLAYING THROTTLE SOUNDAI HANDLING THROTTLE OF MAGNITUDE :: 1298.323 FROM VEHICLE physics oneAUDIO PLAYING THROTTLE SOUNDAI HANDLING THROTTLE OF MAGNITUDE :: 1298.323 FROM VEHICLE physics oneAUDIO PLAYING THROTTLE SOUNDAI HANDLING THROTTLE OF MAGNITUDE :: 32 FROM VEHICLE physics twoAUDIO PLAYING THROTTLE SOUNDAI HANDLING THROTTLE OF MAGNITUDE :: 1298.323 FROM VEHICLE physics oneAUDIO PLAYING THROTTLE SOUNDAI HANDLING THROTTLE OF MAGNITUDE :: 1298.323 FROM VEHICLE physics oneAUDIO PLAYING THROTTLE SOUNDAI HANDLING THROTTLE OF MAGNITUDE :: 32 FROM VEHICLE physics twoAUDIO PLAYING THROTTLE SOUNDAI HANDLING THROTTLE OF MAGNITUDE :: 1298.323 FROM VEHICLE physics oneAUDIO PLAYING THROTTLE SOUNDAI HANDLING THROTTLE OF MAGNITUDE :: 32 FROM VEHICLE physics twoAUDIO PLAYING THROTTLE SOUNDAI HANDLING THROTTLE OF MAGNITUDE :: 1298.323 FROM VEHICLE physics one


forever, so you can tell that it works without any direct references. Is there any reason you didn't go for an event system?

[Edited by - return0 on April 12, 2010 6:17:29 AM]
Sounds good, but I'm curious as to what this:

Entity* e = new Entity;FooComponent* f = new FooComponent;e->AddComponent( "foo", f );e->AddComponent( "bar1", new BarComponent(f) );e->AddComponent( "bar2", new BarComponent(f) );

Translates to in 'real' code.

In other words, what does the code that parses the entity definition, creates and adds the specified components, and passes previously created components as arguments to other components' constructors (if requested) look like? I assume it doesn't look like the above, since that hard-codes a particular set of relationships. Basically, the reason I ask is that it seems like you'd still have the dreaded downcasts, it'd just be moved to a different place in the code. (Maybe I'm wrong about that though, which is why I'm curious about the details.)
Quote:Original post by return0
Why not a pub/sub event model for communication between the components of an entity?
I'm doing a signally type event system as well, mostly for designer-level scripting stuff. For things like pushing an updated transformation matrix from a physics body to a graphics node though, a generic event system is way too bulky.
For things that happen once a second I'm happy to use events, but for things that happen 10,000 times per frame, I'd rather that the components communicate directly.
Quote:Original post by jyk
In other words, what does the code that parses the entity definition, creates and adds the specified components, and passes previously created components as arguments to other components' constructors (if requested) look like? I assume it doesn't look like the above, since that hard-codes a particular set of relationships.Basically, the reason I ask is that it seems like you'd still have the dreaded downcasts, it'd just be moved to a different place in the code.
I'm still writing the part that interprets this data file ;)
At the moment though, I'm using a reflection system in my components that provides run-time info on the constructors, interfaces, member functions and member variables.
I can use this system at the moment to look up a list of constructors (by type-name) and then find one that matches the user's data.
e.g. if the user writes: BarComponent bar1( foo, 42 );, then I'll:
* look in my local component table for a member named "foo"
* get a list of type-hashes (inheritance list) from the instance associated with foo
* determine the constant '42' is an integer, and retrieve the integer-type-hash:
* look up the constructor list for BarComponent.
* search this list for one that matches (any foo type-hash, int type hash)
* if a match is found I get two function pointers from the list entry
** the first function pointer constructs a struct{FooComponent*,int} object, which implements a generic set/get interface
** on this object I call
*** Set(0, GetComponent("foo"))
*** Set(1, atoi("42"))
** Then I use the second function pointer to call a constructor-proxy, passing the struct I just filled in, which returns:
*** new BarComponent(args.Get<FooComponent*>(0), args.Get<int>(1));
* then the newly constructed component will by added to the entity under the name "bar1"

The FooComponent may be upcast to a Component and then downcast again to a FooComponent at some point, but there's type-hash checks done along the line to ensure these casts are safe.
The actual management of the above function-pointers, dynamic-structures and proxy functions is all handled by the reflection system and will be transparent to the user, except for the pain of having to use a macro to 'register' their constructors. e.g.
class MyComponent{  MyComponent();          CONSTRUCTOR( () );  MyComponent( int i );   CONSTRUCTOR( (int) );};


[Edited by - Hodgman on April 11, 2010 11:53:20 PM]
Quote:Original post by Hodgman
Problem #1 was that each entity could only hold 1 of each component type. What if my crosshair entity needs 3 sprite components, what if my vehicle entity needs 5 model components?
For most types of components, it doesn't make sense to have more than one per entity. Designing the system around these exceptions strikes me as more complicated than simply making a sprite component that can refer to more than one sprite.

Quote:
//declare a new prototype/template/descriptor/whatever-you-want-to-call-itentity MyEntity{//when creating a 'MyEntity' construct a FooComponent using it's default constructor  FooComponent foo;//then construct a BarComponent passing the address of the previously constructed FooComponent to it's constructor  BarComponent bar1( foo );//do it again  BarComponent bar2( foo );}

It seems like you're introducing a lot of extra complexity into your "data" file. Assuming FooComponent is of the sort that doesn't make sense to have more than one of, do you always name the FooComponent foo? If so, what's the point of the name? And what's the point of having to remember the arguments that BarComponent's constructor takes, if there will be only one of each of those in MyEntity?

Basically, it seems like you're coming up with a DSL which has few advantages over the language it's supplanting. If your hard layer is hard, make your soft layer softer.
Quote:Original post by Sneftel
For most types of components, it doesn't make sense to have more than one per entity. Designing the system around these exceptions strikes me as more complicated than simply making a sprite component that can refer to more than one sprite.
Obviously I disagree! ;D
In "traditional" game code, how many of your classes have more that one member of the same type? How many of the same types if you recursively count your member's members?

e.g. If I want a car entity that can be dropped in and is fully complete and working, it's got at least 5 (possibly more) physics components, for each body there's probably at least one or more associated graphics components, each graphics component could possibly have an associated animation controller....
I can split it up so that there is an entity type that has a physics component + a graphics component, and then compose 5 of these entities into another entity, but then the top-level entity still has 5 entity components.

It just seems completely arbitrary to say that you can compose any data together that you want to, but you can only have 1 of any particular type of data. Looking at the problem from scratch, I can't see any reason to add in this restriction, other than the fact that other component systems have the same restriction. The "make sprite component handle more than one sprite" work-around is what we did in the previous system I mentioned, but it really seemed like a work-around for a problem with the system... and really it just shifts the issue around. In my first post I mentioned using indices to refer to components of the same type; with this solution the sprite component has to contain all the indexing logic... With named components of the same type, no-one has to deal with indices.

My goal is to enable data-driven composition of C++ classes (and a few other things, like parallelism), where most components are leaves and entities are nodes -- What benefit do you get from enforcing this 'one instance per composition' rule? Does this rule appear anywhere in traditional literature on composition, or is it just an invention of game-component systems?
Quote:It seems like you're introducing a lot of extra complexity into your "data" file.
In my first implementation I'm actually using XML for that file (my default serializer), so it's even more complex... I hate editing XML so the example I posted above is my more readable (in a programmers eyes) format.
<entity name="MyEntity">  <component type="FooComponent" name="foo" />  <component type="BarComponent" name="bar">    <argument type="Component" data="foo"/>  </component>  ...</entity>
Quote:And what's the point of having to remember the arguments that BarComponent's constructor takes, if there will be only one of each of those in MyEntity?
Lumping this complexity onto designers would be pretty bad I agree, so there'll be a GUI for editing these files, which will show some kind of property-list dialog to fill in the information required by a component (if any).
e.g. You just added an animation controller to this entity - which model do you want to attach that animation to? The GUI can mark their entity in red showing there's a problem with it until they hook things together into something that makes sense.
Quote:Assuming FooComponent is of the sort that doesn't make sense to have more than one of, do you always name the FooComponent foo? If so, what's the point of the name?
A lot of the time you won't need names. You could also just write: FooComponent; or FooComponent("someData"); (or <component type="FooComponent"/>).
...but in this case I'm using a name so I can pass it to the BarComponents (I guess I could have left these unnamed in my example).
Quote:Basically, it seems like you're coming up with a DSL which has few advantages over the language it's supplanting.
The advantage over C++ is that new gameplay classes can be constructed by designers (in a GUI environment, eventually) instead of by programmers :/

[Edited by - Hodgman on April 12, 2010 12:49:29 AM]
What you're designing doesn't really sound like a "component system" to me, so as far as I can tell comparing it to other component systems is apples and oranges. If what you really want to do is to facilitate data-driven composition of C++ objects, then that's exactly what you should do without giving your design such a loaded label [smile]

I recommend you take a look at Google's protocol buffers, which allow you to declared a structure data layout much like what you're interested in. If however you design a similar system and add the ability for data to reference C++ types in code, I think you'd be close to your original goal. It doesn't necessarily need to be based on generated code files either, because your data objects will be backed by pre-existing types.
Hodgman, how do you intend to allow dynamic addition/exchange of components, for instance when your unit/entity learned a new ability or when the EasyAI component is replaced by a DifficultAI component.
I'm just at the beginning of implementing a component based entity system, so I cannot prove the following (yet) ...

Quote:Original post by Hodgman
Quote:Original post by Sneftel
For most types of components, it doesn't make sense to have more than one per entity. Designing the system around these exceptions strikes me as more complicated than simply making a sprite component that can refer to more than one sprite.
Obviously I disagree! ;D
In "traditional" game code, how many of your classes have more that one member of the same type? How many of the same types if you recursively count your member's members?
Okay, but what is the purpose of the 2nd instance, e.g. a 2nd mesh or sprite?

If its purpose is to be able to switch from one to the other, then IMHO the belonging sub-system (e.g. the graphics rendering sub-system) should still "see" just 1 of them, namely the active one.

A composition of several instances needs a prescription how the composition has to be done. Okay, sometimes you can enforce a prescription to be derived from the order of components or using an implicit one, e.g. for the order of spatial transformations or that meshes should be composed by boolean OR, but in general you need a storage palce for it anyway, and IMHO using the component seems me natural and tidy.

Quote:Original post by Hodgman
e.g. If I want a car entity that can be dropped in and is fully complete and working, it's got at least 5 (possibly more) physics components, for each body there's probably at least one or more associated graphics components, each graphics component could possibly have an associated animation controller....
I can split it up so that there is an entity type that has a physics component + a graphics component, and then compose 5 of these entities into another entity, but then the top-level entity still has 5 entity components.
But sub-entities need not be components at all. Using sub-entities express some kind of relation, not more.

What would you do with the PlacementComponent, i.e. that component that relates the entity spatially with the world? Would you have 5 of them directly inside the entity, 1 for the car and 4 for the wheels, distinguished by name? And how is the parenting relation expressed? Is the first one found (e.g. at index 0) the model placement, and all others are local in some kind? And how to relate mesh N with physics body M?

Quote:Original post by Hodgman
It just seems completely arbitrary to say that you can compose any data together that you want to, but you can only have 1 of any particular type of data. ...
As written above: The composition itself often needs to be parametrized. A simple drop-in may hence be insufficient. It is clear that the availability of a shape mesh addresses the graphics rendering, but 2 shapes IMHO introduce confusion on what to do exactly. Deriving specialized ShapeComponent classes here, would solve this more elegantly, in my current opinion.
Quote:Original post by Zipster
What you're designing doesn't really sound like a "component system" to me, so as far as I can tell comparing it to other component systems is apples and oranges. If what you really want to do is to facilitate data-driven composition of C++ objects, then that's exactly what you should do without giving your design such a loaded label [smile]
How do you define a "component system"?
In my experience, the main point of one is to enable runtime composition, so you can easily create game objects though data-driven composition instead of old-school deep inheritance trees.
It's a system where the entity/game-object stops doing much work (or often just becomes an ID) and all the behaviour is pushed into these 'components' (which is also the name given to them in the "composite pattern").
Usually given a entity/game-object you can send it messages, find it's components by type, and it's properties/attributes by name (which may reside in the components or the entity, or somewhere else). As far as I can tell, my only real difference is that I'm giving (optional) names to the components as well (and relaxing the 'one component of each type' restriction).

In the cowboyprogramming.com description, my entities are the "object as component container" type.

The previous component systems that I've used haven't been concurrent (i.e. of the sub-system-allocated variety), so I've taken inspiration from insomniac too (1, 2, 3).
Quote:I recommend you take a look at Google's protocol buffers, which allow...
I really like 'protocol buffers' =D I've already implemented a serialisation library very closely based off that spec, but I'm using it for network replication, not this constructor stuff (it's already implemented using macro/template code generation).
Quote:Original post by nmi
Hodgman, how do you intend to allow dynamic addition/exchange of components, for instance when your unit/entity learned a new ability or when the EasyAI component is replaced by a DifficultAI component.
In the commercial system I mentioned in my first post, we didn't have this ability (the structures had to be pre-defined) and it didn't cause us any problems, so I haven't put much thought into dynamic/changing compositions. You can just have an AI component that has states or PIMPLs for easy/difficult.
There's no reason that my e->AddComponent( new ... ) function couldn't be used in the middle of the game, however I'm designing parts of the system on the assumption that components have the same lifetime as their parent entities do, so removing components isn't feasible without redesigning the thread-safe memory management.

[Edited by - Hodgman on April 12, 2010 5:12:32 AM]

This topic is closed to new replies.

Advertisement