Related Posts Plugin for WordPress, Blogger...

4-8 Centralizing Special Ability Code

這次重構要將Player內的技能系統分離出來。首先將Energy的名稱改為SpecialAbilities,以後技能設定跟發動將統一放在這邊。

改名以後,Player原本放置Energy的地方會顯示Nothing Selected,請刪除。

重新加入SpecialAbilities。

接著,我們將AbilityConfig.cs中的struct AbilityParams刪除,這個物件太複雜了,而且傳入的參數中包含Enemy,使得我們的技能系統程式侷限在Player攻擊Enemy上,無法讓Enemy也共用技能系統。

刪除以後,會發現使用到AbilityParams的程式碼出現了紅字,請大家統一改成GameObject target。往後被指定攻擊的target可以是Player,也可以是Enemy。

以下列出修改後的SpecialAbilities,Player,AbilityBehaviour。
SpecialAbilities.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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using RPG.CameraUI;
 
namespace RPG.Character{
 public class SpecialAbilities : MonoBehaviour {
 
  [SerializeField] Image energyOrb;
  [SerializeField] float maxEnergyPoints = 100f;
  [SerializeField] float regenPointPerSecond = 1f;
 
  [SerializeField] AbilityConfig[] abilities;
 
  float currentEnergyPoints;
  AbilityBehaviour[] abilityBehaviour;
 
  float energyAsPersent{ get { return currentEnergyPoints / maxEnergyPoints; }}
 
  void Start () {
   currentEnergyPoints = maxEnergyPoints;
   AttachSpecialAbility();
  }
 
  void Update(){
   if (currentEnergyPoints < maxEnergyPoints) {
    AddEnergyPoint ();
    UpdateEnergyBar ();
   }
  }
 
  private void AttachSpecialAbility(){
   abilityBehaviour = new AbilityBehaviour[abilities.Length];
   for (int i = 0; i < abilities.Length; i++) {
    // 儲存ISpecialAbility物件
    abilityBehaviour[i] = abilities [i].AttachAbilityTo (gameObject);
   }
  }
 
  public void AttemptSpecialAbility(int abilityIndex, GameObject target = null){
   // 取得技能需要的能量消耗量
   float energyCost = abilities [abilityIndex].GetEnergyCost ();
 
   if (energyCost <= currentEnergyPoints) {
    ConsumeEnergy (energyCost);
    // 發動技能,並傳入被鎖定的敵人
    abilityBehaviour [abilityIndex].Use(target);
   }
  }
 
  public float GetNumberOfAbilities(){
   return abilities.Length;
  }
 
  private void AddEnergyPoint(){
   // 每秒自動恢復能量,由於Update是每個Frame都會執行,故須使用deltaTime取得Frame的間隔時間
   float pointsToAdd = regenPointPerSecond * Time.deltaTime;
   currentEnergyPoints = Mathf.Clamp (currentEnergyPoints + pointsToAdd, 0, maxEnergyPoints);
  }
 
  // 消耗的魔力由SpecialAbilityConfig決定,傳到amount
  public void ConsumeEnergy(float amount){
   float newEnergyPoint = currentEnergyPoints - amount;
   currentEnergyPoints = Mathf.Clamp (newEnergyPoint, 0, maxEnergyPoints);
   UpdateEnergyBar ();
  }
 
  void UpdateEnergyBar(){
   energyOrb.fillAmount = energyAsPersent;
  }
 }
}

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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.SceneManagement;
using RPG.CameraUI; // TODO consider re-wiring
using RPG.Core;
using RPG.Weapons;
 
namespace RPG.Character{
 public class Player : MonoBehaviour {
 
  [SerializeField] float baseDamage = 50f;
  [SerializeField] AnimatorOverrideController animatorOverrideController;
  [SerializeField] Weapon currentWeaponConfig;
  [Range(.1f, 1.0f)] [SerializeField] float criticalHitChance = 0.1f;
  [SerializeField] float criticalHitMultiplier = 2.5f;
  [SerializeField] ParticleSystem criticalHitParticle;
 
  // 定義Trigger的字串常數
  const string ATTACK_TRIGGER = "Attack";
  const string DEFAULT_ATTACK = "DEFAULT ATTACK";
 
  float lastHitTime;
  float currentHealthPoint;
  Enemy currentEnemy;
  CameraRaycaster cameraRaycaster;
  Animator animator;
  SpecialAbilities abilities;
  GameObject weaponInUse;
 
  void Start(){
   abilities = GetComponent<specialabilities> ();
 
   RegisterForMouseClcik ();
   PutWeaponInHand (currentWeaponConfig);
   OverrideAnimatorController ();
  }
 
  void Update(){
   float healthAsPercentage = GetComponent<healthsystem> ().healthAsPercentage;
   if (healthAsPercentage > Mathf.Epsilon) {
    ScanForAbilityKeyDown ();
   }
  }
 
