事件驱动程序设计

思考并回答以下问题:

本章涵盖:

  • 1.事件
  • 2.事件管理

对于一段时间内按一定规律执行的代码,MonoBehaviour对象的Update事件提供了一个较为方便的环境,并可跨越多个帧以及多个场景。当创建某些持久行为时,例如针对敌方角色或连续运动的人工智能行为,除了编写Update函数之外似乎别无他法,其中包含了大量的if和switch语句,取决于对象的当前需求,以使代码分支于不同的方向。当通过该方式考察Update事件时,即作为默认函数实现某些复杂行为,对于大型、复杂场景,这将会产生性能问题。当对此经过深入分析后,不难发现其中所隐藏的原因。一般情况下,游戏中涵盖了大量的行为,场景中时刻会出现各种事件,仅在Update函数中对此予以实现往往不切实际。例如,考察某一敌方角色,此类角色需要了解玩家何时进入或离开其视线,何时生命值处于危险状态,何时武器失效,何时身处于危险的地形中,何时遭受攻击,何时应处于运动状态等。初看之下,由于敌方角色需要及时了解此类属性的变化时机(作为玩家输入结果),因而需对此予以持续关注,这也是Update函数的主要适用原因。但这里存在一类较好的替代方案,即事件驱动编程。当采用事件方式考察游戏和应用程序时,性能将得到极大的提升。本章主要讨论事件问题,以及如何在游戏中对其加以管理。

事件

游戏场景世界可表示为一个确定性系统。在Unity中,场景定义为3D笛卡尔空间和时间轴,其中包含了某些GameObject。各种事件在游戏逻辑和代码的支配下产生于该空间内。例如,对象仅可在代码和特定条件允许时方可处于运动状态,如玩家按下某个键时。从中可以看出,行为并非是随机状态而是彼此间相互关联的—对象仅在产生键盘事件时处于运动状态。因此,动作间往往存在一个重要的连接,且动作间存在某种传递行为。这一类连接称作事件,各个独立的连接表示为独立的事件。事件往往表示为被动状态(非主动态),体现了某种时机而非自身动作,例如按下某个键、鼠标单击操作、对象进入碰撞器空间、玩家遭受攻击等,这均可视为事件的各种示例,但并未表明程序的执行内容,只是展现了所发生的某种情形。作为一类通用概念,事件驱动编程始于事件的识别,并将游戏中的各种情形视为事件的实例化结果。也就是说,事件与时间相关,理解游戏事件对于后续内容的学习十分有帮助,游戏中的全部动作均直接响应于特别的事件。特别地,事件连接于响应行为,即事件发生后将触发某一响应操作。进一步讲,该响应结果可再次形成某一事件,并触发后续响应行为等。换而言之,游戏场景表示为一个完整的、由事件和响应构成的集成系统。通过这一方式考察游戏场景时,随之而来的问题则是,如何通过Update函数提升性能,并使得各种行为在各帧中向前推进。对此,需要获取对应方式以降低事件的频度。该方案略显抽象,但却十分重要。为了进一步展示这一概念,下面对敌方角色进行考察。在于玩家战斗过程中,该角色将对玩家开火射击。

在游戏体验中,敌方角色需要跟踪多个属性。首先是生命值,当该值较小时,敌方角色会搜寻医药箱,在其辅助下,其生命值将得到迅速提升。其次是弹药量,若该值较小,该敌方角色将收集更多的弹药。另外,该角色还需判断何时向玩家射击。例如,仅当视线未被阻挡时,敌方角色即向玩家射击。通过上述方案,可在动作间确定某些连接,即事件。但在进一步讨论之前,还需查看基于Update函数的、此类行为的实现方式,如下代码所示。随后,还将进一步讨论事件对于该实现方案的改进方法。此代码绑定在敌方角色的对象上。

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
void Update()
{
// 检查血量
if (Health < 0)
{
Die();
return;
}

// 血量低就寻找医药箱
if (Health < 20)
{
RunAndFindHealthRestore();
return;
}

// 子弹没有了就寻找
if (Ammo <= 0)
{
SearchMore();
return;
}

// 看见游戏玩家就射击
if (HaveLineOfSight)
{
FireAtPlayer();
}
}

