Related Posts Plugin for WordPress, Blogger...

3-9 Special Abilities System Overview

本章要來介紹在Unity實作特殊技能系統,之前的文章已經向大家介紹過如何製作Energy Bar,並使用滑鼠的右鍵進行發動了。但是,舊的做法太過死板,無法動態的新增與更換技能,所以本次要來改進這個問題,依此利用ScriptableObject概念設計新的技能系統,可以在Inspector視窗進行各種客製化與增刪,非常方便。

如果不知道怎麼做Energy Bar,或是不知道什麼是ScriptableObject的話,請先看以下文章:
2-25 Introducing Scriptable Objects

3-7 Adding An Energy Mechanic 

首先,請大家在專案中新增Special Abilities資料夾,並新增兩個Script檔案,分別為ISpecialAbility、SpecialAbilityConfig,再新增一個Power Attack資料夾。

先介紹ISpecialAbility,單純是一個interface定義了Use方法,為的是讓其他的Script呼叫這邊定義的Use方法來發動技能。如不懂interface的妙用,大家可以參考這篇文章:
2-14 Using Interfaces In C#
以下說明為了方便,將程式碼截圖一部份,最後面會提供所有修改的程式碼。

SpecialAbilityConfig則是用來儲存技能的設定,如energyCost就是發動技能要消耗的能量、AddComponent方法則是為了將技能發動後的持續發動效果套用至Player。更詳細地來說,AddComponent可以將繼承MonoBehaviour的物件套用到Player,如補血技能要每個Frame都持續加血,就會需要用到Update,故借用AddComponent將補血技能的物件套用到Player上。

再來是Power Attack資料夾內,新增PowerAttackBehaviour、PowerAttackConfig。PowerAttackBehaviour便是我上述提到要套用至Player的物件,它可以使用Start跟Update方法,是用來發動技能的物件。PowerAttackConfig則是儲存技能的相關資料,如增加額外的攻擊力Buff,或是持續扣血的燃燒數值等等。

在PowerAttackConfig中繼承了SpecialAbilityConfig,並實作AddComponent方法。大家可以見到於方法內將PowerAttackBehaviour套用到gameObjectAttachTo這個參數,而此參數將會由Player回傳自己。

然後在PowerAttackBehaviour中,實作了ISpecialAbility方法,在其覆寫的Use方法中會呼叫target的TakeDamage方法,此方法亦是由Player將Enemy回傳,所以能傷害到Enemy。

所以,Energy的OnMouseOverEnemy方法就不需要了,應該由Player來決定攻擊時機才正確。

Player的部分,在OnMouseOverEnemy增加滑鼠右鍵的判斷。

並在Player撰寫AttemptSpecialAbility方法,發動技能。

接著,以下提供完整程式碼,並於最下面教大家如何在Inspector中很方便地使用這個技能系統。

ISpecialAbility.cs

1
2
3
4
5
6
7
8
9
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
namespace RPG.Character{
 public interface ISpecialAbility {
  void Use(AbilityParams useParams);
 }
}

SpecialAbilityConfig.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using RPG.Core;
 
namespace RPG.Character{
 public struct AbilityParams{
  public IDamageable target;
  public float baseDamage;
 
  public AbilityParams(IDamageable target, float baseDamage){
   this.target = target;
   this.baseDamage = baseDamage;
  }
 }
 
 public abstract class SpecialAbilityConfig : ScriptableObject {
  [Header("Special Ability General")]
  [SerializeField] float energyCost = 10f;
 
  abstract public ISpecialAbility AddComponent (GameObject gameObjectAttachTo);
 
  public float GetEnergyCost(){
   return energyCost;
  }
 }
}

PowerAttackConfig.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
namespace RPG.Character{
 [CreateAssetMenu(menuName= ("RPG/Special Ability/Power Attack"))]
 public class PowerAttackConfig : SpecialAbilityConfig {
  [Header("Power Attack Specific")]
  [SerializeField] float extraDamage = 10f;
 
  public override ISpecialAbility AddComponent (GameObject gameObjectAttachTo){
   var behaviourComponent = gameObjectAttachTo.AddComponent<powerattackbehaviour> ();
   behaviourComponent.SetConfig (this);
 
   return behaviourComponent;
  }
 
  public float GetExtraDamage(){
   return extraDamage;
  }
 }
}
</powerattackbehaviour>

