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:
Character:
改完後,請記得檢查Player Movement的參數是否有缺失,並執行遊戲看看,是否能讓角色移動。
首先,請大家先將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的參數是否有缺失,並執行遊戲看看,是否能讓角色移動。
留言
張貼留言