Unity3D的委托和事件的用法详解

前言

一、委托(Delegate)基础

对惹,这里有一 个游戏开发交流小组,希望大家可以点击进来一起交流一下开发经验呀!

1.1 委托的定义与使用

复制代码
// 1. 定义委托类型
public delegate void SimpleDelegate();
public delegate void ParameterDelegate(string message);
public delegate int CalculateDelegate(int a, int b);

// 2. 使用委托
public class DelegateExample : MonoBehaviour
{
    private SimpleDelegate myDelegate;
    private ParameterDelegate paramDelegate;
    
    void Start()
    {
        // 3. 赋值方法
        myDelegate = PrintHello;
        paramDelegate = PrintMessage;
        
        // 4. 调用委托
        myDelegate?.Invoke();
        paramDelegate?.Invoke("Hello World");
        
        // 5. 多播委托(多个方法)
        myDelegate += PrintWorld;
        myDelegate += () => Debug.Log("Lambda表达式");
        myDelegate?.Invoke();
        
        // 6. 移除方法
        myDelegate -= PrintHello;
    }
    
    void PrintHello()
    {
        Debug.Log("Hello");
    }
    
    void PrintWorld()
    {
        Debug.Log("World");
    }
    
    void PrintMessage(string msg)
    {
        Debug.Log(msg);
    }
}

1.2 Unity内置委托类型

复制代码
public class UnityBuiltInDelegates : MonoBehaviour
{
    // UnityAction - 无返回值委托
    private UnityAction unityAction;
    private UnityAction<int, float> paramAction;
    
    // UnityEvent - 序列化的事件
    public UnityEvent onEventTriggered;
    public UnityEvent<string> onMessageEvent;
    
    void Start()
    {
        // UnityAction用法
        unityAction = () => Debug.Log("Action triggered");
        paramAction = (x, y) => Debug.Log($"x={x}, y={y}");
        
        // 添加监听
        onEventTriggered.AddListener(OnTriggered);
        onMessageEvent.AddListener(OnMessageReceived);
        
        // 触发
        onEventTriggered?.Invoke();
        onMessageEvent?.Invoke("Test Message");
    }
    
    void OnTriggered()
    {
        Debug.Log("事件被触发");
    }
    
    void OnMessageReceived(string msg)
    {
        Debug.Log($"收到消息: {msg}");
    }
    
    void OnDestroy()
    {
        // 重要:清理监听
        onEventTriggered.RemoveAllListeners();
        onMessageEvent.RemoveAllListeners();
    }
}

二、事件(Event)高级用法

2.1 标准事件模式

复制代码
// 1. 定义事件参数类
public class GameEventArgs : EventArgs
{
    public string EventName { get; }
    public object Data { get; }
    public DateTime Timestamp { get; }
    
    public GameEventArgs(string name, object data)
    {
        EventName = name;
        Data = data;
        Timestamp = DateTime.Now;
    }
}

// 2. 事件发布者
public class EventPublisher : MonoBehaviour
{
    // 定义事件(使用EventHandler标准模式)
    public event EventHandler<GameEventArgs> OnGameEvent;
    public event EventHandler OnSimpleEvent;
    
    // 触发事件的保护方法
    protected virtual void RaiseGameEvent(string eventName, object data)
    {
        OnGameEvent?.Invoke(this, new GameEventArgs(eventName, data));
    }
    
    // 示例:触发事件
    public void PlayerDied()
    {
        RaiseGameEvent("PlayerDied", new { score = 100, position = transform.position });
    }
    
    public void LevelCompleted()
    {
        OnSimpleEvent?.Invoke(this, EventArgs.Empty);
    }
}

2.2 事件订阅者

复制代码
public class EventSubscriber : MonoBehaviour
{
    [SerializeField] private EventPublisher publisher;
    
    void OnEnable()
    {
        if (publisher != null)
        {
            publisher.OnGameEvent += HandleGameEvent;
            publisher.OnSimpleEvent += HandleSimpleEvent;
        }
    }
    
    void OnDisable()
    {
        // 重要:避免内存泄漏
        if (publisher != null)
        {
            publisher.OnGameEvent -= HandleGameEvent;
            publisher.OnSimpleEvent -= HandleSimpleEvent;
        }
    }
    
