为了以防读者不明白Flappy Bird是什么游戏,先放出一张自制的flappy Bird小游戏来让大家有个印象。
这个游戏最大的特点就是有无限个上下对齐的柱子,玩家控制的小鸟必须从上下柱子的缝隙中穿过才算得分。当然,为了录屏我把小鸟调小了点,让我能多过了几根柱子O(∩_∩)O~
那么,在游戏制作过程中,我们自然不可能去Instantiate几百几千根柱子,并且,万一真有玩家达到了这个地步,我们难道再来Instantiate几百几千根柱子?显然,这是不可能的。所以,我们在这里需要对象池(Object Pooling)技术了。
对象池用于那些频繁创建销毁的object上,比如上述游戏的柱子,它从最右侧屏幕外生成出来,移动到最左侧,出屏幕后就销毁,这种情况非常适合使用对象池。
对象池原理很简单:
- 创建一个池子(比如一个List),将若干个object放在池子里
- 要使用object时从池子里拿出一个未被使用中的object,并标记为使用中
- 用完时不要销毁,将其标记为未使用并放回池子,为了不让玩家看到这个object一般来说可以把它的位置放在一个摄像机看不到的地方,或者直接
SetActive(false)
于是,基于上述理念,我们来用代码实现一下。
public class Parallaxer : MonoBehaviour
{
PoolObject[] poolObjects;
class PoolObject{
public Transform transform;
public bool inUse;
public PoolObject(Transform t){
transform = t;
}
public void Use(){
inUse = true;
}
public void Dispose(){
inUse = false;
}
}
void Configure(){
poolObjects = new PoolObject[poolSize];
for (int i = 0; i < poolObjects.Length; i++)
{
Transform t = Instantiate(prefab).transform;
t.SetParent(transform);
t.position = Vector3.one * 1000;
poolObjects[i] = new PoolObject(t);
}
}
void Spwan(){
Transform t = GetPoolObject();
if (t == null){
return;
}
Vector3 pos = Vector3.zero;
pos.x = defaultSpwanPos.x;
pos.y = Random.Range(ySpawnRange.min,ySpawnRange.max);
t.position = pos;
}
void Shift(){
for (int i = 0; i < poolObjects.Length; i++)
{
poolObjects[i].transform.localPosition += -Vector3.right * shiftSpeed * Time.deltaTime;
CheckDisposeObject(poolObjects[i]);
}
}
void CheckDisposeObject(PoolObject obj){
if (obj.transform.position.x < -defaultSpwanPos.x){
obj.Dispose();
obj.transform.position = Vector3.one * 1000;
}
}
Transform GetPoolObject(){
for (int i = 0; i < poolObjects.Length; i++)
{
if (!poolObjects[i].inUse){
poolObjects[i].Use();
return poolObjects[i].transform;
}
}
return null;
}
}
我们先写个柱子类,类里面有个对象池类,Use()
和Dispose()
用来标记是否使用中。然后我们用Config()
来初始化对象池,池的大小可由外部控制,所有在对象池里的东西先挪到一个看不见的位置Vector3.one * 1000
。然后我们用Spawn()
从对象池里取出柱子并将它放到可见区域,再用Shift()
来移动柱子(是的,这个游戏不是小鸟在动,而是柱子在动!),如果柱子到了不可见区域,我们用CheckDisposeObject ()
将其放回对象池。
这样一来,我们就不必频繁地创建删除柱子了,也不需要创建数量庞大的柱子让他们动起来了。
这个项目里,另外一个我想讲得技术就是委托(delegate)。刚开始搞unity的时候,其实我是用unity自带的方法SendMessage
来进行脚本间的通信的,但根据大神陈嘉栋在《Unity 3D脚本编程:使用C#语言开发跨平台游戏》所说的,这个方法是基于C#的反射机制的,反射本来就消耗比较大;再者,反射调用的方法可能已经被删除,或者废弃,而这时在编译阶段无法抛出的错误,只有运行时才能发觉,却为时已晚了。所以,用委托来实现消息传递优于SendMessage
。
委托是什么?说白了委托就是一个可以存放函数的容器。我们知道变量是程序在内存中开辟的一块空间,用来储存数值或者某个对象的引用。而C#的委托则更进一步,将存储函数(function)变成了可能。更加详细的理论我就不说了,反正也说不好(⊙o⊙)…而且网上各种各样的技术博客已经把委托扒了个底朝天,几乎没有秘密可言了。。。这里我就来专注于如何应用吧。
首先,我们知道在游戏中小鸟穿过两根柱子之间的缝隙得一分,那么我们创建一个控制小鸟的类叫TapController
,里面除了有控制小鸟的逻辑,还有得分这一事件的发送机制。
public class TapController : MonoBehaviour
{
public delegate void PlayerDelegate();
public static event PlayerDelegate OnPlayerScored;
private void OnTriggerEnter2D(Collider2D other) {
if (other.gameObject.tag == "ScoreZone"){
OnPlayerScored();
}
}
}
event是基于delegate的,在这里直接用delegate也行,但event有一个好处,就是不能给它随便赋值,只能用+=
或者-=
为其赋值。强行用=
赋值则报错,像这样。
所以这边这个OnPlayerScored
既不能OnPlayerScored = null
,也不能OnPlayerScored = new event()
了。event
等于是限制了delegate的某些功能,可以更纯粹的实现消息传递机制。
接下来,由于我们的柱子prefab是这样制作的
在上柱与下柱的空隙有个gameObject
,上面挂了个box collider
,给它个tag
叫ScoreZone,利用unity的事件OnTriggerEnter2D
来确定小鸟穿过了柱子,这时就向外发消息(OnPlayerScored()
)说小鸟过了柱子,那么谁订阅了这个消息,谁就收到这个消息,并做一些逻辑处理(比如分数+1)。
我这里是用了另一个类叫GameManager
来订阅这个消息
public class GameManager : MonoBehaviour
{
private void OnEnable() {
TapController.OnPlayerScored += OnPlayerScored;
}
private void OnDisable() {
TapController.OnPlayerScored -= OnPlayerScored;
}
void OnPlayerScored(){
score++;
scoreText.text = score.ToString();
}
}
这个GameManager
在初始化的时候TapController.OnPlayerScored += OnPlayerScored;
订阅了TapController
那边发来的消息,OnPlayerScored()
这个方法用来处理这个消息所带来的逻辑,在这里就是分数往上加并显示出来。而到了GameManager
不存在的时候,我们退订这个消息,避免造成内存泄漏。
这样一路下来,一个消息机制我们就完成了。
当然,有人如果见过这些
public delegate TResult Func<in T,out TResult>(T arg);
public delegate void Action<in T>(T obj);
public delegate bool Predicate<in T>(T obj);
不要惊讶,是不是delegate里面还有什么其他东西。其实这些都是C#为我们在用delegate时提供的模板,有很多时候用delegate我们不需要自己全都写好,用这些模板就行了。其中Func是带返回值的delegate,参数最多可传入16个,Action是不带返回值的delegate,参数也是最多可传入16个,Predicate是只返回bool型返回值的delegate,传入参数只能1个。当然,不想用这些自己直接用delegate也没问题的,直接用delegate可以最多传入32个参数。
如果还有见过UnityAction
和UnityEvent
的话,其实他们也是delegate,不过是unity自己包装C#的delegate所弄出来的一套东西,其中UnityAction
和UnityEvent
都可以传入最多4个参数,并且
//因为UnityEvent<T0>是抽象类,所以需要声明一个类来继承它
public class MyEvent:UnityEvent<int>{}
//然后就可以用了
public MyEvent myEvent = new MyEvent();
还有就是UnityEvent
可以显示在Inspector上
delegate的大概介绍完了,具体的操作还是需要在实际项目中多磨炼。至于有人对Flappy Bird这个项目感兴趣,可以从下面的地址找到工程,慢慢研究。
参考
Develop and Publish Flappy Bird in 3 Hours With Unity3D
TappyBird
C# Event/UnityEvent辨析
Unity 对象池(Object Pooling)理解与简单应用
UnityAction和UnityEvent的用法详解
《Unity 3D脚本编程:使用C#语言开发跨平台游戏》