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:

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
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<specialabilities> ();
   character = GetComponent<character> ();
 
   RegisterForMouseClcik ();
   PutWeaponInHand (currentWeaponConfig);
   OverrideAnimatorController ();
  }
 
  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 OverrideAnimatorController(){
   // 取得Animator物件
   animator = GetComponent<animator> ();
   // 將人物的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> ();
   // 計算取得有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> ();
   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<healthsystem>().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);
  }
 }
}
</healthsystem></cameraraycaster></dominanthand></animator></healthsystem></character></specialabilities>

Character:

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
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> ();
   capsuleCollider.center = colliderCenter;
   capsuleCollider.radius = colliderRadius;
   capsuleCollider.height = colliderHeight;
 
   myRigidbody = gameObject.AddComponent<rigidbody> ();
   myRigidbody.collisionDetectionMode = CollisionDetectionMode.Continuous;
   myRigidbody.constraints = RigidbodyConstraints.FreezeRotation;
 
   AudioSource audioSource = gameObject.AddComponent<audiosource> ();
   audioSource.spatialBlend = spatialBlend;
 
   animator = gameObject.AddComponent<animator> ();
   animator.runtimeAnimatorController = animatorController;
   animator.avatar = avatar;
 
   navMeshAgent = gameObject.AddComponent<navmeshagent> ();
   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;
   }
  }
 }
}
</navmeshagent></animator></audiosource></rigidbody></capsulecollider>


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

留言