3-9 Special Abilities System Overview
本章要來介紹在Unity實作特殊技能系統,之前的文章已經向大家介紹過如何製作Energy Bar,並使用滑鼠的右鍵進行發動了。但是,舊的做法太過死板,無法動態的新增與更換技能,所以本次要來改進這個問題,依此利用ScriptableObject概念設計新的技能系統,可以在Inspector視窗進行各種客製化與增刪,非常方便。
如果不知道怎麼做Energy Bar,或是不知道什麼是ScriptableObject的話,請先看以下文章:
2-25 Introducing Scriptable Objects
先介紹ISpecialAbility,單純是一個interface定義了Use方法,為的是讓其他的Script呼叫這邊定義的Use方法來發動技能。如不懂interface的妙用,大家可以參考這篇文章:
2-14 Using Interfaces In C#
以下說明為了方便,將程式碼截圖一部份,最後面會提供所有修改的程式碼。
再來是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
SpecialAbilityConfig.cs
PowerAttackConfig.cs
PowerAttackBehaviour.cs
Energy.cs
Player.cs
OK!我相信上述的修改是一個大工程,不過用起來很方便。請大家在Project視窗按右鍵,Create/RPG/Special Ability/Power Attack。
最後回到Player的Inspector視窗,先將Abilities的Size設定為1,並設定成Power Attack L1。進行到這邊大家可以想像到了嗎?如果我需要升級技能,我還可以再新增L2 L3 L4,並修改Extra Damage就能夠簡單增加技能,所以程式寫好以後就讓負責遊戲數值設計的人去折騰技能的數值,不用每次要增加技能就要修改程式碼。
同理,要再新增補血技能、範圍技能,或者給敵人Debuff的技能等等都很簡單,只要如PowerAttackConfig中繼承了SpecialAbilityConfig那樣,大家要再新增HealConfig跟HealHaviour都很快速方便,而且HealHaviour也有Update方法供大家使用。
最後測試遊戲,可以成功用右鍵發動技能攻擊到敵人嘍!以後應該會再新增更多技能,不過技能系統已經介紹給大家了,有興趣的話就自己試試看吧!
如果不知道怎麼做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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace RPG.Character{
public interface ISpecialAbility {
void Use(AbilityParams useParams);
}
}
SpecialAbilityConfig.cs
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
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 ();
behaviourComponent.SetConfig (this);
return behaviourComponent;
}
public float GetExtraDamage(){
return extraDamage;
}
}
}
PowerAttackBehaviour.cs
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
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
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.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取代為我們剛剛新增的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的物件的數量
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 ();
// 取得技能需要的能量消耗量
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);
}
}
}
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方法供大家使用。
最後測試遊戲,可以成功用右鍵發動技能攻擊到敵人嘍!以後應該會再新增更多技能,不過技能系統已經介紹給大家了,有興趣的話就自己試試看吧!













留言
張貼留言