How to Implement Custom Events Using ImplementableEventAttribute in Ceres

Mark any C# method with [ImplementableEvent] to generate a graph-editable event node that routes EventBase<T> payloads through Ceres-Flow, enabling visual scripting integration for custom runtime events.

The ImplementableEventAttribute bridges compiled C# code with Ceres-Flow visual graphs through compile-time source generation. When applied to a method, this attribute triggers the Ceres source generator to create an ExecutableEvent node that can be placed in a Flow Graph, along with a matching CustomExecutionEvent type that routes runtime EventBase<T> instances to that node. This architecture enables developers to fire events from code and handle them visually in the graph, or override them programmatically at runtime.

Core Concepts and Architecture

Understanding how Ceres-Flow processes custom events requires familiarity with the components that connect attributed methods to graph execution.

Component Purpose Source File
ImplementableEventAttribute Marks methods for source-generation of graph-editable event nodes Runtime/Flow/Annotations/ImplementableEventAttribute.cs
CustomExecutionEvent Abstract base for generated event nodes; maintains ID-to-name mapping for event routing Runtime/Flow/Models/Nodes/Core/CustomExecutionEvent.cs
ProcessEvent Helper method that fires events through the graph's execution pipeline Runtime/Flow/Models/FlowGraph.cs (lines 33-86)
OverrideEventImplementation Runtime extension that replaces default graph logic with custom delegates Runtime/Flow/Models/FlowGraph.cs (lines 606-608)

The source generator creates concrete ExecutableEvent_{ClassName} nodes for every EventBase<T> type used with [ImplementableEvent], as implemented in the template at Runtime/SourceGenerators/Source~/Ceres.SourceGenerator/Generators/CustomEventGeneratorTemplate.cs.

Step-by-Step Implementation Guide

Define a Custom Event Type

Create a class derived from EventBase<T> to serve as your event payload. This type identifies the event within the Ceres event system and carries data to graph nodes.

using Chris.Events;

public sealed class TestGlobalEvent : EventBase<TestGlobalEvent>
{
    // Optional payload fields
    public int Value;
}

When the project compiles, the Ceres.SourceGenerator emits a concrete ExecutableEvent_TestGlobalEvent node deriving from CustomExecutionEvent<TestGlobalEvent>.

Mark Methods with ImplementableEventAttribute

Add a method in a MonoBehaviour or any class implementing IFlowGraphRuntime and annotate it with [ImplementableEvent].

using Ceres.Graph.Flow;
using Ceres.Graph.Flow.Annotations;
using UnityEngine;

public class MyFlowBehaviour : MonoBehaviour, IFlowGraphRuntime
{
    public GameObject gameObject => this.gameObject; // required by IFlowGraphRuntime
    public FlowGraph Graph => /* obtain your compiled FlowGraph instance */;

    [ImplementableEvent]                     // ← Attribute triggers source generation
    private void OnTestGlobalEvent()
    {
        // This body executes when the custom event is raised
        Debug.Log("Custom event received – custom logic runs here.");
    }
}

The source generator detects the attribute, creates the corresponding ExecutableEvent_TestGlobalEvent node, and inserts a delegate port that calls OnTestGlobalEvent during graph execution.

Fire Events from Code

Invoke the generated event using ProcessEvent<T>() or SendEvent() from any C# method.

using Ceres.Graph.Flow;

public void Trigger()
{
    // Create a pooled instance of the custom event, fill payload if needed
    using var evt = TestGlobalEvent.GetPooled(42);
    // Send it to the graph – this locates the generated ExecutableEvent node
    this.SendEvent(evt);               // alternative: this.ProcessEvent<TestGlobalEvent>();
}

SendEvent routes the EventBase to the CallbackEventHandler, which looks up the event name via CustomExecutionEvent.GetEventName(eventBase.EventTypeId) and executes the matching graph node.

Override Implementations at Runtime (Optional)

Replace the default graph node behavior with custom logic using OverrideEventImplementation<TEvent>().

using Ceres.Graph.Flow;

public void Init()
{
    // Replace the generated node with a custom callback
    this.OverrideEventImplementation<TestGlobalEvent>(evt =>
    {
        // Custom handling – no graph node runs
        Debug.Log($"Override: received {evt.Value}");
        // Prevent default graph execution if desired
        evt.PreventDefault();
    }).AddTo(this); // disposes automatically with the MonoBehaviour
}

This uses FlowGraphRuntimeExtensions.OverrideEventImplementation<TEvent> as implemented in Runtime/Flow/Models/FlowGraph.cs (lines 606-608). The override remains active only while the returned IDisposable is alive.

Complete Working Example

The following MonoBehaviour demonstrates the full workflow: event definition, method annotation, event firing, and runtime override.

// 1️⃣ Custom event definition
using Chris.Events;

public sealed class TestGlobalEvent : EventBase<TestGlobalEvent>
{
    public int Value;
}

// 2️⃣ Behaviour exposing the event
using Ceres.Graph.Flow;
using Ceres.Graph.Flow.Annotations;
using UnityEngine;

