RoguelikeToolkit.Entities 5.1.0

There is a newer version of this package available.
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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="RoguelikeToolkit.Entities" Version="5.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RoguelikeToolkit.Entities" Version="5.1.0" />
                    
Directory.Packages.props
<PackageReference Include="RoguelikeToolkit.Entities" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add RoguelikeToolkit.Entities --version 5.1.0
                    
#r "nuget: RoguelikeToolkit.Entities, 5.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package RoguelikeToolkit.Entities@5.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=RoguelikeToolkit.Entities&version=5.1.0
                    
Install as a Cake Addin
#tool nuget:?package=RoguelikeToolkit.Entities&version=5.1.0
                    
Install as a Cake Tool

RoguelikeToolkit.Entities

NuGet openupm

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 .yaml or .json formats.
  • 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/function do not leak between scripts, return exits early); bindings (entity, component, source/target, arguments) are cleared afterwards.
  • entity.HasComponent/GetComponent/SetComponent/RemoveComponent(Type) take any type exposed via ScriptHostOptions.ReferenceTypes. Value-type components come back as copies: write them back with SetComponent.
  • Limits: 500 statements and recursion depth 1024 per execution by default; Timeout and MemoryLimitBytes are opt-in.
  • ScriptHost is 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 ignores link.xml files 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 a link.xml of your own under Assets/:

    <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.MemoryLimitBytes off on IL2CPP (Jint polls GC.GetAllocatedBytesForCurrentThread per statement). Jint is pinned to the version RoguelikeToolkit.World.Scripting.Jint uses: a Unity project can only hold one.

Docs

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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
Loading failed

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.