unity 事件、委托

刚学习不到一年unity的彩笔,以下是个人见解,有问题欢迎指正。怀疑会存误导,但是还是写了,记录的过程也是自己学习的过程,慢慢改正更新。

ps:自己初看教程的时候会有困惑,明明用其他更直接的方法也能做出来,为什么还需要这个。只能说时候未到,大概率是确实目前还不需要了。自己学习的目标就是:能实现功能就是好代码!

一、一个使用场景

C#都是才学unity的时候接触到的,光看往上的理论东西很难理解事件到底能起到什么作用,直接快进到在自己写小demo的时候被迫学会这些东西一些场景。
场景
现在场景上有3个Canvas,1个Cube。点击升级:等级、生命、蓝量提升,限时礼包、氪金礼包出现红点让玩家点击,蓝色方块变成其他颜色。
在这里插入图片描述

此时的项目结构如下:每个Canvas挂载一个脚本,角色挂载一个脚本。需要改变多个脚本里面的数据
在这里插入图片描述

才学习应该会想到的方法(起码我是这样):1.通过名字查找或者拖拽的方式,得到所有需要用到的脚本。2.统统添加为单例,直接拿到。(单例确实好用,但是容易让代码耦合度高,适当使用)

//TestCanvas
public class TestCanvas : MonoBehaviour
{
    public Button btnChangeColor;
    public Text level;
    Canvas2 canvas2;
    Canvas3 canvas3;
    RedCube cube;
    int i = 1;
    void Start()
    {
        level.text = i.ToString() + "级";
        canvas2 = GameObject.Find("Canvas2").GetComponent<Canvas2>();
        canvas3 = GameObject.Find("Canvas3").GetComponent<Canvas3>();
        cube = GameObject.Find("角色").GetComponent<RedCube>();
        btnChangeColor.onClick.AddListener(OnButtonClick);
    }
    private void OnButtonClick()
    {
        i += 1;
        level.text = i.ToString() + "级";
        canvas2.ShowDots();
        canvas3.AddNum();
        cube.ChangeColor();
    }
}

public class Canvas2 : MonoBehaviour
{
    public GameObject dot1;
    public GameObject dot2;

    public void ShowDots()
    {
        dot1.SetActive(true);
        dot2.SetActive(true);
    }
}
public class RedCube : MonoBehaviour
{
    MeshRenderer cubeColor;
    Transform cubeTransform;
    void Start()
    {
        cubeColor = GetComponent<MeshRenderer>();
        cubeTransform = GetComponent<Transform>();
    }

    public void ChangeColor()
    {
        cubeColor.material.color = Color.blue;
        cubeTransform.localRotation = Quaternion.Euler(0, 45, 0);
    }

}

确实可以得到最后的结果(把不同脚本的参数在一个地方修改其实也不太好)。但是如果现在场景上有更多需要传递的信息,比如想要一次性改变几十个物体的颜色之类的,每次添加或者更改都相当痛苦。其实不需要场景上有很多东西,在多跟着网上的不同教程打了点代码后,发现场景稍微复杂一点,有那么几个函数在好几个脚本之间相互调用(有空再补个更基础的单例的使用),就已经开始怀疑自己了,到底有什么方案。
换一种思路,希望能点一下button可以通知其他脚本,而不希望每次都Find到具体需要的脚本。

  • 再举个例子: 就像老师喊:班上身高160以上的举手。
    老师不需要知道班上到底谁身高超过160。每个同学自己知道自己的身高,这时候他们符合条件的自己会举手。

如果希望点一下button能让这些脚本自觉执行代码,就像这个例子。button(发布器(publisher))只需要喊出这句话:现在升级了。符合条件的Cube(订阅器(subscriber) )都会监听到这句话,便会执行对应的代码。此时button并不知道到底哪些方块听到了这句话,也不需要关注。大概就是计算机网络里面的广播性质。
(如果需要返回值的话最好还是再考虑考虑。毕竟可能不是一对一的关系,大概率是一对多的,这时候只会返回最后一个添加的函数返回值。)

二、一个基础事件

参考菜鸟教程上面的方法写一个适合当前使用的事件,分别对应菜鸟教程上面的4点:
在这里插入图片描述

菜鸟教程C#事件

//TestCanvas
public class TestCanvas : MonoBehaviour
{
    public Button btnChangeColor;
    public Text level;
    public GameObject red;
	
	// 1.定义一个委托类型,用于事件处理程序
    public delegate void LevelManager();
    // 2.声明事件
    public static event LevelManager LevelUp;
    int i = 1;
    void Start()
    {
        level.text = i.ToString() + "级";
        btnChangeColor.onClick.AddListener(OnButtonClick);
    }

