原文地址:http://www.narkii.com/club/thread-283590-1.html
最近分析了一下Unity自带的AngryBots中的代码,发现了一个相当强大的信号传递框架,并且代码并不复杂。它之所以有用是因为此框架维护起来非常方便,就像我之前介绍的那个状态机框架的道理是一样的。这个框架仅仅只有一个脚本,你可能对此产生不屑,不过,当你看到了它的魔术般的效果时,你一定会对你之前的藐视而感到惭愧的。好了,话多无益,实践才是硬道理。
首先,我们来思考一个问题:假如在某一时刻,我们需要做一连串动作,且执行该行为的接收者也各不相同,甚至是同一个接收者在此条件下得执行不同脚本中的某些函数,此时我们的代码该怎样写?例如:当我按下攻击键,我自身必须播放射击的动画,角色自身是此攻击信号的第一个接收者;然后角色的枪口要克隆出一个子弹,并且产生火花,还要播放子弹出膛的音频,此时枪口可以设计成第二个攻击信号的接收者,且要连续执行三个不同的行为,如果主角此时拿的是火箭筒,那么此时的接收者主角还要向后挪动一小段距离,也就是说此接收者还要多执行一个动作。通常情况下,我们会事先在枪口的某一个位置安放一个子弹出膛点,然后绑定一个音频源,再挂接相应的脚本,当我们按下开枪按键时,会获取有关的脚本,然后调用里面的相应的函数,执行该动作。可是我们会发现,这样做的话,代码会变得异常混乱,并不利于我们今后对代码的维护,所以我们得尽量想其他的办法。这就是我今天介绍这个框架的原因。
我将AngryBots里面的那个代码给翻译成了C#,途中出了一些挫折,原因出在类的序列化,先看代码:
using UnityEngine; using System.Collections; [System.Serializable] public class ReceiverItem{ public GameObject receiver;//信号接收者 public string action = "OnSignal";//默认执行的函数名,可以在Inspector面板上面修改 public float delay;//执行在执行该行为之前的等待时间,默认是不必等待的 public IEnumerator SendWithDelay(MonoBehaviour sender) { yield return new WaitForSeconds(delay);//等待delay秒 if (receiver)//如果接收者存在 receiver.SendMessage(action,SendMessageOptions.DontRequireReceiver);//就执行该接收者附着的脚本中的名为action的函数 else Debug.LogWarning("No receiver of signal "" + action + "" on object " + sender.name + " (" + sender.GetType().Name + ")", sender); } } [System.Serializable] public class SignalSends { public ReceiverItem[] receives;
public void SendSignal(MonoBehaviour sender){ for (int i = 0; i < receives.Length; i++){ sender.StartCoroutine(receives.SendWithDelay(sender));//执行协同函数 } } }
这就是此框架的核心,你也许会说,这么短的代码,能起多大作用?好吧,我们还是以一个例子来说明这一切吧!为了节省时间,我还是以我的上一篇帖子中的工程作为基础,修改一下里面的几个脚本。
第一步,往工程里面添加一个Audio文件夹,并导入两个音频片(我自己找的两个音频片),如下:
第二步,修改代码及Inspector面板中的信号变量的拖拽:SpawnBullet.cs修改如下:
SpawnBulletPoint.cs的修改:
using UnityEngine; using System.Collections; public class SpawnBulletPoint : MonoBehaviour { public float radius = 2f; public GameObject bullet; void OnDrawGizmos(){ Gizmos.color = Color.green; Gizmos.DrawSphere(transform.position, radius); } void InstantiateBullet(){ Instantiate(bullet, transform.position, Quaternion.identity); } void PlayAudio(){
if(audio){
audio.Play(); }
} }
下面我们开始最关键的一步:
选中MainCamera,然后在Inspector面板上将连个信号接收实例定义为一个,且里面的接收者元素有两个,如下:
我们看SpawnSignal,此时该变量下元素类型为Receives的实例有两个,但都为SpawnPoint,只是Action不同,留心观察一下,这两个Action是可以在Inspector面板中临时填写的,当前填写的名字正好是SpawnBulletPoint
.cs脚本中的两个新添加的函数名。这就是这个信号发射系统的关键所在。接下来我们修改BulletController.cs脚本:
using UnityEngine; using System.Collections; public class BulletController : MonoBehaviour { public float speed = 1f; public float distance = 0.1f; public SignalSends explosionSignal; public AudioClip explosion; private LayerMask mask; // Use this for initializationvoid void Start () { mask = 1 << LayerMask.NameToLayer("wall1"); } // Update is called once per framevoid void Update () { transform.Translate(transform.forward * speed * Time.deltaTime ); RaycastHit hit; if(Physics.Raycast(transform.position,transform.forward,out hit,distance,mask)) { explosionSignal.SendSignal(this); } } void cuteExplosion() { if(explosion) { AudioSource.PlayClipAtPoint(explosion,transform.position); } Destroy(this.gameObject); } }
修改的目的是:当子弹撞击第二块挡板时播放爆炸的音频。但用的是信号发射框架,具体修改部分我还是将工程上传上去大家下载下来自行研究吧!运行之后,功能全都实现了。我们再回头看看这个信号发射框架中的后一个类的代码:
[System.Serializable] public class SignalSends { public ReceiverItem[] receives; public void SendSignal(MonoBehaviour sender){ for (int i = 0; i < receives.Length; i++) { sender.StartCoroutine(receives.SendWithDelay(sender)); } } }
整个流程丝毫不拖泥带水,我们理论上可以在Inspector面板中的SignalSends实例中定义无数个接收者,并执行相应的名为Action函数。我们只需调用SendSignal(this)函数,就不用在当前脚本中编写纷繁复杂的行为代码了,只需在特定的脚本中编写Action函数就行,然后再Inspector面板中将此Action函数名添加进去就行了。如此一来,我们就省去了获取GameObject实例,获取脚本实例,然后才能调用特定函数且有可能传递一些参数的这些步骤了,我们就能更方便的编写行为代码,且当行为增加时,我们只需增加SignalSends实例中的ReceiverItem元素个数并编写相应的行为函数然后将该行为函数替换进去就行了。认真体会一下吧!我想,这个短小精悍的代码应该会在某些时刻发生一些意想不到的作用的!