通过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#语言开发跨平台游戏》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343