Skip to content
kimja edited this page Jul 31, 2026 · 10 revisions

πŸͺ½ Matter (events)

πŸ“œ Matter is the base carrier of event data. Every matter has a Guid (its unique identity) and a list of Circumstances (a collection of matter that led to the emission of this one).

Extend Matter to define your own event types:

class PlayerDied : Matter
{
    public string Cause { get; }
    public PlayerDied(string cause) => Cause = cause;
}

class EnemyDefeated : Matter
{
    public int XpReward { get; }
    public EnemyDefeated(int xpReward) => XpReward = xpReward;
}



On unserializable matter fields

πŸ“œπŸ§­ Mark engine-native fields on matter with [JsonIgnore] (from System.Text.Json.Serialization).

  • Game matter routinely carries engine handles - PackedScene returned from a scene load, a Node reference, a Texture2D, etc.
  • These belong on matter, they just shouldn't go over the dev wire to Eris, because System.Text.Json can't serialize them.
public class LoadSceneResponse : Response<LoadSceneRequest>
{
    [JsonIgnore] public PackedScene PackedScene { get; }

    public LoadSceneResponse(LoadSceneRequest request, PackedScene packedScene)
        : base(request, wasSuccessful: true)
    {
        PackedScene = packedScene;
    }
}
  • Strings, lists, nested IMatter, and your own plain data classes serialize fine and need no annotation.
  • Forgetting [JsonIgnore] on an engine-native field breaks Eris casuality and downstream matter occurences will display Missing circumstances.
  • When that happens Eris emits a Horror about that. Your game runs unchanged, only the debugger enters trouble-land when that happens.



On Matter Immutability

πŸ“œπŸ§¨ Try to keep your Matter instances immutable - avoid mutable reference-type fields and set accessors. Mutating matter after emission produces three specific failure modes:

  • Snapshot warping: subscribers that process matter on different frames see different values for what is supposedly the same event.
  • Circumstance pollution: matter lives in multiple Circumstances lists, mutationas mess up the causal DAG by causing rewrites within its nodes.
  • Eris timeline confusion: mutations between emission and serialization show up as drift in the recorded history.

Two common ways the temptation appears, and the rzeka-native answer is:

  • "I want this to update over time and have everyone see the new value." That's state, not an event. Mark the type [HasState] and evolve it through a reducer Loom - see Attributes.
  • "I want consumers to be able to call X on this matter." X is a service the consumer depends on, not data that belongs on matter. Inject it using Microsoft.Extensions.DependencyInjection (or your preferred equivalent) and keep matter as data.

πŸ“œπŸŒ± I understand the temptation is sometimes too great, and that's okay. When it stops feeling occasional and starts feeling structural, one of the above two patterns is almost always the better fit.



Circumstances

πŸ“œ Circumstances describe context of a given matter emission.

They are usually attached automatically, but there are situations where you have to do it manually, to read more on that see Circumstance rules. Nothing horrible happens if you forget it, game runs fine, but Eris, rzeka's debugger will lose casuality tracking in such situations.

For example, a DamageDealt matter could carry the AttackExecuted that triggered it as a circumstance.

  • This allows you to track the causality chains through Eris.
  • Or to check in your game logic if a given matter is caused by another, anywhere up its circumstance chain, using someMatter.IsCircumstancedBy(anotherMatter).
// pseudocode, just to show relations between matter, see API section for where you will actually instantiate matter
var attack = new AttackExecuted(attacker: "dragon");
var damage = new DamageDealt(amount: 40).WithCircumstances<DamageDealt>(attack); // manual circumstances

// Later, check if a piece of matter is causally linked to something specific
bool causedByDragon = damage.IsCircumstancedBy(attack); // true



Naming Conventions

Name matter in the past tense - matter represents facts that already happened (GameStarted, PlayerMoved, EnemyDefeated). Reading a circumstance chain like DamageReceived ← AttackLanded ← FireballCast tells a coherent story.

Two shapes to avoid:

  • Present continuous (GameLoading) usually wants to be state instead - use [HasState] with a State suffix. Otherwise you end up re-emitting the same matter to mean "still loading," which is exactly what [HasState] exists to replace.
  • Imperative (StartGame) usually wants to be a request instead - use a Shuttle Request/Response pair. The fact emitted after the request resolves is the past-tense matter (GameStarted).

πŸ§šπŸ»β€β™€οΈ Fairy infestation test: "X happened, and as a result Y also happened." If the sentence doesn't parse with your matter names, it's probably state or a request wearing the wrong shape.

πŸ“œπŸ§­ Requested vs Request. Use past-tense Requested (ScreenShakeRequested, HealthBarUpdateRequested) for regular matter that asks for something fire-and-forget, but reserve the noun Request for the IRequest half of a Shuttle pair (SaveGameRequest). The -ed vs -t ending doubles as a call-site cue: Loom<…, FooRequested> is one-way, Shuttle<FooRequest, FooResponse> is round-trip.

Clone this wiki locally