[Unity Script] 偵測滑鼠是否碰觸遊戲畫面邊緣 Detect If Mouse on Edge of Screen

[Unity Script] 偵測滑鼠是否碰觸遊戲畫面邊緣 Detect If Mouse on Edge of Screen

此腳本是使用傳統 Input Manager 實作,可以取得滑鼠於螢幕邊緣的狀態,可回傳上、下、左、右、上左、上右、下左與下右,如果滑鼠於非邊緣而在畫面中則回傳未知狀態 (Unknown)。

The script was created with legacy Input Manager. It can simply get your mouse state when it is on edge of the screen. The “GetMouseEdgeState” method can return Top, Down, Left, Right, Top Left, Top Right, Down Left, and Down Right state. It also returns an unknown state when the mouse is over the game window.

public enum MouseEdgeState
{
    Unknown,
    Left, Right, Top, Down,
    TopLeft, TopRight,
    DownLeft, DownRight
}
private MouseEdgeState GetMouseEdgeState()
{
    var mouseX = Mathf.RoundToInt(Input.mousePosition.x);
    var mouseY = Mathf.RoundToInt(Input.mousePosition.y);
    if (mouseX >= Screen.width)
    {
        if (mouseY >= Screen.height)
        {
            return MouseEdgeState.TopRight;
        }
        else if (mouseY <= 0)
        {
            return MouseEdgeState.DownRight;
        }
        else
        {
            return MouseEdgeState.Right;
        }
    }
    else if (mouseX <= 0)
    {
        if (mouseY >= Screen.height)
        {
            return MouseEdgeState.TopLeft;
        }
        else if (mouseY <= 0)
        {
            return MouseEdgeState.DownLeft;
        }
        else
        {
            return MouseEdgeState.Left;
        }
    }
    else
    {
        if (mouseY >= Screen.height)
        {
            return MouseEdgeState.Top;
        }
        else if (mouseY <= 0)
        {
            return MouseEdgeState.Down;
        }
        else
        {
            return MouseEdgeState.Unknown;
        }
    }
}

Unity 骨架控制器「Animation Rigging」手部骨架基礎操作

想要在 Unity 裡控制 3D 模型的骨架,除了最知名的「Final IK」付費套件可以用外,如果你的 Unity 引擎版本在 2020 版以上,也可以來嘗試看看官方支援的「Animation Rigging」。

在這篇內容中我僅能介紹基礎的用法,因為我對於 3D 模型相關的知識目前處於入門等級,尚在學習中。


前情提要

  • 模型必須先綁好骨架並蒙皮。
  • 建議先在主流 3D 建模軟體中完成動畫設計,再由 Animation Rigging 進行動態控制。

套件資訊


套件安裝

Animation Rigging 已經是正式版套件,可以直接透過 Package Manager 進行安裝。


模型配置

一、匯入模型,我在 Mixamo 中找了一個動畫附帶模型作為操作範例,此模型已帶有骨架與動畫。

二、將模型拉入場景,為物件新增 Animator 組件和 Animator Controller,並將模型自帶的動畫拉入控制器中並設為預設。

三、顯示骨架,先選中場景上的人物模型,從頂部選單執行 Bone Renderer Setup。

四、模型將會被新增一個 Bone Renderer 組件以渲染人物骨架,可透過調整 Bone Size 與 Color 來使骨架清楚顯示。


建立手部控制器

一、先選中人物模型,只不過這次要執行的是 Rig Setup,這將會新增 Rig Builder 組件與一個子物件 Rig1。

二、在 Rig1 下建立一個空物件,先給他新增一個「Two Bone IK Constraint」,再將模型骨架中的手部物件拖入 Tip 欄位,最後在組件的標題上滑鼠右鍵選單執行 Auto Setup from Tip Transform,其他的組件欄位將會自動設置完畢,也會多出 target 和 hint 兩個子物件。

三、這邊產生的 LeftHand_target 物件就是我們的手部骨架控制器,但現在他的座標還是在 (0, 0, 0),需要進行對齊。請按著鍵盤 Ctrl,先選 LeftHand_target 後再選手部骨架,再透過頂部選單進行 Align Transform 對齊。

