SRPG游戏开发(四十)第九章 战斗系统 - 二 计算战斗数据(Calculate Combat Data)

返回总目录

第九章 战斗系统(Combat System)

在SRPG中,大多数情况是指角色与角色之间的战斗。而这种战斗一般有两种模式:

  • 地图中直接战斗;

  • 有专门的战斗场景。

这两种模式的战斗在数据上没有任何区别,只有战斗动画的区别。

就像之前描述的,SRPG也是RPG,所以战斗方式和回合制RPG的战斗几乎是相同的。主要区别是RPG每个回合需要手动操作(也有自动战斗的),直到战斗结束;而SRPG是自动战斗,且多数情况只有一个回合(额外回合也是由于技能、物品或剧情需要)。且RPG多数是多人战斗,而SRPG多数是每边就一个。

我们这一章就来写一个战斗系统。


文章目录

  • 第九章 战斗系统(Combat System)
    • 二 计算战斗数据(Calculate Combat Data)
      • 1 角色战斗变量(Role Combat Variable)
      • 2 战斗每一步(Combat Step)
      • 3 战斗(Combat)
        • 3.1 战斗开始(Battle Begin)
        • 3.2 计算每一步(Calculate Battle Step)
        • 3.3 判断战斗是否结束(Is Battle End)
        • 3.4 战斗结束(Battle End)
      • 4 测试战斗数据(Test Combat)


二 计算战斗数据(Calculate Combat Data)

在计算战斗数据时,我们应该知道每一个回合需要可能改变的属性:

  • 生命值(受到伤害,是否死亡);

  • 魔法值(使用技能);

  • 耐久度(使用武器);

除了这些,我们在战斗中还应该知道一些信息:

  • 角色是否可攻击;

  • 角色是否行动过;

  • 角色攻击是否爆击。

当然,为了之后播放动画还应该知道:

  • 播放哪个动画。

以上所提到的都可能在每次行动时改变。

而播放动画,我们用一个枚举来判断:

namespace DR.Book.SRPG_Dev.CombatManagement
{
    public enum CombatAnimaType
    {
        Unknow,
        Prepare, // 准备
        Attack, // 攻击
        Heal, //治疗
        Evade, // 躲闪
        Damage, // 受到攻击
        Dead // 死亡

        // 其它自定义
    }
}

基于以上,我们来创建一个计算数据的类。


1 角色战斗变量(Role Combat Variable)

这里要建立的变量,就是每一个回合每个角色可能改变的变量。其中,position是不变的,代表位置。

创建战斗变量CombatVariable

using System;

namespace DR.Book.SRPG_Dev.CombatManagement
{
    [Serializable]
    public struct CombatVariable
    {
        /// 
        /// 位置
        /// 
        public int position;

        /// 
        /// 生命值
        /// 
        public int hp;

        /// 
        /// 魔法值
        /// 
        public int mp;

        /// 
        /// 是否可攻击
        /// 
        public bool canAtk;

        /// 
        /// FE4武器耐久度(机器人大战技能数量)
        /// 
        public int durability;

        /// 
        /// 动画类型
        /// 
        public CombatAnimaType animaType;

        /// 
        /// 是否爆击
        /// 
        public bool crit;

        /// 
        /// 是否行动过
        /// 
        public bool action;

        /// 
        /// 是否已经死亡
        /// 
        public bool isDead
        {
            get { return hp <= 0; }
        }

        public CombatVariable(int position, int hp, int mp, bool canAtk, CombatAnimaType animaType)
        {
            this.position = position;
            this.hp = hp;
            this.mp = mp;
            this.canAtk = canAtk;
            this.durability = 0;
            this.animaType = animaType;
            this.crit = false;
            this.action = false;
        }

        public CombatVariable(int position, int hp, int mp, bool canAtk, int durability, CombatAnimaType animaType)
        {
            this.position = position;
            this.hp = hp;
            this.mp = mp;
            this.canAtk = canAtk;
            this.durability = durability;
            this.animaType = animaType;
            this.crit = false;
            this.action = false;
        }
    }
}