在上述代码中,Update函数包含了大量的条件检测和响应操作。实际上,Update函数希望将事件处理和响应行为合二为一,但实际结果则包含了某些冗余处理过程。当考察不同处理间的事件连接时(生命值和弹药量检测),将会发现代码可实现进一步的优化。例如,弹药量仅在两种情形下发生变化:敌方角色射击时,或者弹药被再次填充时。类似地,生命值的变化也包含两种情形:当敌方角色遭受玩家攻击时,或者该角色找到了第一个医药箱。对于前者,生命值将有所减少;而对于后者,生命值将增加。

鉴于属性发生变化(事件),因而对应值需要得到进一步确认。如下代码重构了敌方角色对象,其中包含了C#属性以及简化后的Update函数。

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
using UnityEngine;
using System.Collections;

public class EnemyObject : MonoBehaviour
{
// C# accessors for private variable
public int Health
{
get{ return _health;}
set
{
// Clamp(固定) health between 0-100
_health = Mathf.Clamp(value, 0, 100);
// Check if dead
if (_health <= 0)
{
OnDead();
return;
}
// Check health and raise event if required
if (_health <= 20)
{
OnHealthLow();
return;
}
}
}

public int Ammo
{
get {return _ammo;}
set
{
// Clamp ammo between 0-50
_ammo = Mathf.Clamp(value, 0, 50);
// Check if ammo empty
if(_ammo <= 0)
{
// Call expired event
OnAmmoExpired();
return;
}
}
}
// Internal variables for health and ammo
private int _health = 100;
private int _ammo = 50;

void Update()
{
}

void OnHealthLow()
{
}

void OnDead()
{
}

void OnAmmoExpired()
{

}
}

敌方角色类重新设计为事件驱动模式,Ammo和Health这一类属性不再设置于Update函数中,而是采用了赋值操作。自此,根据最新赋值结果将生成相关事件(函数触发写在了属性里)。当采用事件驱动设计时,性能将得到优化,同时代码将更为简洁。具体而言,在4-1代码中,Update函数的内容将大大减少,并消除了某些数值检测操作。相反,示例代码4-2中采用了特定的数值事件驱动代码,且仅在相关时刻对其进行调用。

事件管理

事件驱动程序设计大大简化了操作过程,但问题也随之而来。特别地,在上面的代码中,基于生命值和弹药量的C#属性用于检测、验证相关变化,并于随后生成相关事件(例如OnDead事件)。该过程理论上工作良好,至少可将自身事件告知自身。然而,如果该角色需要了解另一个敌方角色的生命值,或者需要知晓其他角色何时被射杀,情况又当如何?当然,对于特定情况,可返回至上面代码中的敌方角色类并对其进行修正,随后可利用相关函数,例如SendMessage函数,针对其他角色(而非仅针对当前角色)调用OnDead事件,但这依然无法解决一般意义上的问题。理想状态下,各个对象应可随意监听各种事件类型并被告知,仿佛是自身事件一样。因而,当前问题则是如何对优化后的系统进行编码,并按照当前方式简化事件管理系统。简而言之,需要一个允许对象倾听特定事件的EventManager类(新的专门管理的类),该系统依赖于下列3个核心概念。

  • EventListener:监听器是指需要被告知所发生的事件,甚至是自身事件的对象。在实际操作过程中,几乎每个对象均可针对至少一个事件定义为监听器。例如,某一敌方角色需要了解其他角色的生命值和弹药量。此时,该对象应至少针对两个独立事件定义为一个监听器。因此,无论何时对象需要被告知所发生的事件时,该对象均可定义为一个监听器。

  • EventPoster:与监听器相比,当对象检测到所发生的事件后,应发送与其相关的消息,并告知全部监听器。在上面的代码中,敌方角色类通过相关属性检测Ammo和Health事件,并于随后在必要时调用内部事件。对此,对象需要在全局层次上生成相关事件。

  • EventManager:最后,还需定义一个单例持久对象EventManager,并提供全局访问行为。该对象可将监听器连接至消息传递者。另外,该对象接收传递者发送的事件消息,并于随后将其以事件形式传递至相应的监听器中。

基于接口的事件管理

