Related Posts Plugin for WordPress, Blogger...

1-9 Bug Fixes - Using Delegate in C#

今天要來修正目前專案已有的一些Bug。

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,然後再來快速移動滑鼠,發現鼠標不會自己移動了。


 再來重新把Cursor Affordance打開,並將下列其中一行程式碼註解。我註解的是設定Enemy的圖標,結果發現在Walkable圖標的時候還是會有Bug發生。

由此可以假設這個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;
  }
 }
}


留言