2-10 Fully Event Based Raycasting
今天要來介紹新版的CameraRaycaster,大家還記得舊版的程式中我們定義了一個enum型別的Layer,並透過CameraRaycaster來判斷滑鼠鼠標經過了哪些Layer嗎?同時我們也透過該程式來定義Cursor Affordance,以及判斷移動或者攻擊敵人等動作。
如果大家忘記的話,請先透過下列的連結看看之前的文章唷!
1-4 Using Raycasts To Query Click
1-5 Click Mouse To Move
1-7 Using Cursor Affordances
1-9 Bug Fixes - Using Delegate in C#
首先,依據1-9的教學,教大家如何使用C#的委派,做到偵測鼠標碰觸的Layer有變化時,才會改變Cursor Affordance的方法。本次新版的CameraRaycaster還要再新增滑鼠Click事件的委派,以及改善enum型別定義Layer造成的麻煩。
(i.e. 如果每次要新增Layer,就必須到enum中新增,又要在CameraRaycaster的layerPriorities中新增,造成要同時維護多部分程式碼的麻煩,這問題大家自行增加Layer就可以體會到了。)
好的!接下來我們先刪除專案中的Utility.cs吧,此為原先儲存enum型別的Layer所在之處。
再來匯入課程提供的package,請大家不要全部匯入,僅新增下列圖片中的兩份檔案即可,CameraRaycaster.cs和CameraRaycasterEditor.cs。下載地址為:
然後,場景中要新增UI/Event System,因為新版的CameraRaycaster使用Event System判斷鼠標是否移動到UI層上,若鼠標在UI層上進行操作,就不需要呼叫Cursor Affordance跟滑鼠Click事件了。
新增Event System後,將其拉進Project中變成prefab。
並將該prefab標籤設定為SceneSetup,當作新增場景時必備的物件之一。若不清楚這一步驟的原因,請參考之前的文章2-7 Standardising Scene Setup
https://rpgcorecombat.blogspot.tw/2017/12/2-7-standardising-scene-setup.html
接著,來看看CameraRaycaster.cs的內容吧,我替這份程式寫了一些中文註解。
using UnityEngine;
using UnityEngine.EventSystems;
using System.Linq;
using System.Collections.Generic;
public class CameraRaycaster : MonoBehaviour
{
// INSPECTOR PROPERTIES RENDERED BY CUSTOM EDITOR SCRIPT
[SerializeField] int[] layerPriorities;
float maxRaycastDepth = 100f; // Hard coded value
int topPriorityLayerLastFrame = -1; // So get ? from start with Default layer terrain
// Setup delegates for broadcasting layer changes to other classes
public delegate void OnCursorLayerChange(int newLayer); // 宣告一個新的委派型別
public event OnCursorLayerChange notifyLayerChangeObservers; //初始化委派的實體
public delegate void OnClickPriorityLayer(RaycastHit raycastHit, int layerHit); // 宣告一個新的委派型別
public event OnClickPriorityLayer notifyMouseClickObservers; //初始化委派的實體
void Update()
{
// 確認鼠標是否放在GameObject之上(這個上面在遊戲中亦代表UI層)
if (EventSystem.current.IsPointerOverGameObject ())
{
NotifyObserersIfLayerChanged (5); //5 是UI Layer
return; // 既然鼠標在UI層,直接跳過以下的判斷事件
}
// Raycast to max depth, every frame as things can move under mouse
Ray ray = Camera.main.ScreenPointToRay (Input.mousePosition);
RaycastHit[] raycastHits = Physics.RaycastAll (ray, maxRaycastDepth);
RaycastHit? priorityHit = FindTopPriorityHit(raycastHits);
if (!priorityHit.HasValue) // 若碰觸到的Layer不在我們定義的Priority清單中,則預設為Default Layer
{
NotifyObserersIfLayerChanged (0); // broadcast default layer
return;
}
// 當鼠標碰觸到的Layer有變換時,通知委派執行事件
var layerHit = priorityHit.Value.collider.gameObject.layer; //取得鼠標碰到的Layer
NotifyObserersIfLayerChanged(layerHit);
// 當滑鼠點擊Layer時,通知委派執行事件
if (Input.GetMouseButton (0))
{
notifyMouseClickObservers (priorityHit.Value, layerHit);
}
}
void NotifyObserersIfLayerChanged(int newLayer)
{
// 確認鼠標碰到的layer是否跟前一個layer不同,不同的話便呼叫委派通知更換Cursor圖標
if (newLayer != topPriorityLayerLastFrame)
{
topPriorityLayerLastFrame = newLayer;
notifyLayerChangeObservers (newLayer);
}
}
RaycastHit? FindTopPriorityHit (RaycastHit[] raycastHits)
{
// Form list of layer numbers hit
List layersOfHitColliders = new List ();
foreach (RaycastHit hit in raycastHits)
{
layersOfHitColliders.Add (hit.collider.gameObject.layer);
}
// 從layerPriorities中依順序偵測
foreach (int layer in layerPriorities)
{
foreach (RaycastHit hit in raycastHits)
{
if (hit.collider.gameObject.layer == layer)
{
return hit; // stop looking
}
}
}
return null; // because cannot use GameObject? nullable
}
}
大家應該可以主要關注這次新增的兩個委派事件,OnCursorLayerChange和OnClickPriorityLayer,可以自己先思考看看,如何運用這兩個委派事件來更改程式碼,讓程式能夠正常運作。
提示:要更改的檔案有PlayerMovement.cs、CursorAffordance.cs。將PlayerMovement中有關Gamepad Mode切換的程式碼註解掉,現在遊戲只要支援Mouse Click的移動即可。
以下提供參考答案。
PlayerMovement.cs
using System;
using UnityEngine;
using UnityStandardAssets.Characters.ThirdPerson;
[RequireComponent(typeof (ThirdPersonCharacter))]
public class PlayerMovement : MonoBehaviour
{
[SerializeField] float walkMoveStopRadius = 0.2f;
[SerializeField] float attackMoveStopRadius = 5f;
ThirdPersonCharacter theThirdPersonCharacter; // A reference to the ThirdPersonCharacter on the object
CameraRaycaster cameraRaycaster;
Vector3 currentDestination, clickPoint;
bool isInDircetMode = false;
private void Start()
{
cameraRaycaster = Camera.main.GetComponent();
theThirdPersonCharacter = GetComponent();
currentDestination = transform.position;
cameraRaycaster.notifyMouseClickObservers += ProcessMouseMovement;
}
// Fixed update is called in sync with physics
/*private void FixedUpdate()
{
if(Input.GetKeyDown(KeyCode.G)){
//切換GamePad Mode
isInDircetMode = !isInDircetMode;
//解決Gamepad模式切換時,人物角色會自動回到上一個滑鼠點擊位置的Bug
currentDestination = transform.position;
}
if (isInDircetMode) {
ProcessDirectMovement ();
} else {
ProcessMouseMovement ();
}
}*/
private void ProcessDirectMovement(){
// read inputs
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
// calculate camera relative direction to move:
Vector3 cameraForward = Vector3.Scale(Camera.main.transform.forward, new Vector3(1, 0, 1)).normalized;
Vector3 movement = v*cameraForward + h*Camera.main.transform.right;
theThirdPersonCharacter.Move(movement, false, false);
}
private void ProcessMouseMovement(RaycastHit raycastHit, int layerHit){
const int walkable = 8, enemy = 9;
clickPoint = raycastHit.point;
switch (layerHit) {
case walkable:
//取得滑鼠點擊到的物件的位置
currentDestination = ShortDestination (clickPoint, walkMoveStopRadius);
break;
case enemy:
currentDestination = ShortDestination (clickPoint, attackMoveStopRadius);
print ("Not moving to enemy.");
break;
default:
print ("Unexpected layer found.");
break;
}
}
void FixedUpdate(){
WalkToDestination ();
}
private void WalkToDestination(){
//將點擊到的物件位置減去自己的位置,取得相減後的向量
Vector3 playerToClickPoint = currentDestination - transform.position;
//計算該向量的距離,若距離太短,就不要移動了(可避免人物角色原地移動)
if (playerToClickPoint.magnitude >= 0) { //TODO 設為0導致原本角色原地移動的Bug又出現了
//告知ThirdPersonCharacter物件依向量移動,後面兩個false分別為蹲下和跳躍
theThirdPersonCharacter.Move(playerToClickPoint, false, false);
} else {
theThirdPersonCharacter.Move(Vector3.zero, false, false);
}
}
Vector3 ShortDestination(Vector3 destination, float shortening){
//將滑鼠點擊的位置減去自己所在的位置後,normalized取得單位向量,保持向量方向不變長度為1後
//再乘以shortening亦即要縮短幾倍的乘量,故reductionVector會變為移動至該位置方向且縮短指定倍率的Vector
Vector3 reductionVector = (destination - transform.position).normalized * shortening;
//將滑鼠點擊位置減去reductionVector,就會得到一個縮短距離後的Vector
return destination - reductionVector;
}
void OnDrawGizmos(){
//繪製移動路徑
Gizmos.color = Color.black;
Gizmos.DrawLine (transform.position, clickPoint);
Gizmos.DrawSphere (currentDestination, 0.15f);
Gizmos.DrawSphere (clickPoint, 0.1f);
//繪製攻擊範圍
Gizmos.color = new Color(255f, 0f, 0f, 0.5f);
Gizmos.DrawWireSphere (transform.position, attackMoveStopRadius);
}
}
CursorAffordance.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent (typeof(CameraRaycaster))]
public class CursorAffordance : MonoBehaviour {
[SerializeField] Texture2D walkCursor = null;
[SerializeField] Texture2D targetCursor = null;
[SerializeField] Texture2D unknownCursor = null;
[SerializeField] Vector2 cursorHotspot = new Vector2 (0, 0);
CameraRaycaster cameraRaycaster;
void Start () {
//取得Camera Arm中的Camera Raycaster,因為CursorAffordance.cs也是放在Camera Arm之下,故可以直接用GetComponent取得。
cameraRaycaster = GetComponent ();
//向委派註冊執行的事件
cameraRaycaster.notifyLayerChangeObservers += OnLayerChange ;
}
void OnLayerChange (int newLayer) {
//於Console中顯示滑鼠點擊到的Layer
//print (newLayer);
const int walkable = 8, enemy = 9;
switch (newLayer) {
case enemy:
Cursor.SetCursor(targetCursor, cursorHotspot, CursorMode.Auto);
break;
case walkable:
Cursor.SetCursor (walkCursor, cursorHotspot, CursorMode.Auto);
break;
default:
Cursor.SetCursor(unknownCursor, cursorHotspot, CursorMode.Auto);
break;
}
}
}






留言
張貼留言