好吧,在看到了前面那個可怕的Unit案例後,那所謂的鬆耦合架構又要怎麼實踐呢?
我這邊會將整個Unit的程式分成四個部分來思考,分別為資料層、視覺層、邏輯層跟組合層。
資料層就是用來處理像是玩家的血量、攻擊力、移動步數等等這些資料。
以Unity內建來說,可以用ScriptableObject
來儲存固定的資料,之後的章節會特別介紹這個,這邊先給各位看一下它的程式。
UnitData.cs
using LinXuan.TBSF.Units;
using Sirenix.OdinInspector;
using System.Collections.Generic;
using TbsFramework.Cells;
using TbsFramework.Units;
using UnityEngine;
namespace LinXuan.TBSF.Data
{
[CreateAssetMenu(fileName = "UnitData", menuName = "SaveData/UnitData")]
[InlineEditor]
public sealed class UnitData : ScriptableObject
{
[SerializeField] private int m_MaxHp;
[SerializeField] private int m_UnitNumber;
[SerializeField] private int m_MaxStamina;
[SerializeField] private int m_MaxMovementTimes;
[SerializeField] private int m_MaxActionTimes;
[SerializeField] private int m_AttackPoint;
[SerializeField] private int m_AttackRange;
[SerializeField] private int m_MoveRange;
[SerializeField] private UnitView m_UnitView;
[SerializeField] protected string m_UnitName;
private int m_CurrentStamina;
private int m_CurrentMovementTimes;
private int m_CurrentActionTimes;
private List<(Buff buff, int timeLeft)> Buffs;
public int CurrentStamina { get => m_CurrentStamina; set { Mathf.Clamp(value, 0, m_MaxStamina); } }
public UnitView UnitView { get => m_UnitView; private set => m_UnitView = value; }
public int PlayerNumber;
public bool Obstructable = true;
public string UnitName { get => m_UnitName; set => m_UnitName = value; }
[SerializeField]
[HideInInspector]
private Cell cell;
public Cell Cell
{
get
{
return cell;
}
set
{
cell = value;
}
}
}
}
而這是產生出來ScriptableObject在Inspector的資料。
從UI到角色動畫還有特效撥放等等這些東西的展示我都歸類在這塊,因為Unity的顯示基本上都跟場景裡的GameObject有所關聯,所以大部分的用來組合顯示的資料的class我都會掛。
以Turn based strategy framework+我稍微修改的做法中,CALUnitHighlighterAnimation是我用來接Animator的地方,只要放上對應的Animator跟Trigger名稱,就可以觸發相對應的動畫撥放。
攻擊傷害倍率、敵方AI行動、回合控制這些我都會比較偏邏輯層的部分,這些也基本上不會掛上MonoBehaviour,因為...根本沒必要啊,這些都只要在需要時new一個實體出來就夠了。
UnitFightingCalculator.cs
namespace LinXuan.TBSF.Math
{
public class UnitFightingCalculator
{
... Calculate function.
}
}
IUnit.cs
using LinXuan.TBSF.Math;
using UnityEngine;
namespace LinXuan.TBSF.Units
{
public abstract class IUnit : MonoBehaviour
{
private UnitFightingCalculator m_FightingCalculator;
protected void UseActionAbility()
{
m_FightingCalculator = new UnitFightingCalculator();
... Use UnitFightingCalculator.
}
}
}
今天不管怎麼拆跟分類功能,總還是要有個地方可以將資料展示的內容組合起來的地方,而組合層就是擔任這個任務的層。這層基本上主要是有包含視覺層的會去掛MonoBehaviour
的,其他我會盡量不掛。
以上面視覺層例子來說,這些物件最後就是用UnitHighlighterAggregator
組合起來的。
這邊可以來切看看,就今天把Unit
程式用上面的邏輯重新整理的話會是什麼概念。
資料層
視覺層
邏輯層
組合層
整體程式碼大概長這樣。
IUnit.cs
using LinXuan.TBSF.Math;
using LinXuan.TBSF.Math.Algorithms.PathFinding;
using LuLuBearStudio.CALAbilities;
using UnityEngine;
namespace LinXuan.TBSF.Units
{
public abstract class IUnit : MonoBehaviour
{
[SerializeField] private UnitData m_UnitData = null;
private UnitView m_UnitView;
[SerializeField] private AbilityEditor m_AbilityEditor;
public UnitData UnitData { get => m_UnitData; set => m_UnitData = value; }
private UnitEventHandler m_UnitEventHandler;
private UnitInputController m_UnitInputEventHandler;
private UnitPathFinding m_PathFinding;
private UnitFightingCalculator m_FightingCalculator;
public IUnitState UnitStates { get; set; }
...
}
}
應該整體下來IUnit
有甚麼東西可以比較清楚看到IUnit
包含的東西,不會像前面的Unit
只能看到一小部分了吧?
PlayerUnit.cs
using LinXuan.TBSF.Enums;
using UnityEngine;
namespace LinXuan.TBSF.Units
{
public class PlayerUnit : IUnit
{
[SerializeField] PlayerUnitType m_PlayerUnitType;
}
}
PlayerUnit
現在就先當成代替Clency
的class就好。
版面有比較乾淨了吧?
當然這邊只是最簡單的分類方式,實際上來說,遊戲其內容遠遠比一般應用軟體還複雜的多,所以在功能的切割上應該要更加仔細,這邊只是大概讓人知道這些程式大概可以用什麼方式去分辨切割方式提供一個思路。
備註:
這邊我其實寫得有點不確定會不會太多讓人困惑的資訊,如果覺得有甚麼地方有問題的話可以先說,等完賽後我會抽出時間把大部分的文章再整理一次的。(如果有時間的話也是有可能接近完賽時就整理)
除此之外如果裡面有陌生不屬於Unity的顯示方式的話,那是一個名叫Odin工具的效果,這個我到簡易編輯器那篇時會說。
簡單來說就是把程式碼改個寫法,從名稱的改動到整體程式架構的更改都在重構的範圍內,畢竟現實中不可能建構出完美的架構,將混亂的程式碼不斷整理成井然有序的樣子,才是真正穩健的程式架構逐漸演變的樣子。
如果有稍微涉略架構的知識,應該會發現這基本上跟MVC架構十分貌似。其實我這邊分成的四層是從MVC參考、我這邊在用我自己比較好懂的方式去分。所以今天去問別人的話,最好用MVC這個關鍵字去問應該會比多人知道你在說什麼。
Turn Based Strategy Framework
流離之歌
MVC