5-23 Throw Out Weapon Pickup Point When Enemy Died
今天要來製作敵人死亡後會掉落武器的機制,玩家或者敵人經過武器以後,都可以將武器撿起來使用。首先,掉落在地圖上的武器的設計,在以前的文章已經有介紹過了,如有人不清楚請先參考這篇文章:
4-3 Weapon Pickup Points
再來是Weapon.cs需要增加一個屬性,pickupPointPrefab,由於我的遊戲設定是敵人死後,會噴出他手中的武器,所以Weapon Pickup Points的Prefab物件便放在Weapon設定中吧。敵人藉由Weapon.cs決定他手中的武器是什麼,亦藉由Weapon.cs決定他噴出的武器是什麼。
然後,在WeaponSystem.cs增加方法ThrowOutWeaponPickupPoint,從Weapon設定中取得Prefab物件,並用Instantiate產生出來。然後,使用Rigidbody的AddForce方法,讓武器向上然後偏左一點噴出來。偏左一點,是為了避免武器的Collider碰到死掉的敵人,保險一點的話當然是加個檢查最好。
將新設定的WeaponPickupPoint拖到地圖上看看效果,會看見武器在地上旋轉,然後一直有粒子噴射。
因為我是複製之前弓箭的WeaponPickupPoint,所以圖示上還是弓箭。
記得按下Apply。
圖案就會變回矛的樣子了。
如此一來,只要有裝配Lancer Spear這個WeaponConfig的敵人,都會在死後掉落Spear這個武器。為什麼掉落的武器要跟WeaponConfig綁在一起,而非跟敵人綁在一起呢?因為敵人也有可能在中途更換武器(意即更換WeaponConfig),若敵人換了一把劍,但是卻掉落一把矛,實在不合理。所以,將PickupPoint與WeaponConfig綁在一起才能符合遊戲需求。
殺死拿著茅的敵人後。
敵人的身上噴出了一把矛。
玩家經過矛以後,就會更換武器了。大家可能會覺得目前有以下問題:
1. 拿著矛的姿勢怪怪的。
2. 玩家獲得武器以後,應該要將新武器放進Inventory System。
上述問題會再往後的文章跟大家介紹改進方法。
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。
上述問題會再往後的文章跟大家介紹改進方法。
留言
張貼留言