1-9 Bug Fixes - Using Delegate in C#
今天要來修正目前專案已有的一些Bug。
1. 從滑鼠模式切換到Gamepad後,於Gamepad模式中移動一段距離再切換回滑鼠模式,人物會自動移動到上次滑鼠模式點擊的位置。
2. 滑鼠的Cursor圖標在快速切換的過程中,若即時將滑鼠停下來,會發現鼠標仍舊朝一個方向自動移動。
1. 第一個問題很好修正,首先PlayerMovement.cs中看ProcessMouseMovement方法,可見滑鼠點擊後,會將點擊位置紀錄在變數currentClickTarget。
但是,切換到Gamepad模式後,直接用鍵盤移動人物位置,故變數currentClickTarget維持在舊的位置資料。此時,若切換回滑鼠模式,人物會自動移動到上一個位置。
修正方式如下,我們只要在切換模式的時候,將人物的目前位置記錄到currentClickTarget即可。
2. 第二個問題,我們要使用C#的委派(Delegate)來解決。
首先,Bug的發生原因,我們可以關閉Cursor Affordance,然後再來快速移動滑鼠,發現鼠標不會自己移動了。
再來重新把Cursor Affordance打開,並將下列其中一行程式碼註解。我註解的是設定Enemy的圖標,結果發現在Walkable圖標的時候還是會有Bug發生。
以下完整程式碼上菜!
CameraRaycaster.cs
CursorAffordance.cs
1. 從滑鼠模式切換到Gamepad後,於Gamepad模式中移動一段距離再切換回滑鼠模式,人物會自動移動到上次滑鼠模式點擊的位置。
2. 滑鼠的Cursor圖標在快速切換的過程中,若即時將滑鼠停下來,會發現鼠標仍舊朝一個方向自動移動。
1. 第一個問題很好修正,首先PlayerMovement.cs中看ProcessMouseMovement方法,可見滑鼠點擊後,會將點擊位置紀錄在變數currentClickTarget。
if (Input.GetMouseButton(0)) { //print("Cursor raycast hit" + cameraRaycaster.currentLayerHit); switch (cameraRaycaster.currentLayerHit) { case Layer.Walkable: //取得滑鼠點擊到的物件的位置 currentClickTarget = cameraRaycaster.hit.point; break; case Layer.Enemy: print ("Not moving to enemy."); break; default: print ("Unexpected layer found."); break; } }
但是,切換到Gamepad模式後,直接用鍵盤移動人物位置,故變數currentClickTarget維持在舊的位置資料。此時,若切換回滑鼠模式,人物會自動移動到上一個位置。
修正方式如下,我們只要在切換模式的時候,將人物的目前位置記錄到currentClickTarget即可。
private void FixedUpdate() { if(Input.GetKeyDown(KeyCode.G)){ //切換GamePad Mode isInDircetMode = !isInDircetMode; //解決Gamepad模式切換時,人物角色會自動回到上一個滑鼠點擊位置的Bug currentClickTarget = transform.position; } if (isInDircetMode) { ProcessDirectMovement (); } else { ProcessMouseMovement (); } }
2. 第二個問題,我們要使用C#的委派(Delegate)來解決。
首先,Bug的發生原因,我們可以關閉Cursor Affordance,然後再來快速移動滑鼠,發現鼠標不會自己移動了。
由此可以假設這個Bug是因為在Update一直呼叫Cursor.SetCursor導致的,所以我們要讓設定Cursor的執行頻率不要如此頻繁,最好是在Layer切換的時候再更換圖標就好。
循此解決方式,我們就要使用Observer Pattern的概念來實作,亦即當CameraRaycast執行時發現滑鼠指向的Layer變了,這時必須通知負責的Observer去執行Cursor.SetCursor。
如不懂Observer Pattern和C#的委派(Delegate),可循下列教學看看:
[Design Pattern] 觀察者模式 (Observer Pattern) 我也能夠辦報社
重新整理Observer Pattern
[C#.NET] 如何 使用 委派 Delegate / 事件 event
C#的委派與事件的使用
簡言之,Observer Pattern中會有一個觀察者,並且需要向觀察者註冊執行事件。當觀察者於程式運作中發現必須執行時,就會執行所有向他註冊的事件。委派便是一個能方便實作Observer Pattern的好用語法。
首先,我們先在CameraRaycaster宣告委派事件:
接著,再到CursorAffordance修改LateUpdate方法為OnLayerChange,並於Start註冊該方法為委派的事件。
最後,回到CameraRaycaster原本判斷Layer的地方,撰寫判斷式,何時需要通知委派執行事件。
以下完整程式碼上菜!
CameraRaycaster.cs
using UnityEngine; public class CameraRaycaster : MonoBehaviour { //設定偵測Layer的順序,亦即先偵測Enemy,若沒有Enemy才偵測Walkable public Layer[] layerPriorities = { Layer.Enemy, Layer.Walkable }; //使用SerializeField屬性,該變數會出現於Inspector中,且同時為private [SerializeField] float distanceToBackground = 100f; Camera viewCamera; RaycastHit raycastHit; public RaycastHit hit { get { return raycastHit; } } Layer layerHit; public Layer currentLayerHit { get { return layerHit; } } public delegate void OnLayerChange(Layer newLayer); // 宣告一個新的委派型別 public event OnLayerChange layerChangeObservers; //初始化委派的實體 void Start() { viewCamera = Camera.main; } void Update() { // 從layerPriorities中依順序偵測 foreach (Layer layer in layerPriorities) { var hit = RaycastForLayer(layer); if (hit.HasValue) { raycastHit = hit.Value; // 確認滑鼠點中的layer是否跟前一個layer不同,不同的話便呼叫委派通知更換Cursor圖標 if (layerHit != layer) { layerHit = layer; layerChangeObservers (layerHit); // 呼叫委派事件 } return; } } // 都沒偵測到,回傳RaycastEndStop raycastHit.distance = distanceToBackground; if (layerHit != Layer.RaycastEndStop) { layerHit = Layer.RaycastEndStop; layerChangeObservers (layerHit); // 呼叫委派事件 } } RaycastHit? RaycastForLayer(Layer layer) { int layerMask = 1 << (int)layer; // See Unity docs for mask formation Ray ray = viewCamera.ScreenPointToRay(Input.mousePosition); RaycastHit hit; // used as an out parameter bool hasHit = Physics.Raycast(ray, out hit, distanceToBackground, layerMask); if (hasHit) { return hit; } return null; } }
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.layerChangeObservers += OnLayerChange ; } void OnLayerChange (Layer newLayer) { //於Console中顯示滑鼠點擊到的Layer print (cameraRaycaster.currentLayerHit); switch (newLayer) { case Layer.Enemy: Cursor.SetCursor(targetCursor, cursorHotspot, CursorMode.Auto); break; case Layer.Walkable: Cursor.SetCursor (walkCursor, cursorHotspot, CursorMode.Auto); break; case Layer.RaycastEndStop: Cursor.SetCursor(unknownCursor, cursorHotspot, CursorMode.Auto); break; default: Debug.LogError ("Don't know what cursor to show."); break; } } }
留言
張貼留言