这里变量并没有考虑声音问题,还应有一个变量指代使用哪个音频,武器的声音可能是不同的。


2 战斗每一步(Combat Step)

我们的战斗不是只有一个角色,且为了播放动画每一次行动都要被记录,也就是说每一次行动后的结果也要被记录。

创建战斗每一步结果CombatStep

using UnityEngine;

namespace DR.Book.SRPG_Dev.CombatManagement
{
    /// 
    /// 战斗每一步结果
    /// 
    public class CombatStep
    {
        /// 
        /// 当前进攻方
        /// 
        public CombatVariable atkVal { get; private set; }

        /// 
        /// 当前防守方
        /// 
        public CombatVariable defVal { get; private set; }

        public CombatStep(CombatVariable atker, CombatVariable defer)
        {
            this.atkVal = atker;
            this.defVal = defer;
        }

        /// 
        /// 根据位置获取战斗变量
        /// 
        /// 
        /// 
        public CombatVariable GetCombatVariable(int position)
        {
            if (atkVal.position == position)
            {
                return atkVal;
            }

            if (defVal.position == position)
            {
                return defVal;
            }

            Debug.LogError("CombatStep -> position is out of range.");
            return default(CombatVariable);
        }
    }
}

其中,你要注意的是,CombatStep不是每一个回合,因为每一个回合包含两次进攻与防守(各进攻一次与防守一次)。


3 战斗(Combat)

有了战斗单位(CombatUnit)、战斗变量(CombatVariable)和战斗每一步结果(CombatStep), 已经初步做好了战斗的准备。在不考虑群攻的情况下,我们先来建立战斗类Combat

创建战斗主类Combat

using System.Collections.Generic;
using UnityEngine;

namespace DR.Book.SRPG_Dev.CombatManagement
{
    using DR.Book.SRPG_Dev.Maps;
    using DR.Book.SRPG_Dev.Models;

    [DisallowMultipleComponent]
    [AddComponentMenu("SRPG/Combat System/Combat")]
    public class Combat : MonoBehaviour
    {
        public CombatUnit unit0 { get; protected set; } // 如果是群攻,所有unit用List或数组
        public CombatUnit unit1 { get; protected set; } // 如果是群攻,所有unit用List或数组
        public List<CombatStep> steps { get; protected set; }

        public bool isLoaded
        {
            get { return unit0.mapClass != null && unit1.mapClass != null; }
        }

        public int stepCount
        {
            get { return steps.Count; }
        }

        private void Awake()
        {
            unit0 = new CombatUnit(0);
            unit1 = new CombatUnit(1);
            steps = new List<CombatStep>();
        }

        private void OnDestroy()
        {
            unit0.Dispose();
            unit0 = null;
            unit1.Dispose();
            unit1 = null;
            steps = null;
        }

        public bool LoadCombatUnit(MapClass mapClass0, MapClass mapClass1)
        {
            return unit0.Load(mapClass0) && unit1.Load(mapClass1);
        }

        public CombatUnit GetCombatUnit(int position)
        {
            switch (position)
            {
                case 0:
                    return unit0;
                case 1:
                    return unit1;
                default:
                    Debug.LogError("Combat -> GetCombatUnit: index is out of range.");
                    return null;
            }
        }

        // TODO 开始战斗、计算战斗和结束战斗

    }
}

3.1 战斗开始(Battle Begin)

当战斗开始的时候,我们要做所有关于战斗的准备工作。并且角色进入战斗状态。

创建方法:

        /// 
        /// 开始战斗
        /// 
        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;
            }

            // TODO

        }

