Related Posts Plugin for WordPress, Blogger...

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:

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);
  }
 }
}

留言