四、現在的 LeftHand_target 已經與手部骨架對齊,接下來選中他,並點選場景中 Animation Rigging 浮動視窗中的 + 按鈕以建立可視的控制器。

五、可調整 Shape、Size、Rotation 等屬性以使控制器符合模型現況。

六、接下來讓 LeftHand_hint 也做一樣的對齊、加 Effector 動作,但是這個物件放置的座標要注意一下,因為人的手肘是往後彎曲的,要將他放在手的後面。

LeftHand_hint 是拿來讓 Unity 判斷控制器移動時骨架要彎曲的方向,這個物件是可選的,當然我們也可刪掉 LeftHand_hint,讓 Unity 自行判斷。

LeftHand_hint 物件位置對於彎曲行為的影響:

https://tedliou.com/wp-content/uploads/2022/03/2022-03-25-23-58-11-1.mp4

完成。

https://tedliou.com/wp-content/uploads/2022/03/2022-03-26-00-00-30-1.mp4

Unity 假彈道製作「二次方貝茲曲線」原理與語法

想要在 Unity 中讓物體進行拋物線運動,平常的我可能會直接用 Rigidbody 來施加力,讓系統自己去運算物理的大小事。但有時候我只是要做個「看起來」是拋物線運動的效果,沒有必要弄到非常真實,就可以套個曲線公式來施作。

原理

我們最常看到的貝茲曲線是只有一個原點、一個終點與一個控制點的「二次方貝茲曲線」,你可以透過移動控制點來修改曲線。在推導「二次方貝茲曲線」前,必須先了解「線性貝茲曲線」,它等同於我們常用的線性插值。

線性貝茲曲線

線性貝茲曲線等同線性差值,我們現在已知 A 與 B 的座標位置,也假定總共移動所需的時間 t 為 1,t 介於 0~1,可以直接想像成此次移動的完成百分比 (0~100%)。

t 代表時間,A 和 B 分別代表原點與終點,C 就是結果。

先以簡單的一維空間來解釋…

假設 A = 0、B = 10,求 t = 0.2 時 C 點的位置。這時就可以用 A~B 的距離乘 t 來表示 C 的位置:

但是這個假設是建立在原點 = 0 的情況,所以正確的式子是:

用分配律整理一下,這就是「線性貝茲曲線」的公式:

接下來帶入剛才假設的數值,得 C = 2:

二次方貝茲曲線

進入主題,二次方貝茲曲線是由三條線性貝茲曲線組合出來的產物,以下方影片為例,我們將使用到

  1. 先定義由左到右的座標分別為 A、B、C 點。
  2. 分別計算 A~B 與 B~C 上的點於指定 t 的時候所在的座標,可得出的 D、E 兩個座標點。
  3. 將 D 與 E 帶入公式最後得出我們要的 F 座標:

https://tedliou.com/wp-content/uploads/2022/03/movie_002.mp4

能用三個線性貝茲曲線算出座標後,是時候將他整理成一個公式…

首先是剛才計算 AB、 BC 線段上的 D、E 座標式子:

還有最終的 F 座標式子:

將 D 和 E 帶入 F 座標的式子:

整理後可得以下公式,t 介於 0~1 之間:


二次方貝茲曲線公式


語法

Unity 中的次方可使用 Mathf.Pow 來實現,最後的 Bt 座標就是指定時間點 t 的貝茲曲線座標。

public Transform P0, P1, P2, Bt;

private void Start()
{
    var t = 0.5f;
    var p0 = P0.position;
    var p1 = P1.position;
    var p2 = P2.position;
    Bt.position = Mathf.Pow(1 - t, 2) * p0 + 2 * t * (1 - t) * p1 + Mathf.Pow(t, 2) * p2;
}

也可以放到 Update 中並給予時間與線段特效,以下影片為例:

https://tedliou.com/wp-content/uploads/2022/03/movie_003-1.mp4

完整腳本:

using UnityEngine;

public class BezierCalculator : MonoBehaviour
{
    public int Duration = 5;
    public Transform P0, P1, P2, Bt;

    private float _time;
    private float _duration;