    void HandleGameEvent(object sender, GameEventArgs e)
    {
        Debug.Log($"事件: {e.EventName}, 数据: {e.Data}, 时间: {e.Timestamp}");
        
        switch (e.EventName)
        {
            case "PlayerDied":
                HandlePlayerDeath(e.Data);
                break;
            case "ItemCollected":
                HandleItemCollection(e.Data);
                break;
        }
    }
    
    void HandleSimpleEvent(object sender, EventArgs e)
    {
        Debug.Log("简单事件触发");
    }
    
    void HandlePlayerDeath(object data)
    {
        // 处理玩家死亡逻辑
        Debug.Log("玩家死亡处理");
    }
    
    void HandleItemCollection(object data)
    {
        // 处理物品收集
    }
}

三、Unity中的实际应用场景

3.1 UI事件系统

复制代码
public class UIEventHandler : MonoBehaviour
{
    // 自定义UI事件
    public event Action<Button> OnButtonClicked;
    public event Action<Slider, float> OnSliderValueChanged;
    public event Action<Toggle, bool> OnToggleChanged;
    
    [SerializeField] private Button playButton;
    [SerializeField] private Slider volumeSlider;
    [SerializeField] private Toggle soundToggle;
    
    void Start()
    {
        // Unity UI组件事件转换为自定义事件
        playButton.onClick.AddListener(() => OnButtonClicked?.Invoke(playButton));
        volumeSlider.onValueChanged.AddListener(value => 
            OnSliderValueChanged?.Invoke(volumeSlider, value));
        soundToggle.onValueChanged.AddListener(isOn => 
            OnToggleChanged?.Invoke(soundToggle, isOn));
    }
}

3.2 游戏状态管理

复制代码
public class GameManager : MonoBehaviour
{
    public static GameManager Instance { get; private set; }
    
    // 游戏状态事件
    public event Action<GameState> OnGameStateChanged;
    public event Action<int> OnScoreChanged;
    public event Action<int> OnPlayerHealthChanged;
    
    public enum GameState { Menu, Playing, Paused, GameOver }
    private GameState currentState;
    
    void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }
    
    public void ChangeGameState(GameState newState)
    {
        currentState = newState;
        OnGameStateChanged?.Invoke(newState);
        
        switch (newState)
        {
            case GameState.Playing:
                StartGame();
                break;
            case GameState.GameOver:
                EndGame();
                break;
        }
    }
    
    public void AddScore(int points)
    {
        // 分数逻辑...
        OnScoreChanged?.Invoke(currentScore);
    }
    
    // 其他方法...
}

3.3 成就系统

复制代码
public class AchievementSystem : MonoBehaviour
{
    // 成就事件
    public event Action<string> OnAchievementUnlocked;
    public event Action<int> OnTotalAchievementsChanged;
    
    private Dictionary<string, bool> achievements = new Dictionary<string, bool>();
    
    public void UnlockAchievement(string achievementId)
    {
        if (!achievements.ContainsKey(achievementId) || !achievements[achievementId])
        {
            achievements[achievementId] = true;
            OnAchievementUnlocked?.Invoke(achievementId);
            OnTotalAchievementsChanged?.Invoke(achievements.Count(a => a.Value));
            
            // 显示成就UI等
            Debug.Log($"成就解锁: {achievementId}");
        }
    }
}

// 成就触发器
public class AchievementTrigger : MonoBehaviour
{
    void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Player"))
        {
            AchievementSystem.Instance?.OnAchievementUnlocked?.Invoke("First_Secret_Found");
        }
    }
}

四、高级模式与最佳实践

4.1 事件总线模式

复制代码
public class EventBus : MonoBehaviour
{
    private static EventBus instance;
    public static EventBus Instance => instance;
    
    // 事件字典
    private Dictionary<Type, Delegate> eventTable = new Dictionary<Type, Delegate>();
    
