傳回總目錄
第九章 戰鬥系統(Combat System)
在SRPG中,大多數情況是指角色與角色之間的戰鬥。而這種戰鬥一般有兩種模式:
- 地圖中直接戰鬥;
- 有專門的戰鬥場景。
這兩種模式的戰鬥在資料上沒有任何差別,隻有戰鬥動畫的差別。
就像之前描述的,SRPG也是RPG,是以戰鬥方式和回合制RPG的戰鬥幾乎是相同的。主要差別是RPG每個回合需要手動操作(也有自動戰鬥的),直到戰鬥結束;而SRPG是自動戰鬥,且多數情況隻有一個回合(額外回合也是由于技能、物品或劇情需要)。且RPG多數是多人戰鬥,而SRPG多數是每邊就一個。
我們這一章就來寫一個戰鬥系統。
文章目錄
- 第九章 戰鬥系統(Combat System)
-
- 四 計算戰鬥資料II(Calculate Combat Data II)
-
- 1 分離每一次行動(Battle Action)
- 2 準備戰鬥(Prepare Battle)
- 3 計算戰鬥(Calculate Battle)
- 4 說明與測試(Description and Test)
四 計算戰鬥資料II(Calculate Combat Data II)
在之前,我們隻計算了一般情況下的戰鬥資料,這一節,我們來擴充它,讓它可以計算其它種類的資料。
1 分離每一次行動(Battle Action)
在計算中,我們曾經:
-
中計算了準備階段的資料;public void BattleBegin()
-
中計算了攻擊階段的資料。private void CalcBattle(CombatVariable atkVal, CombatVariable defVal)
我們将這些計算方式分離出
Combat
,單獨建立一個類來存儲這些方法。
建立類
BattleAction
:
using UnityEngine;
namespace DR.Book.SRPG_Dev.CombatManagement
{
public enum BattleActionType
{
Unknow,
Prepare,
Attack,
MageAttack,
Heal,
// 其它自定義類型
}
public abstract class BattleAction : ScriptableObject
{
private string m_Message = "Unknow battle message.";
public string message
{
get { return m_Message; }
protected set { m_Message = value; }
}
public abstract BattleActionType actionType { get; }
public abstract CombatStep CalcBattle(Combat combat, CombatVariable atkVal, CombatVariable defVal);
public abstract bool IsBattleEnd(Combat combat, CombatVariable atkVal, CombatVariable defVal);
public sealed override string ToString()
{
return m_Message;
}
}
}
而在
Combat
中,我們隻需要調用這個類就可以了。
為了能更好的控制它們,首先需要存儲它們,在
Combat
中添加:
public BattleAction[] m_BattleActions;
private Dictionary<BattleActionType, BattleAction> m_BattleActionDict = new Dictionary<BattleActionType, BattleAction>();
private void Awake()
{
if (m_BattleActions != null && m_BattleActions.Length > 0)
{
for (int i = 0; i < m_BattleActions.Length; i++)
{
if (m_BattleActions[i] == null)
{
continue;
}
BattleAction action = m_BattleActions[i];
if (m_BattleActionDict.ContainsKey(action.actionType))
{
Debug.LogWarningFormat(
"Battle Action {0} is exist. OVERRIDE.",
action.actionType.ToString());
}
m_BattleActionDict[action.actionType] = action;
}
}
unit0 = new CombatUnit(0);
unit1 = new CombatUnit(1);
steps = new List<CombatStep>();
}
2 準備戰鬥(Prepare Battle)
在
public void BattleBegin()
中,我們之前是計算了戰鬥開始的準備階段。
為了分離它們,我們先建立
PrepareAction
:
using UnityEngine;
namespace DR.Book.SRPG_Dev.CombatManagement
{
using DR.Book.SRPG_Dev.Models;
[CreateAssetMenu(fileName = "CombatPrepareAction.asset", menuName = "SRPG/Combat Prepare Action")]
public class PrepareAction : BattleAction
{
public override BattleActionType actionType
{
get { return BattleActionType.Prepare; }
}
public override CombatStep CalcBattle(Combat combat, CombatVariable atkVal, CombatVariable defVal)
{
CombatUnit atker = combat.GetCombatUnit(0);
CombatUnit defer = combat.GetCombatUnit(1);
// 防守者是否可反擊
bool canDeferAtk = false;
if (defer.role.equipedWeapon != null)
{
Vector3Int offset = defer.mapClass.cellPosition - atker.mapClass.cellPosition;
int dist = Mathf.Abs(offset.x) + Mathf.Abs(offset.y);
WeaponUniqueInfo defInfo = defer.role.equipedWeapon.uniqueInfo;
// 如果在反擊範圍内
if (dist >= defInfo.minRange && dist <= defInfo.maxRange)
{
canDeferAtk = true;
}
}
// 根據速度初始化攻擊者與防守者
if (canDeferAtk)
{
if (atker.speed < defer.speed)
{
CombatUnit tmp = atker;
atker = defer;
defer = tmp;
}
}
// 更新資訊
this.message = "戰鬥開始";
atkVal = new CombatVariable(atker.position, atker.hp, atker.mp, true, atker.durability, CombatAnimaType.Prepare);
defVal = new CombatVariable(defer.position, defer.hp, defer.mp, canDeferAtk, defer.durability, CombatAnimaType.Prepare);
// 準備階段
CombatStep firstStep = new CombatStep(atkVal, defVal);
return firstStep;
}
public override bool IsBattleEnd(Combat combat, CombatVariable atkVal, CombatVariable defVal)
{
return false;
}
}
}
這和之前計算過程唯一的差別就是新增了
message
,它是戰鬥中顯示的文字,不是必須的。
而在
public void BattleBegin()
中,由于已經分離了方法,是以修改後:
/// <summary>
/// 開始戰鬥
/// </summary>
public void BattleBegin()
{
if (!isLoaded)
{
Debug.LogError("Combat -> StartBattle: please load combat unit first.");
return;
}
if (stepCount > 0)
{
Debug.LogError("Combat -> StartBattle: battle is not end.");
return;
}
BattleAction action;
if (!m_BattleActionDict.TryGetValue(BattleActionType.Prepare, out action))
{
Debug.LogError("Combat -> StartBattle: BattleActionType.Prepare is not found, check the code.");
return;
}
// 準備階段
CombatStep firstStep = action.CalcBattle(this, default(CombatVariable), default(CombatVariable));
steps.Add(firstStep);
if (!action.IsBattleEnd(this, firstStep.atkVal, firstStep.defVal))
{
CalcBattle(firstStep.atkVal, firstStep.defVal);
}
}
3 計算戰鬥(Calculate Battle)
在
private void CalcBattle(CombatVariable atkVal, CombatVariable defVal)
中,我們之前隻有計算普通攻擊的方法。而現在,由于計算資料的多樣化,我們需要判斷使用哪種計算方式。
在遊戲中,我們計算資料的方式由武器決定(當然你可以添加其它參數控制):
/// <summary>
/// 擷取行動方式(如何計算戰鬥資料)
/// </summary>
/// <param name="weaponType"></param>
/// <returns></returns>
private BattleActionType GetBattleActionType(WeaponType weaponType)
{
// TODO 由于沒有動畫支援,是以并沒有其他武器
// 你可以添加其他武器到這裡
switch (weaponType)
{
case WeaponType.Sword:
//case WeaponType.Lance:
//case WeaponType.Axe:
//case WeaponType.Bow:
return BattleActionType.Attack;
//case WeaponType.Staff:
//if ( 如果法杖是治療 )
//{
// return BattleActionType.Heal;
//}
//else if ( 法杖是其它等 )
//{
// return BattleActionType.自定義類型;
//}
//case WeaponType.Fire:
//case WeaponType.Thunder:
//case WeaponType.Wind:
//case WeaponType.Holy:
//case WeaponType.Dark:
// return BattleActionType.MageAttack;
default:
return BattleActionType.Unknow;
}
}
我們根據武器類型判斷了執行的計算方法,你也可以在物品或技能中加入參數,選擇使用哪種計算方法。
這樣,我們計算方法:
/// <summary>
/// 計算戰鬥資料
/// </summary>
private void CalcBattle(CombatVariable atkVal, CombatVariable defVal)
{
CombatUnit atker = GetCombatUnit(atkVal.position);
BattleActionType actionType = GetBattleActionType(atker.weaponType);
BattleAction action;
if (!m_BattleActionDict.TryGetValue(actionType, out action))
{
Debug.LogErrorFormat(
"Combat -> StartBattle: BattleActionType.{0} is not found, check the code.",
actionType.ToString());
return;
}
CombatStep step = action.CalcBattle(this, atkVal, defVal);
steps.Add(step);
// 如果戰鬥沒有結束,交換攻擊者與防守者
if (!action.IsBattleEnd(this, step.atkVal, step.defVal))
{
if (step.defVal.canAtk)
{
CalcBattle(step.defVal, step.atkVal);
}
else
{
// 如果防守方不可反擊
defVal = step.defVal;
defVal.action = true;
if (!action.IsBattleEnd(this, defVal, step.atkVal))
{
CalcBattle(step.atkVal, defVal);
}
}
}
else
{
// TODO 如果死亡,播放死亡動畫(我把死亡動畫忘記了)
// if (step.defVal.isDead) 播放死亡動畫
}
}
在之前的
private void CalcBattle(CombatVariable atkVal, CombatVariable defVal)
中,我們計算了普通攻擊方式的計算,還需要移植到新的方式中。
建立
AttackAction
:
using UnityEngine;
namespace DR.Book.SRPG_Dev.CombatManagement
{
[CreateAssetMenu(fileName = "CombatAttackAction.asset", menuName = "SRPG/Combat Attack Action")]
public class AttackAction : BattleAction
{
public sealed override BattleActionType actionType
{
get { return BattleActionType.Attack; }
}
public override CombatStep CalcBattle(Combat combat, CombatVariable atkVal, CombatVariable defVal)
{
CombatUnit atker = combat.GetCombatUnit(atkVal.position);
CombatUnit defer = combat.GetCombatUnit(defVal.position);
atkVal.animaType = CombatAnimaType.Attack;
// 真實命中率 = 攻擊者命中 - 防守者回避
int realHit = atker.hit - defer.avoidance;
// 機率是否擊中
int hitRate = UnityEngine.Random.Range(0, 100);
bool isHit = hitRate <= realHit;
if (isHit)
{
bool crit = false; // TODO 是否爆擊
int realAtk = atker.atk;
///
// TODO 觸發傷害技能
// 這裡寫觸發技能後傷害變化(比如武器特效等),
// 或者觸發某些狀态(比如中毒等)
//
if (crit)
{
realAtk *= 2; // 假定爆擊造成雙倍傷害
}
// 掉血 = 攻擊者攻擊力 - 防守者防禦力
// 最少掉一滴血
int damageHp = Mathf.Max(1, realAtk - defer.def);
if (damageHp > defVal.hp)
{
damageHp = defVal.hp;
}
defVal.hp = defVal.hp - damageHp;
atkVal.crit = crit;
defVal.animaType = CombatAnimaType.Damage;
// 更新此次攻擊資訊
this.message = string.Format(
"{0} 對 {1} 的攻擊造成了 {2} 點傷害{3}。",
atker.role.character.info.name,
defer.role.character.info.name,
damageHp,
crit ? "(爆擊)" : string.Empty);
if (defVal.isDead)
{
this.message += string.Format(" {0}被擊敗了。", defer.role.character.info.name);
}
}
else
{
defVal.animaType = CombatAnimaType.Evade;
// 更新此次躲閃資訊
this.message = string.Format(
"{1} 躲閃了 {0} 的攻擊。",
atker.role.character.info.name,
defer.role.character.info.name);
}
// 隻有玩家才會減低耐久度
if (atker.role.attitudeTowards == AttitudeTowards.Player)
{
// 攻擊者武器耐久度-1
atkVal.durability = Mathf.Max(0, atkVal.durability - 1);
}
// 攻擊者行動過了
atkVal.action = true;
CombatStep step = new CombatStep(atkVal, defVal);
return step;
}
public override bool IsBattleEnd(Combat combat, CombatVariable atkVal, CombatVariable defVal)
{
// 防守者死亡
if (defVal.isDead)
{
return true;
}
// 如果防守者行動過了
if (defVal.action)
{
//CombatUnit atker = GetCombatUnit(atkVal.position);
//CombatUnit defer = GetCombatUnit(defVal.position);
// TODO 是否繼續攻擊,必要時需要在 CombatVariable 加入其它控制變量
// 比如,觸發過技能或物品了
// atker.role.skill/item 包含繼續戰鬥的技能或物品
// defer.role.skill/item 包含繼續戰鬥的技能或物品
//if ( 觸發繼續戰鬥 )
//{
// // return false;
//}
return true;
}
return false;
}
}
}
4 說明與測試(Description and Test)
我們在之前已經完全分離了計算方式,你可以自己建立各種各樣的計算方法。
你也可以添加額外的變量來計算它們,例如狀态屬性:石化,睡眠,沉默等等(在
Role
中添加狀态屬性,在《FE4》中有對應法杖與特效武器)。
而我把
BattleAction
繼承自
ScriptableObject
是因為,你可以在每個地圖能夠使用不同的計算方式(比如不同地圖使用不同的
BattleActionType.Attack
的Asset)。
這以上種種由創造力策劃決定。
說道這裡,我們來測試它們是否工作正常:
- 建立Assets:
- 圖 9.5 BattleAction Assets
- 添加元件并儲存Prefab:
- 圖 9.6 MapGraph Inspector
- 最後運作遊戲,檢視動畫播放是否正常,輸出資訊是否正常。