然后进行准备工作:

  • 首先,我们要明确攻击方与防守方,这里我们按速度判断:

                // 根据速度初始化攻击者与防守者
                CombatUnit atker;
                CombatUnit defer;
                if (unit0.speed >= unit1.speed)
                {
                    atker = unit0;
                    defer = unit1;
                }
                else
                {
                    atker = unit1;
                    defer = unit0;
                }
    
  • 其次,我们要知道防守方是否可反击,这里用是否在装备武器攻击范围内决定:

                // 是否可反击
                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;
                    }
                }
    
  • 最后,创建我们这一步结果,并播放准备动画:

                // 准备阶段
                CombatStep firstStep = new CombatStep(
                    new CombatVariable(atker.position, atker.hp, atker.mp, true, atker.durability, CombatAnimaType.Prepare),
                    new CombatVariable(defer.position, defer.hp, defer.mp, canDeferAtk, defer.durability, CombatAnimaType.Prepare));
    
                steps.Add(firstStep);
                CalcBattle(firstStep.atkVal, firstStep.defVal);
    

CalcBattle(firstStep.atkVal, firstStep.defVal);是计算的具体实现,这是我们接下来的工作。准备工作到这里就完成了,之后应该能够计算我们的数据。

3.2 计算每一步(Calculate Battle Step)

我们在计算每一步的时候,先不要考虑过多因素,先确保能够正确运行,做一个最简单的物理攻击。

创建方法:

        /// 
        /// 计算战斗数据
        /// 
        private void CalcBattle(CombatVariable atkVal, CombatVariable defVal)
        {
            CombatUnit atker = GetCombatUnit(atkVal.position);
            CombatUnit defer = GetCombatUnit(defVal.position);

            // 攻击方动画
            atkVal.animaType = CombatAnimaType.Attack;

            // TODO

        }

在计算时主要考虑的就是是否命中攻击(真实命中 = 攻击方命中 - 防守方回避),而其播放的动画只有三个:攻击方攻击,防守方受伤和防守方躲闪。

  • 首先,判断是否命中:

                // 真实命中率 = 攻击者命中 - 防守者回避
                int realHit = atker.hit - defer.avoidance;
    
                // 概率是否击中
                int hitRate = UnityEngine.Random.Range(0, 100);
                bool isHit = hitRate <= realHit;
    
  • 其次,命中与躲闪;如果命中时再判断是否爆击(如果按照《FE4》应该先判断爆击,如果爆击了100%命中),然后播放动画:

                if (isHit)
                {
                    bool crit = false; // TODO 是否爆击
                    int realAtk = atker.atk; // TODO 爆击后伤害
                    // 掉血 = 攻击者攻击力 - 防守者防御力
                    // 最少掉一滴血
                    int damageHp = Mathf.Max(1, realAtk - defer.def); 
                    defVal.hp = Mathf.Max(0, defVal.hp - damageHp);
    
                    atkVal.crit = crit;
                    defVal.animaType = CombatAnimaType.Damage;
                }
                else
                {
                    defVal.animaType = CombatAnimaType.Evade;
                }
    
  • 再次,要注意武器耐久度的变化(只有玩家会减少耐久度):

                // 只有玩家才会减低耐久度
                if (atker.role.attitudeTowards == AttitudeTowards.Player)
                {
                    // 攻击者武器耐久度-1
                    atkVal.durability = Mathf.Max(0, atkVal.durability - 1);
                }
    
  • 然后,完成这一步:

                // 攻击者行动过了
                atkVal.action = true;
    
                CombatStep step = new CombatStep(atkVal, defVal);
                steps.Add(step);
    
  • 最后,交换攻击者防御者,继续下一轮战斗:

                // 如果战斗没有结束,交换攻击者与防守者
                if (!IsBattleEnd(atkVal, defVal))
                {
                    if (defVal.canAtk)
                    {
                        CalcBattle(defVal, atkVal);
                    }
                    else
                    {
                        // 如果防守方不可反击
                        defVal.action = true;
                        if (!IsBattleEnd(defVal, atkVal))
                        {
                            CalcBattle(atkVal, defVal);
                        }
                    }
                }
                else
                {
                    // TODO 如果死亡,播放死亡动画(我把死亡动画忘记了)
                    // if (defVal.isDead) 播放死亡动画
                }
    

    IsBattleEnd(atkVal, defVal)是判断是否战斗结束的,等一下我们来完成它。

3.3 判断战斗是否结束(Is Battle End)

