UNITY_委托和事件
UNITY_委托和事件
参考资料: Unity3D脚本编程-使用C#语言开发跨平台游戏-陈嘉栋
观察者模式
主题(Subject)管理某些数据,当主题的数据发生改变时,会通知已经注册(Register)的观察者(Observer),而这些已经注册的观察者会受到数据改变的通知并作出相应反应。
观察者模式定义了对象之间的一对多依赖,当一个对象改变状态时,它的所有依赖者都会受到通知并自动更新。
Unity提供的机制:SendMessage()和BroadcastMessage()
缺点:
过于依赖反射机制(reflection)来查找消息对应的被调用函数
1. 频繁使用反射会影响性能
2. 更会大大增加代码的维护成本 -- 字符串标识对应方法
3. 能够调用private的方法 -- 若有一个是有方法在声明的类中没有被使用,那正常情况下都会把它认为是废代码从而删除,这时隐患就出现了
C#的委托机制
c#中提供的回调函数的机制便是委托(类型安全)
委托的使用
public class DelegateScript: MonoBehaviour{
internal delegate void MyDelegate(int num); // 声明委托类型(参数列表+返回类型)\
MyDelegate myDelegate; // 声明变量 void Start(){
myDelegate = PrintNum; // 给委托类型MyDelegate的实例赋值引用的方法
myDelegate(); myDelegate = DoubleNum;
myDelegate();
} void PrintNum(int num){
...
}
void DoubleNum(int num){
...
}
}
这里myDelegate = PrintNum; 将一个方法"赋值"给了一个委托
在c#2中为委托引入了方法组转换机制,支持从方法到兼容的委托类型的隐式转换
之所以成为方法"组"转换,则是因为方法的重载
若有delegate void Delegate1(int num)
和delegate void Delegate2(int num, int num2)
且有方法 void PrintNum(int num)
和 void PrintNum(int num, int num2)
则 myDelegate1 = PrintNum;
myDelegate2 = PrintNum;
向 myDelegate1或myDelegate2赋值时,都可以使用PrintNum作为方法组(因为重载了多个方法)
而编译器会自动选择合适的重载
委托参数的逆变性
逆变性: 可以是类型的基类
即委托对应方法的参数可以是委托的参数类型的基类
委托返回类型的协变性
协变性: 可以是类型派生出来的一个派生类
即委托对应方法的返回类型可以是委托的返回类型的一个派生类
逆变性和协变性仅针对引用类型,若是值类型或void则不支持
委托的编译器内部实现机理
(略) -- p154~164
委托链
委托调用多个方法 -- 委托链
委托链是委托对象的集合 -- 可以利用委托链来调用集合中的委托所代表的全部方法
public class DelegateScript : MonoBehaviour {
delegate void MyDelegate(int num); void Start(){
MyDelegate myDelegate1 = new MyDelegate(PrintNum1);
MyDelegate myDelegate2 = new MyDelegate(PrintNum2);
MyDelegate myDelegate3 = new MyDelegate(PrintNum3); MyDelegate myDelegates = null;
myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate1);
myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate2);
myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate3); Print(, myDelegates);
} void Print(int num, MyDelegate md){
if(md != null){
md(value);
}
} void PrintNum1(int num) { Debug.Log("1 result Num: " + num); }
void PrintNum2(int num) { Debug.Log("2 result Num: " + num); }
void PrintNum3(int num) { Debug.Log("3 result Num: " + num); }
}
刚开始时myDelegates = null; 表示没有对应要回调的方法
第一次myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate1);时myDelegates引用了myDelegate1所引用的委托实例
第二次时,myDelegates内部实现的_invocationList字段被初始化,并且_invocationList[0]指向和_invocationList[1]分别指向两个委托实例myDelegate1和myDelegate2
第三次是将一个委托实例myDelegate3合并到一个委托链中。编译器内部发生的与第二次的大同小异。需要注意的是,第二次得到的委托链中的_invocationList所引用的委托实例数组不再需要,被垃圾回收
将myDelegates变量(委托链)作为参数传入Print(),Print方法中的代码会隐式调用myDelegates所引用的委托实例的Invoke()方法,
此时会执行一个循环来遍历_invocationList中的所有委托实例并按顺序调用每个委托实例中包装的回调方法,即PrintNum1(), PrintNum2()和PrintNum3()
对应Delegate.Combine(), 也提供了Remove()方法用于移除委托实例
Remove()每次仅仅移除一个匹配的委托实例,而不是所有和目标委托实例匹配的委托实例
myDelegates = (MyDelegate)Delegate.Remove(myDelegates, new MyDelegate(PrintNum2));
当Remove方法被调用时,会从后向前扫描myDelegates中的委托实例数组,并对比委托实例的_target和_methodPtr的值是否与需要Remove的对应字段值相同。
若匹配,则删除。如果委托实例数组为空,则返回null;如果委托实例数组长度为1,则直接返回那个委托实例;
否则,会创建一个新的委托实例(与Combine出委托链类似),对应的_invocationList会引用由删除了目标委托实例后剩余委托实例组成的委托实例数组。
C#编译器为委托类型的实例重载了 += 和 -= 操作符,对应Delegate.Combine()和Delegate.Remove()
事件
观察者模式可以通过事件机制来实现
C#中的事件机制是以委托作为基础的
一个定义了事件成员的类型需要提供这些功能来实现交互机制
1. 方法能够订阅对某事件的关注
2. 方法能够取消订阅对该事件的关注
3. 事件发生时,订阅了该事件的方法会收到通知
实例:
一个游戏单位(BaseUnit类)被攻击而掉血,那么掉血(OnSubHp事件)就可以被作为一个事件。
订阅了该事件的对象在游戏单位掉血时,会收到游戏单位掉血的通知。
具体需求:掉血时,需要显示掉血信息,掉血信息中有多个值。
思路:为了区分开游戏单位和显示信息的逻辑(降低逻辑的耦合性),将掉血信息的显示逻辑交给模块BattleInfoComponent
--> 即游戏单位的掉血事件OnSubHp发生时,通知BattleInfoComponent模块来处理显示功能。
实现:
1. 定义委托类型(回调方法原型) -- 事件是以委托为基础的
public delegate void SubHpHandler(BaseUnit source, float subHp, DamageType damageType, HpShowType showType);
-- source: 受伤害的单位;subHp: 伤害;damageType: 伤害方式;showType: 显示方式
2. 定义事件成员
使用关键字event定义事件成员
public event SubHpHandler OnSubHp;
表示事件OnSubHp的类型为SubHpHandler,意味着事件OnSubHp的所有订阅者都必须提供和SubHpHandler委托类型所确定的方法原型相匹配的回调方法,即void Method(BaseUnit .., float .., DamageType .., HpShowType ..);
3. 事件的触发
这里的BaseUnit可以视为一个基类,派生出比如英雄类、士兵类等。
因此,触发事件的方法可以定义为一个虚方法
本例中,OnSubHp事件是受到攻击而导致的,因此
protected virtual void OnBeAttacked(float damage, bool isCritical, bool isMissed){
DamageType damageType = DamageType.Normal;
HpShowType showType = HpShowType.Damege;
if(isCritical) damageType = DamageType.Critical;
if(isMissed) showType = HpShowType.Miss;
// 如果有方法订阅了OnSubHp事件,则调用(通知)
if(OnSubHp != null) {
OnSubHp(this, damage, damageType, showType);
}
}
而BaseUnit的派生类可以通过重写OnBeAttack()来控制事件的触发
优化:业务单一原则
OnBeAttack()方法应该仅仅用来触发事件
因此
BeAttack()方法用来将敌人的攻击伤害转化为掉血事件的触发
public void BeAttack() {
bool isCritical = Random.value > 0.5f;
bool isMissed = Random.value > 0.5f;
float damage = 10000f; OnBeAttacked(damage, isCritical, isMissed);
}
4. 事件的订阅和观察者的回调方法
之前提到的BattleInfoComponent类是用来进行伤害信息显示的
而伤害信息显示的时机是在BaseUnit受到伤害的时候
因此BattleInfoComponent需要订阅BaseUnit.OnSubHp事件。
public class BattleInfoComponent: MonoBehaviour {
public BaseUnit baseUnit;
... private void AddListener () {
// 订阅事件this.unit.OnSubHp
this.unit.OnSubHp += new BaseUnit.SubHpHandler(this.OnSubHp);
// 可简写为
unit.OnSubHp += OnSubHp;
} private void RemoveListener () {
// 不要忘记取消事件的订阅
this.unit.OnSubHp -= new BaseUnit.SubHpHandler(this.OnSubHp);
} private void OnSubHp(BaseUnit source, float damage, DamageType dmgType, HpShowType showType) {
// 实现伤害信息的显示功能
Debug.Log(source.name + ....);
}
}
c#的+=操作符可用于注册事件
this.unit.OnSubHp += new BaseUnit.SubHpHandler(Method);
可简写为
this.unit.OnSubHp += Method;
上述两行代码的内部在编译器内部其实都等效于
this.unit.add_OnSubHp(new BaseUnit.SubHpHandler(this.OnSubHp));
在订阅事件时,编译器内部需要调用事件的add_OnSubHp方法来向事件内部添加新的委托对象
取消回调事件的订阅也相似,使用-=操作符
在编译器内部等效于
this.unit.remove_OnSubHp(new BaseUnit.SubHpHandler(this.OnSubHp));
总结:
当UnitBase受到攻击时,执行UnitBase.BeAttack()
--> UnitBase.OnBeAttacked()被调用
--> 触发UnitBase.OnSubHp事件
由于BattleInfoComponent.OnSubHp()方法订阅了UnitBase.OnSubHp事件
--> 因此在UnitBase.OnSubHp事件触发时,BattleInfoComponent.OnSubHp()方法被调用
而OnSubHp()方法会受到来自UnitBase.OnSubHp事件传来的参数source等
基于这些参数,OnSubHp()方法得以将伤害信息显示出来
事件的编译器内部实现机理
(略) -- p169~172、175
优点:
在事件机制中,事件成员(OnSubHp)才拥有数据,
这些数据不属于观察者,但是观察者需要依赖Subject的这些数据做出响应
如果有很多不同的观察者通过订阅同一个Subject,Subject的数据变化而触发了事件时,所有观察者会受到相应通知
通过事件机制可以将对象之间的相互依赖降到最低 -- 松耦合
观察者模式的意义也在于此,让主题和观察者之间实现松耦合的设计模式
当两个对象之间松耦合,即使不清楚彼此的细节,也可以进行交互
在有新类型的观察者出现时,主题(Subject)的代码无需修改,而新类型只需要实现匹配的回调方法即可注册成观察者
UNITY_委托和事件的更多相关文章
- .NET面试题系列[7] - 委托与事件
委托和事件 委托在C#中具有无比重要的地位. C#中的委托可以说俯拾即是,从LINQ中的lambda表达式到(包括但不限于)winform,wpf中的各种事件都有着委托的身影.C#中如果没有了事件,那 ...
- .NET基础拾遗(4)委托、事件、反射与特性
Index : (1)类型语法.内存管理和垃圾回收基础 (2)面向对象的实现和异常的处理基础 (3)字符串.集合与流 (4)委托.事件.反射与特性 (5)多线程开发基础 (6)ADO.NET与数据库开 ...
- [转载]C#深入分析委托与事件
原文出处: 作者:风尘浪子 原文链接:http://www.cnblogs.com/leslies2/archive/2012/03/22/2389318.html 同类链接:http://www.c ...
- [转载]C#委托和事件(Delegate、Event、EventHandler、EventArgs)
原文链接:http://blog.csdn.net/zwj7612356/article/details/8272520 14.1.委托 当要把方法作为实参传送给其他方法的形参时,形参需要使用委托.委 ...
- C#委托与事件
一.在控制台下使用委托和事件 我们都知道,C#中有"接口"这个概念,所谓的"接口"就是定义一套标准,然后由实现类来具体实现其中的方法,所以说"接口,是 ...
- C#委托与事件的简单使用
前言:上一篇博文从原理和定义的角度介绍了C#的委托和事件.本文通过一个简单的小故事,来说明C#委托与事件的使用方法及其方便之处. 在阅读本文之前,需要你对委托和事件的基本概念有所了解.如果你是初次接触 ...
- C#之委托与事件
委托与事件 废话一堆:网上关于委托.事件的文章有很多,一千个哈姆雷特就有一千个莎士比亚,以下内容均是本人个人见解. 1. 委托 1.1 委托的使用 这一小章来学习一下怎么简单的使用委托,了解一些基本的 ...
- [ASP.NET MVC 大牛之路]02 - C#高级知识点概要(1) - 委托和事件
在ASP.NET MVC 小牛之路系列中,前面用了一篇文章提了一下C#的一些知识点.照此,ASP.NET MVC 大牛之路系列也先给大家普及一下C#.NET中的高级知识点.每个知识点不太会过于详细,但 ...
- .NET委托和事件
.net学习之委托和事件 1.什么是委托 通俗的说:委托就是一个能够存储符合某种格式(方法签名)的方法的指针的容器 上传图片: 2.委托语法 准备一个方法:string Hello(string ...
随机推荐
- 解决redis-cli command not found问题
在使用其他服务器连接Redis服务器时,一般使用的语句是 [redis-cli -h IP -p port] 但是早上连接时报错:redis-cli command not found 在redis服 ...
- 1003. [ZJOI2006]物流运输【区间DP+最短路】
Description 物流公司要把一批货物从码头A运到码头B.由于货物量比较大,需要n天才能运完.货物运输过程中一般要转 停好几个码头.物流公司通常会设计一条固定的运输路线,以便对整个运输过程实施严 ...
- 集合之hascode方法
在前面三篇博文中LZ讲解了(HashMap.HashSet.HashTable),在其中LZ不断地讲解他们的put和get方法,在这两个方法中计算key的hashCode应该是最重要也是最精华的部分, ...
- 适合自己的adblock过滤列表
轻微完美主义,极简主义 已屏蔽广告: 1.CSDN的广告 2.百度侧栏热点搜索 3. 知乎广告 4.stackoverflow的推送广告 5.LeetCode的推送的是否见过这个题 bbs.csdn. ...
- ZOJ 2476 Total Amount 字符串模拟
- Total Amount Time Limit:2000MS Memory Limit:65536KB 64bit IO Format:%lld & %llu Submit ...
- K2 BPM介绍(1)
K2 BPM介绍(1) 官网访问地址: 中文官网 英文官网 它是一个强大的BPM产品 K2 BPM详解 产品特性 与任何内容集成 Integrate with Anything 功能丰富的窗体 Fea ...
- jQuery Nestable2 使用总结
最近,因为公司的一个新项目,用了一个基于bootstrap二次改造的国外友人的框架.感觉很一般吧,要求更换框架,客户拒绝.只能搞这个,发现里面一个jQuery插件-[Nestable]但是源作者长时间 ...
- 大数据入门:Hadoop安装、环境配置及检测
目录 1.导包Hadoop包 2.配置环境变量 3.把winutil包拷贝到Hadoop bin目录下 4.把Hadoop.dll放到system32下 5.检测Hadoop是否正常安装 5.1在ma ...
- 数据库oracle安装与卸载
安装的版本是oracle12-OraDb10g_home1服务端,先来卸载,如果电脑安装了oracle,在计算机-->管理-->服务里面可以看见下面三个oracle服务 首先我们要把它这里 ...
- C语言 有关内存的思考题
非原创. 今天笔试时候遇到的问题,原文链接见底部. 1 void GetMemory(char *p) { p = (); } void Test(void) { char *str=NULL; Ge ...