4-7 Extracting a DamageSystem Component
這次要重構IDamageable跟HealthOrb,主要是希望將血量、傷害等方法跟Player分離出來,統一放置於新的類別HealthSystem中,並讓Player跟Enemy兩方都能共用。首先,於Character資料夾內新增HealthSystem。
接著將有實作IDamageable的地方都刪掉,如Player跟Enemy,同時也需要將覆寫的方法TakeDamage一併刪除。
接著刪除IDamageable.cs檔案。
PlayerHealthBar.cs檔案也不需要了,請刪除。
接著撰寫HealthSystem.cs,主要包含血量、受傷、死亡、控制血量條等方法都統一寫在這個類別中,完整程式碼如下:
由於原本在Player跟Enemy中的TakeDamage方法不見了,血量屬性也消失了,大家應該於Console視窗中會看見如下圖一大堆的錯誤訊息。
所以接下來大致修改方向就是使用GetComponent<HealthSystem>去取得血量,或是呼叫TakeDamage方法,如下圖。
如下圖Enemy.GetComponent<HealthSystem>().TakeDamage就可以對敵人造成傷害了。
給玩家補血的Heal方法也同樣放在HealthSystem中。
最後記得將Health System放進Player中,並設定相關參數唷!
接著將有實作IDamageable的地方都刪掉,如Player跟Enemy,同時也需要將覆寫的方法TakeDamage一併刪除。
接著刪除IDamageable.cs檔案。
PlayerHealthBar.cs檔案也不需要了,請刪除。
接著撰寫HealthSystem.cs,主要包含血量、受傷、死亡、控制血量條等方法都統一寫在這個類別中,完整程式碼如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
namespace RPG.Character{
public class HealthSystem : MonoBehaviour {
[SerializeField] float maxHealthPoints = 800f;
[SerializeField] Image healthOrb;
[SerializeField] AudioClip[] damageSounds;
[SerializeField] AudioClip[] deathSounds;
const string DEATH_TRIGGER = "Death";
float currentHealthPoint = 0;
Animator animator;
AudioSource audioSource;
CharacterMovement characterMovement;
public float healthAsPercentage{
get{ return currentHealthPoint / maxHealthPoints; }
}
void Start () {
animator = GetComponent ();
audioSource = GetComponent ();
characterMovement = GetComponent ();
SetCurrentMaxHealth ();
}
// Update is called once per frame
void Update () {
UpdateHealthOrb ();
}
public void Heal(float points){
currentHealthPoint = Mathf.Clamp (currentHealthPoint + points, 0f, maxHealthPoints);
}
public void TakeDamage(float damage){
// Math.Clamp可以確保數值運算後不會低於最小值或者高於最大值
currentHealthPoint = Mathf.Clamp (currentHealthPoint - damage, 0f, maxHealthPoints);
// 隨機播放受傷的音效
AudioClip clip = damageSounds [Random.Range (0, damageSounds.Length)];
audioSource.PlayOneShot (clip);
bool characterDies = currentHealthPoint <= 0;
if (characterDies) {
StartCoroutine (KillCharacter ());
}
}
IEnumerator KillCharacter(){
// 阻止Enemy在Player已經死亡的狀態下仍繼續攻擊
StopAllCoroutines ();
// 阻止死亡後仍可以繼續移動
characterMovement.Kill ();
// 播放死亡動畫
animator.SetTrigger (DEATH_TRIGGER);
// 播放死亡音效
audioSource.clip = deathSounds [Random.Range (0, deathSounds.Length)];
audioSource.Play ();
// 等待一段時間(依音效長度而定)
yield return new WaitForSecondsRealtime (audioSource.clip.length);
Player player = GetComponent ();
if (player && player.isActiveAndEnabled) {
// 玩家死亡需重載關卡
SceneManager.LoadScene (0);
} else {
// 敵人死亡要銷毀物件
Destroy(gameObject);
}
}
private void UpdateHealthOrb(){
if (healthOrb) {
healthOrb.fillAmount = healthAsPercentage;
}
}
private void SetCurrentMaxHealth(){
currentHealthPoint = maxHealthPoints;
}
}
}
由於原本在Player跟Enemy中的TakeDamage方法不見了,血量屬性也消失了,大家應該於Console視窗中會看見如下圖一大堆的錯誤訊息。
所以接下來大致修改方向就是使用GetComponent<HealthSystem>去取得血量,或是呼叫TakeDamage方法,如下圖。
如下圖Enemy.GetComponent<HealthSystem>().TakeDamage就可以對敵人造成傷害了。
給玩家補血的Heal方法也同樣放在HealthSystem中。
最後記得將Health System放進Player中,並設定相關參數唷!
以下提供重構後的程式碼給大家參考:
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;
[SerializeField] AbilityConfig[] abilities;
[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;
AbilityBehaviour[] abilityBehaviour;
Animator animator;
GameObject weaponInUse;
void Start(){
RegisterForMouseClcik ();
PutWeaponInHand (currentWeaponConfig);
OverrideAnimatorController ();
AttachSpecialAbility();
}
void Update(){
float healthAsPercentage = GetComponent ().healthAsPercentage;
if (healthAsPercentage > Mathf.Epsilon) {
ScanForAbilityKeyDown ();
}
}
private void ScanForAbilityKeyDown(){
for (int keyIndex = 1; keyIndex <= abilities.Length; keyIndex++) {
if (Input.GetKeyDown (keyIndex.ToString())) {
AttemptSpecialAbility (keyIndex);
}
}
}
private void AttachSpecialAbility(){
abilityBehaviour = new AbilityBehaviour[abilities.Length];
for (int i = 0; i < abilities.Length; i++) {
// 儲存ISpecialAbility物件
abilityBehaviour[i] = abilities [i].AttachAbilityTo (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] = 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為使用第一個技能
AttemptSpecialAbility(0);
}
}
private void AttemptSpecialAbility(int abilityIndex){
Energy energyComponent = GetComponent ();
// 取得技能需要的能量消耗量
float energyCost = abilities [abilityIndex].GetEnergyCost ();
if (energyComponent.IsEnergyAvailable (energyCost)) {
energyComponent.ConsumeEnergy (energyCost);
// 發動技能,並傳入Player的baseDamage
AbilityParams abilityParams = new AbilityParams (currentEnemy, baseDamage);
abilityBehaviour [abilityIndex].Use(abilityParams);
}
}
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();
}
}
}
Enemy.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using RPG.Core;
using RPG.Weapons;
namespace RPG.Character{
public class Enemy : MonoBehaviour {
[SerializeField] float attackRadius = 3.0f;
[SerializeField] float chaseRadius = 10.0f;
[SerializeField] float damagePerShot = 9f;
[SerializeField] float firingPeriodInS = 0.5f;
[SerializeField] float firingPeriodVariation = 0.1f;
[SerializeField] GameObject projectileToUse;
[SerializeField] GameObject projectileSocket;
[SerializeField] Vector3 aimOffset = new Vector3(0, 1f, 0);
bool isAttacking = false;
float currentHealthPoint;
Player player;
IEnumerator coroutine;
WaitForSeconds waitForShots;
void Start(){
player = GameObject.FindObjectOfType();
float randomisedDelay = Random.Range (firingPeriodInS - firingPeriodVariation, firingPeriodInS + firingPeriodVariation);
waitForShots = new WaitForSeconds (randomisedDelay);
}
void Update(){
// 計算Player跟Enemy的距離
float distanceToPlayer = Vector3.Distance (player.transform.position, transform.position);
// 若彼此之間的距離小於attackRadius,就讓Enemy開始攻擊Player
if (distanceToPlayer <= attackRadius && !isAttacking) {
isAttacking = true;
coroutine = SpawnProjectile ();
StartCoroutine (coroutine);
}
if (distanceToPlayer > attackRadius && isAttacking) {
isAttacking = false;
StopCoroutine (coroutine);
}
// 若彼此之間的距離小於chaseRadius,就讓Enemy開始追蹤Player
if (distanceToPlayer <= chaseRadius) {
// aiCharacterControl.SetTarget (player.transform);
} else {
// aiCharacterControl.SetTarget (transform);
}
}
IEnumerator SpawnProjectile(){
while (true) {
yield return waitForShots;
// Instantiate可以生成GameObject,Quaternion.identity為方向不進行任何轉向
GameObject newProjectile = Instantiate(projectileToUse, projectileSocket.transform.position, Quaternion.identity);
// 取得Projectile
Projectile projectileComponent = newProjectile.GetComponent ();
// 設定Projectile的攻擊威力
projectileComponent.SetDamage(damagePerShot);
// 設定Projectile的Shooter為自己
projectileComponent.SetShooter (gameObject);
// 計算發射Projectile到Player之間的單位向量
Vector3 unitVectorToPlayer = (player.transform.position + aimOffset - projectileSocket.transform.position).normalized;
float projectileSpeed = projectileComponent.GetDefaultLaunchSpeed();
// 將單位向量乘以發射速度,透過velocity將Projectile發射出去吧!
newProjectile.GetComponent ().velocity = unitVectorToPlayer * projectileSpeed;
}
}
void OnDrawGizmos(){
//繪製攻擊範圍
Gizmos.color = new Color(255f, 0f, 0f, 0.5f);
Gizmos.DrawWireSphere (transform.position, attackRadius);
//繪製移動範圍
Gizmos.color = new Color(0f, 0f, 255f, 0.5f);
Gizmos.DrawWireSphere (transform.position, chaseRadius);
}
}
}
CharacterMovement.cs
using System;
using UnityEngine;
using UnityEngine.AI;
using RPG.CameraUI;
namespace RPG.Character{
[RequireComponent(typeof (NavMeshAgent))]
public class CharacterMovement : MonoBehaviour
{
[SerializeField] float walkMoveStopRadius = 0.2f;
[SerializeField] float attackMoveStopRadius = 5f;
[SerializeField] float moveSpeedMultiplier = 1f;
[SerializeField] float animationSpeedMultiplier = 1f;
[SerializeField] float movingTurnSpeed = 1800;
[SerializeField] float stationaryTurnSpeed = 1800;
[SerializeField] float moveThreshold = 1f;
NavMeshAgent agent = null;
Animator animator = null;
Rigidbody myRigidbody = null;
float turnAmount;
float forwardAmount;
private void Start()
{
CameraRaycaster cameraRaycaster = Camera.main.GetComponent();
animator = GetComponent ();
myRigidbody = GetComponent ();
myRigidbody.constraints = RigidbodyConstraints.FreezeRotation;
agent = GetComponent ();
agent.updateRotation = false;
agent.updatePosition = true;
cameraRaycaster.onMouseOverWalkable += OnMouseOverWalkable;
cameraRaycaster.onMouseOverEnemy += OnMouseOverEnemy;
}
public void Move(Vector3 movement)
{
SetForwardAndTurn (movement);
ApplyExtraTurnRotation();
UpdateAnimator();
}
public void Kill(){
// TODO 需要禁止角色移動
}
private void SetForwardAndTurn(Vector3 movement){
// 將Wolrd相對的向量轉換成Local相對的向量
if (movement.magnitude > moveThreshold) {
movement.Normalize ();
}
// 將Worldspace的方向轉換成LocalSpace
Vector3 localMovement = transform.InverseTransformDirection(movement);
turnAmount = Mathf.Atan2(localMovement.x, localMovement.z);
forwardAmount = localMovement.z;
}
void UpdateAnimator()
{
// update the animator parameters
animator.SetFloat("Forward", forwardAmount, 0.1f, Time.deltaTime);
animator.SetFloat("Turn", turnAmount, 0.1f, Time.deltaTime);
animator.speed = animationSpeedMultiplier;
}
void ApplyExtraTurnRotation()
{
// help the character turn faster (this is in addition to root rotation in the animation)
float turnSpeed = Mathf.Lerp(stationaryTurnSpeed, movingTurnSpeed, forwardAmount);
transform.Rotate(0, turnAmount * turnSpeed * Time.deltaTime, 0);
}
void Update(){
if (agent.remainingDistance > agent.stoppingDistance) {
Move(agent.desiredVelocity);
} else {
Move (Vector3.zero);
}
}
void OnMouseOverWalkable(Vector3 destination){
if (Input.GetMouseButton (0)) {
// 設置地點為滑鼠點擊的位置
agent.SetDestination(destination);
agent.stoppingDistance = walkMoveStopRadius;
}
}
void OnMouseOverEnemy(Enemy enemy){
if (Input.GetMouseButton (0)) {
// 設置地點為Enemy的位置
agent.SetDestination(enemy.transform.position);
agent.stoppingDistance = attackMoveStopRadius;
}
}
void OnAnimatorMove(){
if (Time.deltaTime > 0) {
Vector3 velocity = (animator.deltaPosition * moveSpeedMultiplier) / Time.deltaTime;
// 保持y軸的速度不變
velocity.y = myRigidbody.velocity.y;
myRigidbody.velocity = velocity;
}
}
void OnDrawGizmos(){
//繪製攻擊範圍
Gizmos.color = new Color(255f, 0f, 0f, 0.5f);
Gizmos.DrawWireSphere (transform.position, attackMoveStopRadius);
}
}
}
Projectile.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using RPG.Core;
using RPG.Character;
namespace RPG.Weapons{
public class Projectile : MonoBehaviour {
[SerializeField] float projectileSpeed;
[SerializeField] GameObject shooter;
float damageCaused;
public void SetShooter(GameObject shooter){
this.shooter = shooter;
}
public void SetDamage(float damage){
damageCaused = damage;
}
public float GetDefaultLaunchSpeed(){
return projectileSpeed;
}
void OnCollisionEnter(Collision collision){
int layerCollidedWith = collision.gameObject.layer;
// 若被碰撞的物體所處的Layer跟shooter的Layer不同,才會觸發攻擊機制
// 如敵人就不會攻擊到敵人
if (shooter && layerCollidedWith != shooter.layer) {
DamageIfDamageable (collision);
}
}
private void DamageIfDamageable(Collision collision){
// 取得HealthSystem
HealthSystem healthSystem = collision.gameObject.GetComponent();
if (healthSystem) {
healthSystem.TakeDamage (damageCaused);
}
Destroy(gameObject);
}
}
}
PowerAttackBehaviour.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace RPG.Character{
public class PowerAttackBehaviour : AbilityBehaviour{
public override void Use(AbilityParams useParams){
DealDamage (useParams);
PlayParticleEffectOnTarget (useParams);
PlayEffectAudio ();
}
private void DealDamage(AbilityParams useParams){
float damageToDeal = (config as PowerAttackConfig).GetExtraDamage () + useParams.baseDamage;
useParams.target.GetComponent().TakeDamage (damageToDeal);
}
}
}
SelfHealBehaviour.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using RPG.Core;
namespace RPG.Character{
public class SelfHealBehaviour : AbilityBehaviour {
Player player;
void Start(){
player = GetComponent ();
}
public override void Use(AbilityParams useParams){
player.GetComponent ().Heal ((config as SelfHealConfig).GetExtraHealth ());
PlayParticleEffect ();
PlayEffectAudio ();
}
}
}
EnemyHealthBar.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace RPG.Character{
public class EnemyHealthBar : MonoBehaviour
{
RawImage healthBarRawImage = null;
Enemy enemy = null;
void Start()
{
enemy = GetComponentInParent(); // Different to way player's health bar finds player
healthBarRawImage = GetComponent();
}
void Update()
{
float healthAsPercentage = enemy.GetComponent ().healthAsPercentage;
float xValue = -(healthAsPercentage / 2f) - 0.5f;
healthBarRawImage.uvRect = new Rect(xValue, 0f, 0.5f, 1f);
}
}
}









留言
張貼留言