    private void OnButtonClick()
    {
        i += 1;
        level.text = i.ToString() + "级";
        //3.触发事件
        LevelUp?.Invoke();
    }
}

//订阅器触发
public class RedCube : MonoBehaviour
{
    MeshRenderer cubeColor;
    Transform cubeTransform;
    void Start()
    {
        cubeColor = GetComponent<MeshRenderer>();
        cubeTransform = GetComponent<Transform>();
        //4.添加订阅,LevelUp?.Invoke()执行后会触发ChangeColor方法
        TestCanvas.LevelUp += ChangeColor;
    }

    public void ChangeColor()
    {
        cubeColor.material.color = Color.blue;
        cubeTransform.localRotation = Quaternion.Euler(0, 45, 0);
    }
    void OnDestroy()
	{
		// 4.取消订阅,需要保证订阅和取消订阅成对存在,不然真的出问题的时候,报错都不好找(遇到过相关问题)
	    TestCanvas.LevelUp -= ChangeColor; 
	}

}
//订阅器2
public class Canvas2 : MonoBehaviour
{
    public GameObject dot1;
    public GameObject dot2;
    void Start()
    {
    	//添加订阅,LevelUp?.Invoke()执行后会触发ShowDots方法
        TestCanvas.LevelUp += ShowDots;
    }
    public void ShowDots()
    {
        dot1.SetActive(true);
        dot2.SetActive(true);
    }
    void OnDestroy()
	{
		// 取消订阅
	    TestCanvas.LevelUp -= ShowDots;
	}
}

//订阅器3
public class Canvas3 : MonoBehaviour
{
    public Text hp;
    public Text mp;

    int hpNum = 10;
    int mpNum = 20;
    void Start()
    {
        hp.text = CharaAttributes(hpNum);
        mp.text = CharaAttributes(mpNum);
        //添加订阅
        TestCanvas.LevelUp += AddNum;
    }
    public void AddNum()
    {
        hpNum += 5;
        mpNum += 5;
        hp.text = CharaAttributes(hpNum);
        mp.text = CharaAttributes(mpNum);
    }
    public string CharaAttributes(int num)
    {
        string attr = $"属性: {num}";
        return attr;
    }
    void OnDestroy()
	{
	    TestCanvas.LevelUp -= AddNum; // 取消订阅
	}
}

在后续添加其他脚本的时候,可以不用对TestCanvas进行修改,而只在其他脚本添加添加删减订阅。

public static event LevelManager LevelUp;还是用到了 static静态事件,来方便其他脚本直接通过类名进行订阅,而不需要获取对象实例。按照我目前的能力,目前应该也是无法删减的了。

三、简单封装事件

unity自带的事件用多了,肯定会遇到一个问题,那就是,根本不知道在哪里使用过、、、很难去寻找自己到底用到了多少的事件,分布在使用的各处。
想办法把事件放在一起管理会方便一点。
(看一看GameFramwork,最开始学习的时候会思考真的是对所有需要的GetComponent,static吗,会对代码有影响吗。实际上无法避免的,感觉前期学习也不能排斥去使用,只要能写出想要的功能才行。)在这里插入图片描述
参考GameFramework和Deepseek老师进行一个非常简单的封装:
在这里插入图片描述

//这个名字无所谓,可以把自己封装的功能都统一到一个namespace
namespace GameEntry.Event
{
    public static class EventSystem
    {
        private static readonly Dictionary<int, EventHandler<object[]>> _eventHandlers = new Dictionary<int, EventHandler<object[]>>();

        /// <summary>
        /// 订阅事件
        /// </summary>
        /// <param name="eventId">事件ID</param>
        /// <param name="handler">事件处理函数</param>
        public static void Subscribe(int eventId, EventHandler<object[]> handler)
        {
            if (_eventHandlers.ContainsKey(eventId))
            {
                _eventHandlers[eventId] += handler;
            }
            else
            {
                _eventHandlers[eventId] = handler;
            }
        }

        /// <summary>
        /// 取消订阅事件
        /// </summary>
        /// <param name="eventId">事件ID</param>
        /// <param name="handler">事件处理函数</param>
        public static void Unsubscribe(int eventId, EventHandler<object[]> handler)
        {
            if (_eventHandlers.ContainsKey(eventId))
            {
                _eventHandlers[eventId] -= handler;
            }
            else
            {
                _eventHandlers.Remove(eventId);
            }
        }

        /// <summary>
        /// 触发事件
        /// </summary>
        /// <param name="eventId">eventId</param>
        /// <param name="sender">触发事件的对象,一起发送给订阅器</param>
        /// <param name="args"></param>
        public static void Fire(int eventId, object sender, params object[] args)
        {
            if (_eventHandlers.TryGetValue(eventId, out var handler))
            {
                handler?.Invoke(sender, args);
            }
        }

