Jiahonzheng's Blog

Unity BroadcastMessage

字数统计: 1.4k阅读时长: 6 min
2019/09/09 Share

发送消息的三种方法

在 Unity 中,每个游戏对象都有 SendMessageBroadcastMessageSendMessageUpwards 三种发送消息的方法:

  • SendMessage:在自身中查找对应方法。
  • BroadcastMessage:在自身和所有的父对象中查找对应方法。
  • SendMessageUpwards:在自身和所有的子对象中查找对应方法。

下面,我们通过实际的代码来说明上述三种方法的区别。首先,我们按照下图的游戏对象层次构建游戏场景,其中的 A-F 都是 Empty 游戏对象。

我们为 A-F 游戏对象都添加脚本文件,实现 Echo 函数:

1
2
3
4
5
6
7
8
9
10
11
using UnityEngine;

public class A : MonoBehaviour
{
// 在 A 绑定的脚本中,我们在 Echo 函数中输出 A: content 的内容。
// 我们需要确保该不同的游戏对象中的 Echo 函数对同一输入的输出是不同的。
void Echo(string content)
{
Debug.Log("A: " + content);
}
}

SendMessage

我们修改游戏对象 DStart 函数内容:

1
2
3
4
5
void Start()
{
// 只在当前对象中搜索 Echo 函数,并以 Hello! 参数调用该函数。
SendMessage("Echo", "Hello!");
}

运行场景后,可得下图所示的日志输出。

根据输出结果可知,SendMessage 函数只在当前游戏对象中查找对应方法

BroadcastMessage

我们修改游戏对象 DStart 函数内容:

1
2
3
4
5
void Start()
{
// 只在当前对象和所有子对象中搜索 Echo 函数,并以 Hello! 参数调用该函数。
BroadcastMessage("Echo", "Hello!");
}

运行场景后,可得下图所示的日志输出。

根据输出结果可知,BroadcastMessage 函数只在当前游戏对象和所有子对象中查找对应方法

SendMessageUpwards

我们修改游戏对象 DStart 函数内容:

1
2
3
4
5
void Start()
{
// 只在当前对象和所有父对象中搜索 Echo 函数,并以 Hello! 参数调用该函数。
SendMessageUpwards("Echo", "Hello!");
}

运行场景后,可得下图所示的日志输出。

根据输出结果可知,SendMessageUpwards 函数只在当前游戏对象和所有父对象中查找对应方法

实现 BroadcastMessage

对于上述三种发送消息的方法,我们都可以用自己的方式实现。

使用反射技术

该实现的实现是调用 GetComponentsInChildren 函数,获得当前对象和所有子对象中的 MonoBehaviour 实例,随后使用反射判断是否具备指定的方法,若存在则调用,否则继续遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using UnityEngine;

public class D : MonoBehaviour
{
void Start()
{
// 只在当前对象和所有子对象中搜索 Echo 函数,并以 Hello! 参数调用该函数。
OwnBroadcastMessage("Echo", "Hello!");
}

void Echo(string content)
{
Debug.Log("D: " + content);
}

// 反射实现 BroadcastMessage
void OwnBroadcastMessage(string methodName, object parameter)
{
// 遍历对象树
foreach (Transform child in this.transform)
{
// 获取 Script 部件实例(可能存在多个)
var monos = child.gameObject.GetComponentsInChildren<MonoBehaviour>();
foreach (var mono in monos)
{
var type = mono.GetType();
// 设定方法绑定属性
var bindingAttr = System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public;
// 获取指定名称的方法
var method = type.GetMethod(methodName, bindingAttr);
// 判断方法是否存在
if (method != null)
{
// 若存在方法,传递参数并调用
method.Invoke(mono, new object[] { parameter });
}
}
}
}
}

根据输出结果可知,我们的 OwnBroadcastMessage 函数的实现是正确的。

发布 / 订阅

我们可以使用发布订阅模式,通过事件实现对象之间的通信。在这里,我实现了一个 EventBus 事件总线,为了不与系统已有类冲突,我使用了 Utils 作为命名空间的名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Utils
{
public class EventBus
{
// 事件响应函数。
public delegate void EventHandler(object parameters);

// 定义事件。
public class Event
{
public GameObject gameObject;
public EventHandler handler;

public Event(GameObject gameObject, EventHandler handler)
{
this.gameObject = gameObject;
this.handler = handler;
}
}

// 存储事件及其响应函数函数。
static Hashtable events = new Hashtable();

// 必须在 Emit 函数调用前注册完毕。
public static void RegisterEvent(string eventName, GameObject gameObject, EventHandler handler)
{
if (string.IsNullOrEmpty(eventName))
{
return;
}
// 若无此事件,则创建响应函数数组。
if (!events.ContainsKey(eventName))
{
events[eventName] = new List<Event> { new Event(gameObject, handler) }; ;
return;
}
// 追加响应函数至数组中。
List<Event> eventsList = (List<Event>)events[eventName];
if (eventsList.Find(a => a.gameObject == gameObject) == null)
{
eventsList.Add(new Event(gameObject, handler));
events[eventName] = eventsList;
}
}

// 取消注册事件响应函数。
public static void UnregisterEvent(string eventName, GameObject gameObject)
{
if (string.IsNullOrEmpty(eventName))
{
return;
}
List<Event> eventsList = (List<Event>)events[eventName];
if (eventsList == null)
{
return;
}
if (eventsList.Find(a => a.gameObject == gameObject) != null)
{
eventsList.RemoveAll(a => a.gameObject == gameObject);
events[eventName] = eventsList;
}
// 当事件对应的响应函数数组长度为零时,释放内存。
if (eventsList.Count == 0)
{
events.Remove(eventName);
}
}

// 触发事件。
public static void Emit(string eventName, object parameters)
{
if (string.IsNullOrEmpty(eventName))
{
return;
}
List<Event> eventsList = (List<Event>)events[eventName];
if (eventsList == null)
{
return;
}
foreach (Event e in eventsList)
{
e.handler(parameters);
}
}
}
}

现在,我们测试 Utils.EventBus 能否正常工作:从 D 对象发布事件 EchoEvent,从而调用 A 对象的 Echo 方法。

修改 A 对象所绑定的脚本为以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using UnityEngine;

public class A : MonoBehaviour
{
void Awake()
{
// 由于事件处理函数的注册一定要在事件触发之前,因此我们在 Awake 函数中注册事件响应函数。
Utils.EventBus.RegisterEvent("EchoEvent", gameObject, Echo);
}

void Echo(object parameters)
{
Debug.Log("A: " + (string)parameters);
}
}

修改 D 对象所绑定的脚本为以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using UnityEngine;

public class D : MonoBehaviour
{
void Start()
{
// 由于我们在 A 对象中的 Awake 函数注册了事件响应函数,因此在 Start 函数触发事件是可行的。
Utils.EventBus.Emit("EchoEvent", "Hello!");
}

void Echo(string content)
{
Debug.Log("D: " + content);
}
}

根据输出结果可知,我们的 Utils.EventBus 的实现是正确的。

CATALOG
  1. 1. 发送消息的三种方法
    1. 1.1. SendMessage
    2. 1.2. BroadcastMessage
    3. 1.3. SendMessageUpwards
  2. 2. 实现 BroadcastMessage
    1. 2.1. 使用反射技术
    2. 2.2. 发布 / 订阅