Related Posts Plugin for WordPress, Blogger...

5-23 Throw Out Weapon Pickup Point When Enemy Died

今天要來製作敵人死亡後會掉落武器的機制,玩家或者敵人經過武器以後,都可以將武器撿起來使用。首先,掉落在地圖上的武器的設計,在以前的文章已經有介紹過了,如有人不清楚請先參考這篇文章:

4-3 Weapon Pickup Points

先跟大家解釋一下本次的Weapon Pickup Points有什麼更動,原本的設計使用Trigger,而新的設計要改用普通的Collider,以及RigidBody。這是因為我希望敵人死後,武器會從身體『噴出來』,亦即需要受重力影響,且武器會停留在地面。

若仍用Trigger設計,武器受重力影響後會穿越地面一直往下墜落,所以新的Weapon Pickup Points的Rigidbody設定如下。

接著再來撰寫新的程式碼,首先修改WeaponPickupPoint.cs,增加OnCollisionEnter的方法偵測碰撞,並確認碰撞體擁有WeaponSystem的Component。因為敵人跟玩家都共用WeaponSystem,如此便能替換敵人或者玩家手中的武器了。

再來是Weapon.cs需要增加一個屬性,pickupPointPrefab,由於我的遊戲設定是敵人死後,會噴出他手中的武器,所以Weapon Pickup Points的Prefab物件便放在Weapon設定中吧。敵人藉由Weapon.cs決定他手中的武器是什麼,亦藉由Weapon.cs決定他噴出的武器是什麼。

然後,在WeaponSystem.cs增加方法ThrowOutWeaponPickupPoint,從Weapon設定中取得Prefab物件,並用Instantiate產生出來。然後,使用Rigidbody的AddForce方法,讓武器向上然後偏左一點噴出來。偏左一點,是為了避免武器的Collider碰到死掉的敵人,保險一點的話當然是加個檢查最好。


以下提供本次修改後的完整程式碼。
WeaponPickupPoint.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using RPG.Character;
using RPG.Core;

namespace RPG.Weapons{
 // 在編輯模式執行程式碼
 [ExecuteInEditMode]
 public class WeaponPickupPoint : MonoBehaviour {

  [SerializeField] Weapon weaponConfig;
  [SerializeField] AudioClip pickupSFX;

  [Header("Rotation")]
  [SerializeField] float xRotationsPerMinute = 1f;
  [SerializeField] float yRotationsPerMinute = 1f;
  [SerializeField] float zRotationsPerMinute = 1f;

  [Header("Box Collider")]
  [SerializeField] Vector3 colliderCenter = new Vector3(0, 0.7f, 0);
  [SerializeField] Vector3 colliderSize = new Vector3(1, 1, 1);

  AudioSource audioSource;
  BoxCollider myCollider;

  void Start () {
   if (Application.isPlaying) {
    audioSource = gameObject.AddComponent ();
    myCollider = gameObject.AddComponent ();

    myCollider.center = colliderCenter;
    myCollider.size = colliderSize;
   }
  }
  
  void Update () {
   // 判斷要在編輯模式下才執行程式
   if (!Application.isPlaying) {
    DestroyChildren ();
    InstantiateWeapon ();
   }
   RotateTransform ();
  }

  void DestroyChildren(){
   Transform simulation = transform.GetChild (0);
   foreach (Transform child in simulation.transform) {
    DestroyImmediate (child.gameObject);
   }
  }

  // 產生預覽用的武器物件於場景中
  void InstantiateWeapon(){
   Transform simulation = transform.GetChild (0);
   GameObject weapon = weaponConfig.GetWeaponPrefab();
   weapon.transform.position = Vector3.zero;
   Instantiate (weapon, simulation);
  }

  // 觸發時更換武器
  void OnCollisionEnter(Collision collision){
   WeaponSystem weaponSystem = collision.gameObject.GetComponent ();
   if (weaponSystem) {
    weaponSystem.PutWeaponInHand (weaponConfig);
    audioSource.PlayOneShot (pickupSFX);
    Destroy (gameObject);
   }
  }

  void RotateTransform(){
   Transform simulation = transform.GetChild (0);
   //xRotationsPerMinute為1代表轉1圈,1圈有360度,將圈數乘以360度後再除以60秒,便得出1秒轉幾度
   //Time.deltaTime為Update每次執行Frame經過了幾秒的基本時間單位,故將此單位乘以1秒所需旋轉的度數
   //故能得出每個Frame要旋轉幾度
   float xDegreesPerFrame = (xRotationsPerMinute * 360) / 60 * Time.deltaTime; 
   simulation.RotateAround (simulation.position, simulation.right, xDegreesPerFrame);

   float yDegreesPerFrame = (yRotationsPerMinute * 360) / 60 * Time.deltaTime;
   simulation.RotateAround (simulation.position, simulation.up, yDegreesPerFrame);

   float zDegreesPerFrame = (zRotationsPerMinute * 360) / 60 * Time.deltaTime;
   simulation.RotateAround (simulation.position, simulation.forward, zDegreesPerFrame);
  }
 }
}