PowerAttackBehaviour.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
namespace RPG.Character{
 public class PowerAttackBehaviour : MonoBehaviour, ISpecialAbility {
 
  PowerAttackConfig config;
 
  public void SetConfig(PowerAttackConfig config){
   this.config = config;
  }
 
  public void Use(AbilityParams useParams){
   float damageToDeal = config.GetExtraDamage () + useParams.baseDamage;
   useParams.target.TakeDamage (damageToDeal);
  }
 
  // Use this for initialization
  void Start () {
    
  }
   
  // Update is called once per frame
  void Update () {
    
  }
 }
}

Energy.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using RPG.CameraUI;
 
namespace RPG.Character{
 public class Energy : MonoBehaviour {
 
  [SerializeField] RawImage energyBarRawImage;
  [SerializeField] float maxEnergyPoints = 100f;
 
  float currentEnergyPoints;
 
  void Start () {
   currentEnergyPoints = maxEnergyPoints;
  }
 
  public bool IsEnergyAvailable(float amount){
   return amount <= currentEnergyPoints;
  }
 
  // 消耗的魔力由SpecialAbilityConfig決定,傳到amount
  public void ConsumeEnergy(float amount){
   float newEnergyPoint = currentEnergyPoints - amount;
   currentEnergyPoints = Mathf.Clamp (newEnergyPoint, 0, maxEnergyPoints);
   UpdateEnergyBar ();
  }
 
  void UpdateEnergyBar(){
   float xValue = -(EnergyAsPersent() / 2f) - 0.5f;
   energyBarRawImage.uvRect = new Rect(xValue, 0f, 0.5f, 1f);
  }
 
  float EnergyAsPersent(){
   return currentEnergyPoints / maxEnergyPoints;
  }
 }
}

Player.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Assertions;
using RPG.CameraUI; // TODO consider re-wiring
using RPG.Core;
using RPG.Weapons;
 
namespace RPG.Character{
 public class Player : MonoBehaviour, IDamageable {
 
  [SerializeField] float maxHealthPoints = 100f;
  [SerializeField] float baseDamage = 50f;
  [SerializeField] AnimatorOverrideController animatorOverrideController;
  [SerializeField] Weapon weaponInUse;
  [SerializeField] SpecialAbilityConfig[] abilities;
  ISpecialAbility[] specialAbility;
 
  Animator animator;
  float currentHealthPoint;
  float lastHitTime;
  CameraRaycaster cameraRaycaster;
 
  public float healthAsPercentage{
   get{ return currentHealthPoint / maxHealthPoints; }
  }
 
  void Start(){
   // 初始化血量
   currentHealthPoint = maxHealthPoints;
   cameraRaycaster = FindObjectOfType<cameraraycaster> ();
   // 註冊滑鼠碰到敵人的事件
   cameraRaycaster.onMouseOverEnemy += OnMouseOverEnemy;
   // 初始化武器,放置到手中
   PutWeaponInHand ();
   // 覆寫人物角色中的Animation
   OverrideAnimatorController ();
   // 替Player增加技能的Behaviour,並回傳ISpecialAbility物件
   AttachSpecialAbility();
  }
 
  private void AttachSpecialAbility(){
   specialAbility = new ISpecialAbility[abilities.Length];
   for (int i = 0; i < abilities.Length; i++) {
    // 儲存ISpecialAbility物件
    specialAbility[i] = abilities [i].AddComponent (gameObject);
   }
  }
 
  private void OverrideAnimatorController(){
   // 取得Animator物件
   animator = GetComponent<animator> ();
   // 將人物的Animator取代為我們剛剛新增的Player Animator Override
   animator.runtimeAnimatorController = animatorOverrideController;
   // Player Animator Override中有一預設的DEFAULT ATTACK,將其取代成Weapon物件中的Animation
   // 亦即人物的Animation將會依據不同的Weapon物件,而使用有不同的動畫
   animatorOverrideController ["DEFAULT ATTACK"] = weaponInUse.GetAnimClip ();
  }
 