事件处理系统中的基本实体是监听器—可向其通知特定的事件。如果可能,任意对象和类均可定义为监听器,并简单地向其通知特定的事件。简单地讲,监听器需要通过EventManager并针对一个或多个特定事件注册为监听器。当产生相关事件时,可通过函数调用消息监听器。因此,从技术角度上讲,对于EventManager而言这将会产生类型问题。也就是说,如果监听器可表示为任意类型的对象,管理器如何在其上调用某一事件?当然,前述内容中曾提到,可利用SendMessage或BroadcastMessage函数解决这一问题。实际上,网络上的某些事件处理系统即采用了这种处理方式,例如NotificationCenter,然而,鉴于SendMessage或BroadcastMessage函数过度依赖于反射机制,本章将避免使用,而是采用了多态机制。特别地,本章将定义一个接口,且全部监听器对象均继承自该接口。

在C#语言中,接口类似于一个空的抽象基类。接口将方法和函数集整合至单一模板单元中,这一点与类十分相似。然而,与类结构不同,接口仅可定义函数原型,例如名称、返回类型以及函数的参数,且不可于其中定义函数体。其原因在于,接口仅定义了派生类应有的全部函数集。派生类在必要时须实现对应函数,接口的存在使得其他类可通过多态机制调用函数,且无须了解各个派生类的具体类型。据此,接口适用于创建Listener对象。通过定义Listener接口(全部对象均继承自该接口),各对象均可定义为事件监听器。示例代码显示了简单的Listener接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using UnityEngine;
using System.Collections;

//Enum defining all possible game events
//More events should be added to the list
public enum EVENT_TYPE{
GAME_INIT,
GAME_END,
AMMO_EMPTY,
HEALTH_CHANGE,
DEAD
};

///Listener interface to be implemented on Listener classes
public interface IListener
{
//Notification function invoked when events happen.
void OnEvent(EVENT_TYPE Event_Type, Component Sender, Object Param = null);
}

