unity 事件、委托

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

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

一、一个使用场景

C#都是才学unity的时候接触到的,光看往上的理论东西很难理解事件到底能起到什么作用,直接快进到在自己写小demo的时候被迫学会这些东西一些场景。
场景

现在场景上有3个Canvas,1个Cube。点击升级:等级、生命、蓝量提升,限时礼包、氪金礼包出现红点让玩家点击,蓝色方块变成其他颜色。

此时的项目结构如下:每个Canvas挂载一个脚本,角色挂载一个脚本。需要改变多个脚本里面的数据

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

csharp 复制代码
//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#事件

csharp 复制代码
//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老师进行一个非常简单的封装:

csharp 复制代码
//这个名字无所谓,可以把自己封装的功能都统一到一个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;
        }
    }

}

这样对事件的使用就可以变成:

csharp 复制代码
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)。重新新增两个事件,需要传不同的参数进去:

csharp 复制代码
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当中可以很方便的管理自己所有的事件。

相关推荐
L X..6 小时前
Unity反射调用 ReactiveProperty<T>(泛型类型)内部方法时崩溃
unity·c#·游戏引擎·.net
向宇it11 小时前
【推荐100个unity插件】将您的场景渲染为美丽的冬季风景——Global Snow 2
unity·游戏引擎·风景
浅丿忆十一11 小时前
关于unity一个场景中存在多个相机时Game视图的画面问题
unity·游戏引擎
WLJT12312312320 小时前
方寸之间见天地:新兴高端印章的当代破局与价值重构
unity·游戏引擎
软件黑马王子21 小时前
2025Unity中的核心数学工具(三)四元数(穿插Unity实战相关案例)
unity·游戏引擎
千忧散1 天前
Unity Socket学习笔记 (三)TCP&UDP
笔记·学习·unity·c#
君莫愁。1 天前
【Unity】构建超实用的有限状态机管理类
unity·c#·游戏引擎·有限状态机
EQ-雪梨蛋花汤1 天前
【Unity笔记】Unity Lighting Settings 全解析:一文读懂烘焙光照的每个参数(VR项目Lighting优化)
笔记·unity·vr
BrightMZM2 天前
记录一下Unity的BUG,Trial Version
unity·bug·打包·trial