Entities
Entities are the 'things' that exist in your game and can 'contain' a collection of components which signify the properties of your entity. Entities themselves have no defined data or behaviour; components contain the data while systems take care of the behaviour.
Although entities sound a lot like game objects, when delving deeper into their inner workings you find that they are really nothing more than an index ID.
The following diagram visualises the aforementioned by showing three entities and the relationship they have with their component data.
Simply put, entities are just integer ID's that index your unique collection of components, or put another way they identify what data belongs together.
I will explain later on why ECS lays the data out in this manner and what an archetype is, but the most important aspect to grasp at this stage is that an entity is simply an ID to an array index.
Create Entities In Unity IDE
To create entities via the Unity IDE you will need to add a Convert To Entity script (supplied out of the box with Unity) to a game object; this will convert the game object (and its components, if it can) into pure entity data accessible via systems.
Once a game object has the above script attached, when entering play mode you should see the following output in the Entity Debugger.
On the right had side I have highlighted the ECS components that are created when this game object is converted. Notice how the Transform component of the original game object has been converted and replaced by three separate components, LocalToWorld, Rotation and Translation. These three components are accessible by ECS systems and act as the Transform component would on a standard game object.
LocalToWorld is a required component if you want your entity to be able to move in world space. Detailed information on why this is the case is available here; a simplified explanation is that a system will combine any combinations of Translation, Rotation and Scale components and write them to the LocalToWorld component (if present). An entity will only be rendered if it has both LocalToWorld and RenderMesh components associated with it.
Create Entities In Code
To create entities via code we will need to use the ECS Entity Manager.
Below I have supplied code that will use the EntityManager to create one entity which is then displayed on screen at a specified Translation and Rotation.
Under the Public Fields section we create two private variables (which are exposed to the Unity IDE via the use of the SerializeField attributes) which takes a Mesh and Material that will be used by our entity (I have dragged a capsule and a blue material into these slots.)
The first line in the Awake method gets the current EntityManager and stores it in a variable named entityManager (funnily enough). This EntityManager is used for the creation and configuration of entities.
On the following line we actually create our entity (named shipEntity), supplying all the components (discussed later) that will be associated with it via the typeof parameters.
Next we configure our entity to be associated with a shared component (discussed later) for it's RenderMesh, setting its mesh and material fields to our capsuleMesh and blueMaterial references (dragged in via the Unity IDE). This is what will give our entity a form so that it is visible on screen and because the RenderMesh is shared, new entities can use this data rather than hold its own copy.
The final two lines of code simply configures our entities Translation and Rotation components to the values of our choosing. In this case we are setting our entities Translation value to (1,1,1) and our Rotation value to (1,1,1,1).
Below is the output from this code, as you can see a blue capsule is place at (1, 1, 1) world units with a quaternion rotation value of (1, 1, 1, 1).
We will delve a little deeper into the EntityManager later on but for the time being this should be enough to get you started.
Now its time to gain an understanding of what a component actually is.
Components
Components define no behaviour and represent the data of our game. They are simply structs that implement one of the following interfaces.
IComponentData - Best used for data that varies between entities. Entities with the same set of components are placed into the same chunk of memory for efficiency.
ISharedComponentData - Best used when data is shared between entities, such as a MeshRender or Material. There is almost no extra memory cost per entity because all entities that share the same components are placed into the same chunk of memory. Because of this a system can very efficiently iterate over them.
ISystemStateComponentData - Behaves similarly to IComponentData and ISharedComponentData with the notable exception that, when a corresponding entity is destroyed, the entity manager will 'not' remove any system state components or recycle their entity ID's.
When an entity is destroyed all components that reference that particular entity ID are found and deleted, once this has taken place the entity ID is then available for reuse. If however the entity 'contains' a system state component, then all 'other' components will still be found and deleted but the system state component will remain and it's entity ID will 'not' be recycled for reuse. Only when the remaining system state components are removed will the entity ID become available. This is useful when you need to clean-up any state or resources that reference a particular entity ID.
In a sense, a system state component can be viewed as a tag. The following code shows how it can be used to tag entities as 'Dead', ready to be processed by a death system which 'could' clean up resources, remove the SystemStateComponentData and then delete the entity.
ISharedSystemStateComponentData - Follows the same principle as a system state component but with the shared nature of ISharedComponentData.
Archetypes
When you associate components with an entity a archetype is automatically created which signify's an identifier for that 'unique' collection of components. The EntityManager uses these archetypes to group entities together which share the same components. If you removed a component from an entity and a archetype for that combination of components does not already exist, then the EntityManager will automatically create a new archetype for you. Alternatively you can always explicitly create an archetype yourself, via code.
Below is an example of how we can create an archetype in code which encapsulates our previous shipEntity components.
The topmost highlight shows how to create an archetype in code. We simply use the entityManager.CreateArchetype method and pass in all the components we want this archetype to 'encapsulate'. We then store the returned EntityArchetype in a variable named shipArchetype.
The last highlight creates an entity using our new shipArchetype. We pass our archetype into the entityManager.CreateEntity method (rather than specifying the individual components, as we did in the previous example) storing the returned entity in our shipEntity variable.
Now we have created a new entity via our archetype which is associated with the LocalToWorld, Rotation, Translation and RenderMesh components.
When running this code in Unity we get the exact same output as in our previous example.
It's worth noting at this time that you can also create a NativeArray (explain in the Job Component System tutorial) of entities that use our archetype.
Below is the code required to create 2000 entities, notice how we create an array of 2000 entities first in our shipEntityArray. Then we use the entityManager.CreateEntity method to feed in both our shipArchetype and shipEntityArray as arguments.
And that's it, 2000 shipEntity's have been created that are all associated with our shipArchetype, simples.
Finally we will move on to chunks, explaining how and why ECS configures the data in memory as it does.
Chunks
The archetype associated with an entity tells ECS where the components of that entity should be stored in memory. A chunk of memory (represented in code by a ArchetypeChunk struct) contains entities of the same archetype only, if a chunk of memory is full then another chunk will be created, ready to take new entities of the same archetype.
If you were to add or remove components from an entity this would alter the archetype it is associated with, hence the entity and components would be moved to a chunk that conforms to that archetype. If an archetype does not already exist which conforms to the new component grouping then a new archetype will be created and the entity and components would be moved to a new chunk.
Below is a visualisation of the one archetype (Position, Quaternion and RenderMesh components) to many chunks relationship within ECS.
It's worth noting that although chunks are tightly packed they are 'not' stored in any specific order. If for example Entity 3 is removed from the above chunk and a new Entity 20 is created which conforms to this archetype, then Entity 20 will be placed into the first chunk with room, as shown below.
One benefit to this approach is that if ECS has to find all entities with a given set of components then it would only need to search through all archetypes rather than all entities. But the main reason for laying out the data in this manner is the increased performance via the principle of locality making cache hits far more likely.
Principle of Locality
There are few different types of locality that can be utilised but by far away the most frequently used are Temporal locality and Spatial locality.
Temporal Locality - A location in memory is 'likely' to be re-accessed in a short amount of time. For example, if you were to change the values in the Rotation component of an entity, it wouldn't be too out there to suggest that you may wish to change them again a few lines of code later.
Spatial Locality - If a location in memory is accessed then there is a 'good chance' that a location near it will also need to be accessed. For example, its not unusual to iterate through an array starting from index 0 and finishing at the last index. In this example, it would be optimal if the CPU stored the whole array in it's primary cache so that it would not have to go off and fetch each element from main memory in turn (accessing main memory is slow).
Cache Line & Cache Hits
Cache Line - Data is transferred from memory to cache in fixed size blocks called cache lines (or cache blocks). This cache line data fits perfectly with the Spatial Locality principle as a cache line not only contains the requested data but also the data surrounding it.
Cache Hit - When the CPU needs to access a location in memory it will first check the cache line to see if the location is available there. If it is available then this is considered to be a cache hit and is extremely efficient due to the speed of cache access. If the CPU checks the cache line and the requested location is not available then this is considered to be a cache miss. The CPU will then go off, fetch the data from main memory (slow), and store it (and the surrounding data) in the cache line for future use.
So as you can see, laying out component data as ECS does gives the above locality principles (especially Spatial Locality) the best chance of being realised as related data is tightly pack together and significantly increases the likelihood of cache hits.
That's it for this tutorial, now you have grasped the aforementioned concepts you are ready to move onto my Systems tutorial which will guide you through the commonalities of both the Component and Job Component systems.
Until next time.
Link : ECS - Systems (Part 3)
References
Comentarios