1.3 事件响应接口
通常对于事件传递,绑定消息处理时都要写很多相同的框架级别方法,例如:
//register m_btn1.onClick.AddListener(OnBtnClick1); m_btn2.onClick.AddListener(OnBtnClick2); m_btn3.onClick.AddListener(OnBtnClick3); //Implement public void OnBtnClick1() { //do sth in btn1 } public void OnBtnClick2() { //do sth in btn2 } public void OnBtnClick3() { //do sth in btn2 }
请问是否有方法对其简化,减轻开发编码量?
问题分析
这个问题并不难。但很多时候,并没有想过从框架层解决冗余代码。这个问题有时也会换个更开放的方法来问:“说说你的UI框架吧。”具体落实到代码上,回答它的重点跟这个问题有很大的重合性。
我们都知道,编程时有个DRY原则,即“Don't Repeat Yourself”。它主张不要编写重复的代码。落实到实际,就是避免在代码中无脑地复制粘贴。
在《重构:改善既有代码的设计》中,不合理的代码结构被称为“代码的坏味道”,破坏DRY原则就很难写出高质量的代码。如何去除代码中的“坏味道”,在这个问题中能得到很好的体现。
添加分层
很多年以前,计算机领域的先驱,David Wheeler就提出过这样的说法:
“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”
针对这个问题,我们也可以将分层作为突破点。在现有的基础上添加一层,是最容易完成需求的做法。
按照这样的思路,我们需要创建一个容器类,它可以自动将可响应事件的子节点找到,并为其添加监听。代码大概是这样:
using UnityEngine; using System.Collections; using UnityEngine.UI; public class MyPanel : MonoBehaviour { void Awake() { var btns=GetComponentsInChildren<Button>(); for(int i=0; i < btns.Length; ++i) { btns[i].onClick.AddListener(OnBtnClick); } } public void OnBtnClick() { Debug.Log("btn click"); } }
然后在新场景中创建一个挂载这个脚本的节点。创建好后再为其添加三个按钮子节点。直接运行游戏,单击按钮即可看到输出,如图1.9所示。
图1.9
优化事件接口
现在点击可以自动注册了,但无法区分出单击的是哪个按钮。
所以要增加回调的标识,采用一个简化的方式:以节点被搜到的顺序作为Key,将节点的索引值传到回调中,就可以知道单击的是哪个按钮了。
在正式的代码中,可以用节点名或属性值作为标记,以防止顺序更改出现对应的错误。也可以使用更加稳妥的方法,例如,使用GameObject的GUID,或创建一套ID映射机制,用于标定具体对象的对应关系,实现这个功能的代码如下:
using UnityEngine; using System.Collections; using System.Collections.Generic; public class MyPanel : MonoBehaviour { void Awake() { var btns=GetComponentsInChildren<Button>(); for(int i=0; i< btns.Length; ++i) { int index=i; btns[i].onClick.AddListener(()=>{ OnBtnClick(index); }); } } public void OnBtnClick(int index) { Debug.Log("btn click:"+index); } }
我们在这段函数的循环中,创建了一个index变量。需要注意的是,这个变量不可以移到函数外,因为它分配出的内存要跟着回调传递。这其中的内在原理可以查询C#中匿名函数的相关知识。为了保证主题明确,此处不再展开,感兴趣的读者可以去网上自行搜索。
扩展问题
(1)有办法对各种控件添加统一的响应接口吗?
(2)有时候界面中会有大量不显示的按钮,这会影响我们的封装吗([:hiddenbtn])?