Related Posts Plugin for WordPress, Blogger...

4-10 A SetDestination Movement API

這次重構要將原先撰寫滑鼠點擊地板跟點擊敵人的功能從Character移出去,改放到PlayerMovement裡面。因為往後要讓Enemy也使用Character,所以Player專用的功能必須移走,讓Character更單純化。

首先,請大家先將Player改名成PlayerMovement。

改名會導致原本在Player中的組件消失,出現Nothing Selected,記得刪除。

並接著將PlayerMovement新增回來。

如下圖,本次重構主要針對OnMouseOverEnemy跟OnMouseOverWalkable這兩個方法,由於Character類別要給Enemy使用,所以這兩個給Player專用的方法便須變更到PlayerMovement中。

以下提供PlayerMovement跟Character的完整程式碼:
PlayerMovement:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.SceneManagement;
using RPG.CameraUI;
using RPG.Core;
using RPG.Weapons;

namespace RPG.Character{
 public class PlayerMovement : MonoBehaviour {

  [SerializeField] float walkMoveStopRadius = 0.2f;
  [SerializeField] float attackMoveStopRadius = 5f;
  [SerializeField] float baseDamage = 50f;
  [SerializeField] AnimatorOverrideController animatorOverrideController;
  [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;
  float currentHealthPoint;
  Enemy currentEnemy;
  Character character;
  Animator animator;
  SpecialAbilities abilities;
  GameObject weaponInUse;

  void Start(){
   abilities = GetComponent ();
   character = GetComponent ();

   RegisterForMouseClcik ();
   PutWeaponInHand (currentWeaponConfig);
   OverrideAnimatorController ();
  }

  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 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 RegisterForMouseClcik(){
   CameraRaycaster cameraRaycaster = FindObjectOfType ();
   cameraRaycaster.onMouseOverEnemy += OnMouseOverEnemy;
   cameraRaycaster.onMouseOverWalkable += OnMouseOverWalkable;
  }

  void OnMouseOverEnemy(Enemy enemy){
   currentEnemy = enemy;

   if (Input.GetMouseButton (0)) {
    // 設置地點為Enemy的位置
    character.SetDestination(enemy.transform.position);
    character.SetStopDistance(attackMoveStopRadius);
    if (IsTargetInRange (enemy.gameObject)) {
     AttackTarget ();
    }
   } else if (Input.GetMouseButtonDown (1)) {
    // 0為使用第一個技能 
    abilities.AttemptSpecialAbility(0, currentEnemy.gameObject);
   }
  }

  void OnMouseOverWalkable(Vector3 destination){
   if (Input.GetMouseButton (0)) {
    // 設置地點為滑鼠點擊的位置
    character.SetDestination(destination);
    character.SetStopDistance (walkMoveStopRadius);
   }
  }

  private void AttackTarget(){
   // 確認本次攻擊時間離上一次攻擊時間須大於minTimeBetweenHits,相當於技能冷卻時間
   if ( (Time.time - lastHitTime > currentWeaponConfig.GetMinTimeBetweenHits())) {
    // 每次攻擊前都重設武器的攻擊動畫
    OverrideAnimatorController();
    // 呼叫Trigger啟動攻擊動畫
    animator.SetTrigger(ATTACK_TRIGGER);
    // 呼叫攻擊Target的方法
    currentEnemy.GetComponent().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();
  }

  void OnDrawGizmos(){
   //繪製攻擊範圍
   Gizmos.color = new Color(255f, 0f, 0f, 0.5f);
   Gizmos.DrawWireSphere (transform.position, attackMoveStopRadius);
  }
 }
}

Character:

using System;
using UnityEngine;
using UnityEngine.AI;
using RPG.CameraUI;

namespace RPG.Character{
 [SelectionBase]
 public class Character : MonoBehaviour
 {
  [Header("Audio")]
  [Range(0.0f, 1.0f)][SerializeField] float spatialBlend = 1f;

  [Header("Capsule Collider")]
  [SerializeField] Vector3 colliderCenter = new Vector3(0, 1, 0);
  [SerializeField] float colliderRadius = 0.3f;
  [SerializeField] float colliderHeight = 1.5f;

