In this article we will learn how components resolve their dependencies with other components and how the custom attribute [RequiredComponent] works.
Entity and Components
As you know, an Entity represents a logic element of our game and it is an empty box where you can put component insides which define what the entity can do.
The logic hierarchy is handle by the EntityManager which stores the struct of all the entities in our scene. For example:
Imagine this simple scene, the EntityManager stores all the root entities and they are connected with their children. This represents the logic hierarchy of our scene.
Each entity contains a list of components, for example:
This simple entity contains six components, and some of these component require to know or change properties stored in other components of the same entity. For example, the ModelRenderer component needs to know what is the geometry that he has to draw, which is stored in Model component, and where is the location where he has to draw this, stored in the Transform3D component.
We are going to build a simple sample to study how the dependencies between components are resolved. If we create a simple spin cube by code:
public class MyScene : Scene { protected override void CreateScene() { this.Load(WaveContent.Scenes.MyScene); Entity cube = new Entity("Cube") .AddComponent(new Transform3D()) .AddComponent(Model.CreateCube()) .AddComponent(new Spinner() { AxisTotalIncreases = new Vector3(1, 2, 3) }) .AddComponent(new MaterialsMap()) .AddComponent(new ModelRenderer()); EntityManager.Add(cube); } }
If we run this project we will see a simple white cube rotating. We can observe that if we change the order in which the components are added the result is the same, it doesn’t matter if the Transform3D component is the last component added to the entity. However, the Spinner component needs to be connected to the Transform3D to modify the rotation vector, so how does this work?
During the scene creation, WaveEngine initializes each entity and all its components, and in order to initialize the components in the right order it does the following: It take the first component and asks if all its component dependencies are already initializes, if true it initializes this component, but if not this component should wait for the next iteration. This loop finishes when all components are initialized.
Once we understand how all components are initialized, it is important to know how we can code that our components depend of other components. We are going to remove the spinner component and create our custom spinner behavior component.
public class MyScene : Scene { protected override void CreateScene() { this.Load(WaveContent.Scenes.MyScene); Entity cube = new Entity("Cube") .AddComponent(new Transform3D()) .AddComponent(Model.CreateCube()) .AddComponent(new MyBehavior()) .AddComponent(new MaterialsMap()) .AddComponent(new ModelRenderer()); EntityManager.Add(cube); } } public class MyBehavior : Behavior { private Transform3D transform; protected override void ResolveDependencies() { base.ResolveDependencies(); if ((transform = Owner.FindComponent<Transform3D>()) == null) throw new InvalidOperationException("MyBehavior cannot find a Transform3D component."); } protected override void DeleteDependencies() { base.DeleteDependencies(); transform = null; } protected override void Update(TimeSpan gameTime) { transform.LocalRotation += new Vector3(0, (float)gameTime.TotalSeconds, 0); } }
This MyBehavior component performs a rotation in the Y axis of the rotation vector of the Transform3D component. To have access to the Transform3D we have to override methods ResolveDependencies and DeleteDependencies to handle it. Notice that there is a DeleteDependencies method, that way we can add or remove components to this entity in real time so both methods can be called during the game.
We have simplified the use of dependencies in WaveEngine, so we created a custom attribute called [RequiredComponent]. If we use this attribute in this sample, we just have to put the attribute on top of our transform field, and WaveEngine will use reflection to find a component in the same entity which has the exact type of the field type and connect the field with the instance or throw an exception if it doesn’t exist.
public class MyBehavior : Behavior { [RequiredComponent] private Transform3D transform; protected override void Update(TimeSpan gameTime) { transform.LocalRotation += new Vector3(0, (float)gameTime.TotalSeconds, 0); } }
If we want to have more flexibility in the definition of the required components of our component, we can use the isExactType parameter to indicate that a derived type if also valid.
Another interesting custom attribute was added in WaveEngine 2.3, and the behavior is very similar [RequiredService].