In our lessons on Inheritance, we introduced a way of managing complexity. There, we arranged our code into a hierarchy where children inherited all the capabilities of their parent. This additive process created more and more powerful objects.
Inheritance is very useful and, by itself, it is sufficient for managing the complexity of fairly simple projects. At larger scales however, problems start to arise. Lets imagine some capabilities our actors might need to have:
We shouldn't put all this capability in a single class. Doing so would create an unmanagably complex class. Additionally, most individual objects will only need a tiny fraction of these capabilities. Creating unnecessarily complicated objects that contain a huge amount of unused data is a waste of resources.
However, equally, we can't effectively use inheritance here. These capabilities don't fit into any sort of hierarchy - objects can have any combination of these abilities. A tree blowing in the wind might need deformation, physics and audio; a campfire might need particles and audio; an enemy monster might need deformation, UI and audio.
Were we to define a class for every possible combination of these 5 capabilities, we'd have over 30 classes. Add a 6th capability, and that doubles to over 60 possible combinations. We need a better design.
Composition is the simple idea of combining objects to create more complex entities. Typically, objects that are designed to be combined with other objects are called components.
Under composition, no longer is our enemy monster a single object - it is a combination of components working together to simulate a single, more complex entity
[img]
The benefits of this are immediately obvious. Our entities can pick and choose only the capabilities they need. There is no need for elaborate class hierarchies.
To set this up, there are generally 3 aspects:
Actor
ActorComponent
PhysicsComponent
, which inherits from ActorComponent
The class structure might look something like this:
Here, the Actor
and ActorComponent
class are responsible for managing the interaction between actors and their components. Actors can maintain a list of all their attached components, as well as methods to allow components to be added and removed. Our ActorComponent
base class provided similar methods, which give all of its child objects the ability to access and modify the actor they are attached to.
With this design, the focus of our Actor
class shifts away from implementing behaviour directly. It can still do that, if needed, but most of its focus is on managing a collection of components.
For example, every frame, our game will call Tick
on our actor, letting it invoke its behaviour. Given that behaviour is now implemented in the components it has attached, the actor needs to call Tick
on every one of those components.
Similarly, when our actor is deleted, it needs to make sure all of its components are deleted too.
Component systems can get arbitrarily complex when creating intricate software. One common requirement is for components not just to be attached to an actor, but rather attached to a specific position in that actor.
For example, consider an actor that represents a torch. To create the fire effect, we might need a particle component. To ensure the fire effect is created in the correct place, that component needs attached to a particular position on the actor.
This can get more complex still - now imagine the torch actor is being carried by a character that is walking around. We're now dealing with a hierarchy of actors and components. We have a complete torch actor with its own set of components, and that torch actor is attached to a skeletal animation component of a totally different actor.
But, the concepts of composition allow us to implement these hierarchies quite easily. For example, we could have our SkeletonComponent
maintain its own list of ActorComponents
, along with a bone they're attached to. This would give us the ability to attach a ParticleComponent
to the location of one of our actor's bones, allowing the effect to move realistically as our actor is animated:
class SkeletonComponent : public ActorComponent {
public:
AttachToBone(ActorComponent* Component, Bone* AttachmentPoint);
private:
list<pair<ActorComponent*, Bone*>> AttachedComponents;
}
Similarly, we could have an ActorComponent
that is designed to contain another Actor
, thereby allowing complete actors, not just components, to be attached to other actors.
Combined with the SkeletonComponent
s ability to have its own components, and for those components' positions to be kept in sync with animations, this would allow our actor to carry another actor in their hand:
class ActorAttachment : public ActorComponent {
public:
AttachActor(Actor* ActorToAttach);
private:
Actor* AttachedActor;
}
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way
Free, unlimited accessSubscribe to this course to return to it later, and get updates on future changes