    private void Update()
    {
        if (_duration > Duration)
        {
            _duration = 0;
        }

        var t = _duration / Duration;
        Bt.position =
            Mathf.Pow(1 - t, 2) * P0.position +
            2 * t * (1 - t) * P1.position +
            Mathf.Pow(t, 2) * P2.position;

        _duration += Time.deltaTime;
    }
}

Unity 以任意角度取圓周上座標之原理與語法

因為要在 Unity 上做一個能在圓周上隨機生成物件的功能,回去研究了一下關於圓與角度的數學。

原理

一、我們會先定義好圓心和半徑,這邊的圓心為 (0, 0)、C = 半徑、θ 為角度,可繪製出以下圖形:

(B, A) 就是我們要求的座標。

二、帶入三角函數公式,其實這就是邊的比例。


先取 座標,我們已知 C = 半徑,為了拿到 B 值,要用 C 乘

已知 等於 ,所以等同

去分母後就是 B 值。


座標同理,只不過改成 C 乘

一樣去分母…


語法

知道為什麼需要乘 後,接下來就是用腳本來實現他。

一、定義圓心與半徑。

var center = new Vector3(0, 0, 0);
var radius = 3;

二、計算弧度,因為 Unity 自帶的 Mathf.Cos 和 Mathf.Sin 只能輸入弧度並非角度,所以需要事先進行換算。( = 弧度、 = 角度)

以 20 度為例:

var rad = 20 * Mathf.PI / 180;

三、計算 XY 座標,如圓心不在 (0, 0) 時需要加上圓心座標。

var x = center.x + radius * Mathf.Cos(rad);
var y = center.y + radius * Mathf.Sin(rad);

四、最後加入物件生成後會像這樣,指定於角度 20 度座標位置生成一個 Ball 物件:

public GameObject Ball;

private void Start()
{
    var center = new Vector3(0, 0, 0);
    var radius = 3;
    var rad = 20 * Mathf.PI / 180;
    var x = center.x + radius * Mathf.Cos(rad);
    var y = center.y + radius * Mathf.Sin(rad);
    Ball.transform.position = new Vector3(x, y, center.z);
    Instantiate(Ball);
}

Unity 預覽,紅點為圓心,白點為生成的 Ball。

五、實際運用方面,我們可以用迴圈達成「在圓周上每 20 度生成一個物件」的目的。

public GameObject Ball;

private void Start()
{
    var center = new Vector3(0, 0, 0);
    var radius = 3;
    for (int i = 0; i < 360; i += 20)
    {
        var rad = i * Mathf.PI / 180;
        var x = center.x + radius * Mathf.Cos(rad);
        var y = center.y + radius * Mathf.Sin(rad);
        Ball.transform.position = new Vector3(x, y, center.z);
        Instantiate(Ball);
    }
}

輸出結果

Unity 圓周繞行運動「transform.RotateAround」語法說明

圓周繞行至少有兩種方式可以實現,一是利用 Unity 父子物件相對位置固定的特性,以旋轉父物件來達到目的,另一個就是本篇將提到的「transform.RotateAround」語法。

格式

transform.RotateAround(圓心座標, 繞行軸向, 角度);

如果我們指定物件要繞行的圓的圓心為 (0, 0, 0),且以 Z 為軸向旋轉,可以寫成這樣 (放在 Update 中):

transform.RotateAround(new Vector3(0, 0, 0), new Vector3(0, 0, 1), 360 * Time.deltaTime);

軸向也能給斜的,只不過在開發 2D 遊戲時不太會用到。

最後面表示角度的 360 * Time.deltaTime 是代表一秒轉一圈,我們可以額外乘上一個變數來控制速度。當這個角度參數為正時會逆時鐘旋轉,小於零時順時鐘。

transform.RotateAround(Vector3.zero, Vector3.forward, 360 * Time.deltaTime * Speed);

另外,因為 transform.RotateAround 會直接動到物件的旋轉角度,如果想要將它固定的話可以直接在下一行改回來。

transform.right = Vector3.right;

改回來後就不會再旋轉了。

結果

https://tedliou.com/wp-content/uploads/2022/03/2022-03-20-20-43-22.mp4

Unity 免 Build 多人連線測試套件「ParrelSync」安裝與使用

