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
Player.cs
AbilityBehaviour.cs
重構完成後,大家可以看見成功將血量跟技能從Player分離出來,分別為Health System與Special Abilities,這兩個組件也能給Enemy使用。
改名以後,Player原本放置Energy的地方會顯示Nothing Selected,請刪除。
重新加入SpecialAbilities。
接著,我們將AbilityConfig.cs中的struct AbilityParams刪除,這個物件太複雜了,而且傳入的參數中包含Enemy,使得我們的技能系統程式侷限在Player攻擊Enemy上,無法讓Enemy也共用技能系統。
刪除以後,會發現使用到AbilityParams的程式碼出現了紅字,請大家統一改成GameObject target。往後被指定攻擊的target可以是Player,也可以是Enemy。
以下列出修改後的SpecialAbilities,Player,AbilityBehaviour。
SpecialAbilities.cs
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
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 ();
RegisterForMouseClcik ();
PutWeaponInHand (currentWeaponConfig);
OverrideAnimatorController ();
}
void Update(){
float healthAsPercentage = GetComponent ().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取代為我們剛剛新增的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的物件的數量
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.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().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();
}
}
}
AbilityBehaviour.cs
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();
// 從config取得技能音效
AudioClip audioClip = config.GetAudioClip ();
// 使用PlayOnShot播放技能音效,可避免和其他技能音效重疊在一起
myAudioSource.PlayOneShot (audioClip);
}
protected void PlayParticleEffect(){
// 初始化粒子系統,綁定到『自己』身上
GameObject particlePrefab = Instantiate(config.GetParticlePrefab(),
transform.position,
Quaternion.identity,
transform);
// 啟動粒子系統
particlePrefab.GetComponent().Play();
// 播放完後自我銷毀
StartCoroutine(DestroyParticleWhenFinished(particlePrefab));
}
protected void PlayParticleEffectOnTarget(GameObject target){
// 初始化粒子系統,綁定到『目標』身上
GameObject particlePrefab = Instantiate(config.GetParticlePrefab(),
target.transform.position,
Quaternion.identity,
target.transform);
// 啟動粒子系統
particlePrefab.GetComponent().Play();
// 播放完後自我銷毀
StartCoroutine(DestroyParticleWhenFinished(particlePrefab));
}
IEnumerator DestroyParticleWhenFinished(GameObject particlePrefab){
while (particlePrefab.GetComponent ().isPlaying) {
yield return new WaitForSeconds (PARTICLE_CLEAN_UP_DELAY);
}
Destroy (particlePrefab);
yield return new WaitForEndOfFrame ();
}
}
}
重構完成後,大家可以看見成功將血量跟技能從Player分離出來,分別為Health System與Special Abilities,這兩個組件也能給Enemy使用。






留言
張貼留言