示例代码中的部分解释内容如下所示。

  • 第6到10行代码:枚举结构定义了可能发生的、完整的游戏事件列表。相应地,代码中仅列出了5种游戏事件,即GAME_INIT、GAME_END、AMMO_EMPTY、HEALTH_CHANGE以及DEAD,当然,用户还可定义多种事件。另外,也可仅采用整数对事件进行编码,此处采用枚举结构旨在提高代码的可读性。

  • 第13到17行代码:监听器接口定义为IListener(C#接口),且仅支持一个事件,即OnEvent。该函数被全部派生类继承,针对注册的监听器,当产生某一事件时该函数将被管理器所调用。需注意的是,OnEvent仅表示为函数原型,而非函数体。

通过IListener接口,仅利用类继承机制即可根据任意对象创建监听器。也就是说,任意对象均可声明为监听器并接收事件。例如,MonoBehaviour组件可通过如下代码转化为监听器,并采用了多重继承机制,即继承自两个类。

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

public class MyCustomListener : MonoBehaviour, IListener
{
//Use this for initialization
void Start() {}

//Update is called once per frame
void Update() {}

//Implement OnEvent function to receive Events
public void OnEvent(EVENT_TYPE Event_Type, Component Sender, Object Param= null)
{
}
}

定义EventManager

如前所述,任意对象均可转化为监听器,但监听器自身仍需通过某种类型的管理器对象进行注册。因此,当产生相关事件时,管理器负责调用监听器上的事件。下面讨论管理器的具体实现细节。此处,管理器类称作EventManager,如示例代码所示。作为持久性单例对象,该类绑定至空场景对象GameObject上,并可通过静态实例属性被其他对象直接访问。代码中的注释内容列出了该类及其应用的更多信息。

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

// Singleton EventManager to send events to listeners
// Works with IListener implementations
public class EventManager : MonoBehaviour
{
#region C# properties
// Public access to instance.
public static EventManager Instance
{
get { return instance; }
set { }
}
#endregion

#region variables
// Notifications Manager instance (singleton design pattern)
private static EventManager instance = null;

// Array of listeners (all obiects registered for events).
private Dictionary<EVENT_TYPE, List<IListener>> Listeners = new Dictionary<EVENT_TYPE, List<IListener>>();
#endregion

#region methods
// Called at start-up to initialize.
void Awake()
{
// If no instance exists, then assign this instance
if (instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
}
else
DestroyImmediate(this);
}

/// <summary>
/// Function to add listener to array of listeners
/// </summary>
/// <param name = "Event_Type" >Event to Listen for</param>
/// <param name = "Listener">Object to listen for event</param>

public void AddListener(EVENT_TYPE Event_Type, IListener Listener)
{
// List of listeners for this event
List<IListener> ListenList = null;

// Check existing event type key. If exists, add to list
if (Listeners.TryGetValue(Event_Type, out ListenList))
{
// List exists, so add new item
ListenList.Add(Listener);
return;
}

// Otherwise create new list as dictionary key
ListenList = new List<IListener>();
ListenList.Add(Listener);
Listeners.Add(Event_Type, ListenList);
}

/// <summary>
/// Function to post event to listeners
/// </summary>
/// <param name="Event_Type">Event to invoke</param>
/// <param name-"Sender">Object invoking event</param>
/// <param name="Param">Optional argument</param>
public void PostNotification(EVENT_TYPE Event_Type, Component Sender, Object Param = null)
{
// Notify all listeners of an event

// List of listeners for this event only.
List<IListener> ListenList = null;

// If no event exists, then exit
if (!Listeners.TryGetValue(Event_Type, out ListenList))
return;

// Entry exists. Now notify appropriate listeners
for (int i=0; i < ListenList.Count; i++)
{
if (!ListenList[i].Equals(null))
ListenList[i].OnEvent(Event_Type, Sender, Param);
}
}

// Remove event from dictionary, including all listeners
public void RemoveEvent(EVENT_TYPE Event_Type)
{
// Remove entry from dictionary
Listeners.Remove(Event_Type);
}

// Remove all redundant(多余的) entries from the Dictionary
public void RemoveRedundancies()
{
// Create new dictionary
Dictionary<EVENT_TYPE, List<IListener>> TmpListeners = new Dictionary<EVENT_TYPE, List<IListener>>();
// Cycle through all dictionary entries
foreach (KeyValuePair<EVENT_TYPE, List<IListener>> Item in Listeners)
{
// Cycle all listeners, remove null objects
for (int i = Item.Value.Count - 1; i >= 0; i--)
{
// If null, then remove item
if (Item.Value[i].Equals(null))
Item.Value.RemoveAt(i);
}

// If items remain in list, then add to tmp dictionary
if (Item.Value.Count > 0)
TmpListeners.Add(Item.Key, Item.Value);
}
// Replace listeners object with new dictionary
Listeners = TmpListeners;
}

// Called on scene change. Clean up dictionary
void OnLevelWasLoaded()
{
RemoveRedundancies();
}

#endregion
}

示例代码4-5中的部分解释内容如下所示。

  • 第3行:System.Collections.Generic命名空间可访问附加的Mono类,同时还包括Dictionary类。该类贯穿于整个EventManager类。简单地讲,Dictionary表示为2D数组,并根据键值对存储数值的数据库。

  • 第3行:EventManager类继承自MonoBehaviour,且应与场景中的空GameObject绑定,并作为持久性单例对象存在。

  • 第3行:private成员变量Listeners采用Dictionary类声明,该结构用于维护键中值对的哈希表数组,并可像数据库那样执行查找和搜索行为。EventManager类的键值对接收形如EVENT_TYPE和List的参数。这意味着,可存储事件类型列表(例如HEALTH_CHANGE),且针对各个类型,可存在0个、1个或多个监听组件,并在产生相关事件时被通知。实际上,Listener成员表示为主数据结构,EventManager以此维护监听目标。

  • 第3行:Awake函数负责实现单例功能,即EventManager表示为单例对象,并在场景中持久存在。

  • 第3行:针对各个监听事件,EventManager的AddListener方法通过Listener对象调用,该方法接收两个参数,即监听的事件(Event_Type)和监听器对象的引用(继承自IListener),并在产生事件时被通知。AddListener函数负责访问Listener字典,生成新的键值对,并存储事件和监听器之间的连接。

  • 第3行:当检测到某一事件时,PostNotification函数可通过任意对象(无论是否为监听器)加以调用。随后,EventManager遍历字典中的全部匹配项,搜索与当前事件连接的所有监听器,通过IListener接口调用OnEvent方法,进而对其予以通知。

  • 第3行:EventManager类中的最后一个方法维护Listener结构的数据完整性(场景产生变化,且EventManager类具有持久性特征)。虽然EventManager类在不同场景间具有持久性特征,但Listener变量中的监听器对象则并非如此。当场景变化时,此类对象将被销毁。对此,场景变化会使某些监听器处于失效状态,从而使得EventManager包含了无效项。因此,可调用RemoveRedundancies方法搜索并消除全部无效项。另外,当场景发生变化时,Unity将自动调用OnLevelWasLoaded事件。

字典的优势不仅体现于作为动态数组的快速访问能力,同时,用户还可通过对象类型和下标操作符与其协同工作。在常规数组中,各数据元素可采用整型索引予以访问,例如MyArray[0]和MyArray[1]。当采用字典时,情况则有所不同。特别地,用户可利用EVENT_TYPE对象访问数据元素,即键值对中的“键”,例如MyArray[EVENT_TYPE.HEALTH_CHANGE]。

代码折叠#region和#endregion

region和#endregion预处理器指令(与代码折叠特性结合使用)对于改善代码的可读性,以及源代码的浏览速度十分有效。在不影响源代码的有效性和执行结果准确性的前提下,可向源代码添加重组和结构化功能。其中,#region标记于代码块的上方,而#endregion则标注于结尾处。当某一区域按照这一方式标记后,该区域将呈现为折叠状态。在代码编辑器中,将显示为收缩状态。当收缩某一代码区域后,其内容将隐藏于视图中,用户可仅关注特定的代码区域。

使用EventManager

下面考察如何从监听器和传送对象的角度将EventManager置于实际工作环境中。首先,事件监听器需要利用EventManager单例实例进行注册。一般情况下,该操作仅在前期执行一次,例如在Start函数中。此处不可使用Awake函数,该函数用于对象的内部初始化行为。示例代码采用了Instance静态属性获取指向处于活动状态的、EventManager单例对象的引用。

1
2
3
4
5
6
// Called at start-up
void Start()
{
// Add myself as listener for health change events
EventManager.Instance.AddListener(EVENT_TYPE.HEALTH_CHANGE, this);
}

当对一个或多个事件注册了监听器后,若检测到事件,对象可向EventManager发送消息,如以下代码所示。

1
2
3
4
5
6
7
8
9
10
11
12
public int Health
{
get {return _health;}
set
{
// Clamp health between 0-100
_health = Mathf.clamp(value, 0, 100);

// Post notification - health has been changed
EventManager.Instance.PostNotification(EVENT_TYPE.HEALTH_CHANGE, this, _health);
}
}

最后,事件消息经传递后,与其关联的监听器通过EventManager自动更新。特别地,EventManager将调用各个监听器的OnEvent函数,解释事件数据并在对应处予以响应,如以下代码所示。

1
2
3
4
5
6
7
8
9
10
11
// Called when events happen
public void OnEvent(EVENT_TYPE EVENT_TYPE, Component Sender,object Param = null)
{
// Detect event type
switch (EVENT_TYPE)
{
case EVENT_TYPE.HEALTH_CHANGE:
OnHealthChange(Sender, (int)Param);
break;
}
}

基于委托机制的替代方案

接口可视为一类高效方案,并简化了事件处理系统的实现过程,但并非是唯一的处理方案。除此之外,还可利用C#语言中的委托机制。实际上,用户可定义一个函数,并在变量中存储指向该函数的引用。该变量可将函数视为引用类型的变量。也就是说,当采用委托机制时,用户可存储指向函数的引用,在后续操作中,可以此调用该函数。其他语言则通过函数指针提供了类似的功能,例如C++语言。当采用委托机制实现事件系统时,可不再使用接口。作为替代方案,示例代码通过委托实现了EventManager。其中,调整内容采用黑体表示,以彰显接口和委托实现之间的差别。除了与委托机制对应的变化内容之外,其他函数均保持不变。

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

//Enum defining all possible game events
//More events should be added to the list
public enum EVENT_TYPE {GAME_INIT,
GAME_END,
AMMO_CHANGE,
HEALTH_CHANGE,
DEAD
};

//Singleton EventManager to send events to listeners
//Works with IListener implementations
public class EventManager : MonoBehaviour
{
#region C# properties
//Public access to instance
public static EventManager Instance
{
get{return instance;}
set{}
}
#endregion

#region variables
//Internal reference to Notifications Manager instance (singleton design pattern)
private static EventManager instance = null;

// Declare a delegate type for events
public delegate void OnEvent(EVENT_TYPE Event_Type, Component Sender, object Param = null);

//Array of listener objects (all objects registered to listen for events)
private Dictionary<EVENT_TYPE, List<OnEvent>> Listeners = new Dictionary<EVENT_TYPE, List<OnEvent>>();
#endregion

#region methods
//Called at start-up to initialize
void Awake()
{
//If no instance exists, then assign this instance
if(instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject); //Prevent object from being destroyed on scene exit
}
else //Instance already exists, so destroy this one. This should be a singleton object
DestroyImmediate(this);
}

/// <summary>
/// Function to add specified listener-object to array of listeners
/// </summary>
/// <param name="Event_Type">Event to Listen for</param>
/// <param name="Listener">Object to listen for event</param>
public void AddListener(EVENT_TYPE Event_Type, OnEvent Listener)
{
//List of listeners for this event
List<OnEvent> ListenList = null;

//New item to be added. Check for existing event type key. If one exists, add to list
if(Listeners.TryGetValue(Event_Type, out ListenList))
{
//List exists, so add new item
ListenList.Add(Listener);
return;
}

//Otherwise create new list as dictionary key
ListenList = new List<OnEvent>();
ListenList.Add(Listener);
Listeners.Add(Event_Type, ListenList); //Add to internal listeners list
}

/// <summary>
/// Function to post event to listeners
/// </summary>
/// <param name="Event_Type">Event to invoke</param>
/// <param name="Sender">Object invoking event</param>
/// <param name="Param">Optional argument</param>
public void PostNotification(EVENT_TYPE Event_Type, Component Sender, object Param = null)
{
//Notify all listeners of an event

//List of listeners for this event only
List<OnEvent> ListenList = null;

//If no event entry exists, then exit because there are no listeners to notify
if(!Listeners.TryGetValue(Event_Type, out ListenList))
return;

//Entry exists. Now notify appropriate listeners
for(int i=0; i<ListenList.Count; i++)
{
if(!ListenList[i].Equals(null)) //If object is not null, then send message via interfaces
ListenList[i](Event_Type, Sender, Param);
}
}

//Remove event type entry from dictionary, including all listeners
public void RemoveEvent(EVENT_TYPE Event_Type)
{
//Remove entry from dictionary
Listeners.Remove(Event_Type);
}

//Remove all redundant entries from the Dictionary
public void RemoveRedundancies()
{
//Create new dictionary
Dictionary<EVENT_TYPE, List<OnEvent>> TmpListeners = new Dictionary<EVENT_TYPE, List<OnEvent>>();

//Cycle through all dictionary entries
foreach(KeyValuePair<EVENT_TYPE, List<OnEvent>> Item in Listeners)
{
//Cycle through all listener objects in list, remove null objects
for(int i = Item.Value.Count-1; i>=0; i--)
{
//If null, then remove item
if(Item.Value[i].Equals(null))
Item.Value.RemoveAt(i);
}

//If items remain in list for this notification, then add this to tmp dictionary
if(Item.Value.Count > 0)
TmpListeners.Add (Item.Key, Item.Value);
}

//Replace listeners object with new, optimized dictionary
Listeners = TmpListeners;
}

//Called on scene change. Clean up dictionary
void OnLevelWasLoaded()
{
RemoveRedundancies();
}

#endregion
}