        /// <summary>
        /// 清理所有事件监听
        /// </summary>
        public static void Clear()
        {
            _eventHandlers.Clear();
        }

        /// <summary>
        /// 获取指定事件的监听者数量
        /// </summary>
        public static int GetListenerCount(int eventId)
        {
            if (_eventHandlers.TryGetValue(eventId, out var handler))
            {
                return handler.GetInvocationList().Length;
            }
            return 0;
        }
    }

}

这样对事件的使用就可以变成:
在这里插入图片描述

public static class EventIds
{
    public static readonly int LevelUp = 1001;//只是用于区分不同的事件,自定义id。
}

public class TestCanvas : MonoBehaviour
{
    public Button btnChangeColor;
    public Text level;
    public GameObject red;

    public delegate void LevelManager();
    public static event LevelManager LevelUp;
    int i = 1;
    void Start()
    {
        level.text = i.ToString() + "级";
        btnChangeColor.onClick.AddListener(OnButtonClick);
    }

    private void OnButtonClick()
    {
        i += 1;
        level.text = i.ToString() + "级";
        //LevelUp?.Invoke();
        GameEntry.Event.EventSystem.Fire(EventIds.LevelUp, this);
    }
}

public class RedCube : MonoBehaviour
{
    MeshRenderer cubeColor;
    Transform cubeTransform;
    void Start()
    {
        cubeColor = GetComponent<MeshRenderer>();
        cubeTransform = GetComponent<Transform>();
        GameEntry.Event.EventSystem.Subscribe(EventIds.LevelUp, ChangeColor);
        //TestCanvas.LevelUp += ChangeColor;
    }

    public void ChangeColor(object sender, object[] args)
    {
        cubeColor.material.color = Color.blue;
        cubeTransform.localRotation = Quaternion.Euler(0, 45, 0);
    }
    void OnDestroy()
    {
        GameEntry.Event.EventSystem.Unsubscribe(EventIds.LevelUp, ChangeColor);
        //TestCanvas.LevelUp -= ChangeColor; // 取消订阅
    }
}

public class Canvas2 : MonoBehaviour
{
    public GameObject dot1;
    public GameObject dot2;

    void Start()
    {
        GameEntry.Event.EventSystem.Subscribe(EventIds.LevelUp, ShowDots);
        //TestCanvas.LevelUp += ShowDots;
    }

    public void ShowDots(object sender, object[] args)
    {
        dot1.SetActive(true);
        dot2.SetActive(true);
    }
    void OnDestroy()
    {
        GameEntry.Event.EventSystem.Unsubscribe(EventIds.LevelUp, ShowDots);
        //TestCanvas.LevelUp -= ShowDots; // 取消订阅
    }
}

以上看着好像和之前没有任何区别,还更麻烦了(多了一个EventIds)。重新新增两个事件,需要传不同的参数进去:

public static class EventIds
{
    public static readonly int LevelUp = 1001;
    public static readonly int ChangeHp = 1002;
    public static readonly int NeedChangeState = 1003;
}

public class TestCanvas : MonoBehaviour
{
    public Button btnChangeColor;
    public Text level;
    public GameObject red;

    public delegate void LevelManager();
    public static event LevelManager LevelUp;
    int i = 1;
    void Start()
    {
        level.text = i.ToString() + "级";
        btnChangeColor.onClick.AddListener(OnButtonClick);
    }

    private void OnButtonClick()
    {
        i += 1;
        level.text = i.ToString() + "级";
        string levelString = level.text;
        int levelInt = i;
        bool isTrue=true;
        //LevelUp?.Invoke();
        GameEntry.Event.EventSystem.Fire(EventIds.LevelUp, this);
        //新增两个事件
        GameEntry.Event.EventSystem.Fire(EventIds.ChangeHp, this, levelString, levelInt);
        GameEntry.Event.EventSystem.Fire(EventIds.NeedChangeState, this, isTrue);

    }
}

public class Canvas2 : MonoBehaviour
{
    public GameObject dot1;
    public GameObject dot2;

    void Start()
    {
        GameEntry.Event.EventSystem.Subscribe(EventIds.LevelUp, ShowDots);
        //新增两个事件
        GameEntry.Event.EventSystem.Subscribe(EventIds.ChangeHp, GetHp);
        GameEntry.Event.EventSystem.Subscribe(EventIds.NeedChangeState, ChangeState);
        //TestCanvas.LevelUp += ShowDots;
    }

    public void ShowDots(object sender, object[] args)
    {
        dot1.SetActive(true);
    }

