返回总目录

第九章 战斗系统(Combat System)

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

  • 地图中直接战斗;

  • 有专门的战斗场景。

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

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

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


文章目录

  • 第九章 战斗系统(Combat System)
    • 三 战斗动画(Combat Animation)
      • 1 战斗动画控制器(Combat Animation Controller)
      • 2 控制器属性(Controller Properties)
      • 3 播放动画(Play Animation)
        • 3.1 播放地图中的动画(Play Animation in Map)
        • 3.2 开始/结束动画事件(Start/End Event)
        • 3.3 每一次行动事件(Step Event)
      • 4 测试(Test)

三 战斗动画(Combat Animation)

在之前,我们已经计算出所有的战斗数据,而且每一步的动画也进行了保存。这样,在这里就可以它们为基准来播放需要的动画。


1 战斗动画控制器(Combat Animation Controller)

同样,我们创建一个组件来控制播放动画(CombatAnimaController):

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;namespace DR.Book.SRPG_Dev.CombatManagement
{using DR.Book.SRPG_Dev.Maps;[DisallowMultipleComponent, RequireComponent(typeof(Combat))][AddComponentMenu("SRPG/Combat System/Combat Anima Controller")]public class CombatAnimaController : MonoBehaviour{private Combat m_Combat;public Combat combat{get{if (m_Combat == null){m_Combat = GetComponent<Combat>();}return m_Combat;}}// TODO 其它属性与方法}
}

它将依赖于Combat组件,所以添加了Attribute(RequireComponent(typeof(Combat)))。


2 控制器属性(Controller Properties)

在播放每一步(或者说角色每一次行动)时,我们需要一个时间间隔。如果连续播放可能会对感官有影响。

        [SerializeField]private float m_AnimationInterval = 1f;/// <summary>/// 每个动画的间隔时间/// </summary>public float animationInterval{get { return m_AnimationInterval; }set { m_AnimationInterval = value; }}

另外,在播放时我们采用Coroutine(当然,你也可以使用void Update(),并使用参数控制播放进程)。

        private Coroutine m_AnimaCoroutine;public bool isAnimaRunning{get { return m_AnimaCoroutine != null; }}

在最后,我们添加一些Combat的代理。

        public bool isCombatLoaded{get { return combat.isLoaded; }}public bool isBattleCalced{get { return combat.stepCount > 0; }}public int stepCount{get { return combat.stepCount; }}/// <summary>/// 初始化战斗双方/// </summary>/// <param name="mapClass0"></param>/// <param name="mapClass1"></param>/// <returns></returns>public bool LoadCombatUnit(MapClass mapClass0, MapClass mapClass1){return combat.LoadCombatUnit(mapClass0, mapClass1);}

这些属性中,有一些不是必须的,这取决于你如何控制动画播放。


3 播放动画(Play Animation)

关于动画的播放,要了解一些情况:

  • 有一些动画并不是所有的游戏中都存在的(例如躲闪和爆击动画),相当一部分都只是弹出文字提醒,除此之外防守者没有动画;

  • 在地图上的攻击动画,需要注意方向问题,是使用四方向还是八方向?使用四方向时,斜线要注意朝向;

  • 还要记住,游戏的UI还没有写,动画中可能要通知UI播放UI动画;

  • 地图上的动画与单独场景的动画只会播放一个,取决于设置。

创建播放动画方法:

        /// <summary>/// 运行动画/// </summary>/// <param name="combat"></param>/// <param name="inMap"></param>public void PlayAnimas(bool inMap){if (combat == null || !isCombatLoaded || isAnimaRunning){return;}// 如果没有计算,则先计算if (!isBattleCalced){combat.BattleBegin();if (!isBattleCalced){Debug.LogError("CombatAnimaController -> calculate error! check the `Combat` code.");return;}}m_AnimaCoroutine = StartCoroutine(RunningAnimas(inMap));}

RunningAnimas(inMap)是播放动画的具体实现,我们暂时先不考虑单独场景的动画。

        private IEnumerator RunningAnimas(bool inMap){if (inMap){// 在地图中yield return RunningAnimasInMap();}else{// TODO 单独场景}m_AnimaCoroutine = null;}

3.1 播放地图中的动画(Play Animation in Map)

RunningAnimasInMap()是播放地图中的动画。

        private IEnumerator RunningAnimasInMap(){CombatUnit unit0 = combat.GetCombatUnit(0);CombatUnit unit1 = combat.GetCombatUnit(1);List<CombatStep> steps = combat.steps;// TODO 具体实现}

在播放地图中的动画之前我们要先确定两个单位的方向。

  • 图 9.3 战斗单位在地图中的方向

我们不说上下左右四个方向,而是看斜线方向。 (图 9.3) 标记了四个点,在一般的SRPG中,这四个点位的方向为:

  • 1 右朝向(Right)

  • 2 右朝向(Right)

  • 3 上朝向(Up)

  • 4 上朝向(Up)

你会观察到,横向距离和纵向距离的大小决定着方向,但当两个距离相等的时候方向以横向距离为准,这是因为在视觉上更合理。

基于以上建立获取方向方法:

        /// <summary>/// 获取方向/// </summary>/// <param name="cellPosition0"></param>/// <param name="cellPosition1"></param>/// <returns></returns>protected Direction GetAnimaDirectionInMap(Vector3Int cellPosition0, Vector3Int cellPosition1){Vector3Int offset = cellPosition1 - cellPosition0;if (Mathf.Abs(offset.x) < Mathf.Abs(offset.y)){return offset.y > 0 ? Direction.Up : Direction.Down;}else{return offset.x > 0 ? Direction.Right : Direction.Left;}}

RunningAnimasInMap()中填充获取方向:

            Direction[] dirs = new Direction[2];dirs[0] = GetAnimaDirectionInMap(unit0.mapClass.cellPosition, unit1.mapClass.cellPosition);dirs[1] = GetAnimaDirectionInMap(unit1.mapClass.cellPosition, unit0.mapClass.cellPosition);

有了方向之后,我们可以播放每一次行动的动画了:

            yield return null;int curIndex = 0;CombatStep step;while (curIndex < steps.Count){step = steps[curIndex];// 根据动画不同,播放时间应该是不同的// 这需要一些参数或者算法来控制// (例如一些魔法,在配置表中加上一个特效的变量,// 人物施法动画是这个,特效还要另算,需要计算在内)// 这里我只是简单的定义为同时播放float len0 = RunAniamAndGetLengthInMap(step.atkVal, step.defVal, dirs);float len1 = RunAniamAndGetLengthInMap(step.defVal, step.atkVal, dirs);float wait = Mathf.Max(len0, len1);yield return new WaitForSeconds(wait);yield return new WaitForSeconds(animationInterval);curIndex++;}

这之中,有一个方法RunAniamAndGetLengthInMap,它的作用是播放动画并获取动画的长度:

        /// <summary>/// 运行动画,并返回长度/// </summary>/// <param name="combat"></param>/// <param name="actor"></param>/// <param name="other"></param>/// <param name="dirs"></param>/// <returns></returns>protected virtual float RunAniamAndGetLengthInMap(CombatVariable actor, CombatVariable other, Direction[] dirs){CombatUnit actorUnit = combat.GetCombatUnit(actor.position);if (actorUnit == null || actorUnit.mapClass == null){return 0f;}ClassAnimatorController actorAnima = actorUnit.mapClass.animatorController;Direction dir = dirs[actor.position];float length = 0.5f;switch (actor.animaType){case CombatAnimaType.Prepare:actorAnima.PlayPrepareAttack(dir, actorUnit.weaponType);break;case CombatAnimaType.Attack:case CombatAnimaType.Heal:actorAnima.PlayAttack();length = actorAnima.GetAttackAnimationLength(dir, actorUnit.weaponType);break;case CombatAnimaType.Evade:actorAnima.PlayEvade();length = actorAnima.GetEvadeAnimationLength(dir);break;case CombatAnimaType.Damage:actorAnima.PlayDamage();length = actorAnima.GetDamageAnimationLength(dir);// TODO 受到爆击的额外动画,假定是晃动// if (other.crit)// {//     CommonAnima.PlayShake(actorUnit.mapClass.gameObject);//     length = Mathf.Max(length, CommonAnima.shakeLength);// }break;case CombatAnimaType.Dead:// TODO 播放死亡动画,我把死亡忘记了break;default:break;}return length;}

到这里,就可以完成动画的播放了。不过不要忘记,我们在整个播放动画的过程中可能存在UI的动画;而且,动画播放结束时需要通知其它地方,我们已经结束动画播放。

3.2 开始/结束动画事件(Start/End Event)

我们希望动画在开始或结束的时候能够通知其它对象来执行其它操作,使用一个事件是比较好的方式。

常见的事件可以使用委托或者UnityEvent,各有利弊。我这里使用UnityEvent,它在UnityEngine.Events命名空间中。

  • 使用UnityEvent的好处之一是,在Inspector面板中可以可视化操作。

  • 使用Delegate的好处之一是,可以有返回值,比如返回IEnumerator,可以更方便的配合Coroutine做动画处理。

创建Unity事件:

        /// <summary>/// 当动画播放开始/结束时。/// Args: ///     CombatAnimaController combatAnima, ///     bool inMap, // 是否是地图动画/// </summary>[Serializable]public class OnAnimaPlayEvent : UnityEvent<CombatAnimaController, bool> { }

创建事件字段与属性:

        [SerializeField]private OnAnimaPlayEvent m_OnPlayEvent = new OnAnimaPlayEvent();/// <summary>/// 当动画播放开始时。/// Args: ///     CombatAnimaController combatAnima, ///     bool inMap, // 是否是地图动画/// </summary>public OnAnimaPlayEvent onPlay{get{if (m_OnPlayEvent == null){m_OnPlayEvent = new OnAnimaPlayEvent();}return m_OnPlayEvent;}set { m_OnPlayEvent = value; }}[SerializeField]private OnAnimaPlayEvent m_OnStopEvent = new OnAnimaPlayEvent();/// <summary>/// 当动画播放结束时。/// Args: ///     CombatAnimaController combatAnima, ///     bool inMap, // 是否是地图动画/// </summary>public OnAnimaPlayEvent onStop{get{if (m_OnStopEvent == null){m_OnStopEvent = new OnAnimaPlayEvent();}return m_OnStopEvent;}set { m_OnStopEvent = value; }}

在播放动画RunningAnimas(bool inMap)中使用它:

        /// <summary>/// 开始运行动画/// </summary>/// <param name="combat"></param>/// <param name="inMap"></param>/// <returns></returns>private IEnumerator RunningAnimas(bool inMap){onPlay.Invoke(this, inMap);if (inMap){// 在地图中yield return RunningAnimasInMap();}else{// TODO 单独场景}onStop.Invoke(this, inMap);m_AnimaCoroutine = null;}

有了开始/结束事件之后,你可以在事件中使用我们的消息事件系统发送消息,干一些事情(例如打开一些必要的UI,结束时如果角色死亡就回收等)。

3.3 每一次行动事件(Step Event)

我们在每一次行动的时候也要通知UI进行更新,这使得我们也要在每一次行动时能够干一些其它事情(例如播放声音,UI血量变化等)。

创建行动事件:

        /// <summary>/// 当每一次行动开始/结束时。/// Args: ///     CombatAnimaController combatAnima, ///     int index, // step的下标///     float wait, // 每一次行动的动画播放时间///     bool end // step的播放开始还是结束/// </summary>[Serializable]public class OnAnimaStepEvent : UnityEvent<CombatAnimaController, int, float, bool> { }[SerializeField]private OnAnimaStepEvent m_OnStepEvent = new OnAnimaStepEvent();/// <summary>/// 当每一次行动开始/结束时。/// Args: ///     CombatAnimaController combatAnima, ///     int index, // step的下标///     float wait, // 每一次行动的动画播放时间///     bool end // step的播放开始还是结束/// </summary>public OnAnimaStepEvent onStep{get{if (m_OnStepEvent == null){m_OnStepEvent = new OnAnimaStepEvent();}return m_OnStepEvent;}set { m_OnStepEvent = value; }}

我们在行动开始和结束时分别触发这个事件:

        private IEnumerator RunningAnimasInMap(){// 省略其它代码yield return null;int curIndex = 0;CombatStep step;while (curIndex < steps.Count){// 省略其它代码onStep.Invoke(this, curIndex, wait, false);yield return new WaitForSeconds(wait);onStep.Invoke(this, curIndex, animationInterval, true);yield return new WaitForSeconds(animationInterval);curIndex++;}}

到这里,我们几乎完成了这个组件的编写,不过我还需要测试它。


4 测试(Test)

EditorTestCombat中,我们来修改或添加代码。

  • 首先,是修改字段,我们不再需要Combat组件,而是换成CombatAnimaController组件:

            //public Combat m_Combat;private CombatAnimaController m_CombatAnimaController;
    
  • 其次,在void Start()中修改初始化方法(将关于Combat全部注释掉并替换成CombatAnimaController):

            private void Start(){// 省略其它//m_Combat = m_Map.gameObject.GetComponent<Combat>();//if (m_Combat == null)//{//    m_Combat = m_Map.gameObject.AddComponent<Combat>();//}// GetOrAdd 和上面注释的相同,// 只是组件换成了CombatAnimaController。m_CombatAnimaController = Combat.GetOrAdd(m_Map.gameObject);m_CombatAnimaController.onPlay.AddListener(CombatAnimaController_onPlay);m_CombatAnimaController.onStop.AddListener(CombatAnimaController_onStop);m_CombatAnimaController.onStep.AddListener(CombatAnimaController_onStep);// 省略其它ItemModel model = ModelManager.models.Get<ItemModel>();m_TestClass1.role.AddItem(model.CreateItem(0));m_TestClass2.role.AddItem(model.CreateItem(1));// 这里原来是Combat相关代码,全部删除或注释ReloadCombat();}
    

    CombatAnimaController_onPlayCombatAnimaController_onStopCombatAnimaController_onStep是对应事件:

            private void CombatAnimaController_onPlay(CombatAnimaController combatAnima, bool inMap){if (m_DebugInfo){Debug.LogFormat("Begin Battle Animations: {0} animations", combatAnima.combat.stepCount);}}private void CombatAnimaController_onStop(CombatAnimaController combatAnima, bool inMap){combatAnima.combat.BattleEnd();if (m_DebugInfo){Debug.Log("End Battle Animations");}}private void CombatAnimaController_onStep(CombatAnimaController combatAnima, int index, float wait, bool end){if (!m_DebugInfo || !m_DebugStep){return;}CombatStep step = combatAnima.combat.steps[index];CombatVariable var0 = step.GetCombatVariable(0);CombatVariable var1 = step.GetCombatVariable(1);Debug.LogFormat("({4}, {5}) -> Animation Type: ({0}, {1}), ({2}, {3})",var0.position.ToString(),var0.animaType.ToString(),var1.position.ToString(),var1.animaType.ToString(),index,end ? "End" : "Begin");}
    

    ReloadCombat是读取单位:

            private void ReloadCombat(){if (m_DebugInfo){Debug.Log("--------------------");Debug.Log("Reload Combat.");}if (!m_CombatAnimaController.LoadCombatUnit(m_TestClass1, m_TestClass2)){Debug.LogError("Reload Combat Error: Check the code.");}}
    
  • 最后,添加void Update()void OnDestroy()

            private void Update(){if (!m_Map || !m_CombatAnimaController || !m_TestClass1 || !m_TestClass2){return;}if (Input.GetMouseButtonDown(0)){m_CombatAnimaController.PlayAnimas(true);}if (Input.GetMouseButtonDown(1)){ReloadCombat();}}private void OnDestroy(){if (m_Map != null && m_CombatAnimaController != null){m_CombatAnimaController.onPlay.RemoveListener(CombatAnimaController_onPlay);m_CombatAnimaController.onStop.RemoveListener(CombatAnimaController_onStop);m_CombatAnimaController.onStep.RemoveListener(CombatAnimaController_onStep);}}
    

运行游戏,查看动画是否按顺序正常播放,并查看Console面板输出是否正确(如图 9.4)。

  • 图 9.4 Test Combat Animation Console

我们已经完成了大部分内容,下一节我们再回过头来看看数据的计算,因为之前只是简单的写了物理攻击的计算。


SRPG游戏开发(四十一)第九章 战斗系统 - 三 战斗动画(Combat Animation)相关推荐

  1. SRPG游戏开发(三十九)第九章 战斗系统 - 一 战斗属性(Combat Properties)

    返回总目录 第九章 战斗系统(Combat System) 在SRPG中,大多数情况是指角色与角色之间的战斗.而这种战斗一般有两种模式: 地图中直接战斗: 有专门的战斗场景. 这两种模式的战斗在数据上 ...

  2. SRPG游戏开发(六十四)间章 第十一点五章 总结(Summary)

    返回<SRPG游戏开发>导航 间章 第十一点五章 总结(Summary) 这一章,是对第十章与第十一章的一个补充性质的文章. 文章目录 间章 第十一点五章 总结(Summary) 一 说明 ...

  3. 《SRPG游戏开发》导航(2019.03.04更新)

    <SRPG游戏开发>导航 第一章到第五章并没有使用Markdown,且经过CSDN几次改版和取消目录,这几章排版有些怪怪的. 2019.03.04 第十一章(十 - 十二) ,间章 第十一 ...

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

    返回总目录 第九章 战斗系统(Combat System) 在SRPG中,大多数情况是指角色与角色之间的战斗.而这种战斗一般有两种模式: 地图中直接战斗: 有专门的战斗场景. 这两种模式的战斗在数据上 ...

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

    返回总目录 第九章 战斗系统(Combat System) 在SRPG中,大多数情况是指角色与角色之间的战斗.而这种战斗一般有两种模式: 地图中直接战斗: 有专门的战斗场景. 这两种模式的战斗在数据上 ...

  6. SRPG游戏开发(六十三)第十一章 地图动作与地图事件 - 十二 完善地图信息与测试(Perfect MapEventInfo and Testing)

    返回<SRPG游戏开发>导航 第十一章 地图动作与地图事件(Map Action and Map Event) 我们已经有了剧本,而且可以运行剧本,但我们还缺少对地图的操作控制. 我们这一 ...

  7. SRPG游戏开发(六十)第十一章 地图动作与地图事件 - 九 触发事件与切换回合(Trigger Events and Change Turn)

    返回<SRPG游戏开发>导航 第十一章 地图动作与地图事件(Map Action and Map Event) 我们已经有了剧本,而且可以运行剧本,但我们还缺少对地图的操作控制. 我们这一 ...

  8. SRPG游戏开发(六十一)第十一章 地图动作与地图事件 - 十 NPC操作(NPC Control)

    返回<SRPG游戏开发>导航 第十一章 地图动作与地图事件(Map Action and Map Event) 我们已经有了剧本,而且可以运行剧本,但我们还缺少对地图的操作控制. 我们这一 ...

  9. 战棋SRPG游戏开发-序

    什么是SRPG 战略角色扮演游戏(Strategy Role-Playing Game),日本又称角色扮演模拟游戏,简称SRPG或RSLG,最大特性在于战斗系统中拥有类似战略游戏的游戏方式,以及具有类 ...

最新文章

  1. 无废话-SQL Server 2005新功能(1) - TSQL
  2. mysql 安装包_ubuntu下安装mysql全记录
  3. Grails 复用查询条件并分页
  4. Spring Cloud云服务架构 - common-service 项目构建过程
  5. Oracle-多表连接的三种方式解读
  6. oracle11g里sqldeveloper不能打开的问题
  7. Effective Java之通过私有构造器强化不可实例化能力(四)
  8. 从质疑到成为必选项,低代码技术发展及 2022 展望
  9. 用CLIP增强视频语言的理解,在VALUE榜单上SOTA!
  10. 深度学习需要掌握的 13 个概率分布
  11. 专利号校验码php,电子专利证书的三种下载操作方法
  12. 【光学】Matlab模拟相互垂直的光波叠加
  13. latex_子图标题带括号
  14. 在Ubuntu上安装NTL库以及编译测试
  15. html+表格+左侧表头,HTML thead表格表头 标签
  16. 写给即将毕业的同学们
  17. Excel如何永久去除“受保护视图”的打开提醒?
  18. cufft1d c2c
  19. 设计原则 - 开闭原则
  20. 众多跑车壁纸素材一键即可获取

热门文章

  1. C语言实现各个排序算法(直接插入排序,折半插入排序,希尔排序,冒泡排序,简单选择排序)
  2. C与C++中二维数组的动态分配内存方法
  3. GE智能平台针对严苛的仿真、过程控制和数据采集应用推出反射内存节点卡
  4. 第七章、Zigbee定位系统
  5. 开启 MySQL 慢查询日志
  6. GEO数据库数据下载方法总结
  7. ThinkPHP5_模糊查询和分页
  8. LTE调度算法(下行)
  9. 常用spaceclaim脚本(三)
  10. 思科网络技术学院:CCNA各学期章节练习-期末考试-折扣号考试试题