示例代码中的部分解释内容如下所示。

  • 第1行:事件类型枚举结构替换为源自原IListener类的EventManager文件。委托实现避免使用接口和IListener,因而枚举结构转换为管理器源文件。

  • 第1行:public成员变量OnEvent声明为委托类型。需注意的是,声明采用了混合方式,并结合了变量声明和函数原型。这确定了函数原型,并可赋予委托变量中;基于该结构的任意函数可通过类或脚本文件进行赋值。因此,OnEvent函数演变为委托类型,并用于下一条语句中,进而创建内部字典。

  • 第1行:声明了private字典;同时,针对各个事件类型,存储了一个委托数组(而非接口);各个委托将引用一个函数,该函数在产生事件时被调用。

  • 第1行:严格地讲,PostNotification函数在EventManager上被调用,当产生事件时将调用全部委托(监听器函数)。该行为出现于处,即“ListenListi;”语句。这将像调用函数一样调用委托。

MonoBehaviour事件

下面讨论Unity中已经提供的使用事件驱动编程的事件。MonoBehaviour类定义了多种事件,并可在特定条件下自动被调用。这一类函数或事件始于前缀On,并包含了诸如OnGUI、OnMouseEnter、OnMouseDown、OnParticleCollision等事件。

鼠标事件

鼠标输入和触摸输入事件集十分有用,其中包含了OnMouseDown、OnMouseEnter和OnMouseExit。在Unity的早期版本中,此类事件仅通过鼠标事件触发,且不包括触摸输入。近期,触摸输入也加入其中,这也意味着,默认条件下,触击行为注册为鼠标事件。具体而言,按下鼠标键时(同时鼠标指针悬停于某一对象上),OnMouseDown将被调用。然而,该事件并不会重复被调用,直至按键被释放。类似地,当鼠标指针悬停于某一对象上时,OnMouseEnter将被调用;而鼠标指针离开该对象时,OnMouseExit将被调用。这一类事件的成功与否取决于与对象绑定的碰撞器组件,并在鼠标事件检测过程中靠近其空间体。也就是说,如果对象未绑定碰撞器,则不会触发鼠标事件。

