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的參數是否有缺失,並執行遊戲看看,是否能讓角色移動。





留言
張貼留言