SimulationTree.Worlds.Core
0.3.9
Prefix Reserved
dotnet add package SimulationTree.Worlds.Core --version 0.3.9
NuGet\Install-Package SimulationTree.Worlds.Core -Version 0.3.9
<PackageReference Include="SimulationTree.Worlds.Core" Version="0.3.9" />
<PackageVersion Include="SimulationTree.Worlds.Core" Version="0.3.9" />
<PackageReference Include="SimulationTree.Worlds.Core" />
paket add SimulationTree.Worlds.Core --version 0.3.9
#r "nuget: SimulationTree.Worlds.Core, 0.3.9"
#:package SimulationTree.Worlds.Core@0.3.9
#addin nuget:?package=SimulationTree.Worlds.Core&version=0.3.9
#tool nuget:?package=SimulationTree.Worlds.Core&version=0.3.9
Worlds
Library for implementing data as components, arrays, and tags, found on entities.
Entities are stored within these worlds, which can then be serialized, deserialized, and appended to other worlds at runtime.
Creating worlds
Worlds contain a Schema, describing which types are possible to use with it.
When using a type that isn't registered while interacting with a world,
errors will be thrown in debug mode. All types that are used must be registered.
Schemas can be loaded after a world is created, or by passing one to the constructor:
private static void Main()
{
MetadataRegistryLoader.Load(); // register type metadata
Schema schema = new();
schema.RegisterComponent<float>();
schema.RegisterComponent<int>();
schema.RegisterComponent<Fruit>();
using World world = new(schema);
uint entity = world.CreateEntity();
world.AddComponent(entity, 3.14f);
world.AddComponent(entity, 1337);
world.AddComponent(entity, new Fruit(25));
}
public struct Fruit(uint value)
{
public uint value = value;
}
The
MetadataRegistryLoaderis part of thetypesproject and it initializes metadata for all types.
Schema loader
Included is a generator for a SchemaLoader type for projects that have an entry point.
It ensures that all mentioned types with a world are registered. Saving the effort for
manually registering them, and making startup easier:
private static void Main()
{
MetadataRegistryLoader.Load(); // register type metadata
Schema schema = SchemaLoader.Get(); // register components/arrays/tags
using World world = new(schema);
uint entity = world.CreateEntity();
world.AddComponent(entity, 3.14f);
world.AddComponent(entity, 1337);
world.AddComponent(entity, new Fruit(25));
}
Storing values in components
using (World world = new())
{
uint entity = world.CreateEntity();
world.AddComponent(entity, new Fruit(25));
}
Storing multiple values with arrays
Unlike components, arrays offer a way to store multiple of the same type, and can be resized:
Values<char> many = world.CreateArray(entity, "Hello world".AsSpan());
many.Length = 5;
Assert.That(moreMany.AsSpan().ToString(), Is.EqualTo("Hello"));
many.AddRange(" there".AsSpan());
Assert.That(many.AsSpan().ToString(), Is.EqualTo("Hello there"));
Tagging entities
Entities can be tagged with any type, and then queried for:
public struct IsThing
{
}
uint entity = world.CreateEntity();
world.AddTag<IsThing>(entity);
Assert.That(world.Contains<IsThing>(entity), Is.True);
Fetching data and querying
Polling of components, and modifying them can be done through a few different ways.
Manual
This approach performs the quickest:
uint sum = 0;
void Do()
{
int componentType = world.Schema.GetComponentType<Fruit>();
int tagType = world.Schema.GetTagType<IsThing>();
ReadOnlySpan<Chunk> chunks = world.Chunks;
for (int c = 0; c < chunks.Length; c++)
{
Chunk chunk = chunks[c];
if (chunk.componentTypes.Contains(componentType) && !chunk.tagTypes.Contains(tagType))
{
Span<Fruit> components = chunk.GetComponents<Fruit>(componentType);
ReadOnlySpan<uint> entities = chunk.Entities;
for (int i = 0; i < entities.Length; i++)
{
uint entity = entities[i];
ref Fruit component = ref components[i];
component.value *= 2;
sum += component.value;
}
}
}
}
ComponentQuery
This approach is the next quicker, and requires less code to write:
uint sum = 0;
void Do()
{
ComponentQuery<Fruit> query = new(world);
query.ExcludeTags<IsThing>();
foreach (var x in query)
{
uint entity = x.entity;
ref Fruit component = ref x.component1;
component.value *= 2;
sum += component.value;
}
}
Get methods
Other approaches through extension methods like GetAllContaining don't lend themselves
to quicker runtimes:
uint sum;
void Do()
{
foreach (uint entity in world.GetAllContaining<Fruit>())
{
if (world.ContainsTag<IsThing>(entity))
{
continue;
}
//this approach suffers from having to fetch each component individually
ref Fruit component = ref world.GetComponent<Fruit>(entity);
component.value *= 2;
sum += component.value;
}
}
Relationship references to other entities
Components with uint values that are meant to reference other entities will be
susceptible to drift after serialization. This is because the entity value represents
a position, that may be occupied by another existing entity.
This is solved by storing the references locally and accessing them with an rint index.
When worlds are appended to another world, those referenced entities can shift together
as they're added, preserving the relationship.
public struct MyReference(rint entityReference)
{
public rint entityReference = entityReference;
}
using World dummyWorld = new(SchemaLoader.Get());
uint firstEntity = dummyWorld.CreateEntity();
uint secondEntity = dummyWorld.CreateEntity();
rint entityReference = dummyWorld.AddReference(firstEntity, secondEntity);
dummyWorld.AddComponent(firstEntity, new MyReference(entityReference));
//after appending, find the original first entity and its referenced second entity
world.Append(dummyWorld);
world.TryGetFirst(out uint oldFirstEntity, out MyReference component);
uint oldSecondEntity = world.GetReference(oldFirstEntity, component.entityReference);
The Entity wrapper
In addition to the original API, can also use Entity instances. Which wrap the
uint value for the entity and the World instance:
Entity entity = new(world);
entity.AddComponent(new Fruit(1337));
ref Fruit component = ref entity.GetComponent<Fruit>();
component.value *= 2;
Span<char> text = entity.CreateArray<char>("Hello world".AsSpan());
Forming entity types
A commonly reused pattern with components is to formalize them into argued objects, where the
type is qualified by the data present on the entity. For example, if an entity
contains a PlayerName, then its a player entity. This design is supported with the
IEntity interface and its required Describe() method:
public struct PlayerName(ASCIIText32 name)
{
public ASCIIText32 name = name;
}
public readonly partial struct Player : IEntity
{
public readonly ref ASCIIText32 Name => ref GetComponent<PlayerName>().name;
readonly void IEntity.Describe(ref Archetype archetype)
{
archetype.AddComponentType<PlayerName>();
}
public Player(World world, ASCIIText32 name)
{
this.world = world;
value = world.CreateEntity(new PlayerName(name));
}
}
//creating a player using its type's constructor
Player player = new(world, "unnamed");
Only entity types that are partial will have all of the world API available
These types can then be used to transform or interpret existing entities:
//creating an entity, and making it into a player
Entity supposedPlayer = new(world);
Assert.That(supposedPlayer.Is<Player>(), Is.False);
supposedPlayer.Become<Player>();
Assert.That(supposedPlayer.Is<Player>(), Is.True);
Player player = supposedPlayer.As<Player>();
Assert.That(player.IsCompliant, Is.True);
player.Name = "New name";
These entity types can be implicitly casted to Entity, and explicitly back:
Player player = new(world, "unnamed");
Entity entity = player;
player = entity.As<Player>();
Serializing and appending
Each world instance is portable, and can be serialized and deserialized in another executable:
Schema schema = SchemaLoader.Get();
using World prefabWorld = new(schema);
Entity entity = new(prefabWorld);
entity.AddComponent(new Fruit(1337));
entity.CreateArray<char>("Hello world".AsSpan());
using ByteWriter writer = new();
writer.WriteObject(prefabWorld);
ReadOnlySpan<byte> bytes = writer.AsSpan();
using ByteReader reader = new(bytes);
using World loadedWorld = World.Deserialize(reader);
using World anotherWorld = new(schema);
anotherWorld.Append(loadedWorld);
Processing deserialized schemas
When worlds are serialized, they contain the original schema that was used. Storing
the original TypeLayout values for describing each component/array/tag type.
Allowing for them to be processed when loaded in a different executable, and rerouting types
to other types if the original isn't:
using World loadedWorld = World.Deserialize(reader, Process);
static TypeLayout Process(TypeLayout type, DataType.Kind dataType)
{
if (type.Name.SequenceEquals("Fruit") && type.Size == sizeof(uint))
{
//Fruit type not in this project, change to uint
return MetadataRegistry.GetType<uint>();
}
else
{
return type;
}
}
Contributing and design
This library implements the "entity-component-system" pattern of the "archetype" variety.
Created for building programs of whatever kind, with an open door for targeting runtime efficiency. Favoring faster data access and iteration.
Contributions to this goal are welcome.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- SimulationTree.Collections (>= 0.3.9)
- SimulationTree.Types.Core (>= 0.3.9)
- SimulationTree.Unmanaged.Core (>= 0.3.9)
NuGet packages (20)
Showing the top 5 NuGet packages that depend on SimulationTree.Worlds.Core:
| Package | Downloads |
|---|---|
|
SimulationTree.Worlds
Native C# library for ECS |
|
|
SimulationTree.Shaders
Package Description |
|
|
SimulationTree.Meshes
Package Description |
|
|
SimulationTree.Materials
Package Description |
|
|
SimulationTree.Data.Core
Package Description |
GitHub repositories
This package is not used by any popular GitHub repositories.