AceLand Unity Packages
  • Home
  • Getting Started
    • Installation
    • Project Settings
    • Architecture Graph
    • Development Level
  • Tutorial
    • Create Your Package
    • Create Project Settings
  • Packages
    • Library
      • Change Log
      • Editor Tools
      • Mono
        • Follow Object
        • Singleton
      • Attributes
        • Conditional Show
        • Inspector Button
        • ReadOnly Field
      • Build Leveling
      • CVS
      • DataTools
      • Disposable Object
      • Extensions
      • Json
      • Kalman Filter
      • Optional
      • Project Setting
      • Serialization Surrogate
      • Utils
    • Event Driven
      • Change Log
      • Event Bus
      • Signal
    • Input
      • Change Log
    • Memento Service
      • Change Log
    • Node Framework
      • Change Log
      • Mono Node
    • Node For Mono (dev)
    • Player Loop Hack
      • Change Log
    • Pool
      • Change Log
    • Security (dev)
    • States
      • Change Log
    • Task Utils
      • Change Log
    • WebRequest
      • Change Log
Powered by GitBook
On this page
  • Overview
  • Project Settings
  • Why Use It
  • How It Works
  • Recommended Usage
  • Prepare Signal IDs
  • Build Signals
  • Get and Use
  1. Packages
  2. Event Driven

Signal

Data-oriented signal system

Overview

The system provides a robust foundation for type-safe, event-driven communication between components while maintaining clean memory management and thread safety.


Project Settings

Swap Signal On Same Id

If a signal is created with same Id, existed one will be replaced by new one. Otherwise exception throw. default: false

Signal Getter Timeout

Getter will be retry automatically until timeout. default: 1.5s


Why Use It

Consider a UI component that displays a player's life: traditionally, this would require direct references to the Player or Life components. However, such tight coupling makes the UI component less reusable and more difficult to maintain. With Signal, the UI component simply subscribes to life-related signals, making it project-agnostic and highly reusable.

While there are numerous dependency injection and reference management solutions available, many of these packages introduce unnecessary complexity. Unlike engines such as Godot that have built-in signal systems, Unity's traditional GameObject-Component architecture doesn't include a native signal system. However, Unity's flexible architecture allows us to implement custom solutions, which is where this Signal system comes in.

Signals can be created and accessed from anywhere in your codebase, enabling flexible and decoupled system design. This flexibility does come with a responsibility: while powerful, signals should be used thoughtfully and systematically to avoid creating complex, hard-to-debug signal chains.

Key Benefits:

  • Decoupled component communication

  • Improved code reusability

  • Lightweight implementation

  • Flexible system design


How It Works

Signal provides a robust event-driven value management system with built-in observer pattern support and type safety.

// Create a signal using builder pattern
var signal = Signal<int>.Builder()
    .WithId("PlayerLife")    // optional
    .WithValue(100)          // optional
    .WithListener(health => UpdateHealthUI(health))    // optional
    .Build();

// Access and modify value
signal.Value = 75;  // Automatically notifies listeners

// Get an exist signal
var lifeSignal = Signal<int>.Get("PlayerLife");
var readonlyLife = Signal<int>.GetReadonly("PlayerLife");

// Get an signal but not confirm wheather created yet
var lifeSignal = await Signal<int>.GetAsync("PlayerLife");
var readonlyLife = await Signal<int>.GetReadonlyAsync("PlayerLife");

Key Features:

  • Builder Pattern

    • Fluent interface for signal creation

    • Optional ID assignment (string or enum)

    • Initial value setting

    • Listener registration during creation

    • Readonly flag for observer protection

  • Value Management

    • Type-safe value storage and modification

    • Automatic listener notification on value changes

    • Implicit conversion to value type

    • Value comparison support

  • Observer System

    • Add/remove listeners dynamically

    • Optional immediate execution on listener addition

    • Thread-safe observer notifications

    • Protection against null listeners

  • Signal Registry

    • Global signal management

    • Async getter with timeout protection

    • Type validation

    • Automatic cleanup through disposal pattern


Recommended Usage

n any system, regardless of its size, proper signal management is crucial for maintaining code clarity and preventing runtime issues. Even in small applications, you'll likely find yourself handling dozens of signals across multiple objects, making a well-organized structure essential.

