Appearance
kids.kapish.entityreact
unity
Data-driven entity system with reactive views. Entities hold typed properties; views react to changes via reflection binding.
Installation
Add to your Unity project's package manifest:
json
{
"kids.kapish.entityreact": "file:../../src/unity/kids.kapish.entityreact"
}Requires Unity 2022.3+.
Architecture
Entity
Entity is a ScriptableObject asset. It holds:
- Properties (
EntityProperties) -- a typed key-value bag (case-insensitive string keys,LateCastObjectvalues). - Tags (
List<EntityTag>) -- named property sets that extendEntityPropertiesand participate in the same change notification system. - Actions (
List<EntityAction>) -- named, triggerable actions with anExecutedsignal (Signal<EntityAction>) and execution count. - Views -- registered
ViewMonoBehaviours that receive property change broadcasts.
When a property is set via Properties.Set<T>(key, value), the entity broadcasts a SendMessage(key, oldValue, newValue) to all registered views. View registration and deregistration are deferred during broadcast to prevent collection modification.
Entities are created as assets via Create > Carrot > EntityReact > Entity.
Properties
EntityProperties stores runtime state cloned from serialized defaults on ManagedEnable. Each value is a LateCastObject -- a type-tagged object wrapper with a LateCastObjectType discriminator. Deep cloning is performed for Gradient and AnimationCurve to ensure instance independence.
Supported property types: string, float, int, bool, Color, Gradient, Vector2, Vector2Int, Vector3, Vector3Int, Vector4, Sprite, AudioClip, AnimationCurve, and generic object.
Tags
EntityTag extends EntityProperties. Subclasses define DefaultValues which are applied on enable without triggering view broadcasts (the entity reference is temporarily nulled). Tags let you compose entity capabilities -- attach a "Destructible" tag to add Health and MaxHealth properties with defaults, and any view that listens for Health_Changed will react.
Actions
EntityAction is a named, serializable action. Subscribe via action.Executed.Add(handler) (returns an unsubscribe Action) to handle triggers. Actions track their ExecutionCount. The entity exposes StartAction(name), GetAction(name), HasAction(name), and TryGetAction(name, out action).
Views
View is a MonoBehaviour that registers with an entity on Start and deregisters on OnDisable. When a property changes, the view receives it via ReceiveMessage which dispatches through two reflection channels:
- Property setter -- if the view has a C# property matching the key name, its setter is called with the new value.
- Change method -- if the view has a method named
{Key}_Changed(T oldValue, T newValue), it is invoked.
On initialization (first registration), views receive all current property values with the key suffixed _Initialize for property setters.
Reflection lookups are cached per (viewType, key, valueType) in ViewReflection to avoid per-frame reflection overhead.
ViewFallback
ViewFallback is a MonoBehaviour that provides a shared entity reference for multiple View components on the same GameObject. If a view has no explicit entity assigned, it falls back to the ViewFallback on its GameObject.
File Structure
Runtime/
├── Entity.cs # ScriptableObject entity asset
├── EntityProperties.cs # Typed key-value property bag + EntityProperty
├── EntityTag.cs # Composable property set with defaults
├── EntityAction.cs # Named triggerable action
├── LateCastObject.cs # Type-tagged value wrapper + enum + extensions
└── Views/
├── View.cs # Reactive MonoBehaviour view base
├── ViewFallback.cs # Shared entity reference fallback
└── ViewReflection.cs # Cached reflection dispatchDependencies
| Dependency | Kind |
|---|---|
kids.kapish | runtime (Carrot core Unity package) |
Build
Import via Unity Package Manager. Requires Unity 2022.3+.
Usage Guide
Data-driven entity system with reactive views. Entities hold typed properties; views react to changes via reflection binding.
Setup
Add kids.kapish.entityreact to your Unity project manifest. Requires Unity 2022.3+.
Common Patterns
1. Create an entity asset
Right-click in the Project window: Create > Carrot > EntityReact > Entity. Set a UniqueName and configure default properties and tags in the inspector.
2. Set and read properties at runtime
csharp
using Carrot.EntityReact;
// Set a property -- broadcasts to all views
entity.Properties.Set("Health", 100);
entity.Properties.Set("Name", "Hero");
entity.Properties.Set("IsAlive", true);
// Read properties
int health = entity.Properties.Get<int>("Health");
bool alive = entity.Properties.TryGet<bool>("IsAlive", out bool value);
bool hasHealth = entity.Properties.Has("Health");3. Build a reactive view
Subclass View and add matching properties or _Changed methods. The reflection system dispatches automatically -- no manual subscription needed.
csharp
using Carrot.EntityReact.Views;
using UnityEngine;
using UnityEngine.UI;
public class HealthBarView : View
{
[SerializeField] private Slider slider;
// Called when "Health" is set on the entity (property setter)
public int Health
{
set => slider.value = value;
}
// Called when "Health" changes (method handler -- receives old and new)
private void Health_Changed(int oldValue, int newValue)
{
if (newValue < oldValue)
{
Debug.Log($"Took {oldValue - newValue} damage!");
}
}
// Called once on registration with current value
public int Health_Initialize
{
set => slider.maxValue = value;
}
}4. Wire a view to an entity
In the inspector, assign the Entity field on your View component. Alternatively, add a ViewFallback component to the GameObject and assign the entity there -- all sibling views without an explicit entity will use the fallback.
5. Create a custom tag
Tags compose reusable property sets with defaults:
csharp
using System.Collections.Generic;
using Carrot.EntityReact;
public class DestructibleTag : EntityTag
{
public override IEnumerable<KeyValuePair<string, object>> DefaultValues
{
get
{
yield return new KeyValuePair<string, object>("Health", 100);
yield return new KeyValuePair<string, object>("MaxHealth", 100);
yield return new KeyValuePair<string, object>("IsIndestructible", false);
}
}
}Add the tag to an entity at runtime:
csharp
entity.AddTag<DestructibleTag>();
// or
entity.AddTag(typeof(DestructibleTag));Tag properties are broadcast to views on registration, just like entity properties.
6. Define and trigger actions
csharp
using Carrot.EntityReact;
using UnityEngine;
// Create an action at runtime
EntityAction interact = EntityAction.Create(entity, "Interact", interactIcon);
interact.Executed.Add(action =>
{
Debug.Log($"Interacted with {action.Entity.UniqueName} ({action.ExecutionCount} times)");
});
// .Add() returns an unsubscribe Action — capture it if you need to detach later.
entity.AddAction(interact);
// If you need to detach later:
// var unsub = interact.Executed.Add(Handler);
// unsub();
// Trigger it
entity.StartAction("Interact");
// Query
if (entity.TryGetAction("Interact", out EntityAction found))
{
Debug.Log($"Action found, executed {found.ExecutionCount} times");
}Add
kids.kapish.signalsto your asmdef references if usingEntityAction.Executedfrom your own code.
7. Query tags
csharp
if (entity.HasTag<DestructibleTag>())
{
DestructibleTag tag = entity.GetTag<DestructibleTag>();
int health = tag.Get<int>("Health");
}
// By name (matches on type name)
EntityTag tag = entity.GetTag("DestructibleTag");8. Multiple views, shared entity
When several view components on the same GameObject share one entity, use ViewFallback to avoid duplicating the entity reference:
csharp
// On the GameObject:
// - ViewFallback (Entity = myEntity)
// - HealthBarView (Entity = null -- falls back to ViewFallback)
// - NameplateView (Entity = null -- falls back to ViewFallback)Tips
- Properties are case-insensitive --
Health,health, andHEALTHall resolve to the same property. - Dots become underscores -- a property key like
stats.healthdispatches asstats_healthfor reflection matching. - Views register on Start -- properties set before
Startare delivered as initialization messages on registration. - Tag defaults don't broadcast -- default values are applied silently. Only subsequent
Setcalls trigger view updates. - Reflection is cached -- the first lookup per
(viewType, key, valueType)uses reflection; subsequent calls hit the cache. - Deferred view changes -- registering or deregistering a view during a broadcast is safe; changes are flushed after the broadcast completes.