天天看點

SRPG遊戲開發(四十二)第九章 戰鬥系統 - 四 計算戰鬥資料II(Calculate Combat Data II)第九章 戰鬥系統(Combat System)

傳回總目錄

第九章 戰鬥系統(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:
    SRPG遊戲開發(四十二)第九章 戰鬥系統 - 四 計算戰鬥資料II(Calculate Combat Data II)第九章 戰鬥系統(Combat System)
    • 圖 9.5 BattleAction Assets
  • 添加元件并儲存Prefab:
    SRPG遊戲開發(四十二)第九章 戰鬥系統 - 四 計算戰鬥資料II(Calculate Combat Data II)第九章 戰鬥系統(Combat System)
    • 圖 9.6 MapGraph Inspector
  • 最後運作遊戲,檢視動畫播放是否正常,輸出資訊是否正常。

繼續閱讀