    void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
        }
    }
    
    // 订阅事件
    public void Subscribe<T>(Action<T> handler) where T : struct
    {
        Type eventType = typeof(T);
        
        if (!eventTable.ContainsKey(eventType))
        {
            eventTable[eventType] = handler;
        }
        else
        {
            eventTable[eventType] = Delegate.Combine(eventTable[eventType], handler);
        }
    }
    
    // 取消订阅
    public void Unsubscribe<T>(Action<T> handler) where T : struct
    {
        Type eventType = typeof(T);
        
        if (eventTable.ContainsKey(eventType))
        {
            eventTable[eventType] = Delegate.Remove(eventTable[eventType], handler);
        }
    }
    
    // 发布事件
    public void Publish<T>(T eventData) where T : struct
    {
        Type eventType = typeof(T);
        
        if (eventTable.ContainsKey(eventType) && eventTable[eventType] != null)
        {
            (eventTable[eventType] as Action<T>)?.Invoke(eventData);
        }
    }
}

// 使用事件总线
public struct PlayerDiedEvent
{
    public Vector3 DeathPosition;
    public int RemainingLives;
    public string KillerName;
}

public class PlayerController : MonoBehaviour
{
    void Die()
    {
        var deathEvent = new PlayerDiedEvent
        {
            DeathPosition = transform.position,
            RemainingLives = currentLives,
            KillerName = "Enemy"
        };
        
        EventBus.Instance.Publish(deathEvent);
    }
}

public class DeathEffectManager : MonoBehaviour
{
    void OnEnable()
    {
        EventBus.Instance.Subscribe<PlayerDiedEvent>(OnPlayerDied);
    }
    
    void OnDisable()
    {
        EventBus.Instance.Unsubscribe<PlayerDiedEvent>(OnPlayerDied);
    }
    
    void OnPlayerDied(PlayerDiedEvent evt)
    {
        // 播放死亡特效
        Instantiate(deathEffect, evt.DeathPosition, Quaternion.identity);
    }
}

4.2 观察者模式实现

复制代码
// 可观察对象接口
public interface IObservable<T>
{
    void AddObserver(IObserver<T> observer);
    void RemoveObserver(IObserver<T> observer);
    void NotifyObservers(T data);
}

// 观察者接口
public interface IObserver<T>
{
    void OnNotify(T data);
}

// 具体实现
public class HealthSystem : MonoBehaviour, IObservable<int>
{
    private List<IObserver<int>> observers = new List<IObserver<int>>();
    private int currentHealth = 100;
    
    public void AddObserver(IObserver<int> observer)
    {
        if (!observers.Contains(observer))
        {
            observers.Add(observer);
        }
    }
    
    public void RemoveObserver(IObserver<int> observer)
    {
        observers.Remove(observer);
    }
    
    public void NotifyObservers(int health)
    {
        foreach (var observer in observers)
        {
            observer.OnNotify(health);
        }
    }
    
    public void TakeDamage(int damage)
    {
        currentHealth -= damage;
        currentHealth = Mathf.Max(0, currentHealth);
        NotifyObservers(currentHealth);
    }
}

public class HealthUI : MonoBehaviour, IObserver<int>
{
    [SerializeField] private Slider healthSlider;
    [SerializeField] private Text healthText;
    
    void Start()
    {
        var healthSystem = FindObjectOfType<HealthSystem>();
        if (healthSystem != null)
        {
            healthSystem.AddObserver(this);
        }
    }
    
    public void OnNotify(int health)
    {
        healthSlider.value = health / 100f;
        healthText.text = $"HP: {health}";
        
        if (health < 30)
        {
            // 低血量警告
            StartCoroutine(FlashWarning());
        }
    }
    
    IEnumerator FlashWarning()
    {
        // 闪烁效果
        healthText.color = Color.red;
        yield return new WaitForSeconds(0.5f);
        healthText.color = Color.white;
    }
    
    void OnDestroy()
    {
        var healthSystem = FindObjectOfType<HealthSystem>();
        if (healthSystem != null)
        {
            healthSystem.RemoveObserver(this);
        }
    }
}

五、性能优化与注意事项

5.1 性能优化技巧

复制代码
public class OptimizedEventSystem : MonoBehaviour
{
    // 1. 使用缓存减少委托分配
    private UnityAction cachedAction;
    
