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 ListlayersOfHitColliders = 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; } } }
留言
張貼留言