  [Header("Animator")]
  [SerializeField] RuntimeAnimatorController animatorController;
  [SerializeField] AnimatorOverrideController animatorOverrideController;
  [SerializeField] Avatar avatar;

  [Header("Movement")]
  [SerializeField] float moveSpeedMultiplier = 1f;
  [SerializeField] float animationSpeedMultiplier = 1f;
  [SerializeField] float movingTurnSpeed = 1800;
  [SerializeField] float stationaryTurnSpeed = 1800;
  [SerializeField] float moveThreshold = 1f;

  [Header("Nav Mesh Agent")]
  [SerializeField] float steeringSpeed = 1.0f;

  NavMeshAgent navMeshAgent = null;
  Animator animator = null;
  Rigidbody myRigidbody = null;
  float turnAmount;
  float forwardAmount;
  bool isAlive = true;

  void Awake(){
   AddRequiredComponents ();
  }

  private void AddRequiredComponents(){
   CapsuleCollider capsuleCollider = gameObject.AddComponent ();
   capsuleCollider.center = colliderCenter;
   capsuleCollider.radius = colliderRadius;
   capsuleCollider.height = colliderHeight;

   myRigidbody = gameObject.AddComponent ();
   myRigidbody.collisionDetectionMode = CollisionDetectionMode.Continuous;
   myRigidbody.constraints = RigidbodyConstraints.FreezeRotation;

   AudioSource audioSource = gameObject.AddComponent ();
   audioSource.spatialBlend = spatialBlend;

   animator = gameObject.AddComponent ();
   animator.runtimeAnimatorController = animatorController;
   animator.avatar = avatar;

   navMeshAgent = gameObject.AddComponent ();
   navMeshAgent.updateRotation = false;
   navMeshAgent.updatePosition = true;
   navMeshAgent.speed = steeringSpeed;
  }

  void Update(){
   if (navMeshAgent.remainingDistance > navMeshAgent.stoppingDistance && isAlive) {
    Move(navMeshAgent.desiredVelocity);
   } else {
    Move (Vector3.zero);
   }
  }

  void Move(Vector3 movement)
  {
   SetForwardAndTurn (movement);
   ApplyExtraTurnRotation();
   UpdateAnimator();
  }

  public void Kill(){
   // TODO 需要禁止角色移動
   isAlive = false;
  }

  public void SetDestination(Vector3 worldPos){
   navMeshAgent.SetDestination(worldPos);
  }

  public void SetStopDistance(float stoppingDistance){
   navMeshAgent.stoppingDistance = stoppingDistance;
  }

  private void SetForwardAndTurn(Vector3 movement){
   // 將Wolrd相對的向量轉換成Local相對的向量
   if (movement.magnitude > moveThreshold) {
    movement.Normalize ();
   }
   // 將Worldspace的方向轉換成LocalSpace
   Vector3 localMovement = transform.InverseTransformDirection(movement);
   turnAmount = Mathf.Atan2(localMovement.x, localMovement.z);
   forwardAmount = localMovement.z;
  }

  void UpdateAnimator()
  {
   // update the animator parameters
   animator.SetFloat("Forward", forwardAmount, 0.1f, Time.deltaTime);
   animator.SetFloat("Turn", turnAmount, 0.1f, Time.deltaTime);
   animator.speed = animationSpeedMultiplier;
  }

  void ApplyExtraTurnRotation()
  {
   // help the character turn faster (this is in addition to root rotation in the animation)
   float turnSpeed = Mathf.Lerp(stationaryTurnSpeed, movingTurnSpeed, forwardAmount);
   transform.Rotate(0, turnAmount * turnSpeed * Time.deltaTime, 0);
  }

  void OnAnimatorMove(){
   if (Time.deltaTime > 0) {
    Vector3 velocity = (animator.deltaPosition * moveSpeedMultiplier) / Time.deltaTime;
    // 保持y軸的速度不變
    velocity.y = myRigidbody.velocity.y;
    myRigidbody.velocity = velocity;
   }
  }
 }
}


改完後,請記得檢查Player Movement的參數是否有缺失,並執行遊戲看看,是否能讓角色移動。

留言