然而,某些场合下,MouseEvents并不会被触发,即使相关对象绑定了碰撞器。其原因在于,根据当前活动相机的视角,其他对象(包含碰撞器)遮挡了所单击的对象,即可单击对象位于背景中。当然,存在多种方式可处理这一类问题,例如,可简单地将前景对象赋予Ignore aycast层中,以使其免受物理光线操作的影响。

在将某一对象赋予Ignore Raycast层时,可选择该场景对象,单击Object Inspector中的Layer下拉列表,并将当前对象置于Ignore Raycast层中,如下图所示。

在某些情况下,上述措施同样也会失效。通常,这会涉及多部相机以及多个包含碰撞器的对象,并遮挡希望选取或调整的对象(根据鼠标输入事件)。对此,用户需要手动处理鼠标事件。示例代码根据输入内容,以手动方式调用特定的鼠标事件。实际上,代码使用了Raycast(射线)系统,并将手动检测到的输入事件重定向至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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
using UnityEngine;
using System.Collections;

public class ManualMouse : MonoBehaviour
{
//Get collider attached to this object
private Collider Col = null;

//Awake function - called at start up
void Awake()
{
//Get collider
Col = GetComponent<Collider>();
}

//Start Coroutine
void Start()
{
StartCoroutine(UpdateMouse());
}

public IEnumerator UpdateMouse()
{
//Are we being intersected
bool bIntersected = false;

//Is button down or up
bool bButtonDown = false;

//Loop forever
while(true)
{
//Get mouse screen position in terms of X and Y
//You may need to use a different camera, if you have multiple
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;

//Test ray for collision against this collider
if (Col.Raycast(ray, out hit, Mathf.Infinity))
{
//Object was intersected - if not previous intersecting, then send on enter message
if(!bIntersected) SendMessage("OnMouseEnter", SendMessageOptions.DontRequireReceiver);

bIntersected = true;

//Test for mouse events
if(!bButtonDown && Input.GetMouseButton(0)){bButtonDown = true; SendMessage("OnMouseDown", SendMessageOptions.DontRequireReceiver);}
if(bButtonDown && !Input.GetMouseButton(0)){bButtonDown = false; SendMessage("OnMouseUp", SendMessageOptions.DontRequireReceiver);}
}
else
{
//Was previously entered and now leaving
if(bIntersected) SendMessage("OnMouseExit", SendMessageOptions.DontRequireReceiver);

bIntersected = false;
bButtonDown = false;
}

//Wait until next frame
yield return null;
}
}
}