Weapon.cs:

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

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

  [Header("Weapon")]
  [SerializeField] GameObject weaponPrefab;
  [SerializeField] Transform gripTransform;
  [SerializeField] AnimationClip attackAnimation;
  [SerializeField] float minTimeBetweenHits = 1f;
  [SerializeField] float maxAttackRange = 5f;
  [SerializeField] float additionalDamage = 10f;
  [SerializeField] float damageDelay = 0.25f;
  [SerializeField] bool rightHand = true;

  [Header("Audio")]
  [SerializeField] AudioClip weaponAudio;

  [Header("Projectile")]
  [SerializeField] GameObject projectilePrefab;
  [SerializeField] Transform projectileSocket;
  [SerializeField] float projectileSpeed;
  [SerializeField] Vector3 aimOffset = new Vector3(0, 1f, 0);

  [Header("Weapon Pickup Point")]
  [SerializeField] GameObject pickupPointPrefab;

  public float GetMinTimeBetweenHits(){
   return minTimeBetweenHits;
  }

  public float GetMaxAttackRange(){
   return maxAttackRange;   
  }

  public Transform GetGripTransform(){
   return gripTransform;
  }

  public GameObject GetWeaponPrefab(){
   return weaponPrefab;
  }

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

  public float GetAdditionalDamage(){
   return additionalDamage;
  }

  public float GetDamageDelay(){
   return damageDelay;
  }

  public bool IsRightHandWeapon(){
   return rightHand;
  }

  public AudioClip GetWeaponAudio(){
   return weaponAudio;
  }

  public GameObject GetProjectilePrefab(){
   return projectilePrefab;
  }

  public Vector3 GetAimOffset(){
   return aimOffset;
  }

  public Transform GetProjectileSocket(){
   return projectileSocket;
  }

  public float GetProjectileSpeed(){
   return projectileSpeed;
  }

  public GameObject GetPickupPointPrefab(){
   return pickupPointPrefab;
  }

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

WeaponSystem.cs:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Assertions;
using RPG.Weapons;

namespace RPG.Character{
 public class WeaponSystem : MonoBehaviour {

