开发笔记 | 接口与抽象基类说明以及对象池的实现
最近笔者正在试图做一款塔防游戏的框架,如果说我们要实现子弹、敌人在生成时自带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);}}
}
总的来说,对象池是一种结构简单且极其实用的一个开发技巧,非常好!
小结
好的,这篇图文的重点放在了对象池技术上,简单易用,但是接口与抽象基类的应用却没有详细提及,我会在下次通过接口来实现游戏的相关功能,各位下次见~
如有补充纠正欢迎留言。