战斗结束的基本标准是有一方死亡或者行动过了,你可以自己定规则。

创建方法:

        /// 
        /// 战斗是否结束
        /// 
        /// 
        /// 
        /// 
        private bool IsBattleEnd(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 true;
                //}
            }

            return false;
        }

3.4 战斗结束(Battle End)

在战斗结束时(播放动画完毕后),我们需要把改变的属性传回角色中。我们先写一个直观简单的,日后再来修改,要记得有这个工作(尤其是缺失了经验值的获取)。

创建方法:

        /// 
        /// 战斗结束
        /// 
        public void BattleEnd()
        {
            if (stepCount > 0)
            {
                CombatStep result = steps[stepCount - 1];

                CombatVariable unit0Result = result.GetCombatVariable(0);
                CombatVariable unit1Result = result.GetCombatVariable(1);

                // TODO 经验值战利品将结果传回角色中
                unit0.mapClass.OnBattleEnd(unit0Result.hp, unit0Result.mp, unit0.durability);
                unit1.mapClass.OnBattleEnd(unit1Result.hp, unit1Result.mp, unit1.durability);

                steps.Clear();
            }

            unit0.ClearMapClass();
            unit1.ClearMapClass();
        }

传送回角色的方法暂时命名为public void OnBattleEnd(int hp, int mp, int durability)

MapClass中和Role中添加此方法:

        /// 
        /// 战斗结束,此方法在MapClass中
        /// 
        /// 
        /// 
        /// 
        public void OnBattleEnd(int hp, int mp, int durability)
        {
            role.OnBattleEnd(hp, mp, durability);

            if (role.isDead)
            {
                // TODO 死亡
            }
        }
        /// 
        /// 战斗结束,此方法在Role中
        /// 
        /// 
        /// 
        /// 
        public void OnBattleEnd(int hp, int mp, int durability)
        {
            self.hp = hp;
            self.mp = mp;
            if (attitudeTowards == AttitudeTowards.Player)
            {
                equipedWeapon.durability = durability;
            }
        }

最后几点说明,如果你要在Inspector面板显示每次战斗结果:

  • CombatStep将属性改成字段,加上[SerializeField];

  • Combat中,将steps属性更改成字段,加上[SerializeField];

  • CombatStepCombatVariable类上加上[Serializable]

  • 最好自定义UnityEditor使其成为只读属性。

        // 例如在CombatStep中更改属性,这样改其它地方就不用变了:
        [SerializeField]
        private CombatVariable m_AtkVal;
        public CombatVariable atkVal
        {
            get { return m_AtkVal; }
            private set { m_AtkVal = value; }
        }

4 测试战斗数据(Test Combat)

我们来写一个文件来测试我们的数据计算是否正确。初始化的变量代码和之前的寻路测试代码差不多。

不过在这之前, 你要检查配置文件读取的信息是否正确或缺失。并为MapGraph添加Combat组件。

检查好之后创建测试文件:

