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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | 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<audiosource> (); myCollider = gameObject.AddComponent<boxcollider> (); 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<weaponsystem> (); 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); } } } </weaponsystem></boxcollider></audiosource> |
Weapon.cs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | 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<animator> (); character = GetComponent<character> (); audioSource = GetComponent<audiosource> (); 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<healthsystem> ().healthAsPercentage >= Mathf.Epsilon; bool targetStillAlive = target.GetComponent<healthsystem> ().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<projectile> (); 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<rigidbody> ().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<healthsystem>().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<rigidbody> () .AddForce (Vector3.left * 30 + Vector3.up * 300); } private GameObject RequestDominantHand(){ // 從Children中尋找包含DominantHand的物件 Component[] dominantHand; if (currentWeaponConfig.IsRightHandWeapon ()) { dominantHand = GetComponentsInChildren<dominantrighthand> (); } else { dominantHand = GetComponentsInChildren<dominantlefthand> (); } // 計算取得有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; } } } } </dominantlefthand></dominantrighthand></rigidbody></healthsystem></rigidbody></projectile></healthsystem></healthsystem></audiosource></character></animator> |
接下來,我們來設定新版的WeaponPickupPoint吧,
將新設定的WeaponPickupPoint拖到地圖上看看效果,會看見武器在地上旋轉,然後一直有粒子噴射。
因為我是複製之前弓箭的WeaponPickupPoint,所以圖示上還是弓箭。
記得按下Apply。
圖案就會變回矛的樣子了。
接著,將SpearPickup拖曳到Lancer Spear的Weapon Config中。
如此一來,只要有裝配Lancer Spear這個WeaponConfig的敵人,都會在死後掉落Spear這個武器。為什麼掉落的武器要跟WeaponConfig綁在一起,而非跟敵人綁在一起呢?因為敵人也有可能在中途更換武器(意即更換WeaponConfig),若敵人換了一把劍,但是卻掉落一把矛,實在不合理。所以,將PickupPoint與WeaponConfig綁在一起才能符合遊戲需求。
殺死拿著茅的敵人後。
敵人的身上噴出了一把矛。
玩家經過矛以後,就會更換武器了。大家可能會覺得目前有以下問題:
1. 拿著矛的姿勢怪怪的。
2. 玩家獲得武器以後,應該要將新武器放進Inventory System。
上述問題會再往後的文章跟大家介紹改進方法。
留言
張貼留言