public class TestFlowBehaviour : MonoBehaviour, IFlowGraphRuntime
{
    // Provide the runtime graph (omitted here for brevity)
    public FlowGraph Graph => /* your compiled FlowGraph instance */;
    public GameObject gameObject => this.gameObject;

    // Generated node will call this method when the event occurs
    [ImplementableEvent]
    private void OnTestGlobalEvent()
    {
        Debug.Log("Graph‑triggered custom event executed.");
    }

    // Fire the event from anywhere
    public void Send()
    {
        using var evt = TestGlobalEvent.GetPooled(123);
        this.SendEvent(evt);               // or this.ProcessEvent<TestGlobalEvent>();
    }

    // Optional runtime override
    private void Awake()
    {
        this.OverrideEventImplementation<TestGlobalEvent>(e =>
        {
            Debug.Log($"Override received value {e.Value}");
            // Stop the default graph node from running
            e.PreventDefault();
        }).AddTo(this);
    }
}

When Send() is called, the following occurs:

  1. CallbackEventHandler receives TestGlobalEvent.
  2. It queries CustomExecutionEvent.GetEventName(eventBase.EventTypeId)"ExecutableEvent_TestGlobalEvent".
  3. The matching ExecutableEvent_TestGlobalEvent node executes, forwarding to the delegate port → OnTestGlobalEvent.

If an override was registered, the override runs instead, and the graph node is skipped because PreventDefault() is called.

Key Source Files and Implementation Details

File Purpose Direct Link
ImplementableEventAttribute.cs Defines the attribute that triggers source generation for graph-editable events https://github.com/akikurisu/ceres/blob/main/Runtime/Flow/Annotations/ImplementableEventAttribute.cs
CustomExecutionEvent.cs Base class for generated custom events; maintains the mapping between event type IDs and graph node names https://github.com/akikurisu/ceres/blob/main/Runtime/Flow/Models/Nodes/Core/CustomExecutionEvent.cs
FlowGraph.cs (runtime extensions) Contains ProcessEvent helpers, OverrideEventImplementation, and event lookup logic https://github.com/akikurisu/ceres/blob/main/Runtime/Flow/Models/FlowGraph.cs#L33-L86
TestFlowGraphObject.cs (sample) Demonstrates [ImplementableEvent], ProcessEvent, and override usage in a test scenario https://github.com/akikurisu/ceres/blob/main/Tests/Scripts/TestFlowGraphObject.cs
Source generator template Shows how the generator creates ExecutableEvent_{ClassName} nodes from attributed methods https://github.com/akikurisu/ceres/blob/main/Runtime/SourceGenerators/Source~/Ceres.SourceGenerator/Generators/CustomEventGeneratorTemplate.cs

Summary

  • ImplementableEventAttribute bridges C# methods and Ceres-Flow graph nodes through compile-time source generation.

  • Define event payloads by inheriting from EventBase<T>; the generator creates matching ExecutableEvent nodes automatically.

  • Annotate methods with [ImplementableEvent] to expose them as graph entry points that receive runtime events.

  • Fire events via SendEvent() or ProcessEvent<T>() to trigger graph execution from code.

  • Use OverrideEventImplementation<T>() to replace generated graph logic with custom runtime delegates when needed.

Frequently Asked Questions

How does ImplementableEventAttribute differ from standard Unity events?

ImplementableEventAttribute generates a graph-editable node at compile time, whereas standard Unity events like UnityEvent are purely runtime constructs without visual representation. The attribute triggers the Ceres source generator to create a CustomExecutionEvent subclass that maps the event ID to a specific graph node name, allowing the event to be handled visually in the Flow Graph while maintaining type-safe C# integration through the CallbackEventHandler system.

Can I pass custom data with ImplementableEventAttribute?

Yes, by defining a payload class that inherits from EventBase<T>. For example, TestGlobalEvent : EventBase<TestGlobalEvent> can include public fields like int Value. When firing the event via SendEvent() or ProcessEvent<T>(), the payload travels through the CallbackEventHandler to the generated ExecutableEvent node, where it can be accessed by ports in the Flow Graph or by the annotated C# method.

Where is the code generated by ImplementableEventAttribute located?

The source generator creates concrete event node classes during compilation and injects them into the assembly. The generation logic resides in Runtime/SourceGenerators/Source~/Ceres.SourceGenerator/Generators/CustomEventGeneratorTemplate.cs, while the runtime base classes are in Runtime/Flow/Models/Nodes/Core/CustomExecutionEvent.cs. The generated nodes follow the naming convention ExecutableEvent_{ClassName} and derive from CustomExecutionEvent<T>.

How do I prevent the default graph node from executing?

Call PreventDefault() on the event instance within an override callback registered via OverrideEventImplementation<TEvent>(). This method, located in Runtime/Flow/Models/FlowGraph.cs (lines 606-608), returns an IDisposable that maintains the override scope. When PreventDefault() is invoked, the event system skips the standard graph node execution, allowing your custom delegate to handle the event exclusively.

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 →