Closed leanmendoza closed 1 year ago
Versioning the entities is the approach to solve the creation and deletion of entities a thousand times in the SDK but keeping a low memory footprint. We can get some code to get the idea:
const i = 100000000
while(i > 0) {
e = addEntity() // 0x10000001 0x20000001 ...
Transform.set(e, ...)
// SYNC
removeEntity(e)
}
assert(memory == 1 entity)
The next diagram represents how two versions of an entity have the same space in memory, it pointers to the same object.
Also, there is another problem that this approach solves: parenting and indirection of resources to deleted/reused entities. For example, having the entity A
, and B
, with A
being the parent of B
. If we remove the entity A
, B
becomes an orphan entity. But then when entity A is reused, the version increments once, having a different resulting ID, so B is still orphan as expected.
To apply this effect, the entity ID has to be synced with the version and every runtime and CRDT have to be aware of how to manage it. This implies modifying the CRDT rules and adding the checks to always get the most recent entity version.
The proposal for this is to have a new set to store the entities' IDs that were deleted. This can be implemented with an optimization that stores a deleted Map<EntityNumber, Version>
and from the state get the set.
type EntityNumber = number
type EntityVersion = number
const deletedEntities: Map<EntityNumber, EntityVersion> = new Map()
function addDeletedEntityId(entityId:) {
const entityNumber = getEntityNumber(entityId)
const entityVersion = getEntityVersion(entityId)
deletedEntities.set(entityNumber, entityVersion)
}
function getEntitiesDeleted() {
const resultSet: Set<EntityID> = new Set()
for (const [entityNumber, lastVersionDeleted] of Object.entries(deletedEntities)) {
// starting from version 0
Array.from({ length: lastVersionDeleted + 1}).forEach(
(version) => {
resultSet.push(getEntityId(entityNumber, version))
}
)
}
return resultSet
}
Abstract
This ADR describes the definition of an
Entity
, how interacts with runtime parts, the implications in memory terms, and also the synchronization between different parts.What is an Entity?
In the ECS paradigm, the entity is an unequivocal identifier, a number, or a value that represents something abstract. All components referenced by that entity give the meaning of that entity.
The scope of an entity is its own scene, this means that entity
0x1001
in a Scene-A and entity0x1001
in Scene-B are not linked to each other.Entity history
This is more related to CRDT ADR, but it's necessary to define it. The entity is the reference to different components. When we create for example a box shape with collision, this has three components (the SDK7 case): MeshRenderer, MeshCollider, and Transform. This collection of components represents a box shape with collision. When we change the property of each component, we are changing the internal state of CRDT.
When we make a remove component operation in that entity, we need to know if in the future I try to add the component again. If we do the operation with every component, at the same time, we probably are giving it the meaning that is completely another entity for us. But the state that something happened before is important to communicate that is completely different.
For example:
In
t=t2
, we link a Transform to EntityA again, but it's a new version of the Transform that means something different, we know this, because we have the history about the Transform int=t0
, (because of the lamport timestamp)Reusability approach
The reusability of each reserved but unused entity is important to avoid memory leaks. It's not practical having all entities' history. To illustrate this, we can take two scenes, once a static that only creates its entities once, and then nothing happens, and another one that creates and destroys entities with a balance that MUST be zero (otherwise, it's violating the limits).
The static scene has no problem with the versioning entity, it only creates all the entities that need. But in the dynamic one, with no version, the entity number is always incrementing and this could be a problem, a memory problem. With the preposition of the balance as ZERO (creation-rate - destruction-rate = 0), we can set any number to know how fast the problem appears.
For example, if we have 10,000 entities created per frame, and a 30fps game loop, we'd need almost 4 hours to fill all entities' numbers (without taking into account the entities' versions). Although we always have the existence of 20,000 entities, we have a history of about 2^32 (4,294,967,296) entities.
The extreme previous case can be transformed into a typical case if we take the versioning approach. With the previous rates, we know there are 10k new versions of entities each frame, so each second an entity is being reused 30 times, this is its counter version is incrementing by 30/s. In four hours, the counter is going to be incremented to around 432,000. This number is 10,000 times lower than the first case, which means it would take 100 days to drain the first 10k number of entities.
To summarize, the trick is in the version number. We want to reuse the entity number because it has a block of memory in our state, and using the version number allows us to recycle that block.
Technical
This is an implementation proposal example:
Use cases
Extending the conversation to its use cases.
0) Local scene usage
This is the most common use case (at least, until now). When you code your scene and call
engine.addEntity()
, you are having an ID to reference data. Then, you have all the data related to that entity that builds your object.1) Sync between scene and renderer
renderer
has a special appreciation because it shows your scene representation, it completes the image giving the player interaction. In terms of data, the renderer adds the player input to the scene and consume directly the scene responses. The renderer and the scene partially share the same component state.2) Prefab data
Prefab data is static data that you can add to your local scene.
3) Sync between other CRDT peer
We can reference entities with local entities (like an indirection), or mirror the entities.
General and examples
We can reduce all the concepts to one requirement:
With all these sources, we need to have the capability of interacting with each other. For example parent a local entity with a prefab one, or with another scene entity.
A couple of examples of interactions:
Linking an entity with the root of a Prefab
In this snippet the prefab works with its own entities.
This approach has an inexact path, it seems there are many complexity.
Prefab as a copyable state
With this approach, the complexity is only in the loading, with the remapping entities. The entities that already exist will be replaced and incremented in their version. Link a local entity with a prefab one here, is the same as local with local.
Remote server
The local scene is syncing all entities that the remote server wants to sync. They should share the entities' IDs.
The entities are reserved by the local scene on all peers. With our signature, the remote server can handle some rules to decide our write-permissions.
Remote scene
To access other scene entities, we could create an indirection. We mean remote-scene, a scene that is running in the kernel. Both kernel and renderer know about this remote-scene. Like another process in an operative system, we can share memory, but we need to grant access.
I skip the access negotiation and jump directly to the usage.
Avatar scene
ForeignEntity is a reference to another scene entity. In the renderer, this means that the entity created is got the exact object resource of another scene.
Portable Experience Scene with the current scene