RoguelikeToolkit.Entities
5.0.0
See the version list below for details.
dotnet add package RoguelikeToolkit.Entities --version 5.0.0
NuGet\Install-Package RoguelikeToolkit.Entities -Version 5.0.0
<PackageReference Include="RoguelikeToolkit.Entities" Version="5.0.0" />
<PackageVersion Include="RoguelikeToolkit.Entities" Version="5.0.0" />
<PackageReference Include="RoguelikeToolkit.Entities" />
paket add RoguelikeToolkit.Entities --version 5.0.0
#r "nuget: RoguelikeToolkit.Entities, 5.0.0"
#:package RoguelikeToolkit.Entities@5.0.0
#addin nuget:?package=RoguelikeToolkit.Entities&version=5.0.0
#tool nuget:?package=RoguelikeToolkit.Entities&version=5.0.0
RoguelikeTools.Entities
This is a library to allow resolve entity templates for Entity Component System in a similar way Ultimate Adom is doing.
Supports entity template inheritance and creation of entity hierarchies.
It is using the awesome DefaultEcs library to construct the entities.
Features
- YAML and JSON support: Entity templates can be written in human-readable
.yamlor.jsonformats. - Inheritance: Templates can inherit from one or more base templates, mixing components and tags together.
- Embedded templates: Hierarchies are fully supported, you can embed child entities inside templates.
- Scripts: Components can carry JavaScript (Jint) scripts for data-driven behavior; parsed once, shared, AOT-safe.
- Dice parsing: The system seamlessly creates component variables from standard dice notations (
2d6+3).
Quick Start
1. Define Templates
Create a file named templates.yaml in your project containing your entity definitions:
# A base template with generic components
EnemyBase:
Tags:
- AI
- CanMove
Components:
Health:
Max: 10
Current: 10
Speed:
Value: 5
# A concrete template that inherits the EnemyBase
Goblin:
Inherits:
- EnemyBase
Tags:
- Goblin
Components:
Attack:
Damage: "1d4" # This will automatically be parsed into a Dice object!
Health:
Max: 15 # Overrides the EnemyBase's Health Max value
2. Loading Templates & Creating Entities
Load these templates and generate DefaultEcs entities easily:
using DefaultEcs;
using RoguelikeToolkit.Entities.Repository;
using RoguelikeToolkit.Entities.Factory;
var world = new World();
var repository = new EntityTemplateRepository();
// Load the templates
repository.LoadTemplateFolder("path/to/my/templates");
// Initialize the factory
var factory = new EntityFactory(repository, world);
// Create the entity!
if (factory.TryCreate("Goblin", out Entity goblinEntity))
{
// The goblinEntity now has:
// - Health component (Max: 15, Current: 0) -- inheritance overrides whole components, not single fields
// - Speed component (Value: 5)
// - Attack component (Damage: 1d4)
// - Tags: "AI", "CanMove", "Goblin"
}
Advanced Examples
Embedded Entities
You can embed child entities into another template. When EntityFactory creates an OrcRider, it also creates the embedded Wolf and automatically assigns it as a child using DefaultEcs.Entity.SetAsParentOf.
Wolf:
Components:
Speed:
Value: 10
OrcRider:
Components:
Mount:
Type: "Wolf"
# Embeds the 'Wolf' template
Wolf: {}
Querying Templates
repository.TryGetByName("Goblin", out var template); // O(1), case-insensitive
// Tag index: cost follows the matches, not the repository size.
// Requires ALL tags; an empty query returns every template.
var meleeEnemies = repository.GetByTags("AI", "CanMove");
Spawning at Scale
Inheritance is resolved once per template and cached, so spawning thousands of instances of the same blueprint stays flat:
for (var i = 0; i < 2000; i++)
{
factory.TryCreate("Goblin", out var goblin);
}
Global components are first-write-wins: the first spawned entity sets the world's
shared copy, later spawns link to it. Dice strings ("1d4") share one parse per
distinct expression; scripts likewise share one parsed, immutable instance per distinct source.
Scripts
Script-typed component members (EntityScript, EntityComponentScript, EntityInteractionScript, Script, all in
RoguelikeToolkit.Entities.Scripting) are populated from template strings:
Components:
Behavior:
OnHit: |
var h = entity.GetComponent(Health);
h.Current -= damage;
entity.SetComponent(Health, h);
Regen: component.Current += amount;
[Component]
public class Behavior
{
public EntityScript? OnHit { get; set; }
public EntityComponentScript? Regen { get; set; }
}
// one host per thread / simulation loop; it owns the (single) Jint engine
using var host = new ScriptHost(new ScriptHostOptions { ReferenceTypes = new[] { typeof(Health) } });
var behavior = goblin.Get<Behavior>();
behavior.OnHit!.ExecuteOn(host, ref goblin, ("damage", 4));
behavior.Regen!.TryExecuteOn<Health>(host, goblin, ("amount", 2)); // writes the component back via Entity.Set
- Scripts are immutable and hold no state: each distinct source is parsed once per process and the instance is shared by every entity spawned from the template. Keep state in components.
- Each execution runs in its own function scope (
var/functiondo not leak between scripts,returnexits early); bindings (entity,component,source/target, arguments) are cleared afterwards. entity.HasComponent/GetComponent/SetComponent/RemoveComponent(Type)take any type exposed viaScriptHostOptions.ReferenceTypes. Value-type components come back as copies: write them back withSetComponent.- Limits: 500 statements and recursion depth 1024 per execution by default;
TimeoutandMemoryLimitBytesare opt-in. ScriptHostis not thread-safe (neither is a Jint engine); scripts are, and can run on any number of hosts.
Catching Template Typos
A template property that matches no component member (renamed property, field instead of property) warns instead of silently dropping. Hard failures (unknown component type, null value, cycles) throw with template + member context.
using RoguelikeToolkit.Entities;
EntityDiagnostics.WarningHandler = message => Console.WriteLine($"[template] {message}");
Save / Load
Persist the template name plus dynamic state, then re-spawn and re-apply:
// save
Save("Goblin", entity.Get<Health>().Current, entity.Get<Position>());
// load
factory.TryCreate(savedTemplateName, out var entity);
entity.Set(new Health { Current = savedHp, Max = savedMax });
Modding / Hot-Reload
Loading is additive and folder loads are atomic — on any failure an
AggregateException (one inner exception per file) is thrown and the repository
is left unchanged:
await repository.LoadTemplateFolderAsync("Mods/Extra", cancellationToken);
There is no unload: hot-reload a mod by loading it into a fresh
EntityTemplateRepository + EntityFactory pair and dropping the old one.
Template Format
Author in YAML (comments, readability), ship JSON or a packed single file for
spawn-time loading — the same loader reads both (.yaml / .json,
case-insensitive). Relative $ref / $merge-ref paths resolve against the
referencing file's directory.
Unity / IL2CPP
The netstandard2.1 build has no Reflection.Emit on the load/spawn path, so it runs under IL2CPP.
CI publishes a NativeAOT smoke test (stricter than IL2CPP: no JIT, no runtime generic instantiation) to keep it that way.
Unity 2022.1+: works as is.
Stricter AOT (NativeAOT, older IL2CPP): register each component type once at startup so its closed generic code exists:
AotComponentRegistry.Register<Health>();Managed code stripping: component types (and the types of their nested struct/class members) are populated through reflection only. Keep them from being stripped with
[Preserve]or alink.xml:<linker> <assembly fullname="MyGame.Components" preserve="all" /> </linker>Scripts: Jint is a pure managed interpreter (no IL emission); arguments are marshalled without
dynamic. Script interop reaches your component types by reflection, so they need the same stripping protection as above. LeaveScriptHostOptions.MemoryLimitBytesoff on IL2CPP (it pollsGC.GetAllocatedBytesForCurrentThreadper statement). Jint is pinned to the versionRoguelikeToolkit.World.Scripting.Jintuses: a Unity project can only hold one.
Docs
- Usage guide — querying, spawning, pooling, save/load, modding, examples.
- Architecture — internal creation and inheritance flow.
- Integration decisions — why each dependency / perf choice stands.
Benchmarks & Stress Tests
dotnet run --project benchmarks/RoguelikeToolkit.Entities.Benchmarks -c Release
dotnet test Library.sln
Benchmarks (BenchmarkDotNet, allocation + time) catch perf/allocation regressions; the test suite includes stress tests (mass spawn, mass load, concurrent reads). See benchmarks/README.md.
Baseline (Apple M2, macOS Tahoe 26.3.1, .NET 10.0.12, BenchmarkDotNet 0.15.8; spawn rows are per 100-entity batch, resolve rows per single cold resolve + 1 spawn):
| Benchmark | Mean | Allocated |
|---|---|---|
Load_200_Templates |
11.54 ms | 5.02 MB |
BulkLoad_Then_QueryByTag |
10.10 ms | 4.76 MB |
FolderLoad_200_Files |
9.56 ms | 6.63 MB |
Resolve_Cold_SingleLevel |
2.22 ms | 20.28 KB |
Resolve_Cold_TwoLevel |
3.32 ms | 27.70 KB |
Spawn_Simple |
6.84 ms | 1.30 MB |
Spawn_WithEmbedded |
5.40 ms | 1.20 MB |
Spawn_ManyComponents |
13.00 ms | 2.12 MB |
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. 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 is compatible. 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. |
| .NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 is compatible. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.1
- DefaultEcs (>= 0.17.2)
- deniszykov.TypeConversion (>= 3.2.4)
- Jint (>= 4.16.2)
- Microsoft.Extensions.ObjectPool (>= 10.0.5)
- RoguelikeToolkit.Dice (>= 1.0.4)
- YamlDotNet (>= 18.1.0)
-
net10.0
- DefaultEcs (>= 0.17.2)
- deniszykov.TypeConversion (>= 3.2.4)
- Jint (>= 4.16.2)
- Microsoft.Extensions.ObjectPool (>= 10.0.5)
- RoguelikeToolkit.Dice (>= 1.0.4)
- YamlDotNet (>= 18.1.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 6.0.2 | 81 | 10/11/2026 |
| 5.1.0 | 82 | 10/11/2026 |
| 5.0.0 | 96 | 10/11/2026 |
| 4.0.21 | 353 | 1/5/2025 |
| 4.0.20 | 314 | 1/4/2025 |
| 4.0.17 | 346 | 4/25/2024 |
| 4.0.10 | 518 | 1/14/2023 |
| 4.0.6 | 609 | 9/25/2022 |
| 4.0.4 | 640 | 9/14/2022 |
| 4.0.1 | 647 | 9/14/2022 |
| 3.0.0 | 686 | 4/1/2022 |
| 2.2.0 | 607 | 9/25/2021 |
| 2.1.4 | 677 | 9/19/2021 |
| 2.1.3 | 631 | 9/18/2021 |
| 2.1.2 | 564 | 9/1/2021 |
| 2.1.1 | 574 | 8/31/2021 |
| 2.0.10 | 529 | 8/31/2021 |
| 2.0.9 | 571 | 8/30/2021 |
| 2.0.8 | 554 | 8/30/2021 |
| 2.0.7 | 573 | 8/29/2021 |
Changes since version v4.0.6.0
:boom: Breaking Changes:
- The `RoguelikeToolkit.Scripts` dependency is replaced by `RoguelikeToolkit.Entities.Scripting` (Jint 4.16.2):
- namespace `RoguelikeToolkit.Scripts` -> `RoguelikeToolkit.Entities.Scripting`;
- script constructors take only the source (reference types moved to `ScriptHostOptions.ReferenceTypes`);
- every execution takes a `ScriptHost` (owns the engine; one per thread) as its first argument;
- extra arguments are `ScriptArgument` / `(string, object?)` tuples instead of `(string, dynamic)`;
- `EntityComponentScript.TryExecuteOn` takes the entity by `in` instead of `ref`;
- scripts are stateless and shared between entities spawned from the same template: top-level `var`s no longer
persist between executions (state belongs in components).
:sparkles: Features:
- `EntityTemplate.Copy()` creates an editable, unshared variant of a template ("different mobs").
- `EntityTemplateRepository.AddTemplate` stores a defensive copy (clone-on-write at the ownership boundary).
- `EntityTemplateRepository.RemoveTemplate` removes a template together with its tag index entries.
- `EntityTemplateRepository.LoadTemplate` overload accepting in-memory yaml/json string content.
- Shared templates (repository store, effective-template cache) stay shared (flyweight) for reads; mutating one warns through `EntityDiagnostics` (silent by default) instead of corrupting state silently.
- Unity IL2CPP / AOT support: no Reflection.Emit anywhere on the load/spawn path. `Fasterflect` and `ObjectTreeWalker` (Sigil IL emission) are no longer dependencies; component members are written through cached plain reflection, components are set through cached closed generic setters, and primitive/enum/dice/script conversions take an AOT-safe path before falling back to `deniszykov.TypeConversion`.
- `AotComponentRegistry.Register<T>()`: optional per-component AOT hint (closed generic setter + trimming annotations) for runtimes that cannot instantiate generics at runtime (NativeAOT, IL2CPP before 2022.1).
- NativeAOT smoke test (`tests/RoguelikeToolkit.Entities.AotSmoke`, CI job) as the IL2CPP proxy: load, inheritance, nested struct/class, value, dice and global components, scripts, mass spawn.
- `ScriptHost` / `ScriptHostOptions`: one reusable Jint engine with statement, recursion, timeout and (opt-in) memory limits; per-execution function scope and binding cleanup.
- Scripts can use `entity.HasComponent/GetComponent/SetComponent/RemoveComponent(Type)` (exposed types via `ScriptHostOptions.ReferenceTypes`); never worked with the old package.
:zap: Performance:
- Spawning is 6-40x faster with 5-8x less allocation (Spawn_Simple 11.5 ms -> 0.87 ms per 100 entities, 1.28 MB -> 163 KB; same machine, BenchmarkDotNet short job) from dropping per-type IL generation and `MethodInfo.Invoke` on the hot path.
- Script members cost nothing per spawn (shared instance; previously a new Jint engine per entity: ~53 µs, ~21 KB). Executing with an argument: 15.8 µs -> 6.2 µs.
:bug: Fixes:
- `EntityExtensions` child iterator no longer leaks pooled traversal state between calls; tagless entities share an immutable empty tag set; parent-link bookkeeping is thread-safe.
- `GlobalComponentInEntitySetter` actually validates the resolved `SetSameAsWorld` method and warns when a later global value is ignored (first write wins).
- Folder loads stay atomic under racing loads (conflicting batch is rolled back, duplicate raises).
- Duplicate embedded template names and duplicate case-only component properties now warn/fail instead of silently dropping data.
- Single `TypeConversionProvider` factory and single null-guard helper remove four duplicated setups and all repeated `#if` blocks.
- `EntityFactory` spawn errors now name the template and component involved.
- YAML loading no longer depends on reflection-only container constructors (stripped by AOT linkers): `Dictionary`/`List` containers are created directly.
- Component registry tolerates runtimes that cannot enumerate assembly references (scans instead of failing).
- `IValueComponent<T>` detection compares the generic definition instead of a name substring.
- Nested template mappings now report their own unmatched keys through `EntityDiagnostics` (previously only top-level keys were checked).