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