「ParrelSync」套件可快速達成 Unity 專案同步雙開的目的,以快速進行多人遊戲連線測試作業。


套件資訊


安裝與使用

一、從 套件 Github 中取得 Unitypackage 後直接開啟匯入。

二、從上方工具列開啟 Clones Manager (ParrelSync/Clones Manager)。

三、點擊 Create new clone 並等待專案複製完成。

四、點擊 Open in New Editor 啟動剛才複製的專案。

五、等待複製的專案啟動完成後,兩邊的專案內容將完全一致,且當你變更原專案的內容時,複製的專案將會自動完成同步,在多人遊戲開發中即可進行快速測試。

可以從 Clones Manager 中點擊 View Folder 來開啟複製的專案資料夾,你會發現其中 Assets 與 ProjectSettings 目錄是一個類似捷徑的東西。

舉例說明,當原專案場景變更時,滑鼠點開複製的專案時就會要求進行場景 Reload,重載後即完成同步。

另外 ParrelSync 有加入防呆機制,你不可以直接變更複製的專案的內容,在嘗試存檔時會自動阻擋,請一律在原專案中進行內容修改。

解決 Dissonance 於 Quest 2 平台麥克風無作動問題

Dissonance (全名:Dissonance Voice Chat) 是一個運作於 Unity Engine 的線上多人語音系統插件,他本身沒有多人連線功能,而是依附於常見的 UNET、Photon、Mirror、Steamworks.NET 或最新的 Netcode 多人連線架構,讓多人遊戲可同時擁有語音聊天功能。這支插件原價 75 美金,如非急需的話可在特殊節日或黑五等特價日以 35 美元或更低的價格購入。


插件資訊


Dissonance 配置完成後,在 PC 端啟動後可以直接運作,但在 Quest 2 上卻沒有任何反應 (可聽不可說),因為 Quest 2 的作業系統是 Android。現在的 Android 為保障安全性,對於「權限」的使用鎖得很緊,何況是能作為監聽使用的麥克風。

照理說 Unity Engine 應該要在編譯時偵測到 Dissonance 有使用到麥克風時,自動在 APK 中加入麥克風權限的需求。但目前的狀況是,在用 Quest 2 啟動程式時,並不會有任何的權限同意視窗供你選擇,導致此硬體不會有任何作動。

為解決此問題,我們就需要在遊戲啟動時 (可放在 Splash 或登入場景) 讓他執行一個腳本來請求權限,可使用 Unity Engine 的 Application.HasUserAuthorization 與 Application.RequestUserAuthorization 來實作。

using System.Collections;
using System.Linq;
using UnityEngine;

public class DissonanceMicrophoneChecker : MonoBehaviour
{
    private IEnumerator Start()
    {
        // 檢查是否有麥克風權限,否則提出權限請求
        if (!Application.HasUserAuthorization(UserAuthorization.Microphone))
        {
            yield return Application.RequestUserAuthorization(UserAuthorization.Microphone);
        }

        // 再次檢查,如仍然無權限則將遊戲關閉
        if (Application.HasUserAuthorization(UserAuthorization.Microphone))
        {
            Debug.Log("Device: " + Microphone.devices.Select(x => x.ToString()));
        }
        else
        {
            Application.Quit();
        }
    }
}

把這支腳本掛在遊戲啟動後的第一個場景中,在執行程式時就可以看到允許權限的視窗,點選允許後 Dissonance 的語音系統將能正常作動。

Unity Recorder 擷取透明去背的 UI 畫面設定方法

最近遇到需要提供現有的 UI 畫面給美術做角色設計,必須去掉背景和部分人物,也就是要去背。

應該沒人去做螢幕擷圖後 PS 去背的傻事吧?想要在編輯器中擷圖,我們可以用 Unity Recorder 這個 Package,他能輸出完美尺寸的圖片。只不過要用他來擷取透明背景的 UI 的方法比較少人知道,所以就寫這篇來記錄一下。


首先將 Unity Recorder 安裝到你的專案裡,在 2020 版中此套件已經是正式版。

接下來從頂部選單 Window > General > Recorder > Recorder Window 啟動介面。