#region ---------- File Info ----------
/// **********************************************************************
/// Copyright (C) 2018 DarkRabbit(ZhangHan)
///
/// File Name:				EditorTestCombat.cs
/// Author:					DarkRabbit
/// Create Time:			Fri, 16 Nov 2018 17:47:31 GMT
/// Modifier:
/// Module Description:
/// Version:				V1.0.0
/// **********************************************************************
#endregion ---------- File Info ----------

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace DR.Book.SRPG_Dev.CombatManagement.Testing
{
    using DR.Book.SRPG_Dev.Maps;
    using DR.Book.SRPG_Dev.Models;
    using DR.Book.SRPG_Dev.Framework;

    public class EditorTestCombat : MonoBehaviour
    {
        public string m_ConfigDirectory;
        public MapGraph m_Map;
        public Combat m_Combat;
        public MapClass m_TestClassPrefab;

        public bool m_DebugInfo = true;
        public bool m_DebugStep = true;

        private MapClass m_TestClass1;
        private MapClass m_TestClass2;

        #region Unity Callback
#if UNITY_EDITOR
        private void Awake()
        {
            if (string.IsNullOrEmpty(m_ConfigDirectory))
            {
                m_ConfigDirectory = Application.streamingAssetsPath + "/Config";
            }

            ConfigLoader.rootDirectory = m_ConfigDirectory;
        }

        private void Start()
        {
            if (m_Map == null)
            {
                m_Map = GameObject.FindObjectOfType<MapGraph>();
            }

            if (m_Map == null)
            {
                Debug.LogError("EditorTestCombat -> Map was not found.");
                return;
            }

            m_Combat = m_Map.gameObject.GetComponent<Combat>();
            if (m_Combat == null)
            {
                m_Combat = m_Map.gameObject.AddComponent<Combat>();
            }

            m_Map.InitMap();

            if (m_TestClassPrefab == null)
            {
                Debug.LogError("EditorTestCombat -> Class Prefab is null.");
                return;
            }

            m_TestClass1 = m_Map.CreateMapObject(m_TestClassPrefab, new Vector3Int(5, 5, 0)) as MapClass;
            m_TestClass2 = m_Map.CreateMapObject(m_TestClassPrefab, new Vector3Int(6, 5, 0)) as MapClass;

            if (!m_TestClass1.Load(0, RoleType.Unique) || !m_TestClass2.Load(1, RoleType.Unique))
            {
                Debug.LogError("EditorTestCombat -> Load role Error.");
                return;
            }

            ItemModel model = ModelManager.models.Get<ItemModel>();
            m_TestClass1.role.AddItem(model.CreateItem(0));
            m_TestClass2.role.AddItem(model.CreateItem(1));

            Debug.LogFormat("{0}.hp = {1}, {0}.atk = {4}, {2}.hp = {3}, {2}.atk = {5}",
                m_TestClass1.role.character.info.name,
                m_TestClass1.role.hp,
                m_TestClass2.role.character.info.name,
                m_TestClass2.role.hp,
                m_TestClass1.role.attack,
                m_TestClass2.role.attack);

            m_Combat.LoadCombatUnit(m_TestClass1, m_TestClass2);
            m_Combat.BattleBegin();
            for (int i = 0; i < m_Combat.stepCount; i++)
            {
                CombatVariable var0 = m_Combat.steps[i].atkVal;
                CombatVariable var1 = m_Combat.steps[i].defVal;
                Debug.LogFormat("({4}) -> Animation Type: ({0}, {1}), ({2}, {3})",
                    var0.position.ToString(),
                    var0.animaType.ToString(),
                    var1.position.ToString(),
                    var1.animaType.ToString(),
                    i);
            }
            m_Combat.BattleEnd();

            Debug.LogFormat("{0}.hp = {1}, {2}.hp = {3}",
                m_TestClass1.role.character.info.name,
                m_TestClass1.role.hp,
                m_TestClass2.role.character.info.name,
                m_TestClass2.role.hp);
        }
#endif
        #endregion
    }
}

创建好组件,将它挂载在一个新的物体上,并将其他测试物体设置成不显示(图 9.1)。

SRPG游戏开发(四十)第九章 战斗系统 - 二 计算战斗数据(Calculate Combat Data)_第1张图片

  • 图 9.1 Test Combat Hierarchy

点击运行按钮,查看Console面板的信息是否正确(图 9.2)。

SRPG游戏开发(四十)第九章 战斗系统 - 二 计算战斗数据(Calculate Combat Data)_第2张图片

  • 图 9.2 Test Combat Console

图中所显示的数据包含了生命值与攻击力,数值就像我们之前说的那样,这取决于配置文件:

  • 主角攻击力 = 武器(id = 0, attack = 4) + 人物(id = 0, str = 3) + 职业(id = 0, str = 6) = 13

  • 步兵敌人1攻击力 = 武器(id = 1, attack = 6) + 人物(id = 1, str = 2) + 职业(id = 0, str = 6) = 14

能够打印出信息(图 9.2),并且正确就没有问题了。


你可能感兴趣的:(SRPG游戏开发,《SRPG游戏开发》)