The ECS design is very popular right now, mainly because of the success of Watchtower, which has led to the explosion of this technology. The most important design of ECS is the complete separation of logic and data. That is, EC is pure data, System is actually logic, by data-driven logic. What does data-driven logic mean? It is very simple to detect data changes through Update and subscribe to data changes through the event mechanism, which is called data-driven. Other features such as cache hits are not too important in writing logic, modern games are scripted, even the performance of the script can tolerate how will care about cache hits that performance improvement?
For example, in order to reuse, the data must be split into very small particles, which will lead to a very large number of components. But the game is developed cooperatively by multiple people, each person is basically only familiar with their own modules, which may end up causing a large number of redundant components. Another problem is that the common ECS is flat, with only one layer of Entity and Component. It is like a company, the biggest is the boss, the boss with hundreds of people under him, the boss can not know all the people, to complete a task, the boss can not pick out the people they need. A reasonable approach is to have several managers under the boss, several supervisors under each manager, and several workers under each supervisor, so as to form a tree-like management structure that is easy to manage. This is similar to the ET approach, where Entity can manage Component, Component can manage Entity, and even Component can mount Component. e.g. a person is composed of head, body, hands, feet, and the head is composed of eyes, ears, nose, mouth.
Head head = human.AddComponent<Head>();
head.AddComponent<Eye>();
head.AddComponent<Mouse>();
AddComponent<Nose>();
AddComponent<Ear>();
human.AddComponent<Body>();
AddComponent<Hand>();
human.AddComponent<Leg>();
In ET, all data is Entity, including Entity, and Entity can be used either as a component or as a child of other Entity. Generic data is placed on Entity as a member, and less generic data can be hung on Entity as a component. For example, the design of items, all items have fields configured id, quantity, level, these fields are not necessary to make components, put on Entity will be more convenient to use.
class Item: Entity
{
// The configuration Id of the props
public int ConfigId { get; set; }
// The number of props
public int Count { get; set; }
// The level of the props
public int Level { get; set; }
}
This design data of ET is a kind of tree structure, very hierarchical and can understand the whole game architecture very easily. Scene, and the data of different modules are mounted on top of Game. Scene, and each module can mount a lot of data under itself. You don't have to think too much about how to design the classes, where to put the data, whether mounting here will lead to redundancy, etc. for each new feature. For example, if my player needs to do a prop system, I can design an ItemsComponent to be mounted on the Player, and I need to develop a SpellComponent to be mounted on the Player for skills. If the whole service needs to do an activity, get an activity component to hang on top of the Game. This design will be very easy to assign tasks and very modular.
ComponentFactory provides three methods to create components Create, CreateWithParent, CreateWithId. Create is the simplest way to create components, it does several things
a. Construct a component based on the component type
b. Add the component to the event system and throw an AwakeSystem
c. Enables object pooling or not
CreateWithParent provides a Parent object on top of Create, which is set to the Component.Parent field. createWithId is used to create ComponentWithId or its subclasses, and you can set an Id on top of Create itself, Component can choose whether to use a pool of objects when it is created. All three types of factory methods have a fromPool parameter, the default is true.
Component inherits an IDisposable interface. It should be noted that Component has unmanaged resources and must be called to delete a Component. This interface does the following
a. Throw Destroy System
b. If the component was created using an object pool, then it is put back into the object pool here
c. Remove the component from the global EventSystem and set the InstanceId to 0
If the component is mounted on Entity, then when Entity calls Dispose, it will automatically call the Dispose method of all components on it.
Any Component comes with an InstanceId field, which is reset when the component is constructed, or when the component is removed from the object pool, and which identifies the identity of the component. Why is such a field needed? There are several reasons
- the existence of the object pool, the component may not be released, but returned to the object pool. In an asynchronous call, it is likely that the component has already been released and then reused, so that we need a way to be able to distinguish whether the previous component object has been released, such as the following code.
public static async ETVoid UpdateAsync(this ActorLocationSender self)
{
try
{
long instanceId = self.InstanceId;
while (true)
{
if (self.InstanceId ! = instanceId)
{
return;
}
ActorTask actorTask = await self.GetAsync();
if (self.InstanceId ! = instanceId)
{
return;
}
if (actorTask.ActorRequest == null)
{
return;
}
await self.RunTask(actorTask);
}
}
catch (Exception e)
{
Log.Error(e);
}
}
While (true) is an asynchronous method, after await self.GetAsync() it is likely that the ActorLocationSender object has been released, and it is even possible that the object has been utilized again by other logic from the object pool. We can determine whether the object has been released by the change of InstanceId. 2.
2. InstanceId is globally unique and has location information, so you can find the location of the object by InstanceId and send the message to the object. This design will be utilized in the Actor message. Here for the time being will not talk about it.