开发笔记 | 接口与抽象基类说明以及对象池的实现

最近笔者正在试图做一款塔防游戏的框架,如果说我们要实现子弹、敌人在生成时自带buff,类似于“元素抗性”效果,我们可以试着用装饰器来实现。

接口与抽象基类

在 C# 中,抽象基类(abstract class)和接口(interface)都可以作为实现多态、统一行为的方式。但为了知道什么时候该用那种方式,我在此简单总结一下。

这么说来,各种敌人类就可以通过抽象基类实现,而子弹的特殊效果则可以通过接口来实现。

这里贴上我的敌人类,我想让子类可以重写的,就用virtual方法,想让子类必须写的,就用abstract方法,想让所有类用默认方法的,直接写就行。

using UnityEngine;
public abstract class Enemy : MonoBehaviour
{
    EnemySpawn enemySpawn; //敌人生成器
    HealthBar healthBar; //血条
    float maxHealth = 100f; //最大血量
    bool isDead = false; //是否死亡
    //血量值
    public float health = 100;
    //移动速度
    public float speed = 2f;
    private void Awake()
    {
        //获取血条组件
        healthBar = GetComponentInChildren<HealthBar>();
    }
    //游戏对象生成时需要调整的功能
    public void GameObjectSpawn()
    {
        isDead = false;
    }
    //重置敌人状态(在对象池中用的到)
    public void GameObjectReset()
    {
        health = maxHealth; //重置血量
        healthBar.SetHealth(health / maxHealth); //更新血条显示
        //重置状态时,进行对象回收
        enemySpawn.ReturnEnemy(gameObject);
    }
    //获取血量值
    public float GetHealth() { 
        return health; 
    }
    //扣除血量值
    public void MinusHealth(int attack) {
        health -= attack;
        healthBar.SetHealth(health / maxHealth); //更新血条显示
        if (health <= 0)
        {
            isDead = true;
            gameObject.SetActive(false); //敌人死亡
            GameObjectReset(); //重置敌人状态
        }
    }
    //判断的依据可以在子类中重写
    //是否不再需要攻击
    public virtual bool NoMoreShotsNeeded() {
        return isDead; //如果血量小于等于0,则不再需要攻击
    }    
    //获取敌人游戏对象(……好像有点多此一举了)
    public GameObject GetGameObject()
    {
        return gameObject;
    }
    //设置敌人生成器
    public void SetEnemySpawn(EnemySpawn spawn)
    {
        enemySpawn = spawn;
    }
}

后面我会通过装饰器模式来进行对子弹buff的编写,这之中会呈现interface的使用。

对象池

众所周知,频繁调用Instantiate()和Destroy()会产生GC(垃圾回收)卡顿,同时Unity销毁延迟的特性会在大量对象同时销毁时造成严重性能问题。

而对象池技术,不仅实现原理十分简单,还通过控制对象的启用与禁用来复用对象,避免了反复的销毁与创建。

在塔防游戏中,我们常常会遇到一大批波次的敌人在同一段时间内生成,那么对象池就是其生成敌人的不二之选。 

对象池一般用Queue创建,其先进先出的天性适合这个。而List的随机访问特性就显得多余。

对象池通常由三个部分组成:初始化对象,获取对象,回收对象。

生成对象后默认置于禁用状态,并依次入队。

然后获取对象与回收对象的方法也编辑完成了。这时又有新的问题,在什么时机去回收对象?

常见的对象池往往会让对象的类本身来存放一段对象池的类,因为只有对象实例本身才能知晓自己需要被回收的时机。放在这里就是Enemy类需要有一个EnemySpawn字段,在对象初始化时同步赋值。另一种方法就是使用事件监听功能了,这样不会把敌人类和敌人生成类的逻辑交织在一起,但我现在不太会。

然后在生成敌人的上下文中调用对象池方法。

那么如何实现对象池的动态扩容呢?听起来什么“动态”,实际上就是在检测到没有可用对象时初始化若干个对象就行。

以下为敌人生成器的完整脚本。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
using static GlobalData;
public class EnemySpawn : MonoBehaviour
{
    float timer = 0f; //计时器
    int spawnedEnemyCount = 0; //已生成的敌人数量
    public GameObject enemyPrefab; //敌人预制体
    public int enemyNumber = 20; //最大敌人数量
    int poolSize = 10;   //对象池的容量
    public Tilemap roadTilemap;
    private Queue<GameObject> enemyPool; //敌人对象池
    [Range(0f, 5f)]public float spawnInterval = 1f; //生成间隔时间
    void Start()
    {
        // 初始化对象
        enemyPool = new Queue<GameObject>();
        for (int i = 0; i < poolSize; i++)
        {
            GameObject enemy = Instantiate(enemyPrefab);
            Enemy enemyScript = enemy.GetComponent<Enemy>();
            //这一步很重要
            enemy.GetComponent<Move>().roadTilemap = roadTilemap; //设置路径
            enemy.SetActive(false); //初始时不激活
            enemyScript.SetEnemySpawn(this); //设置生成器
            //依次加入对应数量的敌人
            enemyPool.Enqueue(enemy);
        }
    }
    //这种计时方法可以后续改进
    void Update()
    {
        timer += Time.deltaTime;
        if (timer <= spawnInterval) //每秒生成一个敌人
        {
            return;
        }
        if (spawnedEnemyCount < enemyNumber)
        {
            SpawnEnemy();
            spawnedEnemyCount++;
        }
        timer = 0f; //重置计时器
    }
    //获取对象
    public GameObject GetEnemy()
    {
        if(enemyPool.Count <= 0)
        {
            /*目前每次扩容4个*/
            ExpandPool(4);
        }
        //取敌人
        return enemyPool.Dequeue();
    }
    //对象池扩容
    void ExpandPool(int additionalSize)
    {
        for (int i = 0; i < additionalSize; i++)
        {
            //要和初始化时一模一样
            GameObject enemy = Instantiate(enemyPrefab);
            Enemy enemyScript = enemy.GetComponent<Enemy>();
            enemy.GetComponent<Move>().roadTilemap = roadTilemap; //设置路径
            enemy.SetActive(false); //初始时不激活
            enemyScript.SetEnemySpawn(this); //设置生成器
            enemyPool.Enqueue(enemy);
        }
    }
    //回收对象
    public void ReturnEnemy(GameObject enemy)
    {
        enemy.SetActive(false); //禁用敌人
        enemyPool.Enqueue(enemy); //重新加入队列
    }
    //生成敌人,同时是使用对象池的上下文
    void SpawnEnemy()
    {
        //从对象池获取一个敌人
        GameObject oneEnemy = GetEnemy(); 
        //设置生成位置
        oneEnemy.transform.position = transform.position; 
        //激活敌人
        oneEnemy.SetActive(true);
        //激活的同时调用想过方法
        oneEnemy.GetComponent<Enemy>().GameObjectSpawn();
        Enemy enemyScript = oneEnemy.GetComponent<Enemy>();
        if (!globalEnemies.Contains(enemyScript))
        {
            //添加到全局敌人列表
            globalEnemies.Add(enemyScript);
        }
    }
}

​​​​​​​总的来说,对象池是一种结构简单且极其实用的一个开发技巧,非常好!


小结

好的,这篇图文的重点放在了对象池技术上,简单易用,但是接口与抽象基类的应用却没有详细提及,我会在下次通过接口来实现游戏的相关功能,各位下次见~

如有补充纠正欢迎留言。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值