刚学习不到一年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点:
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当中可以很方便的管理自己所有的事件。