  private void PutWeaponInHand(){
   GameObject weaponPrefab = weaponInUse.GetWeaponPrefab ();
   // 生成武器時,將會放置到指定的GameObject之下,此處為WeaponSocket之下
   // 故可將WeaponSocket設置成玩家的右手或左手
   GameObject weaponSocket = RequestDominantHand();
   GameObject weapon = Instantiate (weaponPrefab, weaponSocket.transform);
   // 將武器放置到正確的位置,並有正確的方向
   weapon.transform.localPosition = weaponInUse.gripTransform.localPosition;
   weapon.transform.localRotation = weaponInUse.gripTransform.localRotation;
  }
 
  private GameObject RequestDominantHand(){
   // 從Children中尋找包含DominantHand的物件
   DominantHand[] dominantHand = GetComponentsInChildren<dominanthand> ();
   // 計算取得有DominantHand的物件的數量
   int numberOfDominantHands = dominantHand.Length;
   // 確保該數量不為0
   Assert.AreNotEqual (numberOfDominantHands, 0, "No DominantHand found on player, please add one");
   // 確保該數量不要大於1
   Assert.IsFalse (numberOfDominantHands > 1, "Multiple Dominant script on Player, please remove one");
   // 傳回屬於該DominantHand的GameObject
   return dominantHand [0].gameObject;
  }
 
  void OnMouseOverEnemy(Enemy enemy){
   if (Input.GetMouseButtonDown (0) && IsTargetInRange (enemy)) {
    AttackTarget (enemy);
   }else if (Input.GetMouseButtonDown (1)) {
    // 0為使用第一個技能
    AttemptSpecialAbility(0, enemy);
   }
  }
 
  private void AttemptSpecialAbility(int abilityIndex, Enemy enemy){
   Energy energyComponent = GetComponent<energy> ();
   // 取得技能需要的能量消耗量
   float energyCost = abilities [abilityIndex].GetEnergyCost ();
 
   if (energyComponent.IsEnergyAvailable (energyCost)) {
    energyComponent.ConsumeEnergy (energyCost);
    // 發動技能,並傳入Player的baseDamage
    AbilityParams abilityParams = new AbilityParams (enemy, baseDamage);
    specialAbility [abilityIndex].Use(abilityParams);
   }
 
  }
 
  private void AttackTarget(Enemy enemy){
   // 確認本次攻擊時間離上一次攻擊時間須大於minTimeBetweenHits,相當於技能冷卻時間
   if ( (Time.time - lastHitTime > weaponInUse.GetMinTimeBetweenHits())) {
    // 呼叫Trigger啟動攻擊動畫
    animator.SetTrigger("Attack"); // TODO make const
    // 呼叫攻擊Target的方法
    enemy.TakeDamage(baseDamage);
    // 紀錄目前攻擊的時間
    lastHitTime = Time.time;
   }
  }
 
  // 確認敵人是否在範圍技的攻擊範圍內
  private bool IsTargetInRange(Enemy enemy){
   float distanceToTarget = (enemy.transform.position - transform.position).magnitude;
   return distanceToTarget <= weaponInUse.GetMaxAttackRange();
  }
 
  public void TakeDamage(float damage){
   // Math.Clamp可以確保數值運算後不會低於最小值或者高於最大值
   currentHealthPoint = Mathf.Clamp (currentHealthPoint - damage, 0f, maxHealthPoints);
  }
 }
}
</energy></dominanthand></animator></cameraraycaster>

OK!我相信上述的修改是一個大工程,不過用起來很方便。請大家在Project視窗按右鍵,Create/RPG/Special Ability/Power Attack。

取名為Power Attack L1。

在Power Attack L1的Inspector視窗中,可以設定能量消耗值跟傷害值。

最後回到Player的Inspector視窗,先將Abilities的Size設定為1,並設定成Power Attack L1。進行到這邊大家可以想像到了嗎?如果我需要升級技能,我還可以再新增L2 L3 L4,並修改Extra Damage就能夠簡單增加技能,所以程式寫好以後就讓負責遊戲數值設計的人去折騰技能的數值,不用每次要增加技能就要修改程式碼。

同理,要再新增補血技能、範圍技能,或者給敵人Debuff的技能等等都很簡單,只要如PowerAttackConfig中繼承了SpecialAbilityConfig那樣,大家要再新增HealConfig跟HealHaviour都很快速方便,而且HealHaviour也有Update方法供大家使用。


最後測試遊戲,可以成功用右鍵發動技能攻擊到敵人嘍!以後應該會再新增更多技能,不過技能系統已經介紹給大家了,有興趣的話就自己試試看吧!


留言