How the Rule-Based Event Triggering System Works in Undertale-Changer-Template

The rule-based event triggering system evaluates ScriptableObject rules against active events every frame, automatically firing new events, invoking C# methods, and updating fact values when trigger conditions and optional fact-based criteria are satisfied.

The Undertale-Changer-Template (UCT) implements a data-driven rule-based event triggering system that powers all gameplay logic through Unity ScriptableObjects. This architecture allows designers to author complex game behavior without writing code, while the runtime engine handles the frame-by-frame evaluation of conditions and execution of actions.

Core Architecture Components

The system relies on four primary data structures and one runtime controller to manage the flow from trigger detection to action execution.

RuleTable and RuleEntry Data Containers

RuleTable.cs defines a ScriptableObject that stores a serializable list of RuleEntry objects. Designers create these assets at Assets/Scripts/UCT/EventSystem/RuleTable.cs to hold scene-specific or global rule definitions. Each RuleEntry (defined in RuleEntry.cs) contains the trigger event names, target events to fire, method calls to invoke, fact modifications to apply, and an optional logical criterion for conditional evaluation.

EventController Runtime Engine

EventController.cs (Assets/Scripts/UCT/EventSystem/EventController.cs) is a persistent MonoBehaviour that serves as the runtime engine. It loads rule tables, iterates through active events during each Update cycle, and orchestrates the evaluation and execution phases. This controller maintains references to both global and scene-specific rule tables, concatenating them during evaluation.

Event and Fact State Tables

EventTable and FactTable ScriptableObjects track the current runtime state. The EventTable maintains a list of EventEntry objects with boolean isTriggering flags, while the FactTable stores integer-based FactEntry values that rules read and modify. These tables provide the volatile state that rules query during criterion evaluation.

The Rule Evaluation Flow

The engine processes rules through a deterministic pipeline that runs every frame, ensuring immediate response to gameplay state changes.

1. Table Loading and Initialization

During EventController.Start(), the system loads the global rule table from Tables/RuleTable and the scene-specific table from Tables/<SceneName>/RuleTable. These RuleTable assets populate the runtime evaluation lists.

2. Update Loop and Event Detection

Every frame, EventController.UpdateEvent() processes both the global and scene event tables. For each EventEntry where isTriggering equals true, the Detection method gathers all rules by concatenating ruleTable.rules with globalRuleTable.rules, then evaluates them sequentially via DetectionRule.

3. Criterion and Trigger Validation

DetectionRule performs two validation checks before firing:

  • Criterion Evaluation: If useRuleCriterion is enabled, the system calls rule.ruleCriterion.GetResult() to evaluate the boolean expression against current fact values.
  • Trigger Matching: The current event name must exist in the rule.triggeredBy list, or the rule must be forced via a direct call.

4. Action Execution

When validation succeeds, the rule executes three action types in this specific order:

  • Event Firing: SetTriggering adds names from rule.triggers to the appropriate EventTable, setting their isTriggering flags to true.
  • Method Invocation: InvokeRuleMethod walks the methodNames list and executes corresponding delegates from the MethodDictionary, parsing prefixes like bool:, Vector2Ease:, or scene: to handle type conversion.
  • Fact Modification: SetFact applies the FactModification list (operations like Change, Add, or Subtract) to local or global FactTable instances.

By default, the detection loop breaks immediately when a rule returns isTriggered == true, ensuring only the first matching rule fires per event. Enable isExecuteAllRules on the trigger component to allow multiple rule matches per frame.

Rule Criteria and Logical Evaluation

Complex conditional logic is handled through the recursive RuleCriterion structure without requiring custom code.

Fact-Based Conditions with RuleCriterion

The RuleCriterion.GetResult() method in RuleEntry.cs evaluates boolean expressions through recursive descent:

public bool GetResult()
{
    // Base case: direct fact comparison
    if (criteria.Count == 0) 
    { 
        // Compare fact.value vs detection using CriteriaCompare operators
    }
    
    // Recursive case: combine child criteria with And/Or/None operations
}

The system looks up facts from the local FactTable by default, or the global table when isGlobal is true. The CriteriaCompare enum supports GreaterThan, Equal, LessThan, and other relational operators. Set isResultReversed to true to invert the final boolean result.

Method Invocation via MethodDictionary

EventController maintains a MethodDictionary that maps string descriptors to static method delegates. When InvokeMethodByName processes entries like "bool:PlayerCanMove", it parses the prefix to determine the return type and parameter signature, converts string parameters to C# types, and invokes the delegate. This allows designers to call gameplay methods directly from data.

Fact Modifications and Execution Order