這邊要做一小連串的操作,我們用編號列出來:

  1. 按下左邊 + Add Recorder 新增一個 Image Sequence。
  2. 修改右側內容:
    1. 將 Source 從 Game View 改成 Targeted Camera。
    2. 將 Camera 從 Main Camera 改成 Active Camera。
    3. 勾選 Include UI。
    4. 修改 Media File Format 從 JPEG 改成 PNG。
    5. 勾選 Include Alpha。
  3. 修改頂部內容:
    1. 修改 Recording Mode 從 Manual 改成 Single Frame。
    2. 修改 Target Frame 改成你要的目標幀,一般就用 1。
  4. 按下 START RECORDING 完成擷圖。

右側欄的 Output File 有寫擷圖後輸出的位置,到目標位置取圖即可。

備註:有勾選 Include UI 後,貌似將 Camera 改回 Main Camera 一樣可以成功擷取 UI。

[Unity] 以 Interface 介面搜尋場景上所有物件

有在 Unity 用過 Interface 介面的工程師們應該都知道 GetComponent<介面名稱>; 的用法,但卻沒辦法用 FindObjectsByType<介面名稱> 來找物件。

而在 2014 年的 Unity 論壇中有人提出的 How can I find all objects that have a script that implements a certain interface? 討論串中有人以 LINQ 實作出用介面找物件的功能。

但為了要能更方便使用,我將它重寫成擴充功能,但這個目前沒研究出來能直接在 MonoBehaviour 中使用的辦法,所以就先讓它依附在 GameObject 之下。

編寫邏輯

先新開一個腳本,儲存以下程式碼:

簡單來說就是,因為我們不能直接搜尋 Interface,所以就往更上層的 MonoBehaviour 搜尋,再往下去找有指定介面的物件,感覺就超吃效能,慎用。

用法如下,它會回傳一個 List。

使用範例

IReady 是我自己寫的介面,裡面有個 Ready() 函數。上面先以 iReady 儲存它用 FindInterfacesOfType 找出來的物件 List,再透過 foreach 呼叫 Ready()。

切記,別把這個玩意兒塞在 Update 中讓它瘋狂執行,Find 與 LINQ 系列的程式碼都很吃效能的!

[Unity] Android 專案路徑含有非 ASCII 字元

錯誤訊息位置

請檢查 Gradle build failed. 紀錄。

如出現 Your project path contains non-ASCII characters. 字句,先將 Unity 關閉,再將專案路徑中所有的中文字改成英文,最後啟動 Unity 專案並重新執行編譯。

* What went wrong:
A problem occurred evaluating project ':launcher'.
> Failed to apply plugin [id 'com.android.internal.application']
   > Your project path contains non-ASCII characters. This will most likely cause the build to fail on Windows. Please move your project to a different directory. See http://b.android.com/95744 for details. This warning can be disabled by adding the line 'android.overridePathCheck=true' to gradle.properties file in the project directory.

[Unity] 為 Canvas 加上 Post Processing 後處理效果操作教學

昨天開了一個新專案,打算要來統整關於遊戲 Menu 的系統。其中我為了「將圖片塞入主選單背景,並且要模糊化」找了兩種解決辦法,其一是用 PBR Graph 寫一個疊加多重偏移的模糊效果,另一個就是參考 Standard Assets 中已棄用的 Image Effect 的說明 — Post Processing (中譯:後處理)。

Post Processing 就我來說是一個比較常用在世界座標內、在 UI 的部分少見的特效系統。實際研究過後,我發現後處理直接作用在 Canvas 上會有一個明顯的缺陷,因為鏡頭照的是全範圍且效果是用疊加的,放在上層的 UI Camera 一套用模糊效果後會連帶影響後面的物件。

這個問題應該是有一些解決辦法,在這邊討論會拖太長篇幅,我們之後再將結果整理到其他文章中。

進入正式教學內容,在這邊我們要實作的是「為 Canvas 物件套用模糊效果」,。

安裝 Post Processing

打開 Unity,從頂部選單 Window 開啟 Package Manager,從 Unity Registry 中找到 Post Processing 後安裝。

建立 UI 攝影機

先選擇 Main Camera,取消選取 Culling Mask 的 UI。

