Introduction

System Overview

This is a mechanism that allows specific actions to be executed at designated times, typically including components such as event sources, event listeners, and event dispatchers.

Using an event system, we can more conveniently achieve interaction between two relatively independent systems.

After introducing a robust event system, we can also implement logic with more “Observer Pattern” design.

The Observer is a behavioral design pattern that allows you to define a subscription mechanism, notifying multiple objects that observe a target object when an event occurs. — Observer Design Pattern (refactoringguru.cn)

Design Approach

Our aim is to provide a more comprehensive event system, eliminating the need to create a custom event system for similar requirements.

This system should achieve the following goals:

  • Usability
    • Lower the system’s learning curve as much as possible
      • Improve the readability of the logic
      • Provide complete documentation for the system
      • Standardize the event system’s maintenance process
    • Enable events to be conveniently dispatched from any system
      • Dispatch from C++
      • Dispatch from Blueprints
      • Dispatch from Lua
    • Allow any system to easily listen to events
      • Listen from C++
      • Listen from Blueprints
      • Listen from Lua
  • Extensibility
    • Enable the addition of new events easily
    • Improve the rigor of the technical solution to avoid incompatible updates
  • Completeness
    • Handle synchronization between the DS and client
    • Solve three-way communication between C++, Blueprints, and Lua
    • Manage multi-listener order issues
  • Robustness
    • Minimize mandatory requirements when using the event system
  • Performance
    • Minimize unnecessary event dispatches at runtime

Feature Implementation

Event Definition

Classes are used as event indexes to prevent misoperations caused by name-based identifiers, making it easier to expand by event in the future.

We define an Object type UDidaEvent as the event definition, with the event object managing all its listeners.

We also provide conversions between the Event class and FName, which is particularly useful for marking specific events efficiently.

Event key interfaces like UDidaEventLibrary::GetEventKeyByClass are available for use.

Event Management

We define UEventSubsystem, a subclass of UGameInstanceSubsystem, as the event management class.

The EventSubsystem will create event instances as needed.

Event Triggering

Basic Invocation

We define a function library UDidaEventLibrary, which has a static function InvokeEvent to trigger events.

The trigger function should include the following parameters:

ParameterDescription
OuterUsed as the Outer for instantiating the Payload, also used to access the EventSubsystem
EventThe event to trigger
ChannelIf not None, only listeners with the same Channel or without a Channel will be triggered
PayloadSpecifies the type of payload parameter

Advanced Invocation

To simplify event triggering without requiring manual payload instantiation each time, we define a more user-friendly K2Node.

By inheriting from UK2Node, an advanced node UK2Node_DidaInvokeEvent can be implemented.

The core functions are AllocateDefaultPins and ExpandNode, which define all pins on the node and their connections.

Cross-End Invocation

Among all parameters of the event trigger function, Payload is user-defined, so it requires custom encoding and decoding.

Using UE4’s reflection mechanism, the following function can be implemented:

FString UDidaEventPayload::MakeString_Implementation() const
{
   FString String;
   FJsonObjectConverter::UStructToJsonObjectString(GetClass(), this, String, 0, 0, 0, nullptr, false);
   return String;
}

This function encodes all class data into a JSON string, and similarly, a decode function can be implemented:

void UDidaEventPayload::LoadString_Implementation(const FString& String)
{
   TSharedPtr<FJsonObject> JsonObject;
   const TSharedRef<TJsonReader<>> JsonReader = TJsonReaderFactory<>::Create(String);
   if (!FJsonSerializer::Deserialize(JsonReader, JsonObject) || !JsonObject.IsValid())
   {
      FString Name;
      if(IsValid(GetClass()))
      {
         Name = GetClass()->GetName();
      }
      LOG_CATEGORY_WARN(LogDidaEvent, "{} Failed", Name);
      return;
   }
   FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), GetClass(), this);
}

With this design, the Payload can be transmitted over the network, while allowing developers to define custom encoding and decoding rules.

Event Listening

An IDidaEventListenerInterface is defined to provide generic functions for listeners, primarily for RegisterListener and UnregisterListener functions.

When registered, the listener is added to the DidaEvent’s listener list for lookup during triggering.

Performance Optimization

Most listeners only care about specific triggers, so we introduce a Channel mechanism.

Only events with the same Channel as the listener will trigger it (all Channels are triggered if no Channel is specified).

  • Invoking with None will trigger all listeners (regardless of their Channel).
  • Invoking with a Channel will only trigger listeners with the same Channel.
  • Listening with None will receive all invokes (regardless of their Channel).
  • Listening with a Channel will receive invokes with the same Channel.

The interface UDidaEventLibrary::GetObjectChannel is provided to dynamically generate Channels using Objects.

Listener Order

Generally, multiple listeners do not require specific ordering, as they should execute independently.

In some cases, however, we may prefer listeners to trigger in a defined order, so we introduce the ListenerPriority mechanism.

Listeners should implement the PriorityLayer for hierarchical priorities and ListenerPriority for numerical priorities.

The priority dictates the order of event triggers, with layers sorted first, then numerical values, where higher numbers have higher priority.

To maintain clarity, the following special layers are predefined:

Semantic LayerTranslated As
DefaultLayer_5
LowLayer_3
HighLayer_7

Use layers to handle multi-listener order issues whenever possible, as relying solely on numbers may lead to maintenance difficulties.

Usually, call the relevant functions in the DidaEventLibrary function library, passing in the required parameters.

  • UDidaEventLibrary::CreateListenerVariable creates a listener
  • UDidaEventLibrary::InvokeEvent triggers an event
  • UDidaEventLibrary::SpawnDidaEventPayload<UDidaEventPayload_XXX> creates a payload
  • UDidaEventLibrary::GetObjectChannel gets the Channel of an Object