    public void GetHp(object sender, object[] args)
    {
        string levelString = (string)args[0];
        int levelInt = (int)args[1];

        Debug.Log($"得到事件的第一个参数为:{levelString},第二个参数为:{levelInt}");
    }

    public void ChangeState(object sender, object[] args)
    {
    	//这里的sender可有可无,只是试用一下
        if(sender is TestCanvas)
        {
            bool isTrue = (bool)args[0];
            dot2.SetActive(isTrue);
            Debug.Log($"氪金礼包的小红点是否显示:{isTrue}");
        }
    }

    void OnDestroy()
    {
        GameEntry.Event.EventSystem.Unsubscribe(EventIds.LevelUp, ShowDots);
        GameEntry.Event.EventSystem.Unsubscribe(EventIds.ChangeHp, GetHp);
        GameEntry.Event.EventSystem.Unsubscribe(EventIds.NeedChangeState, ChangeState);
        //TestCanvas.LevelUp -= ShowDots; // 取消订阅
    }
}

在这里插入图片描述
很方便的添加,在EventIds当中可以很方便的管理自己所有的事件。

Unity 中,委托(Delegate)和事件(Event)是实现对象间通信的重要机制,尤其适用于模块化设计、解耦逻辑以及提升代码的可维护性和可扩展性。通过委托事件,可以实现多个对象对某一行为的订阅和响应,而无需直接耦合彼此的逻辑。 ### 委托的基本定义与使用 在 C# 中,委托是一种类型安全的函数指针,可以用来封装方法的引用。定义一个委托使用 `delegate` 关键字,例如: ```csharp public delegate void MyDelegate(); ``` 该委托可以指向任何没有参数且返回值为 `void` 的方法。使用委托时,可以通过 `+=` 和 `-=` 操作符来添加或移除方法。 ### 事件的声明与触发 事件基于委托,通常用于实现观察者模式。在 Unity 中,事件通常被声明为静态事件,以便于在不同对象之间共享[^2]。例如: ```csharp public static event MyDelegate OnMyEvent; ``` 在某个特定条件下,比如按键触发时,调用事件: ```csharp private void Update() { if (Input.GetButtonDown("Defend")) { OnMyEvent?.Invoke(); } } ``` ### 订阅与取消订阅事件 订阅事件通常在 `Start` 或 `Awake` 方法中进行,而取消订阅则在 `OnDisable` 或 ` OnDestroy` 中完成,以避免内存泄漏或空引用异常[^2]。 ```csharp private void Start() { MyEventPublisher.OnMyEvent += MyEventHandler; } private void OnDisable() { MyEventPublisher.OnMyEvent -= MyEventHandler; } void MyEventHandler() { Debug.Log("触发委托事件"); } ``` ### 自定义事件参数 在某些情况下,事件需要传递额外的信息。可以通过自定义事件参数类来实现。例如,定义一个事件参数类: ```csharp public class PlayerAniEventArgs : EventArgs { public string AnimationName { get; set; } } ``` 然后定义一个带有自定义参数的委托事件: ```csharp public delegate void AniChangeHandle(object sender, PlayerAniEventArgs e); public event AniChangeHandle CustomMoveComplete; ``` 在触发事件时传递参数: ```csharp PlayerAniEventArgs args = new PlayerAniEventArgs { AnimationName = "Run" }; CustomMoveComplete?.Invoke(this, args); ``` ### 示例:完整委托事件实现 以下是一个完整的 Unity 委托事件实现示例: ```csharp using UnityEngine; // 定义一个委托 public delegate void MyDelegate(); public class MyEventPublisher : MonoBehaviour { // 定义一个静态事件 public static event MyDelegate OnMyEvent; private void Update() { // 检测是否按下防御键——C if (Input.GetKeyDown(KeyCode.C)) { // 如果事件不为空(有订阅者),调用所有订阅此事件的方法。 OnMyEvent?.Invoke(); } } } public class MyEventSubscriber : MonoBehaviour { private void Start() { // 订阅事件 MyEventPublisher.OnMyEvent += HandleMyEvent; } private void OnDisable() { // 取消订阅事件 MyEventPublisher.OnMyEvent -= HandleMyEvent; } private void HandleMyEvent() { Debug.Log("事件被触发"); } } ``` ### 委托事件的优势 使用委托事件可以有效降低模块之间的耦合度,使得代码更易于维护和扩展。例如,在 UI 按钮点击事件中,使用事件机制可以让多个对象响应同一事件,而不必修改按钮本身的逻辑[^3]。此外,事件机制还能够避免因直接调用方法而可能导致的死循环和逻辑混乱问题[^5]。
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值