WorldType
World identifier type-tag, used to isolate static data when creating different worlds in the same process
- Represented as a user structure without data with a marker interface
IWorldType
Example:
public struct MainWorldType : IWorldType { }
public struct MiniGameWorldType : IWorldType { }
World
Library entry point responsible for accessing, creating, initializing, operating, and destroying world data
- Represented as a static class
World<T>parameterized byIWorldType
Since the type-identifier
IWorldTypedefines access to a specific world
There are three ways to work with the framework:
The first way is as is via full address (very inconvenient):
public struct WT : IWorldType { }
World<WT>.Create(WorldConfig.Default());
World<WT>.CalculateEntitiesCount();
var entity = World<WT>.Entity.New<Position>();
The second way is a little more convenient, use static imports or static aliases (you’ll have to write in each file)
using static FFS.Libraries.StaticEcs.World<WT>;
public struct WT : IWorldType { }
Create(WorldConfig.Default());
CalculateEntitiesCount();
var entity = Entity.New<Position>();
The third way is the most convenient, use type-aliases in the root namespace (no need to write in every file)
This is the method that will be used everywhere in the examples
public struct WT : IWorldType { }
public abstract class W : World<WT> { }
W.Create(WorldConfig.Default());
W.CalculateEntitiesCount();
var entity = W.Entity.New<Position>();
Basic operations:
// Defining the world ID
public struct WT : IWorldType { }
// Register types - aliases
public abstract class World : World<WT> { }
// Creating a world with a default configuration
W.Create(WorldConfig.Default());
// Or a custom one
W.Create(new() {
// Indicates whether the world is independent or dependent (see the "Chunk" section for more details).
Independent = true
// Base size of all component types (number of component types)
BaseComponentTypesCount = 64
// Base size of all varieties of tag types (number of tag types)
BaseTagTypesCount = 64,
// Operation mode of multithreaded processing
// (Disabled - no threads are created, MaxThreadsCount - maximum available number of threads is created, CustomThreadsCount - specified number of threads)
ParallelQueryType = ParallelQueryType.Disabled,
// Number of threads at ParallelQueryType.CustomThreadsCount
CustomThreadCount = 4,
// Strict Query default mode of operation, optionally in the Queries section
DefaultQueryModeStrict = true
});
W.Entity. // Entity access for MainWorldType (world ID)
W.Context. // Access to context for MainWorldType (world ID)
W.Components.// Access to components for MainWorldType (world ID)
W.Tags. // Access to tags for MainWorldType (world ID)
W.Events. // Access to events
// Initialization of the world
W.Initialize(baseEntitiesCapacity = 4096);
// Initialize the world by loading previously saved identifiers
W.InitializeFromGIDStoreSnapshot(snapshot);
// Initializing the world from saved data
W.InitializeFromWorldSnapshot(snapshot);
// Destroying and deleting the world's data
W.Destroy();
// true if the world is initialized
bool initialized = W.IsInitialized();
// true if the world is independent
bool independent = W.IsIndependent();
// number of entities created in the world (active + unloaded)
int entitiesCount = W.CalculateEntitiesCount();
// number of entities loaded in the world
int loadedEntitiesCount = W.CalculateLoadedEntitiesCount();
// current capacity for entities
int entitiesCapacity = W.CalculateEntitiesCapacity();
// Destroys all entities in the world
W.DestroyAllEntities();
Cluster:
A cluster is a set of entity chunks. Entities belonging to the same cluster are grouped together and stored in memory in a segmented manner.
A cluster is represented by a ushort value between 0 and 65535. By default, when the world is initialized, a single cluster with the identifier 0 is created, and all entities are created in it by default.
Main operations:
// Cluster registration can be called after creation or after world initialization.
const ushort NPC_CLUSTER = 1;
const ushort ENVIRONMENT_CLUSTER = 2;
W.RegisterCluster(NPC_CLUSTER);
W.RegisterCluster(ENVIRONMENT_CLUSTER);
// Check if the cluster is registered
bool clusterIsRegistered = W.ClusterIsRegistered(NPC_CLUSTER);
// Enable or disable a cluster; entities from disabled clusters are not included in the iteration.
W.SetActiveCluster(ENVIRONMENT_CLUSTER, false);
// Check if the cluster is enabled
bool active = W.ClusterIsActive(ENVIRONMENT_CLUSTER);
// Release the cluster; all entities in the cluster will be deleted, all chunks and the cluster ID will be released (an error will occur if the cluster is not registered).
W.FreeCluster(ENVIRONMENT_CLUSTER);
// Release the cluster if it is registered
bool free = W.TryFreeCluster(ENVIRONMENT_CLUSTER);
// Destroy all entities in the cluster
W.DestroyAllEntitiesInCluster(NPC_CLUSTER);
// Take a snapshot of the cluster that stores all entity data in this cluster.
// There are method overloads for writing to disk, compression, etc.
// More examples in the "serialization" section.
byte[] clusterSnapshot = W.Serializer.CreateClusterSnapshot(NPC_CLUSTER);
// Unload the cluster from memory, all component and tag chunks will be deleted,
// entities will be marked as unloaded, and only information about identifiers will be saved; entities will not be received in queries.
W.UnloadCluster(NPC_CLUSTER);
// Load from the snapshot of the entity cluster into the world
W.Serializer.LoadClusterSnapshot(clusterSnapshot);
// Get all chunks in the cluster (including empty chunks where there are no loaded entities)
ReadOnlySpan<uint> chunks = W.GetClusterChunks(NPC_CLUSTER);
// Get all chunks in the cluster that have at least one entity loaded
ReadOnlySpan<uint> loadedChunks = W.GetClusterLoadedChunks(NPC_CLUSTER);
// When creating an entity, you can pass the cluster ID (by default, the entity is created in the default cluster W.DEFAULT_CLUSTER = 0).
var npc = W.Entity.New(clusterId: W.DEFAULT_CLUSTER);
// Attempt to create an entity in the cluster; if the world is dependent and there are no free entity identifiers left in it, false will be returned.
var created = W.Entity.TryNew(out var ent, clusterId: ENVIRONMENT_CLUSTER);
// An optional cluster identifier parameter has been added for all overloads.
W.Entity.New(
new Position(),
new Name(),
clusterId: NPC_CLUSTER
);
// Get entity cluster
ushort entityClusterId = npc.ClusterId();
// Get the entity cluster at EntityGID
ushort gidClusterId = npc.Gid().ClusterId;
Chunk:
A chunk is a group of entities with a size of 4096; the entire world consists of chunks. A chunk always belongs to a cluster.
A world can be dependent or independent; this parameter is set in the world configuration when creating W.Create(new() { Independent = true }).
By default, an independent world manages entity IDs and all chunks automatically, creating new chunks when entities are created with W.Entity.New() when needed.
A dependent world does not have entity and chunk IDs available for creating entities via W.Entity.New() when created; the world must be told which chunks are available.
Next, we will look at some examples.
Basic operations:
// Find a free chunk that does not belong to any cluster.
// For an independent world, if there is no free chunk, a new one will be created.
// For a dependent world, if there is no free chunk, an error will occur.
EntitiesChunkInfo chunkInfo = W.FindNextSelfFreeChunk();
uint chunkIdx = chunkInfo.ChunkIdx; // Chunk index
// chunkInfo.EntitiesFrom - first entity identifier in the cluster
// chunkInfo.EntitiesCapacity - chunk size (always 4096)
// Try to find a free chunk that does not belong to any cluster.
// For an independent world, if there is no free chunk, a new one will be created (the result is always true).
// For a dependent world, if there is no free chunk, the result will be false.
bool hasFreeChunk = W.TryFindNextSelfFreeChunk(out EntitiesChunkInfo info);
// Register a free chunk in the cluster (if the chunk is already registered, an error will occur)
W.RegisterChunk(chunkIdx, clusterId: NPC_CLUSTER);
// Register a free chunk in the cluster and assign an ownership type (details below) (If the chunk is already registered, an error will occur)
W.RegisterChunk(chunkIdx, owner: ChunkOwnerType.Self, clusterId: NPC_CLUSTER);
// Attempt to register a free chunk in the cluster (if the chunk is already registered, false will be returned)
bool chunkRegistered = W.TryRegisterChunk(chunkIdx, NPC_CLUSTER);
// Check if the chunk is registered
bool registered = W.ChunkIsRegistered(chunkIdx);
// Get the ID of the cluster to which the chunk belongs
ushort chunkClusterId = W.GetChunkClusterId(chunkIdx);
// Change the chunk cluster; all entities within the chunk will belong to another cluster.
W.ChangeChunkCluster(chunkIdx, ENVIRONMENT_CLUSTER);
// Check if there are entities in the chunk (active + unloaded)
bool hasEntitiesInChunk = W.HasEntitiesInChunk(chunkIdx);
// Check if there are any loaded entities in the chunk
bool hasLoadedEntitiesInChunk = W.HasLoadedEntitiesInChunk(chunkIdx);
// Free up the chunk; all entities in the chunk will be deleted, and the chunk identifier will be freed up.
W.FreeChunk(chunkIdx);
// Destroy all entities in the chunk
W.DestroyAllEntitiesInChunk(chunkIdx);
// Take a snapshot of the chunk that stores all entity data in this chunk.
// There are method overloads for writing to disk, compression, etc.
// More examples in the "serialization" section.
byte[] chunkSnapshot = W.Serializer.CreateChunkSnapshot(chunkIdx);
// Unload the chunk from memory, all components and tags will be removed,
// entities will be marked as unloaded, and only information about identifiers will be saved; entities will not be received in queries.
W.UnloadChunk(chunkIdx);
// Load from the snapshot of the essence into the world
W.Serializer.LoadChunkSnapshot(chunkSnapshot);
// When creating an entity, you can pass the chunk index (without specifying it; the chunk selection is determined by the world).
var entity = W.Entity.New(chunkIdx: chunkIdx);
// Попытаться создать сущность в чанке, если чанк полон вернется false
var created = W.Entity.TryNew(out var ent, chunkIdx: chunkIdx);
// Check the chunk owner
// ChunkOwnerType.Self means that the chunk is managed by this world; only chunks with Self ownership are used to create entities via Entity.New()
// - The independent world by default has all chunks with Self ownership.
// ChunkOwnerType.Other - means that the chunk is not managed by this world, entities created via Entity.New() will never be created in these chunks.
// - The dependent world by default has all chunks with Other ownership.
ChunkOwnerType owner = W.GetChunkOwner(chunkIdx);
// Change the type of ownership of a chunk
W.ChangeChunkOwner(chunkIdx, ChunkOwnerType.Other);
// Creating entities via Entity.New(gid) is only available for chunks with `Other` ownership type.
// Creating entities via Entity.New(chunkIdx) is only available for chunks with the Self ownership type.
Examples of cluster and chunk usage:
Clusters can be used for any user logic, for example:
- Different clusters can define different types of entities, such as a unit cluster, a game environment cluster, an item cluster, or an effect cluster.
- This reduces memory consumption and fragmentation, speeds up iteration, and helps with world serialization and game logic.
- For example, with a large game map that is loaded and unloaded as the player moves, different clusters greatly save memory.
- Another example is using clusters for different game levels and loading/unloading clusters when changing levels.
- The cluster ID can also define the game session. Combined with parallel iteration, it is possible to create a multi-world emulation within a single world.
Chunk management can be used, for example, for:
- World streaming, allowing chunks to be loaded and unloaded during gameplay
- Custom entity ID management
- Quick selection and clearing of large numbers of entities, as an arena memory for temporary entities
Chunk ownership management can be used for client-server interactions, for example:
// On the server side in the Independent world
// Find a free chunk and register it
EntitiesChunkInfo chunkInfo = WServer.FindNextSelfFreeChunk();
// Set the ownership type of the chunk to `Other`, so that the server will never create entities in this range of identifiers.
WServer.RegisterChunk(chunkInfo.ChunkIdx, ChunkOwnerType.Other);
// Send the chunk ID to the client
// On the client side in the Dependent world
// We receive the chunk ID from the server
// Register a chunk with the `Self` ownership type
WClient.RegisterChunk(ChunkIdxFromServer, ChunkOwnerType.Self);
// Now there are 4096 free identifiers available for entities on the client.
// Client entities can be created using W.Entity.New().
// For example, for UI or VFX.
// Similarly, it can be used for p2p network formats
// where there is one Independent host and N Dependent clients