协同程序表示为一种特定类型的函数,从某种程度上讲,其行为类似于线程,并相对于主游戏循环并行或异步运行。也就是说,一旦处于执行状态,协同程序仿佛在后台运行。执行过程并不会暂停或等待,直至该程序像常规函数那样执行完毕。因此,协同程序适用于创建具有异步外观的行为。

从技术角度上讲,全部协同程序需要返回IEnumerator类型,并在程序体中至少包含一条yield语句,并通过StartCoroutine函数进行发布。yield语句表示为一类特殊的语句,并可暂停协同程序的执行,直至其条件得到满足。yield语句返回WaitForSeconds(x),将暂停执行流程x秒,并在间隔后(下一行处)恢复执行。相比较而言,yield语句返回null将针对当前帧延缓执行,并在下一帧恢复执行流程(下一行处)。

应用程序焦点和暂停

读者应注意3个额外的MonoBehaviour事件,其操作容易引起混淆并产生未知结果,即OnApplicationPause、OnApplicationFocus和OnApplicationQuit事件。

OnApplicationQuit将在游戏退出前发送至场景中的全部对象中,随后,场景及其全
部内容将被销毁。如果游戏在编辑器中进行测试,则播放停止时将调用OnApplicationQuit事件。值得注意的是,OnApplicationQuit不会针对iOS设备被调用,此时应用程序处于暂停状态,同时用户可执行其他操作,最终返回至中断点并恢复执行。如果用户希望或需要接收包含暂停态的OnApplicationQuit,则需要在Player Settings窗口中启用相关选项。具体来说,可在应用菜单中选择Edit->Project Settings->Player命令,随后可在ObjectInspector中针对iOS版本打开Other Settings选项卡,并选中Exiton Suspend复选框。