建立一個新的 Camera,重新命名為 UI Camera,一樣要調整 Culling Mask,但這次只選擇 UI,其他取消。

最後確定 Main Camera 的 Depth 比 UI Camera 還要小,如果場景中有放物件的話才不會擋到 UI。

建立後處理物件

在 Hierarchy 中滑鼠右鍵 > 3D Object > Post-process Volume,建立一個 Volume。

選擇建好的 Volume,看它的 Inspector 視窗,點擊 Profile 旁的 New 按鈕,它會建立一個 Post-process Volume Profile。

Profile 的之後做,我們先調整 Post-process Volume 物件的 Layer。這部份看你自己的規劃,我是因為東西少,就直接跟 UI 共用 Layer。

選擇 UI Camera 後看它的 Inspector,為它新增 Post-process Layer 組件,將 Layer 調整成指定剛剛指定給 Post-process Volume 物件的層級。

重點開始,在 Inspector 中滑鼠右鍵 > UI > Canvas。

選擇 Canvas 物件,看到 Inspector 視窗,將 Render Mode 改成 Screen Space – Camera,並將有照到 UI 的攝影機拖拉至 Render Camera。

一定要用 Screen Space – Camera 或 World Space,否則後處理不會作用到 Canvas。(沒記錯的話 Overlay 是在 Post Processing 後進行繪製)

回到 Post-process Volume,將 Is Global 打勾,新增一個 Depth Of Field 特效,將 Focus Distance 勾選並調整為 0.1。

模糊效果測試

在 Canvas 中新增一個 Image,看一下 Game 視窗……
有模糊了,搞定!

但連帶產生的問題就是連後面的 3D 物件都會被模糊掉……
這我們再用另一篇文章來試著解決。

但這做法還是有用,像這種純 UI 的介面,我要將後面的影片模糊掉就可以用 Post Processing!

也有開發者用這個手法做模糊,雖然他是用 URP,但大同小異,我們之後再來說明。

[Unity] 方法帶入 Action 參數,讓程式更易讀且更靈活

平常我們的程式都是由上到下按照順序執行,只要沒放錯位置,後面的功能都有辦法讀到前面方法輸出的結果。

但是如果碰到非同步 Async、協程 Coroutine 之類的會大幅改變程式的執行順序時,原本作法就不管用。在這篇文章中要教你如何用「Action」來解決這個問題,而不是用一個迴圈來頻繁檢查前面的作業是否已完成,是個能讓程式更靈活、更易讀的寫法。

Action 是 System 內的功能,它有分成無帶參數有帶參數兩種用法。它在調用常會使用 Lambda 運算式,初次使用的開發者們可能會被這個外星文嚇跑,但其實這個東西並沒有你想得那麼複雜,其實它的邏輯很簡單!

基礎語法一

Action 在其他同樣是 C# 語言的框架中都能用,我們這邊就以 Unity 來說明。

觀念

Action 是一個將「方法」作為「參數」進行傳遞的功能

以往我們對參數的認知都是「死」的,比方說我輸入整數 1,這個參數在方法中就只能做運算或再丟給其他方法使用,它不會為這個方法帶來新的東西。

那我們用 Action 來將方法變成參數,輸入到這個 RunJob 中如何?

public void RunJob(System.Action onComplete)
{
    Debug.Log("作業中...");
    onComplete();
}

你可以發現我們輸入的 onComplete 參數居然可以使用調用方法的寫法來呼叫!這代表我們能在這個方法中的任意位置調用 onComplete,能依照你給的東西來改變程式最終的結果。

那麼我們要怎麼把方法當參數輸入進去呢?

一、再寫一個方法

目前我們是不額外帶參數的 Action,所以我們可以再寫一個無參數的方法 PrintTips,並再調用 RunJob 時將 PrintTips 直接當參數帶入,僅名稱就好,不可加上括弧。
有括弧就是方法調用了,語法錯誤 ❌⛔

private void Start()
{
    RunJob(PrintTips);
}
public void PrintTips()
{
    Debug.Log("初次登入時請記得修改密碼!");
    Debug.Log("不能忘喔!");
}

二、Lambda

