Unity《ATD》塔防RPG類3D遊戲架構設計(一)
目錄
- 《ATD》 遊戲簡介
- 《ATD》 整體結構
- 《ATD》 遊戲機制
- Buff機制
- Skill機制
- 結語
《ATD》 遊戲簡介
遊戲型別:塔防+RPG的3D遊戲
遊戲要素:3D 塔防 英雄 建築樹 搭配
主體玩法:遊戲裡將會有一波波怪物進攻基地。玩家可以建造塔來防禦敵人,同時也可以控制單獨的個體英雄角色來攻擊敵人。
遊戲模式:
- 第三人稱視角的RPG模式
- 上帝視角的建造模式
控制方式:在遊戲中使用Tab按鍵,切換這兩種操作模式:
- RPG模式下:WASD控制移動,Space跳躍,滑鼠左鍵普通攻擊。
- 建造模式下:滑鼠左鍵建造,E銷燬已建造的建築。
- 數字鍵1,2,3,4,5,6控制物品欄,對應英雄技能或者建築安放。
勝利條件:消滅所有敵人 或者 堅持到時間結束
失敗條件:基地生命值為0 或者 英雄死亡
《ATD》 整體結構
一般來說,整個Unity遊戲專案整體結構,我比較偏向分為如下5部分:
場景物件: 不會產生互動的可視物體物件,例如地型/建築/燈光。
遊戲物件: 參與互動的遊戲物件,例如英雄/怪物/塔。
遊戲邏輯: 負責控制遊戲的邏輯,其邏輯物件一般是單例的。
非遊戲性物件: 負責增強遊戲效果,但不是直接的遊戲邏輯,例如UI/HUD/特效/聲音。
工具: 負責輔助編碼,例如日誌工具,除錯工具。
在《ATD》遊戲專案裡,我是這樣設定遊戲物件目錄的:
注:“個體”在《ATD》裡的術語表示遊戲物件單位。
《ATD》 遊戲機制
通過分析《ATD》策劃案,確立了兩種基本遊戲機制:
- Buff機制
- Skill機制(技能機制)
Buff機制
和策劃商量後,策劃製作了下面一張含所有Buff屬性的Excel表:
由於策劃還沒想好Buff名字,直接套用裝備或者技能名字來命名Buff。
首先,使用了一個數據型別BuffData,用於完全對映Buff在表格的所有屬性:
public class BuffData { public int ID; public string Name; public int HpChange; //血量變化 public double HpChange_p; //血量百分比變化 public int AttackChange; //攻擊力變化 public double AttackChange_p; //攻擊力百分比變化 public double AttSpeedChange_p; //攻擊速度百分比變化 public double SpeedChange_p; //速度百分比變化 public int HpReturnChange; //血量恢復數值 public double HpReturnChange_p; //血量百分比恢復數值 public int AddReviveCount; //增加復活次數 public bool isDecelerate; //減速 public bool isVertigo; //眩暈 public bool isParalysis; //麻痺 public bool isSleep; //睡眠 public bool isBound; //束縛 public bool isBurn; //點燃 public bool isCharm; //魅惑 public bool isIncreaseAttSpeed; //攻速提高 public bool isPoisoning; //中毒 public bool isImmuneControl; //免疫控制 public bool isRevenge; //復仇 public bool isTaunt; //嘲諷 public bool isIncreaseHpReturn; //回血速度提高 public bool isIncreaseAttack; //攻擊力提高 }
然後我們就可以用一個List
//全域性單例類
public class BuffDataBase : MonoBehaviour
{
//讀取excel外掛生成的json檔案
public TextAsset BuffDataJson;
//儲存BuffData的列表
private List<BuffData> buffDatas;
//全域性單例實現
//...
//根據ID獲取相應的BuffData物件
public BuffData GetBuffData(int ID){
//...
}
}
為了表示遊戲物件動態得到/失去一個Buff而從BuffDataBase找到對應並拷貝一份BuffData物件/釋放掉一份BuffData物件顯然是不明智的。(BuffData所佔空間大,開銷大)
正確的做法應該是使用索引/引用的方式,例如某個遊戲物件持有3號索引,則表示它當前受一個3號Buff影響。
為了引入Buff的時間有效性,則進一步封裝索引,於是編寫了下面一個Buff類:
public class Buff
{
public int ID; //BuffData的ID(索引)
public double time; //持續時間
public int repeatCount; //重複次數
public bool isTrigger; //是否觸發型別
}
因為每個Buff的時間有效性都有所不同:有些Buff是一次性觸發Buff;也有一些是持續性Buff,持續N秒;還有一些是被動buff,永久生效。
所以我這裡就總結了個規則,Buff主要分為兩種型別:
- 持續型(Non-Trigger):開始對屬性造成生效影響一次,有效時間結束時造成失效影響一次。例如一段時間內增加攻速Buff
- 非持續型(Trigger):有效時間內,每一幀對屬性造成生效影響一次。例如一次性傷害Buff,光環Buff。
然後Buff的有效時間取決於2個屬性:
- 持續時間(time):每幀持續時間減少DeltaTime
- 觸發次數(repeatCount):每幀觸發次數減一
當一個Buff物件,持續時間 <= 0 並且 觸發次數為0,則應視為失效。特殊地,觸發次數為-1時,表示無限時間。
這樣Buff/BuffData/BuffDataBase基本構造就這樣了:
整個遊戲同種類Buff只用儲存一份BuffData;但是可以有很多個物件持有索引/引用,指向這個BuffData
遊戲物件持有Buff物件,通過BuffDataBase訪問BuffData的資料,然後利用這些資料對遊戲物件屬性造成影響
看到這裡,可能會有人想到前面有個問題:對於任意一種Buff,它往往有很多屬性是false或者0,使用這種完全對映會不會很影響空間佔用或者效率。
首先,空間佔用絕對不用擔心,因為前面BuffDataBase機制保證同種Buff只有唯一BuffData副本,其所有BuffData總共佔用量不過幾kb而已。
其次,至於效率,例如說某個Buff對某個遊戲物件造成影響,因為是完全對映,所以需要對該遊戲物件每個屬性都要進行更新,其實這也並不是太糟糕。而且只要遊戲物件有比較好的Buff計算方式,可以讓一個Buff物件的整個有效週期只對物件造成兩次影響計算(生效影響,失效影響),避免每幀出現影響多餘的計算,這樣就很不錯了。
Skill機制
可以說技能是我比較頭疼的部分。
看到那千奇百怪的Skill需求時,然後才總結出大概這幾個分類:
- 主動Buff技能 = 主動釋放,生成一個Buff
- 被動Buff技能 = 初始化時,生成一個Buff
- 召喚技能 = 生成一個遊戲物件
- 指向性技能 = 主動釋放,對鎖定的目標生成一個Buff
最後我決定使用繼承介面的方式來實現Skill:
技能介面類:
public interface ISkill
{
// 技能初始化介面
void InitSkill(Individual user);
// 使用技能介面
void ReleaseSkill(Individual user);
/// 技能每幀更新
void UpdateSkill(Individual user);
/// 技能是否冷卻
bool IsColdTimeEnd();
// 技能冷卻百分比
float GetColdTimePercent();
}
需要注意的一點是,技能並不是主動釋放時呼叫一個自定義的技能函式即可完事:
例如持續性的範圍技能,需要每幀呼叫散發Buff的函式。
所以一個ISkill物件 該有這3種重要的介面方法:初始化/主動釋放/每幀更新
下面是其中一個派生類的具體實現:
由於進度未完,目前只有兩個派生類:Buff技能類和召喚技能類。
Buff技能類暫時包含了ActiveBuff技能類和PassiveBuff技能類的功能。
// 示例:Buff技能類
public class BuffSkill : ISkill
{
public int buffID; //目的Buff
public bool isAura = true; //光環
public bool releasable = true; //是否主動釋放
public float range = 0.01f; //範圍
private float coldTime = 5.0f; //冷卻時間
private float timer = 5.0f; //冷卻計時
public BuffSkill(int buffID,bool releasable = true,bool isAura = true, float range = 0.01f)
{
this.buffID = buffID;
this.isAura = isAura;
this.range = range;
this.releasable = releasable;
}
public void InitSkill(Individual master)
{
if (!releasable && !isAura)
{
var individual = master.GetComponent<Individual>();
master.GetComponent<MessageSystem>().SendMessage(2, individual.ID,buffID);
}
}
public void ReleaseSkill(Individual master)
{
if (releasable && IsColdTimeEnd())
{
timer = 0.0f;
Factory.TraversalIndividualsInCircle(
(individual) => { master.GetComponent<MessageSystem>().SendMessage(2, individual.ID, buffID); }
, master.transform.position, range);
}
}
public void UpdateSkill(Individual master)
{
//增加計時
timer =Mathf.Min(timer+Time.deltaTime, coldTime+0.1f);
if (!releasable && isAura)
{
Factory.TraversalIndividualsInCircle(
(individual) => { master.GetComponent<MessageSystem>().SendMessage(2, individual.ID, buffID); }
, master.transform.position, range);
}
}
public float GetColdTimePercent()
{
if (!releasable) return 1.0f;
return timer / coldTime;
}
public bool IsColdTimeEnd()
{
return timer > coldTime;
}
}
派生類的建構函式很重要,這樣即使硬編碼了4個技能派生類,通過不同的資料引數傳入,也能產生更多不同的技能物件。
最後還應該再寫一個SkillDataBase全域性單例類,它負責讀取策劃寫的技能配置檔案,來初始化出來一些Skill物件,以供遊戲物件使用。
不過專案程式碼還沒寫完,因此目前是直接在SkillDataBase的初始化函式直接硬編碼3個技能。
//TODO
//目前硬編碼給玩家賦予3個技能
HeroSkills.Add(new BuffSkill(6, true, true, 5.0f)); //主動技能:嘲諷Buff
HeroSkills.Add(new BuffSkill(0, false, false)); //被動技能:回血buff
HeroSkills.Add(new BuffSkill(14, true, false)); //主動技能:攻速戒指buff
以後的話,SkillDataBase的初始化函式應該是讀取某種配置檔案,然後生成若干個對應的技能物件分配給遊戲物件使用:
結語
《ATD》只是社團部門內提出的一個遊戲專案,而我負責這個專案的程式架構設計,然而中途開發因為不少事,我們不得不放棄了這個專案。因此才想寫點東西總結一下開發這個專案時的經驗。
之後還會有新博文來更新這個系列,大概涉及《ATD》的遊戲物件模型,全域性遊戲邏輯,UI/HUD/特效/聲音管理,工具等,也同時會分享一些trick。