3-8 Simplifying Click To Move And Click To Attack
本章主要是來重構一下我們的程式碼,由於過去的大幅增修使得CameraRaycaster過於繁瑣,這次要大幅簡化CameraRaycaster的程式碼。這章不會有太多的解說,總之重構的方向就是將Mouse Click的部分更專注於Walkable跟Enemy。
因為舊版的CameraRaycaster考量到新增Layer的需求,撰寫上多了很多額外的程式碼,原意上是為了未來增加Layer的彈性但如今卻是程式碼繁雜的主因。目前認為Layer只會分成Walkable跟Player,甚至連原本的Enemy Layer都會刪除。
如對舊版的CameraRaycaster有興趣,可以看以前的文章:
2-10 Fully Event Based Raycasting
新版的CameraRaycaster將會使用兩個委派方法,OnMouseOverWalkable及OnMouseOverEnemy,好處將如下圖所示。PlayerMovement.cs省去了Switch方法,提高程式碼的可讀性。
同樣Energy.cs的程式也略為修改。
Player.cs的部分則因OnMouseOverEnemy回傳Enemy物件,使得程式碼更加簡潔。
另外的好處是,CursorAffordance.cs已經不需要了。那麼,接下來提供這次重構改掉的程式碼:
CameraRaycaster.cs:
PlayerMovement.cs:
Energy.cs:
Player.cs:
因為舊版的CameraRaycaster考量到新增Layer的需求,撰寫上多了很多額外的程式碼,原意上是為了未來增加Layer的彈性但如今卻是程式碼繁雜的主因。目前認為Layer只會分成Walkable跟Player,甚至連原本的Enemy Layer都會刪除。
如對舊版的CameraRaycaster有興趣,可以看以前的文章:
2-10 Fully Event Based Raycasting
同樣Energy.cs的程式也略為修改。
Player.cs的部分則因OnMouseOverEnemy回傳Enemy物件,使得程式碼更加簡潔。
另外的好處是,CursorAffordance.cs已經不需要了。那麼,接下來提供這次重構改掉的程式碼:
CameraRaycaster.cs:
using UnityEngine; using UnityEngine.EventSystems; using System.Collections.Generic; using RPG.Character; namespace RPG.CameraUI{ public class CameraRaycaster : MonoBehaviour { [SerializeField] Texture2D walkCursor = null; [SerializeField] Texture2D enemyCursor = null; [SerializeField] Vector2 cursorHotspot = new Vector2 (0, 0); const int POTENTIALLY_WALKABLE_LAYER = 8; float maxRaycastDepth = 100f; // Hard coded value // 重構後的新的委派方法 public delegate void OnMouseOverWalkable(Vector3 destination); public event OnMouseOverWalkable onMouseOverWalkable; public delegate void OnMouseOverEnemy(Enemy enemy); public event OnMouseOverEnemy onMouseOverEnemy; void Update() { // 確認鼠標是否放在GameObject之上(這個上面在遊戲中亦代表UI層) if (EventSystem.current.IsPointerOverGameObject ()) { return; // 既然鼠標在UI層,直接跳過以下的判斷事件 } else { PerformRaycast (); } } void PerformRaycast(){ Ray ray = Camera.main.ScreenPointToRay (Input.mousePosition); if (RaycastForEnemy (ray)) { return; } if (RaycastForWalkable (ray)) { return; } } private bool RaycastForEnemy(Ray ray){ RaycastHit hitInfo; Physics.Raycast (ray, out hitInfo, maxRaycastDepth); GameObject gameObject = hitInfo.collider.gameObject; Enemy enemyHit = gameObject.GetComponent(); if (enemyHit) { Cursor.SetCursor(enemyCursor, cursorHotspot, CursorMode.Auto); onMouseOverEnemy (enemyHit); return true; } return false; } private bool RaycastForWalkable(Ray ray){ RaycastHit hitInfo; LayerMask potentiallyWalkableLayer = 1 << POTENTIALLY_WALKABLE_LAYER; bool potentiallyWalkableHit = Physics.Raycast (ray, out hitInfo, maxRaycastDepth, potentiallyWalkableLayer); if (potentiallyWalkableHit) { Cursor.SetCursor (walkCursor, cursorHotspot, CursorMode.Auto); onMouseOverWalkable (hitInfo.point); return true; } return false; } } }
PlayerMovement.cs:
using System; using UnityEngine; using UnityEngine.AI; using UnityStandardAssets.Characters.ThirdPerson; using RPG.CameraUI; // TODO consider re-wiring namespace RPG.Character{ [RequireComponent(typeof (NavMeshAgent))] [RequireComponent(typeof (AICharacterControl))] [RequireComponent(typeof (ThirdPersonCharacter))] public class PlayerMovement : MonoBehaviour { [SerializeField] float walkMoveStopRadius = 0.2f; [SerializeField] float attackMoveStopRadius = 5f; CameraRaycaster cameraRaycaster = null; AICharacterControl aiCharacterControl = null; GameObject walkTarget = null; private void Start() { cameraRaycaster = Camera.main.GetComponent(); aiCharacterControl = GetComponent (); // 自動在場景中新增GameObject walkTarget = new GameObject ("WalkTarget"); cameraRaycaster.onMouseOverWalkable += OnMouseOverWalkable; cameraRaycaster.onMouseOverEnemy += OnMouseOverEnemy; } void OnMouseOverWalkable(Vector3 destination){ if (Input.GetMouseButton (0)) { // 取得滑鼠點擊到的位置,並把WalkTarget設置在那裡 walkTarget.transform.position = destination; // 追蹤WalkTarget aiCharacterControl.SetTarget (walkTarget.transform); GetComponent ().stoppingDistance = walkMoveStopRadius; } } void OnMouseOverEnemy(Enemy enemy){ if (Input.GetMouseButton (0)) { // 追蹤Enemy aiCharacterControl.SetTarget (enemy.transform); GetComponent ().stoppingDistance = attackMoveStopRadius; } } void OnDrawGizmos(){ //繪製攻擊範圍 Gizmos.color = new Color(255f, 0f, 0f, 0.5f); Gizmos.DrawWireSphere (transform.position, attackMoveStopRadius); } } }
Energy.cs:
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using RPG.CameraUI; namespace RPG.Character{ public class Energy : MonoBehaviour { [SerializeField] RawImage energyBarRawImage; [SerializeField] float maxEnergyPoints = 100f; [SerializeField] float pointPerHits = 10f; float currentEnergyPoints; CameraRaycaster cameraRaycaster; void Start () { cameraRaycaster = Camera.main.GetComponent(); currentEnergyPoints = maxEnergyPoints; cameraRaycaster.onMouseOverEnemy += OnMouseOverEnemy; } void OnMouseOverEnemy(Enemy enemy){ if (Input.GetMouseButtonDown (1)) { UpdateEnergyPoint (); UpdateEnergyBar (); } } void UpdateEnergyPoint(){ float newEnergyPoint = currentEnergyPoints - pointPerHits; currentEnergyPoints = Mathf.Clamp (newEnergyPoint, 0, maxEnergyPoints); } void UpdateEnergyBar(){ float xValue = -(EnergyAsPersent() / 2f) - 0.5f; energyBarRawImage.uvRect = new Rect(xValue, 0f, 0.5f, 1f); } float EnergyAsPersent(){ return currentEnergyPoints / maxEnergyPoints; } } }
Player.cs:
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Assertions; 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 damagePerHit = 50f; [SerializeField] AnimatorOverrideController animatorOverrideController; [SerializeField] Weapon weaponInUse; Animator animator; float currentHealthPoint; float lastHitTime; CameraRaycaster cameraRaycaster; public float healthAsPercentage{ get{ return currentHealthPoint / maxHealthPoints; } } void Start(){ // 初始化血量 currentHealthPoint = maxHealthPoints; cameraRaycaster = FindObjectOfType(); cameraRaycaster.onMouseOverEnemy += OnMouseOverEnemy; // 初始化武器,放置到手中 PutWeaponInHand (); // 覆寫人物角色中的Animation OverrideAnimatorController (); } 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"] = weaponInUse.GetAnimClip (); } private void PutWeaponInHand(){ GameObject weaponPrefab = weaponInUse.GetWeaponPrefab (); // 生成武器時,將會放置到指定的GameObject之下,此處為WeaponSocket之下 // 故可將WeaponSocket設置成玩家的右手或左手 GameObject weaponSocket = RequestDominantHand(); GameObject weapon = Instantiate (weaponPrefab, weaponSocket.transform); // 將武器放置到正確的位置,並有正確的方向 weapon.transform.localPosition = weaponInUse.gripTransform.localPosition; weapon.transform.localRotation = weaponInUse.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; } void OnMouseOverEnemy(Enemy enemy){ if (Input.GetMouseButtonDown (0) && IsTargetInRange (enemy)) { AttackTarget (enemy); } } private void AttackTarget(Enemy enemy){ // 確認本次攻擊時間離上一次攻擊時間須大於minTimeBetweenHits,相當於技能冷卻時間 if ( (Time.time - lastHitTime > weaponInUse.GetMinTimeBetweenHits())) { // 呼叫Trigger啟動攻擊動畫 animator.SetTrigger("Attack"); // TODO make const // 呼叫攻擊Target的方法 enemy.TakeDamage(damagePerHit); // 紀錄目前攻擊的時間 lastHitTime = Time.time; } } // 確認敵人是否在範圍技的攻擊範圍內 private bool IsTargetInRange(Enemy enemy){ float distanceToTarget = (enemy.transform.position - transform.position).magnitude; return distanceToTarget <= weaponInUse.GetMaxAttackRange(); } public void TakeDamage(float damage){ // Math.Clamp可以確保數值運算後不會低於最小值或者高於最大值 currentHealthPoint = Mathf.Clamp (currentHealthPoint - damage, 0f, maxHealthPoints); } } }
留言
張貼留言