  private void ScanForAbilityKeyDown(){
   for (int keyIndex = 1; keyIndex <= abilities.GetNumberOfAbilities(); keyIndex++) {
    if (Input.GetKeyDown (keyIndex.ToString())) {
     if (currentEnemy) {
      abilities.AttemptSpecialAbility (keyIndex, currentEnemy.gameObject);
     } else {
      abilities.AttemptSpecialAbility (keyIndex);
     }
    }
   }
  }
 
  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] = currentWeaponConfig.GetAnimClip ();
  }
 
  public void PutWeaponInHand(Weapon weaponToUse){
   currentWeaponConfig = weaponToUse;
   // 先移除之前手持的武器
   Destroy(weaponInUse);
   GameObject weaponPrefab = weaponToUse.GetWeaponPrefab ();
   // 生成武器時,將會放置到指定的GameObject之下,此處為WeaponSocket之下
   // 故可將WeaponSocket設置成玩家的右手或左手
   GameObject weaponSocket = RequestDominantHand();
   weaponInUse = Instantiate (weaponPrefab, weaponSocket.transform);
   // 將武器放置到正確的位置,並有正確的方向
   weaponInUse.transform.localPosition = currentWeaponConfig.gripTransform.localPosition;
   weaponInUse.transform.localRotation = currentWeaponConfig.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;
  }
 
  private void RegisterForMouseClcik(){
   cameraRaycaster = FindObjectOfType<cameraraycaster> ();
   // 註冊滑鼠碰到敵人的事件
   cameraRaycaster.onMouseOverEnemy += OnMouseOverEnemy;
  }
 
  void OnMouseOverEnemy(Enemy enemy){
   currentEnemy = enemy;
 
   if (Input.GetMouseButtonDown (0) && IsTargetInRange (enemy.gameObject)) {
    AttackTarget ();
   }else if (Input.GetMouseButtonDown (1)) {
    // 0為使用第一個技能
    abilities.AttemptSpecialAbility(0, currentEnemy.gameObject);
   }
  }
 
  private void AttackTarget(){
   // 確認本次攻擊時間離上一次攻擊時間須大於minTimeBetweenHits,相當於技能冷卻時間
   if ( (Time.time - lastHitTime > currentWeaponConfig.GetMinTimeBetweenHits())) {
    // 每次攻擊前都重設武器的攻擊動畫
    OverrideAnimatorController();
    // 呼叫Trigger啟動攻擊動畫
    animator.SetTrigger(ATTACK_TRIGGER);
    // 呼叫攻擊Target的方法
    currentEnemy.GetComponent<healthsystem>().TakeDamage(CalculateDamage());
    // 紀錄目前攻擊的時間
    lastHitTime = Time.time;
   }
  }
 
  private float CalculateDamage(){
   // 產生隨機值,比較該值是否小於致命一擊的機率
   bool isCriticalHit = Random.Range (0f, 1f) <= criticalHitChance;
   float damageBeforeCritical = baseDamage + currentWeaponConfig.GetAdditionalDamage ();
   if (isCriticalHit) {
    // 播放致命一擊的特效
    criticalHitParticle.Play ();
    // 回傳攻擊力乘上致命一擊的乘率
    return damageBeforeCritical * criticalHitMultiplier;
   } else {
    return damageBeforeCritical;
   }
  }
 
  // 確認敵人是否在範圍技的攻擊範圍內
  private bool IsTargetInRange(GameObject target){
   float distanceToTarget = (target.transform.position - transform.position).magnitude;
   return distanceToTarget <= currentWeaponConfig.GetMaxAttackRange();
  }
 }
}
</healthsystem></cameraraycaster></dominanthand></animator></healthsystem></specialabilities>

AbilityBehaviour.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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
namespace RPG.Character{
 public abstract class AbilityBehaviour : MonoBehaviour {
 
  protected AbilityConfig config;
 
  const float PARTICLE_CLEAN_UP_DELAY = 20f;
 
  public abstract void Use(GameObject target = null);
 
  public void SetConfig(AbilityConfig config){
   this.config = config;
  }
 
  protected void PlayEffectAudio(){
   // 取得音效Component
   AudioSource myAudioSource = GetComponent<audiosource>();
   // 從config取得技能音效
   AudioClip audioClip = config.GetAudioClip ();
   // 使用PlayOnShot播放技能音效,可避免和其他技能音效重疊在一起
   myAudioSource.PlayOneShot (audioClip);
  }
 
  protected void PlayParticleEffect(){
   // 初始化粒子系統,綁定到『自己』身上
   GameObject particlePrefab = Instantiate(config.GetParticlePrefab(),
    transform.position,
    Quaternion.identity,
    transform);
   // 啟動粒子系統
   particlePrefab.GetComponent<particlesystem>().Play();
   // 播放完後自我銷毀
   StartCoroutine(DestroyParticleWhenFinished(particlePrefab));
  }
 
  protected void PlayParticleEffectOnTarget(GameObject target){
   // 初始化粒子系統,綁定到『目標』身上
   GameObject particlePrefab = Instantiate(config.GetParticlePrefab(),
    target.transform.position,
    Quaternion.identity,
    target.transform);
   // 啟動粒子系統
   particlePrefab.GetComponent<particlesystem>().Play();
   // 播放完後自我銷毀
   StartCoroutine(DestroyParticleWhenFinished(particlePrefab));
  }
 
  IEnumerator DestroyParticleWhenFinished(GameObject particlePrefab){
   while (particlePrefab.GetComponent<particlesystem> ().isPlaying) {
    yield return new WaitForSeconds (PARTICLE_CLEAN_UP_DELAY);
   }
   Destroy (particlePrefab);
   yield return new WaitForEndOfFrame ();
  }
 }
}
</particlesystem></particlesystem></particlesystem></audiosource>


重構完成後,大家可以看見成功將血量跟技能從Player分離出來,分別為Health System與Special Abilities,這兩個組件也能給Enemy使用。


留言