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

相关推荐
葱白有滋味2 小时前
Session、Token 和 JWT 的区别对比
java
星月心城2 小时前
八股文-JavaScript(第一天)
开发语言·前端·javascript
zwxu_2 小时前
thread堆栈分析报告
java·微服务·消息队列·熔断
百***78752 小时前
gpt-image-1.5极速接入指南:3步上手+图像核心能力解析+避坑手册
android·java·gpt
阿蒙Amon2 小时前
C#每日面试题-值类型与引用类型区别
java·面试·c#
编程小Y2 小时前
Bash 替换机制
开发语言·chrome·bash
我要学脑机2 小时前
一个图谱映射到功能网络yeo7或17的解决方案
开发语言·网络·php
nnsix2 小时前
Unity SenseGlove力反馈手套 基础配置
java·unity·游戏引擎
用户9446814013502 小时前
JUC 小试牛刀:从源码分析「ArrayBlockingQueue」,Java自带的线程安全的、有界的阻塞队列
java·后端