Lambda 的規則說起來麻煩,就不解釋了。我們這邊聚焦在要怎麼直接上手用它。請參考以下:

RunJob(() => {
    Debug.Log("初次登入時請記得修改密碼!");
    Debug.Log("不能忘喔!");
});

首先因為現在沒有讓 Action 也帶入參數,所以要用 () 空括弧來代表 => 後的方法內容,大括弧中的內容就是你的自由發揮空間。

如果裡面只有一行程式碼,還可以省略掉大括弧與結尾分號:

RunJob(() => Debug.Log("初次登入時請記得修改密碼!"));

兩種方式的輸出結果都一樣:

實作練習一

請使用一個無帶參數的 Action 並搭配方法,在開始時印出「開始任務…」後過三秒,再經由輸入的 Action 中打印「任務完成!」字串。

參考解答

using System.Collections;
using UnityEngine;
public class ActionTutorial : MonoBehaviour
{
    private void Start()
    {
        RunJob(() => {
            Debug.Log("任務完成!");
        });
    }
    public void RunJob(System.Action onComplete)
    {
        Debug.Log("開始任務...");
        StartCoroutine(JobTask(onComplete));
    }
    private IEnumerator JobTask(System.Action onComplete)
    {
        yield return new WaitForSecondsRealtime(3);
        onComplete();
    }
}

基礎語法二

前面我們講過無帶參數的用法了,現在來介紹有帶參數要怎麼寫。

在寫方法的時候,在定義的 Action 類別後加上 <類別>。例如你想它帶一個整數,那就是 System.Action<int>,如果要帶布林值,那就寫 System.Action<bool>

最後在調用 Action 時帶入你定義的型別資料,就可以成功帶入囉!

public void RunJob(System.Action<int> onComplete)
{
    Debug.Log("開始隨機取號...");
    onComplete(Random.Range(0, 1000));
}

如果想要帶入更多筆資料,除了用 Struct、Class 之外,也可以這樣做:

public void RunJob(System.Action<int, string> onComplete)
{
    Debug.Log("開始隨機取號...");
    onComplete(Random.Range(0, 1000), "成功的男人背後都有一條脊椎");
}

用逗點分隔就能加參數,很有趣吧?(最多可以加 16 個)

那要怎麼接收帶入的參數呢?

一、用另一個方法

假設 RunJob 的 Action 有帶入一個整數,那就在輸入的方法中加入一個整數參數即可。

private void Start()
{
    RunJob(PrintTips);
}
public void PrintTips(int value)
{
    Debug.Log("你抽到的號碼是" + value);
    Debug.Log("恭喜你囉!");
}

多參數的寫法如下,以此類推。

public void PrintTips(int value, string data)
{
    Debug.Log("你抽到的號碼是" + value);
    Debug.Log("恭喜你囉!");
    Debug.Log(data);
}

二、Lambda

如果你僅帶入一個值,那可以用 xyz、abc 或你隨便亂取的名稱來做為暫存用變數,最後就能在定義的區塊隨意調用它。

RunJob(x => {
    Debug.Log("你抽到的號碼是" + x);
    Debug.Log("恭喜你囉!");
});

多參數的寫法如下,需用括弧包住多筆參數,以此類推。

RunJob((x, y) => {
    Debug.Log("你抽到的號碼是" + x);
    Debug.Log("恭喜你囉!");
    Debug.Log(y);
});

兩種方法的輸出結果都一樣:

實作練習二

請用有帶參數的 Action 並搭配方法,輸出一個範圍 -1000~1000 隨機取值的 XY 二維座標,並判斷這個座標是否在地底(看 Y 是否小於 0)。

參考解答

using UnityEngine;
public class ActionTutorial : MonoBehaviour
{
    private void Start()
    {
        RunJob((a, b) => {
            Debug.Log("座標: " + a);
            Debug.Log("是否在地底: " + b);
        });
    }
    public void RunJob(System.Action<Vector2, bool> onComplete)
    {
        int xPos = Random.Range(-1000, 1000);
        int yPos = Random.Range(-1000, 1000);
        bool isUnderGround = yPos < 0;
        onComplete(new Vector2(xPos, yPos), isUnderGround);
    }
}
Exit mobile version