  [SerializeField] float baseDamage = 50f;
  [SerializeField] Weapon currentWeaponConfig;
  [Range(0.0f, 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;
  GameObject weaponInUse;
  GameObject target;
  Character character;
  Animator animator;
  AudioSource audioSource;

  void Start () {
   animator = GetComponent ();
   character = GetComponent ();
   audioSource = GetComponent ();

   PutWeaponInHand (currentWeaponConfig);
   OverrideAnimatorController ();
  }

  private void OverrideAnimatorController(){
   if (!character.GetOverrideController ()) {
    Debug.Break ();
    Debug.LogAssertion ("Please provide " + gameObject + "with an animator override controller.");
   } else {
    AnimatorOverrideController animatorOverrideController = 
     character.GetOverrideController ();
    // 將人物的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.GetGripTransform().localPosition;
   weaponInUse.transform.localRotation = currentWeaponConfig.GetGripTransform().localRotation;
  }

  public void AttackTarget(GameObject targetToAttack){
   target = targetToAttack;
   StartCoroutine (AttackTargetRepeatedly ());
  }

  IEnumerator AttackTargetRepeatedly(){
   // 確認攻擊者跟目標都活著
   while (AttackerAndTargetStillAlive()) {
    AnimationClip animationClip = currentWeaponConfig.GetAnimClip ();
    // 將攻擊動畫時長除以加速的倍率,取得正確的時長
    float animationClipTime = animationClip.length / character.GetAnimSpeedMultiplier ();
    // 間隔時間為攻擊動畫時長+武器Config設定的攻擊間隔
    float timeToWait = animationClipTime + 
     currentWeaponConfig.GetMinTimeBetweenHits();

    bool isTimeToHitAgain = Time.time - lastHitTime > timeToWait;

    if (isTimeToHitAgain) {
     AttackTargetOnce ();
     lastHitTime = Time.time;
    }
    yield return new WaitForSeconds (timeToWait);
   }
  }

  private bool AttackerAndTargetStillAlive(){
   bool attackerStillAlive = GetComponent ().healthAsPercentage >= Mathf.Epsilon;
   bool targetStillAlive = target.GetComponent ().healthAsPercentage >= Mathf.Epsilon;
   return attackerStillAlive && targetStillAlive;
  }

  void AttackTargetOnce(){
   // 攻擊時要面向目標
   transform.LookAt(target.transform);
   // 每次攻擊前都重設武器的攻擊動畫
   OverrideAnimatorController();
   animator.SetTrigger(ATTACK_TRIGGER);
   // 判斷武器是否有發射物
   GameObject projectile = currentWeaponConfig.GetProjectilePrefab();
   if (projectile) {
    StartCoroutine (SpawnProjectile(target));
   } else {
    float delay = currentWeaponConfig.GetDamageDelay();
    StartCoroutine(DamageAfterDelay(delay));
   }

  }

  IEnumerator SpawnProjectile(GameObject target){
   yield return new WaitForSeconds(currentWeaponConfig.GetDamageDelay());

   GameObject projectileToUse = currentWeaponConfig.GetProjectilePrefab();
   Vector3 aimOffset = currentWeaponConfig.GetAimOffset ();
   Vector3 projectilePosition = CalculateProjectilePosition ();
   // Quaternion.identity為方向不進行任何轉向
   GameObject newProjectile = Instantiate(projectileToUse, projectilePosition, Quaternion.identity);
   Projectile projectileComponent = newProjectile.GetComponent ();
   projectileComponent.SetDamage(CalculateDamage());
   // 設定Projectile的Shooter為自己
   projectileComponent.SetShooter (gameObject);

   // 計算發射Projectile到target之間的單位向量
   Vector3 unitVectorToPlayer = (target.transform.position + aimOffset - projectilePosition).normalized;
   float projectileSpeed = currentWeaponConfig.GetProjectileSpeed();
   // 將單位向量乘以發射速度,透過velocity將Projectile發射出去吧!
   newProjectile.GetComponent ().velocity = unitVectorToPlayer * projectileSpeed;
  }

  private Vector3 CalculateProjectilePosition(){
   Transform projectileSocket = currentWeaponConfig.GetProjectileSocket ();
   Vector3 aimOffset = new Vector3(0, 1, 0);
   return projectileSocket.position + aimOffset + transform.position;
  }

  IEnumerator DamageAfterDelay(float delay){
   yield return new WaitForSecondsRealtime (delay);
   // 呼叫攻擊Target的方法
   target.GetComponent().TakeDamage(CalculateDamage());
   // 播放攻擊音效
   audioSource.PlayOneShot(currentWeaponConfig.GetWeaponAudio());
  }

  public void StopRepeatedlyAttack(){
   StopAllCoroutines ();
  }

  public Weapon GetCurrentWeapon(){
   return currentWeaponConfig;
  }

  public void ThrowOutWeaponPickupPoint(){
   GameObject pickupPointPrefab = currentWeaponConfig.GetPickupPointPrefab();
   GameObject weaponPickupPoint = Instantiate (pickupPointPrefab);
   weaponPickupPoint.transform.position = transform.position + Vector3.left * 1;
   weaponPickupPoint.GetComponent ()
    .AddForce (Vector3.left * 30 + Vector3.up * 300);
  }

  private GameObject RequestDominantHand(){
   // 從Children中尋找包含DominantHand的物件
   Component[] dominantHand;
   if (currentWeaponConfig.IsRightHandWeapon ()) {
    dominantHand = GetComponentsInChildren ();
   } else {
    dominantHand = GetComponentsInChildren ();
   }
   // 計算取得有DominantHand的物件的數量
   int numberOfDominantHands = dominantHand.Length;
   // 確保該數量不為0
   Assert.AreNotEqual (numberOfDominantHands, 0, "No DominantHand found on player, please add one");
   // 確保該數量不要大於2
   Assert.IsFalse (numberOfDominantHands > 2, "Multiple Dominant script on Player, please remove one");
   // 傳回屬於該DominantHand的GameObject
   return dominantHand [0].gameObject;
  }

  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;
   }
  }
 }
}


接下來,我們來設定新版的WeaponPickupPoint吧,

將新設定的WeaponPickupPoint拖到地圖上看看效果,會看見武器在地上旋轉,然後一直有粒子噴射。

因為我是複製之前弓箭的WeaponPickupPoint,所以圖示上還是弓箭。

記得按下Apply。

 圖案就會變回矛的樣子了。

接著,將SpearPickup拖曳到Lancer Spear的Weapon Config中。

如此一來,只要有裝配Lancer Spear這個WeaponConfig的敵人,都會在死後掉落Spear這個武器。為什麼掉落的武器要跟WeaponConfig綁在一起,而非跟敵人綁在一起呢?因為敵人也有可能在中途更換武器(意即更換WeaponConfig),若敵人換了一把劍,但是卻掉落一把矛,實在不合理。所以,將PickupPoint與WeaponConfig綁在一起才能符合遊戲需求。

殺死拿著茅的敵人後。

敵人的身上噴出了一把矛。
 
玩家經過矛以後,就會更換武器了。大家可能會覺得目前有以下問題:

1. 拿著矛的姿勢怪怪的。
2. 玩家獲得武器以後,應該要將新武器放進Inventory System。
上述問題會再往後的文章跟大家介紹改進方法。


留言