WorldType
World identifier type-tag, used to isolate static data when creating different worlds in the same process
- Represented as a user struct with no data and the
IWorldTypemarker interface - Each unique
IWorldTypegets its own fully isolated static storage
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
IWorldTypetype-identifier defines access to a specific world, there are three ways to work with the framework:
First way — full qualification:
public struct WT : IWorldType { }
World<WT>.Create(WorldConfig.Default());
World<WT>.CalculateEntitiesCount();
var entity = World<WT>.NewEntity<Default>();
Second way — static imports:
using static FFS.Libraries.StaticEcs.World<WT>;
public struct WT : IWorldType { }
Create(WorldConfig.Default());
CalculateEntitiesCount();
var entity = NewEntity<Default>();
Third way — type alias in the root namespace:
This is the method used in all examples
public struct WT : IWorldType { }
public abstract class W : World<WT> { }
W.Create(WorldConfig.Default());
W.CalculateEntitiesCount();
var entity = W.NewEntity<Default>();
Lifecycle
Create() → Type registration → Initialize() → Work → Destroy()
WorldStatus:
NotCreated— world not created or destroyedCreated— structures allocated, type registration availableInitialized— world fully operational, entity operations available
Creating the world:
// Define the world identifier
public struct WT : IWorldType { }
public abstract class W : World<WT> { }
// Create with default configuration
W.Create(WorldConfig.Default());
// Or with custom configuration (all parameters are optional — unset values fall back to defaults)
W.Create(new WorldConfig {
// Independent world (manages chunks automatically) or dependent (requires manual chunk management)
Independent = true,
// Initial capacity for component types (default — 64)
BaseComponentTypesCount = 64,
// Initial capacity for clusters (minimum 16, default — 16)
BaseClustersCapacity = 16,
// Number of threads for parallel queries (default — 0, single-threaded)
// 0 — no threads created
// WorldConfig.MaxThreadCount — all available CPU threads
// N — specified number of threads
ThreadCount = 4,
// Worker spin iterations before blocking (default — 256)
WorkerSpinCount = 256,
// Enable entity creation tracking for the Created query filter (default — false)
TrackCreated = true,
});
WorldConfig provides factory methods:
WorldConfig.Default()— standard settings (single-threaded, independent)WorldConfig.MaxThreads()— all available CPU threads All parameters are optional — any unset value falls back toWorldConfig.Default().
Type registration:
W.Create(WorldConfig.Default());
// Register components, tags, and events — only between Create() and Initialize()
W.Types()
.EntityType<Bullet>()
.Component<Position>()
.Component<Velocity>()
.Tag<IsPlayer>()
.Event<OnDamage>();
// Initialize the world
W.Initialize();
Type registration (Types().Component<T>(), Types().Tag<T>(), Types().EntityType<T>()) is only available in the Created state — after Create() and before Initialize(). Event registration (Types().Event<T>()) is also available after initialization.
Auto-registration of types:
Instead of manually registering each type, you can use automatic assembly scanning. RegisterAll() discovers all structs implementing ECS interfaces in one or more assemblies and registers each one via the corresponding Register* API.
W.Create(WorldConfig.Default());
// Parameterless form — scans the assembly that declares the IWorldType struct `WT`
// (resolved as typeof(WT).Assembly). No stack walking.
W.Types().RegisterAll();
// Explicit form — scans the given assemblies only. The first assembly is required
// so an empty call is syntactically impossible.
W.Types().RegisterAll(typeof(MyGame).Assembly, typeof(MyPlugin).Assembly);
// Can be combined with manual registration (fluent chain)
W.Types()
.RegisterAll()
.Component<SpecialComponent>();
W.Initialize();
How the scanned assembly is resolved
| Overload | Scanned assemblies |
|---|---|
RegisterAll() | typeof(TWorld).Assembly — the assembly that declares your IWorldType struct (in the examples, WT — not the alias class W : World<WT>, but the struct itself) |
RegisterAll(Assembly first, params Assembly[] rest) | Exactly the assemblies you pass — TWorld’s assembly is not added implicitly |
The parameterless form deliberately uses typeof(TWorld).Assembly and never calls Assembly.GetCallingAssembly(). This means it works correctly on all runtimes, including:
- .NET Framework / .NET Core / .NET 5+
- Mono and Unity Mono
- Unity IL2CPP
- Unity WebGL
- NativeAOT
On IL2CPP/WebGL/NativeAOT, Assembly.GetCallingAssembly() returns unreliable results because stack walking is stripped or restricted — that is why the implementation derives the assembly from a generic type argument instead. As long as your IWorldType struct (WT) lives in the same assembly as your ECS types, the parameterless form is all you need.
Multi-assembly scenario
If your IWorldType struct and your ECS types live in different assemblies (for example, WT is defined in a shared “core” assembly and your components live in a game assembly), use the explicit overload and list every assembly that contains ECS types:
W.Types().RegisterAll(
typeof(WT).Assembly, // core assembly with the IWorldType struct
typeof(Position).Assembly, // gameplay assembly with components
typeof(AiPlugin).Assembly // another plugin assembly
);
Detected interfaces
| Interface | Registration |
|---|---|
IComponent | Types().Component<T>() |
ITag | Types().Tag<T>() |
IEvent | Types().Event<T>() |
ILinkType | Wrapped in Link<T> and registered as a component |
ILinksType | Wrapped in Links<T> and registered as a component |
IMultiComponent | Wrapped in Multi<T> and registered as a component |
IEntityType | Types().EntityType<T>() |
- The StaticEcs framework assembly itself is always excluded from scanning.
- Abstract types and open generic type definitions are skipped.
- A struct implementing multiple interfaces (e.g. both
IComponentandIMultiComponent) is registered for each applicable interface. - The
Defaultentity type is skipped because it is already registered by the world. RegisterAll()searches for a static field or property of the matching config type inside each struct and uses it if found. Otherwise, default configuration is used. Lookup rules:IComponent— looks forComponentTypeConfig<T>(prefers nameConfig)IEvent— looks forEventTypeConfig<T>(prefers nameConfig)ITag— looks forTagTypeConfig<T>(prefers nameConfig)IEntityType— looks forbyte(prefers nameId)
- Both fields and properties are supported.
- Must be called during the
Createdphase — afterW.Create()and beforeW.Initialize().
Initialization:
// Standard initialization (baseEntitiesCapacity — initial entity capacity)
W.Initialize(baseEntitiesCapacity: 4096);
// After initialization, an existing snapshot can be loaded:
// — entity identifiers only (EntityGID versions)
W.Serializer.RestoreFromGIDStoreSnapshot(snapshot);
// — or full world state (entities and all their data)
W.Serializer.LoadWorldSnapshot(snapshot);
RestoreFromGIDStoreSnapshot restores only entity identifier metadata (GID versions). LoadWorldSnapshot restores the full world state, including all entities and their data. Both require the world to already be initialized.
Destruction:
// Destroy the world and release all resources
W.Destroy();
Basic operations
// Current world status
WorldStatus status = W.Status;
// true if the world is initialized
bool initialized = W.IsWorldInitialized;
// true if the world is independent
bool independent = W.IsIndependent;
// Entity count in the world (active + unloaded)
uint entitiesCount = W.CalculateEntitiesCount();
// Loaded entity count
uint loadedCount = W.CalculateLoadedEntitiesCount();
// Current entity capacity
uint capacity = W.CalculateEntitiesCapacity();
// Destroy all entities in the world (world remains initialized)
W.DestroyAllLoadedEntities();
For details on creating entities and entity operations — see Entity.
For details on world resources — see Resources.
Cluster
A cluster is a group of entity chunks for spatial segmentation of the world. Entities in the same cluster are grouped together and stored in memory in a segmented manner.
- Represented as a
ushortvalue (0–65535) - By default, cluster 0 is created on world initialization
- All entities are created in cluster 0 by default
- A cluster can be disabled — entities from disabled clusters are excluded from iteration
Clusters are designed for spatial grouping: levels, map zones, game rooms. For logical grouping (units, bullets, effects) use entityType.
Basic operations:
// Register clusters (can be called after Create() or after Initialize())
const ushort LEVEL_1_CLUSTER = 1;
const ushort LEVEL_2_CLUSTER = 2;
W.RegisterCluster(LEVEL_1_CLUSTER);
W.RegisterCluster(LEVEL_2_CLUSTER);
// Check if a cluster is registered
bool registered = W.ClusterIsRegistered(LEVEL_1_CLUSTER);
// Enable or disable a cluster — entities from disabled clusters are excluded from iteration
W.SetActiveCluster(LEVEL_2_CLUSTER, false);
// Check if a cluster is active
bool active = W.ClusterIsActive(LEVEL_2_CLUSTER);
// Destroy all entities in a cluster
W.DestroyAllEntitiesInCluster(LEVEL_1_CLUSTER);
// Free a cluster — all entities are deleted, chunks and the identifier are released
W.FreeCluster(LEVEL_2_CLUSTER);
// Safe free — returns false if the cluster is not registered
bool freed = W.TryFreeCluster(LEVEL_2_CLUSTER);
Cluster snapshots and unloading:
// Create a cluster snapshot (stores all entity data)
// Overloads available for writing to disk, compression, etc.
byte[] snapshot = W.Serializer.CreateClusterSnapshot(LEVEL_1_CLUSTER);
// Unload a cluster from memory
// Component and tag data is removed, entities are marked as unloaded
// Only identifier information is preserved, entities are excluded from queries
ReadOnlySpan<ushort> clusters = stackalloc ushort[] { LEVEL_1_CLUSTER };
W.Query().BatchUnload(EntityStatusType.Any, clusters: clusters);
// Load a cluster from a snapshot
W.Serializer.LoadClusterSnapshot(snapshot);
Cluster chunks:
// Get all chunks in a cluster (including empty ones)
ReadOnlySpan<uint> chunks = W.GetClusterChunks(LEVEL_1_CLUSTER);
// Get chunks that have at least one loaded entity
ReadOnlySpan<uint> loadedChunks = W.GetClusterLoadedChunks(LEVEL_1_CLUSTER);
Creating entities in a cluster:
// Specify a cluster when creating an entity (default — cluster 0)
struct UnitType : IEntityType { }
var entity = W.NewEntity<UnitType>(clusterId: LEVEL_1_CLUSTER);
// The clusterId parameter is available in all overloads
W.NewEntity<UnitType>(
new UnitType(), // entity type instance (can carry config data for OnCreate)
clusterId: LEVEL_1_CLUSTER
);
// Get the entity's cluster
ushort entityClusterId = entity.ClusterId;
// Get the cluster from EntityGID
ushort gidClusterId = entity.GID.ClusterId;
Chunk
A chunk is a block of 4096 entities. The entire world consists of chunks. Each chunk belongs to a cluster.
- Independent world (
Independent = true) — manages chunks automatically, creates new ones as needed - Dependent world (
Independent = false) — has no chunks available for entity creation viaNewEntity(), chunks must be explicitly assigned
Basic operations:
// Find a free chunk not belonging to any cluster
// Independent world: if none available — creates a new one
// Dependent world: if none available — error
EntitiesChunkInfo chunkInfo = W.FindNextSelfFreeChunk();
uint chunkIdx = chunkInfo.ChunkIdx;
// chunkInfo.EntitiesFrom — first entity identifier in the chunk
// chunkInfo.EntitiesCapacity — chunk size (always 4096)
// Safe variant (returns false if no free chunks)
bool found = W.TryFindNextSelfFreeChunk(out EntitiesChunkInfo info);
// Register a chunk in a cluster
W.RegisterChunk(chunkIdx, clusterId: LEVEL_1_CLUSTER);
// Register a chunk with a specific ownership type
W.RegisterChunk(chunkIdx, owner: ChunkOwnerType.Self, clusterId: LEVEL_1_CLUSTER);
// Safe registration (returns false if the chunk is already registered)
bool registered = W.TryRegisterChunk(chunkIdx, clusterId: LEVEL_1_CLUSTER);
// Check if a chunk is registered
bool isRegistered = W.ChunkIsRegistered(chunkIdx);
// Get the cluster a chunk belongs to
ushort clusterId = W.GetChunkClusterId(chunkIdx);
// Move a chunk to another cluster
W.ChangeChunkCluster(chunkIdx, LEVEL_2_CLUSTER);
// Check for entities in a chunk
bool hasEntities = W.HasEntitiesInChunk(chunkIdx); // active + unloaded
bool hasLoaded = W.HasLoadedEntitiesInChunk(chunkIdx); // loaded only
// Destroy all entities in a chunk
W.DestroyAllEntitiesInChunk(chunkIdx);
// Free a chunk — all entities are deleted, the identifier is released
W.FreeChunk(chunkIdx);
Chunk snapshots and unloading:
// Create a chunk snapshot
byte[] snapshot = W.Serializer.CreateChunkSnapshot(chunkIdx);
// Unload a chunk from memory (data removed, entities marked as unloaded)
ReadOnlySpan<uint> chunks = stackalloc uint[] { chunkIdx };
W.Query().BatchUnload(EntityStatusType.Any, chunks);
// Load a chunk from a snapshot
W.Serializer.LoadChunkSnapshot(snapshot);
Creating entities in a specific chunk:
// Create an entity in a specific chunk
struct UnitType : IEntityType { }
var entity = W.NewEntityInChunk<UnitType>(chunkIdx: chunkIdx);
// Safe variant (returns false if the chunk is full)
bool created = W.TryNewEntityInChunk<UnitType>(out var entity, chunkIdx: chunkIdx);
// Non-generic variant (entity type known at runtime as byte)
byte entityTypeId = EntityTypeInfo<UnitType>.Id;
var entity = W.NewEntityInChunk(entityTypeId, chunkIdx: chunkIdx);
Chunk ownership (ChunkOwnerType)
The ownership type determines how the world uses a chunk for entity creation:
ChunkOwnerType.Self— chunk is managed by this world. Entities created viaNewEntity()are placed in these chunks- Independent worlds have all chunks with
Selfownership by default
- Independent worlds have all chunks with
ChunkOwnerType.Other— chunk is not managed by this world.NewEntity()will never place entities in these chunks- Dependent worlds have all chunks with
Otherownership by default
- Dependent worlds have all chunks with
// Get the chunk's ownership type
ChunkOwnerType owner = W.GetChunkOwner(chunkIdx);
// Change ownership
// Self → Other: chunk becomes unavailable for NewEntity()
// Other → Self: chunk becomes available for NewEntity()
W.ChangeChunkOwner(chunkIdx, ChunkOwnerType.Other);
Entity creation via NewEntityByGID<TEntityType>(gid) is only available for chunks with Other ownership. Entity creation via NewEntityInChunk<TEntityType>(chunkIdx) is only available for chunks with Self ownership.
Client-server example:
// === Server side (Independent world) ===
// Find a free chunk and register with Other ownership
// The server will not create its own entities in this identifier range
EntitiesChunkInfo chunkInfo = WServer.FindNextSelfFreeChunk();
WServer.RegisterChunk(chunkInfo.ChunkIdx, ChunkOwnerType.Other);
// Send the chunk identifier to the client
// === Client side (Dependent world) ===
// Receive the chunk identifier from the server
// Register with Self ownership — now 4096 entity slots are available
WClient.RegisterChunk(chunkIdxFromServer, ChunkOwnerType.Self);
// The client can create entities via NewEntity()
// For example, for UI or VFX
var vfx = WClient.NewEntity<VfxType>();
// Similarly works for P2P:
// one Independent host + N Dependent clients
Cluster and chunk usage examples
Clusters:
- Levels and map zones — different clusters for different parts of the game world. As the player moves, clusters can be loaded and unloaded to save memory
- Game levels — load/unload clusters when changing levels
- Game sessions — cluster identifier defines a session. Combined with parallel iteration, multi-world emulation within a single world is possible
Chunks:
- World streaming — loading and unloading chunks during gameplay
- Custom identifier management — control over EntityGID distribution
- Arena memory — fast allocation and cleanup of large numbers of temporary entities
Chunk ownership:
- Client-server interaction — server allocates identifier ranges to clients
- P2P network formats — one Independent host and N Dependent clients