傳回總目錄
第九章 戰鬥系統(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
命名空間中。
- 使用
的好處之一是,在Inspector面闆中可以可視化操作。UnityEvent
- 使用
的好處之一是,可以有傳回值,比如傳回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_onPlay
和CombatAnimaController_onStop
是對應事件:CombatAnimaController_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
我們已經完成了大部分内容,下一節我們再回過頭來看看資料的計算,因為之前隻是簡單的寫了實體攻擊的計算。