Below is our recommended approach to signal management, which we use in production. This pattern offers several advantages:

  • Type Safety: Using enums for signal IDs prevents typos and enables IDE auto-completion

  • State Management: Integration with Promise/Task utilities ensures safe asynchronous operations and prevents common runtime issues like timing problems during initialization

  • Code Organization: Clear structural patterns make the system easy to maintain and scale

While this is our recommended approach, feel free to adapt it to your specific needs. The following example demonstrates how we structure signal systems in our own projects.

Prepare Signal IDs

Create Signal IDs by Enum(s)

public enum SignalId
{
    [InspectorName("")] _ = 0,  // Combat Stats
    HP = 10,
    MP = 20,
    SP = 30,
    Atk = 40,
    Def = 50,
    MAtk = 60,
    MDef = 70,
    Acc = 80,
    Eva = 90,
    
    [InspectorName("")] __ = 500,  // Primary Stats
    Str = 510,
    Dex = 520,
    Vit = 530,
    Int = 540,
    Wis = 550,
    Agi = 560,
    Luk = 570,
}

Build Signals

There is two methods on building signals:

  • Owner build it's signals

A reasonable and traditional method.

This method will be recommended if system documentation is created and managed.

In case of change of builder settings of some signals, finding the right class will not spend time if documentation is created.

// Owner build signals

using AceLand.EventDriven.EventSignal;
using AceLand.EventDriven.EventSignal.Core;
using UnityEngine;

public class CombatStats : MonoBehaviour
{
    private Signal<int> _hpSignal;
    private Signal<int> _mpSignal;
    private Signal<int> _spSignal;
    private Signal<int> _atkSignal;
    private Signal<int> _defSignal;
    private Signal<int> _MAtkSignal;
    private Signal<int> _MDefSignal;
    private Signal<int> _AccSignal;
    private Signal<int> _EvaSignal;
    
    // fast way to dispose
    private ISignal[] _signals;

    private void Awake()
    {
        BuildSignal();
    }

    private void OnDestroy()
    {
        foreach (var signal in _signals)
            signal.Dispose();
    }

    private void BuildSignal()
    {
        _signals = new ISignal[]
        {
            // by add .ReadonlyToObserver() on build
            //     can force GetReadonly by other user.
            Signal<int>.Builder().WithId(SignalId.HP).WithValue(12)
                .WithForceReadonly().Build(),
            Signal<int>.Builder().WithId(SignalId.MP).WithValue(8)
                .WithForceReadonly().Build(),
            Signal<int>.Builder().WithId(SignalId.SP).WithValue(9)
                .WithForceReadonly().Build(),
            Signal<int>.Builder().WithId(SignalId.Atk).WithValue(5)
                .WithForceReadonly().Build(),
            Signal<int>.Builder().WithId(SignalId.Def).WithValue(4)
                .WithForceReadonly().Build(),
            Signal<int>.Builder().WithId(SignalId.MAtk).WithValue(6)
                .WithForceReadonly().Build(),
            Signal<int>.Builder().WithId(SignalId.MDef).WithValue(6)
                .WithForceReadonly().Build(),
            Signal<int>.Builder().WithId(SignalId.Acc).WithValue(4)
                .WithForceReadonly().Build(),
            Signal<int>.Builder().WithId(SignalId.Eva).WithValue(3)
                .WithForceReadonly().Build(),
        };
    }
    
    internal void UpdateCombatStats(PrimaryStats primaryStats)
    {
        // calculate combat stats by primary stats
    }
}

public class PrimaryStats : MonoBehaviour
{
    private Signal<int> _strSignal;
    private Signal<int> _dexSignal;
    private Signal<int> _vitSignal;
    private Signal<int> _intSignal;
    private Signal<int> _wisSignal;
    private Signal<int> _agiSignal;
    private Signal<int> _lukSignal;
    
    // fast way to dispose
    private ISignal[] _signals;

    private void Awake()
    {
        BuildSignal();
    }

    private void OnDestroy()
    {
        foreach (var signal in _signals)
            signal.Dispose();
    }