Fact modifications occur after method invocation completes. This sequencing guarantees that any invoked method reading a fact value sees the pre-modification state, preventing race conditions between the action and the state change.

Practical Implementation Examples

Defining Rules in the Unity Inspector

Create a RuleTable asset via Assets → Create → UCT-EventSystem → RuleTable, then add a RuleEntry with these fields:

Field Value
name OpenDoorIfHasKey
triggeredBy ["PlayerNearDoor"]
useRuleCriterion true
ruleCriterion fact: hasKey, compare: Equal, detection: 1, isGlobal: false
triggers ["DoorOpened"]
methodNames ["bool:PlayerCanMove"]
firstStringParams ["false"]
useMethodEvents [true]
methodEvents ["PlayDoorSound"]
factModifications [{fact: "hasKey", operation: Subtract, number: 1}]

This configuration requires the hasKey fact to equal 1, fires the DoorOpened event, disables player movement, plays a sound event, and consumes one key unit.

Creating Rules Programmatically

Instantiate rules at runtime using the RuleEntry struct:

using UCT.EventSystem;
using System.Collections.Generic;

public static RuleEntry CreateDoorRule()
{
    return new RuleEntry
    {
        name = "OpenDoorIfHasKey",
        triggeredBy = new List<string> { "PlayerNearDoor" },
        useRuleCriterion = true,
        ruleCriterion = new RuleCriterion
        {
            isResultReversed = false,
            isGlobal = false,
            fact = new FactEntry { name = "hasKey", value = 0 },
            compare = CriteriaCompare.Equal,
            detection = 1,
            operation = RuleLogicalOperation.And,
            criteria = new List<RuleCriterion>()
        },
        triggers = new List<string> { "DoorOpened" },
        methodNames = new List<string> { "bool:PlayerCanMove" },
        firstStringParams = new List<string> { "false" },
        useMethodEvents = new List<bool> { true },
        methodEvents = new List<string> { "PlayDoorSound" },
        factModifications = new List<FactModification>
        {
            new FactModification
            {
                fact = new FactEntry { name = "hasKey" },
                operation = FactModification.Operation.Subtract,
                number = 1
            }
        }
    };
}

Triggering Events from Gameplay Code

Activate events from anywhere in your codebase to initiate rule evaluation:

// In player collision detection or input handling:
EventController.SetTriggering("PlayerNearDoor");

This sets the isTriggering flag on the corresponding EventEntry, causing the EventController to evaluate matching rules during the next Update cycle.

Summary

  • Data-Driven Architecture: Rules are authored as RuleEntry objects inside RuleTable ScriptableObjects, separating gameplay logic from code.
  • Frame-Synchronous Evaluation: EventController.UpdateEvent() checks all triggered events against loaded rules every frame via the Detection and DetectionRule methods.
  • Hierarchical Logic: RuleCriterion supports nested boolean expressions with AND/OR operations, fact comparisons, and negation through isResultReversed.
  • Sequential Execution: Rules fire events first, then invoke methods via MethodDictionary, and finally apply FactModification operations to ensure consistent state reads.
  • Scoped State: The system distinguishes between global and scene-local facts and rules, loading tables from Tables/RuleTable and Tables/<SceneName>/RuleTable respectively.

Frequently Asked Questions

What file contains the main rule evaluation loop?

EventController.cs (Assets/Scripts/UCT/EventSystem/EventController.cs) contains the core evaluation loop. The UpdateEvent() method drives the process, while Detection() and DetectionRule() handle the specific logic for matching triggered events against rule criteria and executing the resulting actions.

How does the system handle complex logical conditions?

The RuleCriterion struct implements recursive boolean evaluation through the GetResult() method. It supports nested sub-criteria combined via the RuleLogicalOperation enum (And, Or, None), fact comparisons using the CriteriaCompare operators, and result inversion via the isResultReversed flag. This allows designers to build complex conditional trees entirely within the Unity Inspector without code changes.

Can multiple rules fire from a single event trigger?

By default, the system stops evaluating after the first matching rule returns isTriggered == true to prevent cascading conflicts. However, you can enable the isExecuteAllRules flag in the trigger component configuration to allow all matching rules to fire during the same evaluation frame, processing them in the order they appear in the RuleTable.

What is the execution order when a rule fires?

When a rule's conditions are satisfied, the EventController executes actions in this strict sequence: first it fires new events via SetTriggering, then it invokes all specified methods through InvokeRuleMethod, and finally it applies fact modifications via SetFact. This ordering ensures that invoked methods read the pre-modification fact values, while subsequent rules in the chain see the updated values.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →