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

相关推荐
NIKITAshao9 小时前
Unity 跨项目稳定迁移资源
unity·游戏引擎
sindyra11 小时前
Unity资源内存管理与释放
unity·游戏引擎·资源管理·资源释放·内存释放
CreasyChan11 小时前
Unity FairyGUI高斯模糊实现方法
unity·游戏引擎·fgui
avi911111 小时前
Unity半官方的AssetBundleBrowser插件说明+修复+Reporter插件
unity·游戏引擎·打包·assetbundle·游戏资源
郝学胜-神的一滴12 小时前
深入理解Mipmap:原理、实现与应用
c++·程序人生·unity·游戏程序·图形渲染·unreal engine
nnsix1 天前
Unity PicoVR开发 实时预览Unity场景 在Pico设备中(串流)
unity·游戏引擎
一只一只1 天前
Unity之UGUI Button按钮组件详细使用教程
unity·游戏引擎·ugui·button·ugui button
WarPigs1 天前
Unity阴影
unity·游戏引擎
一只一只1 天前
Unity之Invoke
unity·游戏引擎·invoke
tealcwu1 天前
【Unity踩坑】Simulate Touch Input From Mouse or Pen 导致检测不到鼠标点击和滚轮
unity·计算机外设·游戏引擎