    private void BuildSignal()
    {
        _signals = new ISignal[]
        {
            Signal<int>.Builder().WithId(SignalId.Str).WithValue(6).Build(),
            Signal<int>.Builder().WithId(SignalId.Dex).WithValue(11).Build(),
            Signal<int>.Builder().WithId(SignalId.Vit).WithValue(5).Build(),
            Signal<int>.Builder().WithId(SignalId.Int).WithValue(12).Build(),
            Signal<int>.Builder().WithId(SignalId.Wis).WithValue(14).Build(),
            Signal<int>.Builder().WithId(SignalId.Agi).WithValue(4).Build(),
            Signal<int>.Builder().WithId(SignalId.Luk).WithValue(9).Build(),
        };
    }
}
  • Builder class to build all signals

By use Builder class, all Signals will be build in single place.

This method will be recommended if system documentation will not be created.

In case of change of builder settings of some signals, finding the right class will spend much time if documentation is not created.

// Builder Class

using AceLand.EventDriven.EventSignal;
using AceLand.EventDriven.EventSignal.Core;
using UnityEngine;

public class SignalBuilder : MonoBehaviour
{
    private ISignal[] _signals;
    
    private void Awake()
    {
        BuildSignal();
    }

    private void OnDestroy()
    {
        foreach (var signal in _signals)
            signal.Dispose();
    }

    private void BuildSignal()
    {
        _signals = new ISignal[]
        {
            // Combat Stats
            Signal<int>.Builder().WithId(SignalId.HP).WithValue(12).Build(),
            Signal<int>.Builder().WithId(SignalId.MP).WithValue(8).Build(),
            Signal<int>.Builder().WithId(SignalId.SP).WithValue(9).Build(),
            Signal<int>.Builder().WithId(SignalId.Atk).WithValue(5).Build(),
            Signal<int>.Builder().WithId(SignalId.Def).WithValue(4).Build(),
            Signal<int>.Builder().WithId(SignalId.MAtk).WithValue(6).Build(),
            Signal<int>.Builder().WithId(SignalId.MDef).WithValue(6).Build(),
            Signal<int>.Builder().WithId(SignalId.Acc).WithValue(4).Build(),
            Signal<int>.Builder().WithId(SignalId.Eva).WithValue(3).Build(),
            
            // Primary Stats
            Signal<int>.Builder().WithId(SignalId.Str).WithValue(6).Build(),
            Signal<int>.Builder().WithId(SignalId.Dex).WithValue(11).Build(),
            Signal<int>.Builder().WithId(SignalId.Vit).WithValue(5).Build(),
            Signal<int>.Builder().WithId(SignalId.Int).WithValue(12).Build(),
            Signal<int>.Builder().WithId(SignalId.Wis).WithValue(14).Build(),
            Signal<int>.Builder().WithId(SignalId.Agi).WithValue(4).Build(),
            Signal<int>.Builder().WithId(SignalId.Luk).WithValue(9).Build(),
        };
    }
}

Get and Use

Dynamic method to create an UI Text Updater with signal.

using AceLand.EventDriven.EventSignal;
using AceLand.TaskUtils;
using TMPro;
using UnityEngine;

// Generic Signal Type can fulfill any type of Signal
[RequireComponent(typeof(TextMeshProUGUI))]
public abstract class UiTextUpdateWithSignal<T> : MonoBehaviour
{
    // Select Signal ID in Inspector.
    [SerializeField] private SignalId signalId;

    private ReadonlySignal<T> _signal; 
    private TextMeshProUGUI _label;

    private void Awake()
    {
        InitialComponents();
    }
    
    private void OnEnable()
    {
        if (_signal is null) InitialSignal();
        else _signal?.AddListener(OnSignalUpdate, true);
    }
    
    private void OnDisable()
    {
        _signal?.RemoveListener(OnSignalUpdate);
    }
    
    private void InitialComponents()
    {
        _label = GetComponent<TextMeshProUGUI>();
    }

    private void InitialSignal()
    {
        // Get Signal with Promise Awaiter
        Signal<T>.GetReadonlyAsync(signalId)
            .Then(signal =>
            {
                signal.AddListener(OnSignalUpdate, true);
                _signal = signal;
            })
            .Catch(e => Debug.LogError(e, this));
    }

    private void OnSignalUpdate(T value)
    {
        _label.text = ValueToStringProvider(value);
    }

    // custom label display by override Provider.
    protected virtual string ValueToStringProvider(T value)
    {
        return value.ToString();
    }
}

Last updated 3 months ago

please read for details of Promise Awaiter.

Task Utils