Related Posts Plugin for WordPress, Blogger...

3-22 Critical Hit And Weapon Bonus

本章要來替Player的武器增加攻擊力的計算,以及增加發動致命一擊的機率。致命一擊是指普通攻擊之中,有機率揮出超強攻擊的一擊,為普通攻擊的好幾倍,又可以稱為會心一擊、爆擊等等。

首先,將我們想要用在致命一擊的粒子效果放到Player之下。

然後讓我們調整一下粒子系統運作的位置、大小。

記得將Looping跟Play On Awake都關掉。

且有些從Asset Store下載的粒子系統同時包含火焰、煙霧等等,所以在子階層的部分還會有其他粒子系統,記得這邊也要關掉Loop。

然後將調整好的Critical Hit Particles拉進Project變成Prefab。

程式碼主要修改在Player跟Weapon,來看看Player主要修改計算攻擊值的部分。使用Random.Range方法計算是否會出現致命一擊,再將傷害值乘以criticalHitMultiplier,並播放特效。

以下提供完整程式碼。
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, IDamageable {

  [SerializeField] float maxHealthPoints = 100f;
  [SerializeField] float baseDamage = 50f;
  [SerializeField] AnimatorOverrideController animatorOverrideController;
  [SerializeField] Weapon weaponInUse;
  [SerializeField] SpecialAbilityConfig[] abilities;
  [SerializeField] AudioClip[] damageSounds;
  [SerializeField] AudioClip[] deathSounds;
  [Range(.1f, 1.0f)] [SerializeField] float criticalHitChance = 0.1f;
  [SerializeField] float criticalHitMultiplier = 2.5f;
  [SerializeField] ParticleSystem criticalHitParticle;

  // 定義Trigger的字串常數
  const string DEATH_TRIGGER = "Death";
  const string ATTACK_TRIGGER = "Attack";

  float lastHitTime;
  float currentHealthPoint;
  Enemy currentEnemy;
  CameraRaycaster cameraRaycaster;
  ISpecialAbility[] specialAbility;
  AudioSource audioSource;
  Animator animator;

  public float healthAsPercentage{
   get{ return currentHealthPoint / maxHealthPoints; }
  }

  void Start(){
   audioSource = GetComponent ();

   RegisterForMouseClcik ();
   SetCurrentMaxHealth ();
   PutWeaponInHand ();
   OverrideAnimatorController ();
   AttachSpecialAbility();
  }

  void Update(){
   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(){
   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;
  }

  private void SetCurrentMaxHealth(){
   currentHealthPoint = maxHealthPoints;
  }

  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);
    specialAbility [abilityIndex].Use(abilityParams);
   }

  }

  private void AttackTarget(){
   // 確認本次攻擊時間離上一次攻擊時間須大於minTimeBetweenHits,相當於技能冷卻時間
   if ( (Time.time - lastHitTime > weaponInUse.GetMinTimeBetweenHits())) {
    // 呼叫Trigger啟動攻擊動畫
    animator.SetTrigger(ATTACK_TRIGGER);
    // 呼叫攻擊Target的方法
    currentEnemy.TakeDamage(CalculateDamage());
    // 紀錄目前攻擊的時間
    lastHitTime = Time.time;
   }
  }

  private float CalculateDamage(){
   // 產生隨機值,比較該值是否小於致命一擊的機率
   bool isCriticalHit = Random.Range (0f, 1f) <= criticalHitChance;
   float damageBeforeCritical = baseDamage + weaponInUse.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 <= weaponInUse.GetMaxAttackRange();
  }

  public void TakeDamage(float damage){
   // Math.Clamp可以確保數值運算後不會低於最小值或者高於最大值
   currentHealthPoint = Mathf.Clamp (currentHealthPoint - damage, 0f, maxHealthPoints);
   // 隨機播放受傷的音效
   audioSource.clip = damageSounds [Random.Range (0, damageSounds.Length)];
   audioSource.Play ();
   bool playerDies = currentHealthPoint <= 0;
   if (playerDies) {
    StartCoroutine (KillPlayer ());
   }
  }

  public void Heal(float points){
   currentHealthPoint = Mathf.Clamp (currentHealthPoint + points, 0f, maxHealthPoints);
  }

  IEnumerator KillPlayer(){
   // 播放死亡音效
   audioSource.clip = deathSounds [Random.Range (0, deathSounds.Length)];
   audioSource.Play ();
   // 播放死亡動畫
   animator.SetTrigger(DEATH_TRIGGER);
   // 等待一段時間(依音效長度而定)
   yield return new WaitForSecondsRealtime(audioSource.clip.length);
   // 重載關卡
   SceneManager.LoadScene(0);
  }
 }
}

Weapon.cs:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace RPG.Weapons{
 [CreateAssetMenu(menuName="RPG/Weapon")]
 public class Weapon : ScriptableObject {

  public Transform gripTransform;

  [SerializeField] GameObject weaponPrefab;
  [SerializeField] AnimationClip attackAnimation;
  [SerializeField] float minTimeBetweenHits = 1f;
  [SerializeField] float maxAttackRange = 5f;
  [SerializeField] float additionalDamage = 10f;

  public float GetMinTimeBetweenHits(){
   return minTimeBetweenHits;
  }

  public float GetMaxAttackRange(){
   return maxAttackRange;   
  }

  public GameObject GetWeaponPrefab(){
   return weaponPrefab;
  }

  public AnimationClip GetAnimClip(){
   RemoveAnimationEvent ();
   return attackAnimation;
  }

  public float GetAdditionalDamage(){
   return additionalDamage;
  }

  // 避免Asset Pack造成我們的遊戲Crash
  private void RemoveAnimationEvent(){
   attackAnimation.events = new AnimationEvent[0];
  }
 }
}

撰寫好以後,回到Player的設定視窗,讓我們先將Critical Hit Chance調整成1,方便我們測試攻擊時發動致命一擊的效果,並將Player底下的Critical Hit Particles拉進Critical Hit Particle設定。

 來測試遊戲看看吧!攻擊後會確實發動致命一擊,酷炫的火焰特效也有正確發動。


留言