    void Start()
    {
        cachedAction = DoSomething;
        
        // 避免在循环中创建新委托
        for (int i = 0; i < 100; i++)
        {
            someEvent.AddListener(cachedAction); // 好
            // someEvent.AddListener(() => DoSomething()); // 不好,每次创建新委托
        }
    }
    
    // 2. 使用对象池管理事件参数
    public class EventDataPool
    {
        private static ObjectPool<GameEventArgs> pool = 
            new ObjectPool<GameEventArgs>(() => new GameEventArgs(), 10);
        
        public static GameEventArgs Get(string name, object data)
        {
            var args = pool.Get();
            // 初始化...
            return args;
        }
        
        public static void Release(GameEventArgs args)
        {
            pool.Release(args);
        }
    }
    
    // 3. 避免频繁的事件触发
    private float lastEventTime;
    private const float EVENT_COOLDOWN = 0.1f;
    
    public void TryTriggerEvent()
    {
        if (Time.time - lastEventTime > EVENT_COOLDOWN)
        {
            lastEventTime = Time.time;
            OnEvent?.Invoke();
        }
    }
}

5.2 常见陷阱与解决方案

复制代码
public class EventPitfalls : MonoBehaviour
{
    // 陷阱1:忘记取消订阅(内存泄漏)
    private void SubscribeAndForget()
    {
        SomeClass.Instance.OnEvent += HandleEvent;
        // 如果不在适当时候取消订阅,即使对象被销毁,委托仍然持有引用
    }
    
    // 解决方案:在OnDisable或OnDestroy中取消订阅
    void OnEnable()
    {
        SomeClass.Instance.OnEvent += HandleEvent;
    }
    
    void OnDisable()
    {
        SomeClass.Instance.OnEvent -= HandleEvent;
    }
    
    // 陷阱2:空引用检查
    public event Action OnUnsafeEvent;
    
    void TriggerUnsafeEvent()
    {
        OnUnsafeEvent(); // 如果没有订阅者会抛出NullReferenceException
    }
    
    // 解决方案:使用空条件运算符
    void TriggerSafeEvent()
    {
        OnUnsafeEvent?.Invoke();
    }
    
    // 陷阱3:线程安全问题
    private event Action OnThreadEvent;
    private readonly object lockObject = new object();
    
    public void ThreadSafeSubscribe(Action handler)
    {
        lock (lockObject)
        {
            OnThreadEvent += handler;
        }
    }
    
    public void ThreadSafeInvoke()
    {
        Action localCopy;
        lock (lockObject)
        {
            localCopy = OnThreadEvent;
        }
        localCopy?.Invoke();
    }
    
    // 陷阱4:事件链导致无限递归
    private bool isHandlingEvent = false;
    
    void HandleEventA()
    {
        if (isHandlingEvent) return;
        
        isHandlingEvent = true;
        // 处理事件...
        OnEventB?.Invoke(); // 可能触发其他事件
        isHandlingEvent = false;
    }
}

总结

委托与事件的选择指南

  1. 使用委托的情况
  • 需要回调函数
  • 简单的方法传递
  • 需要多播功能
  • 在类内部使用
  • 使用事件的情况
  • 公开的接口,需要封装
  • 观察者模式实现
  • 组件间通信
  • 需要更安全的访问控制
  • UnityEvent的特殊优势
  • 支持序列化,可在Inspector中配置
  • 可视化编辑
  • 适合非程序员使用

最佳实践

  1. 始终使用空条件运算符(?.Invoke()
  2. 及时清理订阅(OnDisable/OnDestroy)
  3. 考虑使用事件总线解耦复杂系统
  4. 为频繁触发的事件添加节流机制
  5. 使用结构体作为事件参数以减少GC
  6. 避免在事件处理中抛出异常
  7. 文档化事件的使用方式和期望行为

掌握委托和事件是Unity开发中的核心技能,合理使用可以创建出松耦合、可维护性高的代码架构。

更多教学视

知乎 - 安全中心www.bycwedu.com/promotion_channels/2146264125

相关推荐
Scott9999HH1 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔1 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社2 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海2 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖3 小时前
JDK 26 新特性详解
java·开发语言
马优晨4 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区6 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大6 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai6 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说7 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json