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:
PlayerMovement.cs:
Energy.cs:
Player.cs:
因為舊版的CameraRaycaster考量到新增Layer的需求,撰寫上多了很多額外的程式碼,原意上是為了未來增加Layer的彈性但如今卻是程式碼繁雜的主因。目前認為Layer只會分成Walkable跟Player,甚至連原本的Enemy Layer都會刪除。
如對舊版的CameraRaycaster有興趣,可以看以前的文章:
2-10 Fully Event Based Raycasting
同樣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);
}
}
}



留言
張貼留言