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
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 | 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
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 | 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<animator> (); character = GetComponent<character> (); 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<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()); } public void StopRepeatedlyAttack(){ StopAllCoroutines (); } public Weapon GetCurrentWeapon(){ return currentWeaponConfig; } private GameObject RequestDominantHand(){ // 從Children中尋找包含DominantHand的物件 DominantHand[] dominantHand = GetComponentsInChildren<dominanthand> (); // 計算取得有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; } } } } </dominanthand></healthsystem></rigidbody></projectile></healthsystem></healthsystem></character></animator> |
Projectile.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 | 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<healthsystem>(); if (healthSystem) { healthSystem.TakeDamage (damageCaused); } Destroy(gameObject); } } } </healthsystem> |
PlayerMovement.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 | 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<specialabilities> (); character = GetComponent<character> (); weaponSystem = GetComponent<weaponsystem> (); RegisterForMouseClcik (); } void Update(){ float healthAsPercentage = GetComponent<healthsystem> ().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> (); 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 ()); } } } } </cameraraycaster></healthsystem></weaponsystem></character></specialabilities> |
在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即可。
接著再執行遊戲試看看,玩家射箭。
箭慢慢地飛向敵人。
啊!射中了!敵人倒下。
留言
張貼留言