4-15 Finishing The Weapon System
本章節要在Weapon System上實作遠距離攻擊的武器,當裝備的武器為『弓』的時候,必須可以射箭,當裝備的武器為『劍』的時候,則必須為近距離攻擊。實作概念為於Weapon Config中設置Projectile,如有Projectile存在時會將其發射,如不存在則會直接令目標受傷。由於WeaponConfig中有AttackRange參數,所以不管武器是近戰武器或是遠距離武器,都可以共用AttackRange的設定。
實作前,需要先解決『撿拾武器』的Bug,如下圖於OnTriggerEnter時呼叫了FindObjectOfType來尋找WeaponSystem,這段程式碼於重構WeaponSystem前撰寫的,所以當時並不會出問題。但重構後的程式碼,讓Enemy也可以共用WeaponSystem,所以更換武器時有可能更換Enemy的武器。
請如下圖進行修改,將FindObjectOfType的對象改成PlayerMovement,並再使用GetComponent取得WeaponSystem,即可解決這個Bug。
如對撿拾武器的機制不熟悉,可先閱讀之前的文章:
4-3 Weapon Pickup Points
https://rpgcorecombat.blogspot.tw/2018/01/4-3-weapon-pickup-points.html
接著要修正攻擊的Delay時間,如下圖於AttackTargetOnce方法中,我們直接指定delay時間為0.25秒,正確做法應該要從WeaponConfig中取得。
請如下圖在Weapon中定義damageDelay參數,然後再請大家自行於AttackTargetOnce方法中,從Weapon取得damageDelay。如不會修改,後面會再提供完整程式碼給大家參考。
接著要進入正題啦!首先請大家準備好弓跟弓箭的模型。如果沒有的話也沒關係,用把劍,然後用之前製作Projectile時的球球也可以,就當作揮劍的時候會發射劍氣吧!如不清楚Projectile的部分,可以先參考以下文章:
2-15 Enemy Attack Spheres And Spawning Enemy Projectile
https://rpgcorecombat.blogspot.tw/2017/12/2-15-enemy-attack-spheres-and-spawning.html
2-18 Using The Trail Renderer Component
接著,請在Project視窗點右鍵,選擇Create/RPG/Weapon建立一個新的WeaponConfig唷!
取名為Simple Bow,然後新增一個Empty GameObject製作成Prefab,取名為SimpleBowGripPosition,這個Prefab要用來定位角色握住弓的位置。如果製作到這個步驟不清楚的話,請先閱讀以前有關武器系統的文章:
2-25 Introducing Scriptable Objects
接著來撰寫程式碼!這次更改到的程式碼有Weapon、WeaponSystem、Projectile、PlayerMovement。
Weapon.cs
WeaponSystem.cs
Projectile.cs
PlayerMovement.cs
撰寫好程式碼後,請在要發射的3D物件,比方我的範例用的Simple Arrow中,加入RigidBody。
在Collision Detection中選擇Continuous Dynamic。這一步不清楚的人請先看先前的文章:
2-20 Preventing Projectile Pass-Through
https://rpgcorecombat.blogspot.tw/2017/12/2-20-preventing-projectile-pass-through.html
Simple Arrow再新增Box Collider,以及Projectile.cs。
調整完以後拉入Project視窗變成Prefab,並從場景中刪除。
上述步驟做完以後,請大家回到Simple Bow的WeaponConfig中,如下圖依序填寫好所有參數,此時應該可以正確填入所有參數。
此時執行遊戲看看,如果有人發現弓的位置錯了,如下圖。
請將遊戲暫停,調整好弓的位置。
然後使用Copy Component的方式將資訊記錄下來。
接著再執行遊戲試看看,玩家射箭。
箭慢慢地飛向敵人。
啊!射中了!敵人倒下。
實作前,需要先解決『撿拾武器』的Bug,如下圖於OnTriggerEnter時呼叫了FindObjectOfType來尋找WeaponSystem,這段程式碼於重構WeaponSystem前撰寫的,所以當時並不會出問題。但重構後的程式碼,讓Enemy也可以共用WeaponSystem,所以更換武器時有可能更換Enemy的武器。
請如下圖進行修改,將FindObjectOfType的對象改成PlayerMovement,並再使用GetComponent取得WeaponSystem,即可解決這個Bug。
如對撿拾武器的機制不熟悉,可先閱讀之前的文章:
4-3 Weapon Pickup Points
https://rpgcorecombat.blogspot.tw/2018/01/4-3-weapon-pickup-points.html
接著要修正攻擊的Delay時間,如下圖於AttackTargetOnce方法中,我們直接指定delay時間為0.25秒,正確做法應該要從WeaponConfig中取得。
請如下圖在Weapon中定義damageDelay參數,然後再請大家自行於AttackTargetOnce方法中,從Weapon取得damageDelay。如不會修改,後面會再提供完整程式碼給大家參考。
接著要進入正題啦!首先請大家準備好弓跟弓箭的模型。如果沒有的話也沒關係,用把劍,然後用之前製作Projectile時的球球也可以,就當作揮劍的時候會發射劍氣吧!如不清楚Projectile的部分,可以先參考以下文章:
2-15 Enemy Attack Spheres And Spawning Enemy Projectile
https://rpgcorecombat.blogspot.tw/2017/12/2-15-enemy-attack-spheres-and-spawning.html
2-18 Using The Trail Renderer Component
接著,請在Project視窗點右鍵,選擇Create/RPG/Weapon建立一個新的WeaponConfig唷!
取名為Simple Bow,然後新增一個Empty GameObject製作成Prefab,取名為SimpleBowGripPosition,這個Prefab要用來定位角色握住弓的位置。如果製作到這個步驟不清楚的話,請先閱讀以前有關武器系統的文章:
2-25 Introducing Scriptable Objects
2-26 Problems With Prefabs
2-27 Import Mechanim Animation Pack
3-2 The Animator Override Controller
接著來撰寫程式碼!這次更改到的程式碼有Weapon、WeaponSystem、Projectile、PlayerMovement。
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; [Header("Projectile")] [SerializeField] GameObject projectilePrefab; [SerializeField] Transform projectileSocket; [SerializeField] float projectileSpeed; [SerializeField] Vector3 aimOffset = new Vector3(0, 1f, 0); 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 GameObject GetProjectilePrefab(){ return projectilePrefab; } public Vector3 GetAimOffset(){ return aimOffset; } public Transform GetProjectileSocket(){ return projectileSocket; } public float GetProjectileSpeed(){ return projectileSpeed; } // 避免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(.1f, 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; void Start () { animator = GetComponent(); character = 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()) { float weaponHitPeriod = currentWeaponConfig.GetMinTimeBetweenHits (); // 將動畫加速的倍率算進等待時間中 float timeToWait = weaponHitPeriod * character.GetAnimSpeedMultiplier(); 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()); } public void StopRepeatedlyAttack(){ StopAllCoroutines (); } public Weapon GetCurrentWeapon(){ return currentWeaponConfig; } 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 float CalculateDamage(){ // 產生隨機值,比較該值是否小於致命一擊的機率 bool isCriticalHit = Random.Range (0f, 1f) <= criticalHitChance; float damageBeforeCritical = baseDamage + currentWeaponConfig.GetAdditionalDamage (); if (isCriticalHit) { // 播放致命一擊的特效 criticalHitParticle.Play (); // 回傳攻擊力乘上致命一擊的乘率 return damageBeforeCritical * criticalHitMultiplier; } else { return damageBeforeCritical; } } } }
Projectile.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using RPG.Core; using RPG.Character; namespace RPG.Weapons{ public class Projectile : MonoBehaviour { GameObject shooter; float damageCaused; public void SetShooter(GameObject shooter){ this.shooter = shooter; } public void SetDamage(float damage){ damageCaused = damage; } void OnCollisionEnter(Collision collision){ int layerCollidedWith = collision.gameObject.layer; // 若被碰撞的物體所處的Layer跟shooter的Layer不同,才會觸發攻擊機制 // 如敵人就不會攻擊到敵人 if (shooter && layerCollidedWith != shooter.layer) { DamageIfDamageable (collision); } } private void DamageIfDamageable(Collision collision){ // 取得HealthSystem HealthSystem healthSystem = collision.gameObject.GetComponent(); if (healthSystem) { healthSystem.TakeDamage (damageCaused); } Destroy(gameObject); } } }
PlayerMovement.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; using RPG.CameraUI; using RPG.Core; using RPG.Weapons; namespace RPG.Character{ public class PlayerMovement : MonoBehaviour { [SerializeField] float walkMoveStopRadius = 0.2f; EnemyAI currentEnemy; Character character; SpecialAbilities abilities; WeaponSystem weaponSystem; void Start(){ abilities = GetComponent(); character = GetComponent (); weaponSystem = GetComponent (); RegisterForMouseClcik (); } void Update(){ float healthAsPercentage = GetComponent ().healthAsPercentage; if (healthAsPercentage > Mathf.Epsilon) { ScanForAbilityKeyDown (); } } private void ScanForAbilityKeyDown(){ for (int keyIndex = 1; keyIndex <= abilities.GetNumberOfAbilities(); keyIndex++) { if (Input.GetKeyDown (keyIndex.ToString())) { if (currentEnemy) { abilities.AttemptSpecialAbility (keyIndex, currentEnemy.gameObject); } else { abilities.AttemptSpecialAbility (keyIndex); } } } } private void RegisterForMouseClcik(){ CameraRaycaster cameraRaycaster = FindObjectOfType (); cameraRaycaster.onMouseOverEnemy += OnMouseOverEnemy; cameraRaycaster.onMouseOverWalkable += OnMouseOverWalkable; } void OnMouseOverEnemy(EnemyAI enemy){ currentEnemy = enemy; if (Input.GetMouseButton (0)) { // 普通攻擊 StartCoroutine (MoveAndAttack (enemy)); } else if (Input.GetMouseButtonDown (1)) { // 技能-強力攻擊 StartCoroutine (MoveAndPowerAttack (enemy)); } } IEnumerator MoveToTarget(GameObject target){ // 設置地點為Enemy的位置 character.SetDestination(target.transform.position); character.SetStopDistance(weaponSystem.GetCurrentWeapon().GetMaxAttackRange()); while (!IsTargetInRange (target)) { yield return new WaitForEndOfFrame (); } yield return new WaitForEndOfFrame (); } IEnumerator MoveAndAttack(EnemyAI target){ // 先移動到目標位置 yield return StartCoroutine (MoveToTarget (target.gameObject)); // 鎖定Enemy後會自動攻擊 weaponSystem.AttackTarget (target.gameObject); } IEnumerator MoveAndPowerAttack(EnemyAI target){ // 先移動到目標位置 yield return StartCoroutine (MoveToTarget (target.gameObject)); // 0為使用第一個技能 abilities.AttemptSpecialAbility(0, currentEnemy.gameObject); } void OnMouseOverWalkable(Vector3 destination){ if (Input.GetMouseButton (0)) { // 要求停止自動攻擊 weaponSystem.StopRepeatedlyAttack(); // 設置地點為滑鼠點擊的位置 character.SetDestination(destination); character.SetStopDistance (walkMoveStopRadius); } } // 確認敵人是否在範圍技的攻擊範圍內 private bool IsTargetInRange(GameObject target){ float distanceToTarget = (target.transform.position - transform.position).magnitude; return distanceToTarget <= weaponSystem.GetCurrentWeapon().GetMaxAttackRange(); } void OnDrawGizmos(){ if (Application.isPlaying) { //繪製攻擊範圍 Gizmos.color = new Color (255f, 0f, 0f, 0.5f); Gizmos.DrawWireSphere (transform.position, weaponSystem.GetCurrentWeapon ().GetMaxAttackRange ()); } } } }
在Collision Detection中選擇Continuous Dynamic。這一步不清楚的人請先看先前的文章:
2-20 Preventing Projectile Pass-Through
https://rpgcorecombat.blogspot.tw/2017/12/2-20-preventing-projectile-pass-through.html
Simple Arrow再新增Box Collider,以及Projectile.cs。
接著在Player底下新增一個Empty GameObject,名為ProjectileSocket,這是用來定位Projectile發射位置的座標。
請如下圖自己在場景中調整發射位置。
調整完以後拉入Project視窗變成Prefab,並從場景中刪除。
上述步驟做完以後,請大家回到Simple Bow的WeaponConfig中,如下圖依序填寫好所有參數,此時應該可以正確填入所有參數。
此時執行遊戲看看,如果有人發現弓的位置錯了,如下圖。
請將遊戲暫停,調整好弓的位置。
然後使用Copy Component的方式將資訊記錄下來。
回到SimpleBowGripPosition使用Paste Component Values即可。
接著再執行遊戲試看看,玩家射箭。
箭慢慢地飛向敵人。
啊!射中了!敵人倒下。
留言
張貼留言