Related Posts Plugin for WordPress, Blogger...

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;
  }
 }
}


留言