RoguelikeToolkit.Entities
5.1.0
See the version list below for details.
dotnet add package RoguelikeToolkit.Entities --version 5.1.0
NuGet\Install-Package RoguelikeToolkit.Entities -Version 5.1.0
<PackageReference Include="RoguelikeToolkit.Entities" Version="5.1.0" />
<PackageVersion Include="RoguelikeToolkit.Entities" Version="5.1.0" />
<PackageReference Include="RoguelikeToolkit.Entities" />
paket add RoguelikeToolkit.Entities --version 5.1.0
#r "nuget: RoguelikeToolkit.Entities, 5.1.0"
#:package RoguelikeToolkit.Entities@5.1.0
#addin nuget:?package=RoguelikeToolkit.Entities&version=5.1.0
#tool nuget:?package=RoguelikeToolkit.Entities&version=5.1.0
RoguelikeToolkit.Entities
Data-driven entity templates for DefaultEcs, in the spirit of
Ultimate ADOM's moddable ECS:
describe entities in YAML/JSON, inherit and compose them, spawn them into a DefaultEcs.World.
Targets net10.0 and netstandard2.1 (Unity 2021.3+, IL2CPP-safe). MIT.
Install
- .NET:
dotnet add package RoguelikeToolkit.Entities - Unity: see Unity / IL2CPP.
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 and no dynamic, so it runs under IL2CPP.
CI runs a NativeAOT smoke test (stricter than IL2CPP: no JIT, no runtime generic instantiation, full trimming) on
every PR to keep it that way.
Install
The UPM package com.rogueliketoolkit.entities ships the prebuilt netstandard2.1 DLL. Its third-party
dependencies (DefaultEcs, YamlDotNet, Jint, ...) come from UnityNuGet
as org.nuget.* packages instead of being bundled: Unity refuses two copies of the same DLL, and Jint/YamlDotNet
are shared with RoguelikeToolkit.World. Add both scoped registries to Packages/manifest.json:
{
"scopedRegistries": [
{
"name": "OpenUPM",
"url": "https://package.openupm.com",
"scopes": ["com.rogueliketoolkit"]
},
{
"name": "UnityNuGet",
"url": "https://unitynuget-registry.openupm.com",
"scopes": ["org.nuget"]
}
],
"dependencies": {
"com.rogueliketoolkit.entities": "5.1.0"
}
}
(openupm add com.rogueliketoolkit.entities does the OpenUPM half.)
Without registries: every GitHub Release also carries RoguelikeToolkit.Entities-<version>-unity-plugins.zip
with the whole DLL closure plus a link.xml. Unzip into Assets/Plugins/RoguelikeToolkit.Entities/, skipping any DLL
your project already has (System.Runtime.CompilerServices.Unsafe often is).
Runtime notes
Unity 2022.1+: works as is.
Older IL2CPP / NativeAOT: register each component type once at startup so its closed generic code exists:
AotComponentRegistry.Register<Health>();Managed code stripping: the package hands UnityLinker its own
link.xml(an editor hook; Unity ignoreslink.xmlfiles inside packages), preserving this library, DefaultEcs, YamlDotNet, TypeConversion and Jint. Your component types (and the types of their nested struct/class members) are populated by reflection only, so they need[Preserve]or alink.xmlof your own underAssets/:<linker> <assembly fullname="MyGame.Components" preserve="all" /> </linker>Scripts: Jint is a pure managed interpreter (no IL emission). Script interop reaches your component types by reflection, so they need the same stripping protection. Leave
ScriptHostOptions.MemoryLimitBytesoff on IL2CPP (Jint 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 |
Releases
Merging a PR into main computes the next version from conventional commits (GitVersion: feat minor, fix/others
patch, !/BREAKING CHANGE major), tags vX.Y.Z, writes the GitHub Release and publishes the NuGet package plus the
UPM tarball and plugins zip (tools/upm/pack.py, template in upm/com.rogueliketoolkit.entities) as release assets,
which OpenUPM picks up.
| 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 v5.0.0.0
:sparkles: New Features:
- Unity package `com.rogueliketoolkit.entities` (OpenUPM): prebuilt `netstandard2.1` DLL, dependencies from UnityNuGet (`org.nuget.*`), and an editor hook that hands the package's `link.xml` to UnityLinker. Every release also attaches a `-unity-plugins.zip` with the full DLL closure.
- Release tags are plain semver (`v5.1.0`; previously `v5.0.0.0`): UPM/OpenUPM reject 4-part versions. Assembly versions are unchanged.
:bug: Bug Fixes:
- `DefaultEcs` was referenced with a floating `*` version; pinned to `0.17.2`.
- `Microsoft.SourceLink.GitHub` leaked `System.IO.Hashing` into the netstandard2.1 runtime closure; its runtime assets are now excluded.