4-3 Weapon Pickup Points
今天要來介紹撿拾地面的武器的機制~玩家經過放置在地上的武器後,會自動更換成玩家手中的武器。首先,在場景中製作一個空白的GameObject。
取名為Weapon Pickup。
拉進Project視窗中變成Prefab。
新增一個Script名為Weapon Pickup Point,並拉進Weapon Pickup的Prefab中。
接著先向大家介紹WeaponPickPoint中比較特別的程式碼,ExecuteInEditMode,在Class名稱之前加上這段程式碼後,Unity便會在編輯模式中自動執行Update內部的程式碼,執行的時機為編輯中有一些變動時,比如用滑鼠點擊不同的物件就會觸發。
再新增一個Audio Source,用來播放撿拾武器的音效。
再來,我們要修改Player.cs內更換武器的機制,原先於Player.cs中已撰寫了一個方法PutWeaponInHand,只要稍加修改後便能拿來利用。
好的,以下提供Player.cs完整程式碼:
完成!讓我們來看看實際效果如何,於編輯模式中如下圖,會顯示該武器的模型在場景中。
執行遊戲看看,玩家手中拿的還是原本的劍。
經過地面上的鋤頭後,玩家手中的武器成功更換為鋤頭了!
取名為Weapon Pickup。
拉進Project視窗中變成Prefab。
新增一個Script名為Weapon Pickup Point,並拉進Weapon Pickup的Prefab中。
接著先向大家介紹WeaponPickPoint中比較特別的程式碼,ExecuteInEditMode,在Class名稱之前加上這段程式碼後,Unity便會在編輯模式中自動執行Update內部的程式碼,執行的時機為編輯中有一些變動時,比如用滑鼠點擊不同的物件就會觸發。
如下圖,我們撰寫產生武器在場景中的程式。
好的!以下提供大家WeaponPickUp.cs的完整程式碼:
using System.Collections; using System.Collections.Generic; using UnityEngine; using RPG.Character; namespace RPG.Weapons{ // 在編輯模式執行程式碼 [ExecuteInEditMode] public class WeaponPickupPoint : MonoBehaviour { [SerializeField] Weapon weaponConfig; [SerializeField] AudioClip pickupSFX; AudioSource audioSource; void Start () { audioSource = GetComponent(); } void Update () { // 判斷要在編輯模式下才執行程式 if (!Application.isPlaying) { DestroyChildren (); InstantiateWeapon (); } } void DestroyChildren(){ foreach (Transform child in transform) { DestroyImmediate (child.gameObject); } } // 產生預覽用的武器物件於場景中 void InstantiateWeapon(){ GameObject weapon = weaponConfig.GetWeaponPrefab(); weapon.transform.position = Vector3.zero; Instantiate (weapon, gameObject.transform); } // 觸發Trigger時更換玩家的武器 void OnTriggerEnter(){ FindObjectOfType ().PutWeaponInHand(weaponConfig); audioSource.PlayOneShot (pickupSFX); } } }
撰寫好後,請將拉入素材進相關參數,Weapon Config要放進一個武器,Pickup SFX要放進撿拾武器的音效。接著,再請大家新增一個Box Collider,勾選Is Trigger,然後請大家調整Box Collider的位置與武器對應,當玩家觸發到Trigger時便會更換玩家手中的武器。
再新增一個Audio Source,用來播放撿拾武器的音效。
再來,我們要修改Player.cs內更換武器的機制,原先於Player.cs中已撰寫了一個方法PutWeaponInHand,只要稍加修改後便能拿來利用。
好的,以下提供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 currentWeaponConfig; [SerializeField] AbilityConfig[] 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"; const string DEFAULT_ATTACK = "DEFAULT ATTACK"; float lastHitTime; float currentHealthPoint; Enemy currentEnemy; CameraRaycaster cameraRaycaster; AbilityBehaviour[] abilityBehaviour; AudioSource audioSource; Animator animator; GameObject weaponInUse; public float healthAsPercentage{ get{ return currentHealthPoint / maxHealthPoints; } } void Start(){ audioSource = GetComponent(); RegisterForMouseClcik (); SetCurrentMaxHealth (); PutWeaponInHand (currentWeaponConfig); 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(){ 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 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); abilityBehaviour [abilityIndex].Use(abilityParams); } } private void AttackTarget(){ // 確認本次攻擊時間離上一次攻擊時間須大於minTimeBetweenHits,相當於技能冷卻時間 if ( (Time.time - lastHitTime > currentWeaponConfig.GetMinTimeBetweenHits())) { // 每次攻擊前都重設武器的攻擊動畫 OverrideAnimatorController(); // 呼叫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 + 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(); } 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); } } }
完成!讓我們來看看實際效果如何,於編輯模式中如下圖,會顯示該武器的模型在場景中。
執行遊戲看看,玩家手中拿的還是原本的劍。
經過地面上的鋤頭後,玩家手中的武器成功更換為鋤頭了!
留言
張貼留言