通过Flappy Bird在Unity中实践Object Pooling和Delegate技术

为了以防读者不明白Flappy Bird是什么游戏,先放出一张自制的flappy Bird小游戏来让大家有个印象。

Flappy Bird

这个游戏最大的特点就是有无限个上下对齐的柱子,玩家控制的小鸟必须从上下柱子的缝隙中穿过才算得分。当然,为了录屏我把小鸟调小了点,让我能多过了几根柱子O(∩_∩)O~

那么,在游戏制作过程中,我们自然不可能去Instantiate几百几千根柱子,并且,万一真有玩家达到了这个地步,我们难道再来Instantiate几百几千根柱子?显然,这是不可能的。所以,我们在这里需要对象池(Object Pooling)技术了。

对象池用于那些频繁创建销毁的object上,比如上述游戏的柱子,它从最右侧屏幕外生成出来,移动到最左侧,出屏幕后就销毁,这种情况非常适合使用对象池。

对象池原理很简单:

  1. 创建一个池子(比如一个List),将若干个object放在池子里
  2. 要使用object时从池子里拿出一个未被使用中的object,并标记为使用中
  3. 用完时不要销毁,将其标记为未使用并放回池子,为了不让玩家看到这个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是这样制作的

柱子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个参数。

如果还有见过UnityActionUnityEvent的话,其实他们也是delegate,不过是unity自己包装C#的delegate所弄出来的一套东西,其中UnityActionUnityEvent都可以传入最多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#语言开发跨平台游戏》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。