Unity3D 實現簡單的Buff系統
今天來考慮一下,想要實現一個buff系統需要什麼功能。
能力不行,寫的不好,請大家指正完善,謝謝~~
在我們接到一個需求的時候,往往需要先分析一下需求,確定我們要實現什麼樣的功能,大部分的情況下需求功能由策劃提供,而你只需要考慮怎麼實現就行了。不過今天特殊,沒有策劃,只能我們自己分析考慮了。
更據以往玩過的遊戲,來看看buff系統都有什麼功能:
1.計時,一個buff往往都會有存在時間,幾秒,幾分,甚至幾天的存在
2.層級,有的buff只有一層,而有的buff可以擁有多層,效果疊加,有了層數,我們還需要考慮到,buff結束後是一次消除,還是逐層消除
3.次數,有的buff只會執行一次,而有的buff可以在一段時間內一直執行
4.間隔呼叫,有的buff比如加血,往往會1秒或兩秒鐘才會執行一次,其他時間是不會執行
5.行為操控,眩暈buff,擊飛buff 等都是對玩家行為進行一種操控,奪取玩家控制權。
根據以上結果,我們需要製作的功能基本上就很明確了:
1.時間控制
2.層級控制
3.執行次數
5.位移控制等操作
下面就開始書寫程式碼了
首先,我們先來實現一個配置類,這個類用來實現載入buff的配置表資訊
[System.Serializable]
public class BuffBase
{
/// <summary>
/// BuffID
/// </summary>
public int BuffID;
/// <summary>
/// Buff型別
/// </summary>
public BuffType BuffType;
/// <summary>
/// 執行此
/// </summary>
public BuffCalculateType BuffCalculate = BuffCalculateType.Loop;
/// <summary>
/// 疊加型別
/// </summary>
public BuffOverlap BuffOverlap = BuffOverlap.StackedLayer;
/// <summary>
/// 消除型別
/// </summary>
public BuffShutDownType BuffShutDownType = BuffShutDownType.All;
/// <summary>
/// 如果是堆疊層數,表示最大層數,如果是時間,表示最大時間
/// </summary>
public int MaxLimit = 0;
/// <summary>
/// 執行時間
/// </summary>
public float Time = 0;
/// <summary>
/// 間隔時間
/// </summary>
public float CallFrequency = 1;
/// <summary>
/// 執行數值 比如加血就是每次加多少
/// </summary>
public float Num;
}
接下來是上面使用的幾個列舉型別
/// <summary>
/// buff型別
/// </summary>
public enum BuffType
{
/// <summary>
/// 恢復HP
/// </summary>
AddHp,
/// <summary>
/// 增加最大血量
/// </summary>
AddMaxHp,
/// <summary>
/// 減血
/// </summary>
SubHp,
/// <summary>
/// 減最大生命值
/// </summary>
SubMaxHp,
/// <summary>
/// 眩暈
/// </summary>
AddVertigo,
/// <summary>
/// 被擊浮空
/// </summary>
AddFloated,
/// <summary>
/// 擊退
/// </summary>
AddRepel,
/// <summary>
/// 衝刺
/// </summary>
AddSprint,
/// <summary>
/// 被擊浮空
/// </summary>
AddDamageFloated,
/// <summary>
/// 新增忽略重力
/// </summary>
AddIsIgnoreGravity,
}
/// <summary>
/// 疊加型別
/// </summary>
public enum BuffOverlap
{
None,
/// <summary>
/// 增加時間
/// </summary>
StackedTime,
/// <summary>
/// 堆疊層數
/// </summary>
StackedLayer,
/// <summary>
/// 重置時間
/// </summary>
ResterTime,
}
/// <summary>
/// 關閉型別
/// </summary>
public enum BuffShutDownType
{
/// <summary>
/// 關閉所有
/// </summary>
All,
/// <summary>
/// 單層關閉
/// </summary>
Layer,
}
/// <summary>
/// 執行型別
/// </summary>
public enum BuffCalculateType
{
/// <summary>
/// 一次
/// </summary>
Once,
/// <summary>
/// 每次
/// </summary>
Loop,
}
BuffType列舉,是我們用來控制buff型別的,比如眩暈,比如增加HP,比如減少HP等等…
BuffOverlap列舉,就是疊加型別,上面我們說,有的buff是疊加層數,有的buff是重置時間,或者增加時間。
BuffShutDownType列舉,這個列舉的作用是控制倒計時結束後,應該是減一層,還是直接清空buff
然後我們建立一個BuffManager類,用來配置我們buff屬性:
public class BuffManager : MonoBehaviour {
private static BuffManager _instance;
public static BuffManager Instance
{
get { return _instance; }
}
public List<BuffBase> buffBase = new List<BuffBase>();
}
因為我們還沒有配置表,所以在檢視面板中直接填寫資料就行了
buff資料有了,我們還需要提供buff的執行方法吧,就是我使用這個buff後這個buff要做什麼,是我們需要知道的。
這就需要我們在寫一個類,實現buff 執行中的功能。
buff使用用到角色身上的,所以我們寫一個指令碼ActorBuff 掛載到角色身上。
這個類的最主要功能:
1.新增buff
2.執行buff
3.buff移除buff
我們先來看看實現:
public class ActorBuff : ActorCompent {
[SerializeField]
private List<BuffData> buffs = new List<BuffData>();
public override void OnInit()
{
InitBuff();
}
void InitBuff()
{
}
void Update()
{
}
/// <summary>
/// 執行buff
/// </summary>
void FixedUpdate()
{
for (int i = buffs.Count - 1; i >= 0; i--)
{
buffs[i].OnTick(Time.deltaTime);
if (buffs[i].IsFinsh)
{
buffs[i].CloseBuff();
buffs.Remove(buffs[i]);
}
}
}
/// <summary>
/// 新增buff
/// </summary>
/// <param name="buffData"></param>
public void AddBuff(BuffData buffData)
{
if (!buffs.Contains(buffData))
{
buffs.Add(buffData);
buffData.StartBuff();
}
}
/// <summary>
/// 移除buff
/// </summary>
/// <param name="buffDataID"></param>
public void RemoveBuff(int buffDataID)
{
BuffData bd = GetBuff(buffDataID);
if(bd!=null)
bd.CloseBuff();
}
/// <summary>
/// 移除buff
/// </summary>
/// <param name="buffData"></param>
public void RemoveBuff(BuffData buffData)
{
if (buffs.Contains(buffData))
{
buffData.CloseBuff();
}
}
/// <summary>
/// 獲取buff
/// </summary>
/// <param name="buffDataID"></param>
/// <returns></returns>
public BuffData GetBuff(int buffDataID)
{
for (int i = 0; i < buffs.Count; i++)
{
if (buffs[i].buffDataID == buffDataID)
return buffs[i];
}
return null;
}
/// <summary>
/// 獲取buff
/// </summary>
/// <param name="buffBaseID"></param>
/// <returns></returns>
public BuffData GetBuffByBaseID(int buffBaseID)
{
for (int i = 0; i < buffs.Count; i++)
{
if (buffs[i].BuffID == buffBaseID)
return buffs[i];
}
return null;
}
/// <summary>
/// 獲取buff
/// </summary>
/// <param name="buffType"></param>
/// <returns></returns>
public BuffData[] GetBuff(BuffType buffType)
{
List<BuffData> buffdatas = new List<BuffData>();
for (int i = 0; i < buffs.Count; i++)
{
if (buffs[i].BuffType == buffType)
buffdatas.Add(buffs[i]);
}
return buffdatas.ToArray();
}
/// <summary>
/// 執行buff
/// </summary>
/// <param name="buffID"></param>
public void DoBuff(int buffID)
{
BuffManager.Instance.DoBuff(Actor,buffID);
}
}
正如上面所說,這個類擁有這幾個方法:
FixedUpdate()
這個方法中用來實現buff的執行,因為我寫的buff有牽扯到位移,所以我放到FixedUpdate中了
DoBuff(int buffID)
這個方法是新增buff 的開端,他會呼叫BuffManager.Instance.DoBuff(Actor,buffID)
方法,將自己和要新增buff傳送過去,在BuffManager
中解析完buff資料後會生成BuffData
資料,然後呼叫 AddBuff(BuffData buffData)
方法,將buff新增到buffs
列表中
AddBuff(BuffData buffData)
正如上面所說 BuffManager
解析完buff資料後會生成BuffData
呼叫進行新增
RemoveBuff
buff執行完畢後移除buff
RemoveBuff
同上
GetBuffByBaseID(int buffBaseID)
,GetBuff(int buffDataID)
,GetBuff(BuffType buffType)
三個方法都是獲取buffdata,不過所需資料不同,其中GetBuffByBaseID
是根據BuffBase
中的BuffID
獲取配置表的,而GetBuff
是根據BuffData
類自己生成的ID來獲取,GetBuff(BuffType buffType)
就是根據buff型別來獲取BuffData
了
在上面的型別呼叫了BuffManager的DoBuff(Actor,buffID)
方法,我們沒有實現,我們先寫個空方法來佔個坑。在BuffManager
寫入兩個方法:
/// <summary>
/// 執行buff
/// </summary>
/// <param name="actor"></param>
/// <param name="buffID"></param>
public void DoBuff(Actor actor,int buffID)
{
DoBuff(actor,GetBuffBase(buffID));
}
/// <summary>
/// 執行buff
/// </summary>
/// <param name="actor"></param>
/// <param name="buff"></param>
public void DoBuff(Actor actor, BuffBase buff)
{
}
功能先不實現,現在我們需要著重的來看一下BuffData
類的實現,這個類是執行中的buff,主要的buff功能均在此類中實現。
寫這個類的時候比較蛋疼的是當初寫的不知道怎麼想的把BuffBase中的屬性在此類中基本上都寫了一遍,其實只要把BuffBase引用過來就行了,不過要過年了我先不該了,你們看的時候知道這個問題就行了。
因為這個BuffData的建立頻率應該比較高,因為新增buff的時候就需要建立一個,使用完了就銷燬,所以為了避免buffdata的重建,我們需要將失效buffdata存起來,後面在需要的時候重置裡面的資料,再次使用。
[Serializable]
public class BuffData
{
/// <summary>
/// 快取棧
/// </summary>
private static Stack<BuffData> poolCache = new Stack<BuffData>();
/// <summary>
/// BuffData下一個ID
/// </summary>
public static int buffIndex { get; private set; }
/// <summary>
/// 構造方法
/// </summary>
private BuffData() {
buffDataID = buffIndex++;
}
/// <summary>
/// 建立BuffData
/// </summary>
/// <returns></returns>
public static BuffData Create()
{
if (poolCache.Count < 1)
return new BuffData();
BuffData buffData = poolCache.Pop();
return buffData;
}
/// <summary>
/// 彈出
/// </summary>
/// <returns></returns>
private static BuffData Pop()
{
if (poolCache.Count < 1)
{
BuffData bd = new BuffData();
return bd;
}
BuffData buffData = poolCache.Pop();
return buffData;
}
/// <summary>
/// 壓入
/// </summary>
/// <param name="buffData"></param>
private static void Push(BuffData buffData)
{
poolCache.Push(buffData);
}
}
這樣,我們在建立BuffData
的時候通過BuffData.Create()
方法建立,它會先去poolCache
快取池中去檢視有沒有空閒的BuffData
,如果有,就取出來使用,沒有就進行建立。
按照邏輯上來說,萬物有始有終,所以我們需要在BuffData
中新增兩個方法,即開始方法StartBuff()
和結束方法CloseBuff()
:
/// <summary>
/// 開始Buff
/// </summary>
public void StartBuff()
{
}
/// <summary>
/// 關閉buff
/// </summary>
public void CloseBuff()
{
}
但是別忘了,我們還需要一箇中間不斷執行的方法OnTick(float delayTime)
:
/// <summary>
/// 執行中
/// </summary>
public void OnTick(float delayTime)
{
}
這樣,我們開始,執行中,結束方法都有了,感覺主題框架搭完了,我們得開始寫實際功能了。
首先,我們需要先實現類的屬性,另外,我們還想在buff執行時候傳送一個事件,和結束的時候傳送一個事件給我們,所以我們還需要定義幾個事件
/// <summary>
/// ID
/// </summary>
public int buffDataID;
/// <summary>
/// 配置表ID
/// </summary>
public int BuffID;
/// <summary>
/// buff型別
/// </summary>
public BuffType BuffType;
/// <summary>
/// 疊加型別
/// </summary>
public BuffOverlap BuffOverlap = BuffOverlap.StackedLayer;
/// <summary>
/// 執行次數
/// </summary>
public BuffCalculateType BuffCalculate = BuffCalculateType.Loop;
/// <summary>
/// 關閉型別
/// </summary>
public BuffShutDownType BuffShutDownType = BuffShutDownType.All;
/// <summary>
/// 最大限制
/// </summary>
public int MaxLimit;
/// <summary>
/// 當前資料
/// </summary>
[SerializeField]
private int _Limit;
public int GetLimit { get { return _Limit; } }
/// <summary>
/// 執行時間
/// </summary>
[SerializeField]
private float PersistentTime;
public float GetPersistentTime { get { return PersistentTime; } }
/// <summary>
/// 當前時間
/// </summary>
[SerializeField]
private float _CurTime;
/// <summary>
/// 呼叫頻率
/// </summary>
public float CallFrequency { get; set; }
/// <summary>
/// 當前時間
/// </summary>
private float _curCallFrequency { get; set; }
/// <summary>
/// 執行次數
/// </summary>
[SerializeField]
private int index = 0;
/// <summary>
/// 開始呼叫
/// </summary>
public Action OnStart;
/// <summary>
/// 結束呼叫
/// </summary>
public Action OnFinsh;
上面很多屬性其實不需要重複的寫,注意了直接引用BuffBase的例項即可,但是以下劃線開頭的屬性,是需要在這個類中定義的,他們控制時間,當前數量等資料。
我們構造方法中最好重置一個部分屬性的值,看起來向這樣:
/// <summary>
/// 構造方法
/// </summary>
private BuffData() {
buffDataID = buffIndex++;
CallFrequency = 1;
PersistentTime = 0;
}
然後我們開始寫開始方法:
/// <summary>
/// 開始Buff
/// </summary>
public void StartBuff()
{
//ChangeLimit(MaxLimit);
_isFinsh = false;
if (OnStart != null)
OnStart();
}
首先我們要確保_isFinsh = false,確保我們buff尚未結束,其次,就是傳送事件了,通知我們buff要開始運行了。
其次,我們寫結束方法:
/// <summary>
/// 關閉buff
/// </summary>
public void CloseBuff()
{
if (OnFinsh != null)
OnFinsh();
Clear();
}
//重置資料
public void Clear()
{
_Limit = 0;
BuffID = -1;
index = 0;
PersistentTime = 0;
_CurTime = 0;
Data = null;
CallFrequency = 0;
_curCallFrequency = 0;
//OnCallBackParam = null;
//OnCallBack = null;
OnStart = null;
OnFinsh = null;
_isFinsh = false;
Push(this);
}
同樣,傳送結束事件,其次,呼叫Clear()
方法重置部分屬性值。最後別忘了Push(this)
,將此類插入快取中。
另外,我們buff疊加方式,有所不同,所以我們想在一些特殊的情況下通知一個事件,所以我在BuffData
又添加了以下幾個事件:
/// <summary>
/// 根據 CallFrequency 間隔 呼叫 結束時會呼叫一次 會傳遞 Data資料
/// </summary>
public Action<object> OnCallBackParam;
/// <summary>
/// /// <summary>
/// 根據 CallFrequency 間隔 呼叫 結束時會呼叫一次 會傳遞 Data資料 int 次數
/// </summary>
/// </summary>
public Action<object, int> OnCallBackParamIndex;
/// <summary>
/// 根據 CallFrequency 間隔 呼叫 結束時會呼叫一次
/// </summary>
public Action OnCallBack;
/// <summary>
/// 根據 CallFrequency 間隔 呼叫 結束時會呼叫一次 int 次數
/// </summary>
public Action<int> OnCallBackIndex;
/// <summary>
/// 當改變時間
/// </summary>
public Action<BuffData> OnChagneTime;
/// <summary>
/// 當新增層
/// </summary>
public Action<BuffData> OnAddLayer;
/// <summary>
/// 當刪除層
/// </summary>
public Action<BuffData> OnSubLayer;
載入上面OnFinsh
事件下就行了。
剩下的就是需要實現OnTick
方法了:
/// <summary>
/// 執行中
/// </summary>
/// <param name="delayTime"></param>
public void OnTick(float delayTime)
{
_CurTime += delayTime;
//判斷時間是否結束
if (_CurTime >= PersistentTime)
{
///呼叫事件
CallBack();
//判斷結束型別 為層方式
if (BuffShutDownType == BuffShutDownType.Layer)
{
SubLayer();
//判讀層數小於1 則結束
if (_Limit <= 0)
{
_isFinsh = true;
return;
}
//重置時間
_curCallFrequency = 0;
_CurTime = 0;
return;
}
_isFinsh = true;
return;
}
//如果是按頻率呼叫
if (CallFrequency > 0)
{
_curCallFrequency += delayTime;
if (_curCallFrequency >= CallFrequency)
{
_curCallFrequency = 0;
CallBack();
}
return;
}
///呼叫回撥
CallBack();
}
/// <summary>
/// 呼叫回撥
/// </summary>
private void CallBack()
{
//次數增加
index++;
//判斷buff執行次數
if (BuffCalculate == BuffCalculateType.Once)
{
if (index > 1) { index = 2; return; }
}
if (OnCallBack != null)
OnCallBack();
if (OnCallBackIndex != null)
OnCallBackIndex(index);
if (OnCallBackParam != null)
OnCallBackParam(Data);
if (OnCallBackParamIndex != null)
OnCallBackParamIndex(Data, index);
}
/// <summary>
/// 加一層
/// </summary>
public void AddLayer()
{
_Limit++;
_CurTime = 0;
if (_Limit > MaxLimit)
{
_Limit = MaxLimit;
return;
}
if (OnAddLayer != null)
OnAddLayer(this);
}
/// <summary>
/// 減一層
/// </summary>
public void SubLayer()
{
_Limit--;
if (OnSubLayer != null)
OnSubLayer(this);
}
/// <summary>
/// 重置時間
/// </summary>
public void ResterTime()
{
_CurTime = 0;
}
/// <summary>
/// 修改 時間
/// </summary>
/// <param name="time"></param>
public void ChangePersistentTime(float time)
{
PersistentTime = time;
if (PersistentTime >= MaxLimit)
PersistentTime = MaxLimit;
if (OnChagneTime != null)
OnChagneTime(this);
}
主要流程控制是在Tick
方法中執行的。而主要的邏輯操作是在提供的幾個事件中進行編輯。這個先不急,我們再在BuffData
中新增幾個方法:
/// <summary>
/// 獲取當前執行時間
/// </summary>
public float GetCurTime
{
get { return _CurTime; }
}
/// <summary>
/// 是否結束
/// </summary>
public bool IsFinsh
{
get { return _isFinsh; }
}
根據需求,我們在寫幾個建立方法,和建構函式:
private BuffData(float persistentTime,Action onCallBack)
{
PersistentTime = persistentTime;
OnCallBack = onCallBack;
buffDataID = buffIndex++;
}
public static BuffData Create(BuffBase buffBase,Action onCallBack)
{
return Create(buffBase,onCallBack,null,null);
}
public static BuffData Create(BuffBase buffBase, Action onCallBack,Action<BuffData> addLayerAcion,Action<BuffData> subLayerAction)
{
BuffData db = Pop();
db.BuffCalculate = buffBase.BuffCalculate;
db.BuffID = buffBase.BuffID;
db.CallFrequency = buffBase.CallFrequency;
db.PersistentTime = buffBase.Time;
db.BuffOverlap = buffBase.BuffOverlap;
db.BuffShutDownType = buffBase.BuffShutDownType;
db.BuffType = buffBase.BuffType;
db.MaxLimit = buffBase.MaxLimit;
db.OnCallBack = onCallBack;
db.OnAddLayer = addLayerAcion;
db.OnSubLayer = subLayerAction;
db._Limit = 1;
return db;
}
以上只是更據需求新增就行了,不是必要的。
至此,我們BuffData
列結束了。
接下來,Buff的流程控制有,我們還需要具體的執行方法,還記得上面提供的幾個事件嗎?在事件通知的時候我們進行邏輯操作就行了。
所以我們需要完善一下BuffManager類。
首先,我們在上面的BuffManager類中添加了一個方法DoBuff(Actor actor, BuffBase buff)
,如果忘記了可以翻上去看看。然後我們只實現了方法,還沒實現功能,接下來我們就需要實現次數功能了。
它看起像這樣:
/// <summary>
/// 執行buff
/// </summary>
/// <param name="actor"></param>
/// <param name="buff"></param>
public void DoBuff(Actor actor, BuffBase buff)
{
if (buff == null) return;
BuffData db = null;
switch (buff.BuffType)
{
case BuffType.AddHp: //增加血量
if (!IsAdd(actor, buff))
{
db = BuffData.Create(buff, delegate
{
actor.ActorAttr.AddHP((int)buff.Num);
});
}
break;
case BuffType.AddMaxHp: //增加最大血量
if (!IsAdd(actor, buff))
{
db = BuffData.Create(buff, delegate
{
actor.ActorAttr.AddMaxHP((int)buff.Num);
}, delegate {
actor.ActorAttr.AddMaxHP((int)buff.Num);
}, delegate {
actor.ActorAttr.SubMaxHp((int)buff.Num);
});
}
break;
case BuffType.SubHp: //減少血量
if (!IsAdd(actor, buff))
{
db = BuffData.Create(buff, delegate
{
actor.ActorAttr.SubHp((int)buff.Num);