当游戏失去焦点时,事件OnApplicationFocus将传递至全部场景对象中。通常在多任务系统中,游戏窗口将会处于非激活状态。这可视为一类较为重要的游戏事件,特别是在多玩家游戏中。其中,公共场景中的动作和事件将持续进行,即使玩家并未实际参与。在这一类情形中,用户需要暂停、恢复特定行为,或者淡入、淡出游戏音乐。

OnApplicationPause表示为一类模糊事件,其原因在于,Unity中的暂停概念并未加以清晰定义。相应地,存在两种暂停方式,即最终(ultimate)暂停和相对暂停。对于前者,游戏中的各项活动和事件均处于暂停状态。期间,时间处于停止状态,且全部事物均呈现为静止状态。相比较而言,相对暂停则更为常见。其中,游戏知晓自身处于暂停状态,并中止某些事件,例如场景中的事件,同时可使其他事件继续执行,例如GUI交互或用户输入,进而可解除游戏的暂停状态。OnApplicationPause事件表示为第一类暂停,(而非后者)。当满足多个条件时,该事件将被调用,稍后将对此予以介绍。

首先,如果Run In Background选项在选项卡中未被选中(位于Resolution分组内),如图4-5所示,则OnApplicationPause仅实现了桌面级调用。当禁用该选项后,且窗口失去焦点时,这将自动暂停桌面游戏。这也意味着,OnApplicationPause将随OnApplicationFocus事件被调用。

当实现自己的相对暂停功能时,应尽量避免使用OnApplicationPause事件。相应地,可使用Time.timeScale变量,或者对更为复杂的综合系统进行编码,进而可对暂停,元素有选择性地加以控制。

小结

本章主要通过EventManager讨论事件驱动框架针对应用程序的各种优势。当实现此类管理器时,可采用接口或委托机制,二者均功能强大且兼具可扩展性。特别地,Update函数中可加入诸多功能项,但也会产生性能问题。较好的方法是分析各功能之间的连接,并将其重构为事件驱动框架。实际上,事件表示为事件驱动系统中的原始资料,体现了动作(起因和响应)间的必要连接。当对事件进行管理时,可创建EventManager类—将传递者链接至监听器的集成类或系统,从传递者处接收与事件相关的消息,并于随后将函数调用分发至基于该事件的全部监听器上。

0%