錄製遊戲影片

錄製遊戲影片是時間管理中的一個特例。保存螢幕圖片的操作需要耗費相當多的時間,這會導致遊戲的正常幀率降低,錄製出的影片無法反映遊戲的實際表現。為了解決這個問題,可以使用 captureFramerate 屬性。該屬性的預設值為 0,表示未錄製遊戲。如果要進行錄製,則需要將該屬性的值設為非零值,此時遊戲時間會變慢,幀更新會在精確的間隔內發生。
幀之間的間隔等於 1/Time.captureFramerate,所以如果將值設置為5.0,則更新每五分之一秒發生一次。由於幀率需求有效降低,在 Update 方法中有時間保存螢幕截圖或執行其他操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ExampleScript : MonoBehaviour {
string folder = "ScreenshotFolder";
int frameRate = 25;

void Start () {
Time.captureFramerate = frameRate;
System.IO.Directory.CreateDirectory(folder);
}

void Update () {
string name = string.Format("{0}/{1:D04} shot.png", folder, Time.frameCount );

// 抓取螢幕截圖,並存放到指定位置
Application.CaptureScreenshot(name);
}
}

返回 時間與幀 大綱

Time 類屬性在幀率變化中的範例

以下將說明各種 Time 類中的屬性是如何回報與響應幀率的巨大變化;下面的表格展示了 Unity 執行了 16 幀的情況,其中第 8 幀出現一個大延遲。

Frame unscaledTime time unscaledDeltaTime deltaTime smoothDeltaTime
1 0.000 0.000 0.018 0.018 0.018
2 0.018 0.018 0.018 0.018 0.018
3 0.036 0.036 0.018 0.018 0.018
4 0.054 0.054 0.018 0.018 0.018
5 0.071 0.071 0.017 0.017 0.018
6 0.089 0.089 0.018 0.018 0.018
7 0.107 0.107 0.018 0.018 0.018
8 1.123 0.440 1.016 0.333 0.081
9 1.140 0.457 0.017 0.017 0.066
10 1.157 0.474 0.017 0.017 0.056
11 1.175 0.492 0.018 0.018 0.049
12 1.193 0.510 0.018 0.018 0.042
13 1.211 0.528 0.018 0.018 0.038
14 1.229 0.546 0.018 0.018 0.034
15 1.247 0.564 0.018 0.018 0.031
16 1.265 0.582 0.018 0.018 0.028

在第 1 到第 7 幀中,

  • timeunscaledTime 可以發現這些值穩定的增加,約為 60 幀, timeScale 設為 1 。

在第 8 幀中,出現了超過一秒的大延遲,

  • 當幀延遲超過 maximumDeltaTime 的值時,Unity 會限制 deltaTime 回報的值以及增加到目前時間的量。這個限制的目的是避免如果時間步長超過這個數值可能會發生的不良副作用。
    • 所謂的不良副作用:像這樣的延遲會導致 deltaTime 的值變得非常大。例如,如果延遲超過一秒,deltaTime 可能會超過 1.0 秒。如果一個物體的移動量是透過 deltaTime 縮放的,這樣大的 deltaTime 值會導致物體在一幀內移動非常大的距離,可能直接穿過遊戲中的牆壁或其他障礙物,導致遊戲中不希望看到的「穿牆」現象。

下圖表中可以看到在第 8 幀時, unscaledDeltaTime 超過一秒(1.016),但是 deltaTime 回報的是 0.333 ,這是因為 deltaTimemaximumDeltaTime 限制。


在第 8 幀時, unscaledTime 增加將近一秒(1.123)的時間,但是 time 只增加較小的時間(0.440),這是因為 time 加上的是被限制的 deltaTime (即 maximumDeltaTime 的值)。簡單的說:

  • unscaledTime 代表從遊戲開始到目前幀的真實經過時間,不受限制。
  • time 代表遊戲開始到目前幀的經過時間,但是受 maximumDeltaTime 限制。

Time.smoothDeltaTime 提供了最近一段時間內 deltaTime 的平滑近似值,這種平滑處理可以避免在計算中突然出現的波動,這種平滑的演算法無法預測未來的變化,但是可以讓回報的時間較平滑,因此可以比較符合實際預期的時間。


maximumDeltaTime 也會影響物理系統,物理系統使用 fixedTimestep 值來決定每個固定步長中模擬的時間量。Unity 嘗試保持物理模擬與實際經過的時間同步,有時會在一幀內執行多次物理更新。然而,如果由於某些重計算或延遲導致物理模擬落後太多,系統可能需要多次步驟來追趕當前時間。這種大量的步驟可能進一步導致性能下降。

  • 為了避免由於試圖追趕而導致的逐步減慢,maximumDeltaTime 值還作為物理系統在任何給定兩幀之間模擬時間的上限。
  • 如果一次幀更新的處理時間超過了 maximumDeltaTime,物理引擎將不會試圖模擬超過 maximumDeltaTime 的時間,而是讓幀處理趕上。一旦幀更新完成,物理模擬將恢復,就像它停止後沒有時間流逝一樣。這樣做的結果是,物理物體將不會像通常那樣以完全實時移動,而是會略微減速。然而,物理的「時鐘」仍然會跟踪它們,就好像它們在正常移動一樣。物理時間的減速通常不容易察覺,並且通常是在遊戲性能與實時物理模擬之間的可接受折衷。

返回 時間與幀 大綱

Unity 的時間邏輯流程圖

flowchart TD
    A["`deltaTime`"] --> B{"`deltaTime 
                             是否比
                             maximumDeltaTime 大?`"}
    B -- 是 --> C[deltaTime 的值設為
maximumDeltaTime] C --> D B -- 否 --> D[ deltaTime
被加到 time 中 ] D --> E{"`fixedTime 是否落後 Time.time 一個固定時間步長?`"} E -- 是 --> F[ fixedDeltaTime
加到 fixedTime中 ] F --> G[ 執行 FixedUpdate ] G --> E E -- 否 --> H[ 執行 Update ]

Time.time :為遊戲開始後經過的時間,以秒為單位,一般會持續穩定的增加,只可讀。

  • timeScale 影響,如果 Time.timeScale 設為 0 ,那麼 Time.time 會停止計時,可以達到遊戲暫停的功能。
  • maximumDeltaTime 限制。
  • 當在 FixedUpdate 中呼叫時,回傳的是 Time.fixedTime 的值。
  • 通常用於關於整場遊戲計時的計算。

Time.deltaTime :從上一個幀到目前幀之間的時間間隔,以秒為單位。

  • 可以在遊戲中用來製作倒數裝置,例如施放技能的時間變化。

Time.timeScale :時間流逝的縮放程度,可以用在實現慢動作效果,預設為 1.0 。

  • 當為 1.0 時,表示遊戲時間和真實時間同步,
  • 如果設為 0.5 則遊戲時間以半速運行。
  • 設為 0.1 可以實現慢動作效果,這表示播放速度為正常速度的 10%。經過十秒之後 Time.time 的值會增加 1。
  • 當為 0 時,則時間停止,但是遊戲邏輯(像是 Update 方法仍然會被呼叫)與渲染事件仍然會被觸發。但 Time.time 不會增加,Time.deltaTime 也為 0。

Time.maximumDeltaTime :上面的屬性會被 Time.maximumDeltaTime 限制,這表示這些屬性任何幀率變化的長度永遠不會超過 Time.maximumDeltaTime,例如,如果發生一秒的延遲,且 maximumDeltaTime 為 0.333,那麼 Time.time 只會增加 0.333,而 Time.deltaTime 也會等於 0.333,儘管實際上已經過了更多時間。

  • unscale版本的屬性(如Time.unscaledTimeTime.unscaledDeltaTime)會忽略這些變化和限制,並回報實際經過的時間。對於需要在遊戲慢動作播放時仍以固定速度響應的任何事物都很有用。例如,用於 UI 交互動畫。

返回 時間與幀 大綱

Reference:

固定時間步長(Fixed Timestep)

在 Unity 中,它的物理系統使用固定時間步長(Fixed Timestep),Unity 提供 FixedUpdate 作為我們程式碼的入口(entry point),你可以在這邊執行與物理相關的程式碼,例如向 Rigidbody 施加力。

fixedDeltaTime 是 Unity 中控制固定時間步長循環間隔的屬性,以秒為單位。

  • 例如,當 fixedDeltaTime為 0.01 時意味著每個固定時間步長持續 1/100 秒,因此每秒會執行 100 次固定更新

當你的APP執行的幀率高於每秒固定時間步長(Fixed Timestep)時,這表示每個幀的持續時間會比單個固定時間步長的持續時間還要短。

  • 例如,如果 fixedDeltaTime 設為 0.02,那麼通常每秒會有 50 次固定更新。
    • 如果你的 APP 以每秒 60 幀的速度執行,這會超過每秒 50 次的固定更新,這表示大約每十幀中會有一幀不會觸發固定更新。

如果你的 APP 的幀率低於固定時間步長值,這意味著每個幀的持續時間比單個固定時間步長還要長。為了解決這個問題,Unity 每幀會執行一個或多個固定更新,以便物理模擬能夠趕上自上一幀以來流逝的時間。

  • 例如,如果你的固定時間步長值為 0.01,每秒會執行 100 次固定更新。如果你的應用以每秒 25 幀的速度運行,每幀的持續時間(0.04 秒)是固定時間步長值(0.01 秒)的四倍, Unity 將每幀執行四次固定更新。在幀率較低時,Unity 通過增加固定更新次數來保持物理模擬的準確性。

fixedDeltaTime 設定的越低,表示越頻繁的執行物理更新,模擬的越精確,但是會加大 CPU 的負擔。

返回 時間與幀 大綱

可變動幀率(Variable Frame Rate)

在 Unity 中, APP 的每一幀所執行的時間會因為程式碼複雜度或是顯示畫面的複雜度而有所不同,此外也會受到裝置設備的效能影響。

  • 例如:當有一百個角色在螢幕上活動時,每一幀的執行速度會比只有一個角色來的慢,這種可變的速率通常被稱為 每秒幀數 FPS
  • 一般來說 Unity 會嘗試以最快的幀率執行你的 APP。

Update 方法

在 Unity 中,提供了 Update 方法作為每一幀執行我們程式碼的入口(entry point),例如在 Update 中取得使用者的輸入讓遊戲角色向前移動。但是要記住遊戲的幀率是可變的,因此Update調用之間的時間間隔也會變化。


幀率變動對遊戲物件移動速度的影響

下面的程式碼想做的事是把這個遊戲物件平移一段距離,但是幀率(frame rate)是會變動的,因此這個遊戲物件在移動時,它的速度也會變化。
例如:

  • 假設遊戲為每秒 100 幀的速度在執行,那麼 Update 這個方法會執行 100 次,因此這個物件每秒會移動 distancePerFrame 一百次
  • 但是如果因為 CPU 負載變高導致速度下降到 每秒 60 幀的速度在執行的話,那麼這個物件會變為每秒移動 distancePerFrame 六十次
1
2
3
4
5
6
7
public class ExampleScript : MonoBehaviour {
public float distancePerFrame;

void Update() {
transform.Translate(0, 0, distancePerFrame); // 錯誤
}
}

大部分的情況下,這是不好的,因為通常在遊戲中會希望物體是以穩定且可預測的速度移動而不受到幀率影響。


使用 Time.deltaTime 確保一致性

Time.deltaTime 為從上一幀到目前幀之間的時間間隔,以秒為單位。這個屬性通常用於在 Update 方法中進行時間相關的計算
例如,你想要從現開始,經過一秒後做某些動作,可以這樣操作:

1
2
3
4
5
6
7
8
9
10
11
public class ExampleScript : MonoBehaviour {
private float timer = 0f;

void Update() {
timer += Time.deltaTime;
if (timer >= 1f) {
Debug.Log("一秒過去了,某些動作");
timer = 0f;
}
}
}

因此解決上面位移問題的方式是根據每一幀經過的時間來縮放每一幀的移動量,這樣無論幀率如何變化,物體每秒移動的總距離都會保持一致。程式碼如下:

1
2
3
4
5
6
7
8
public class ExampleScript : MonoBehaviour {
// 變數的名稱變為 per second ,這是因為此時不再是依靠幀而是秒了
public float distancePerSecond;

void Update() {
transform.Translate(0, 0, distancePerSecond * Time.deltaTime);
}
}

返回 時間與幀 大綱

簡單技能冷卻計時器

以下將建立一個簡單的技能冷卻計時器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SkillTimer : MonoBehaviour
{
private float timer = 0;
private float skillCooldown = 3;

void Update()
{

// 讓計時器隨時間增加
timer += Time.deltaTime;

if (timer > skillCooldown)
{
// 重置計時器
timer = 0;

// 釋放技能...
Debug.Log("施放技能");
}

}
}

timer 變數用於累計經過的時間。

skillCooldown 用於設定技能冷卻時間為 3 秒。

每次 Update 被呼叫時,timer 會不斷的加上 Time.deltaTime

timer 超過 skillCooldown 時,就會重置計時器,並施放技能

返回 時間與幀 大綱

時間(Time)與幀(Frame)

Unity 中有一個 Time 類(class),它提供了與時間相關的重要方法,讓你可以在專案中使用。


在 Unity 中,有兩個追蹤時間(track time)的系統,一個是每步之間的時間量是可變的,另一個是每步之間的時間量是固定的。

  • 可變時間步長(variable time step)系統:在每次繪製畫面和運行你的APP或遊戲程式碼時執行一次。
  • 固定時間步長(fixed time step)系統:以預定義的時間步長前進,並且與畫面的更新無關。這個系統通常與物理系統相關聯,物理系統會按照固定時間步長的速率運行,但你也可以在每個固定時間步長中執行自己的代碼。

可變動幀率(Variable Frame Rate):在 Unity 中每一幀所執行的時間會因為程式碼複雜度或是顯示畫面的複雜度而有所不同,同時也會受到裝置設備的效能影響。

固定時間步長(Fixed Timestep):在 Unity 中,它的物理系統使用固定時間步長(Fixed Timestep),防止突然出現物體在一幀內移動非常大距離的狀況。

Unity 的時間邏輯流程圖:此文將說明 Unity 是如何處理時間的。

Time 類屬性在幀率變化中的範例:在此文中將說明 Time 類中的各屬性是如何處理突如其來的幀率大幅變動。

錄製遊戲影片:錄製遊戲影片是時間管理中的一個特例。保存螢幕圖片的操作需要耗費相當多的時間,這會導致遊戲的正常幀率降低,錄製出的影片無法反映遊戲的實際表現。為了解決這個問題,可以使用 Unity 提供的 captureFramerate 屬性,來調整錄製時的幀率。

一個簡單的技能冷卻計時器:將示範如何使用 Time.deltaTime 來製作技能冷卻時間。


Time 類有以下 static 屬性

captureDeltaTime 減慢你APP的播放時間(playback time),以便 Unity 能夠在幀與幀之間保存截圖。
captureFramerate Time.captureDeltaTime 的倒數(reciprocal),即 1/captureDeltaTime
deltaTime 從上一個幀到目前幀之間的時間間隔,單位是秒 (只可讀)
fixedDeltaTime 固定間隔幀時間
fixedTime 從遊戲開始到現在 FixedUpdate 進行的時間,以秒為單位 (只可讀)
fixedTimeAsDouble fixedTime 相同,但是是 double 型態 (只可讀)
fixedUnscaledDeltaTime 每次 FixedUpdate 之間的固定時間間隔,以秒為單位,且不受timeScale影響(timeScale-independent)。(只可讀)。這表示調整timeScale時(如暫停遊戲),它的值也不受影響
fixedUnscaledTime 表示從遊戲開始以來經過的時間,以固定的時間間隔計算,並且不受 timeScale 影響 (只可讀)。
fixedUnscaledTimeAsDouble 與 fixedUnscaledTime 相同,但是是 double 型態 (只可讀)
frameCount 遊戲開始以來的總幀數 (只可讀)
inFixedTimeStep 如果在 fixed time step callback 中將返回 true 例如MonoBehaviour 的 FixedUpdate,否則返回 false 。(只可讀)
maximumDeltaTime 任何給定的幀中 Time.deltaTime 的最大值. 用來限制兩個幀之間的時間(Time.time)增加,以秒為單位。
maximumParticleDeltaTime 在一幀中可以花在更新粒子(particle)的最大時間。如果在一幀中超過這個時間,那麼就會把這個更新分割為多個較小的更新。
realtimeSinceStartup 它返回從APP啟動到當前時間所經過的時間(以秒為單位)。也就是說,它計算了從APP啟動以來的總實際時間,而不僅僅是應用程序在前台運行的時間 (只可讀)
realtimeSinceStartupAsDouble realtimeSinceStartup 相同,但是是 double 型態 (只可讀)
smoothDeltaTime 一個平滑的 Time.deltaTime (只可讀)
time 從APP開始執行到目前這一幀的總時間長,以秒為單位 (只可讀)
timeAsDouble time 相同,但是是 double 型態 (只可讀)
timeScale 時間流逝的縮放程度,可以用在實現慢動作效果,預設為 1.0 。 當為 1.0 時,表示遊戲時間和真實時間同步,如果設為 0.5 則遊戲時間以半速運行。當為 0 時,則時間停止,但是遊戲邏輯(像是 Update 方法仍然會被呼叫)與渲染事件仍然會被觸發。
timeSinceLevelLoad 自從最後一個非附加場景(non-additive scene)載入完成以來的時間 (以秒為單位)
timeSinceLevelLoadAsDouble timeSinceLevelLoad 相同,但是是 double 型態 (只可讀)
unscaledDeltaTime 自上一幀以來經過的時間(以秒為單位),並且不受 Time.timeScale 影響 (只可讀)
unscaledTime 自遊戲開始以來經過的總時間(以秒為單位),這個時間不受 Time.timeScale 的影響
unscaledTimeAsDouble unscaledTimeAsDouble 相同,但是是 double 型態 (只可讀)

Reference:

線性插值(Lerp)

Mathf.Lerp 是 Unity 中的一個數學函數,用於在兩個值之間進行線性插值(Linear Interpolation)。
這個函數可以幫助你製作平滑的效果,像是顏色轉換,動畫轉換等。

語法為 float Mathf.Lerp(float a, float b, float t)

  • a :起始值
  • b :結束值
  • t :代表 ab 之間的插值比例,範圍在 0 到 1 之間。如果 t 為 0,則返回 a;如果 t 為 1,則返回 b;如果 t 為 0.5,則返回 a 和 b 之間的中間值(0.5)。
  • 例如: Mathf.Lerp (3f, 5f, 0.5f) 會得到 4 。

我們來看一個例子,假設 a=0 , b=20 , t=0.5 。 每一秒執行一次,並將結果儲存回 a

  • 開始: a=0 ,
  • 第一秒: a = Mathf.Lerp (0f, 20f, 0.5f) = 10
  • 第二秒: a = Mathf.Lerp (10f, 20f, 0.5f) = 15
  • 第三秒: a = Mathf.Lerp (15f, 20f, 0.5f) = 17.5

將結果畫為一個座標圖,其中 X 軸代表時間(每一秒), Y 軸代表每一秒執行之後的 a

a: b: t: time:

可得出以下簡單的結論

  • 隨著時間增加 a 會從開始快速增加,但是當越接近 b 時,成長的速度就會越慢。

下面是一個使用 Mathf.Lerp 調整光強度的例子。

  • 假設光的強度從 0 開始
  • 在第一次更新幀後: Mathf.Lerp(0f, 8f, 0.5f) 得到的值為 4
  • 第二次更新幀後: Mathf.Lerp(4f, 8f, 0.5f) 得到的值為 6
  • 第三次更新幀後: Mathf.Lerp(6f, 8f, 0.5f) 得到的值為 7
  • 第四次更新幀後: Mathf.Lerp(7f, 8f, 0.5f) 得到的值為 7.5
  • …依此類推,最後將趨近於 8 ,但是隨著 a 的值接近 b ,其變化的速率將減慢。
    1
    2
    3
    4
    5
    6
    7
    8
    void Start() 
    {
    light.intensity = 0;
    }
    void Update ()
    {
    light.intensity = Mathf.Lerp(light.intensity, 8f, 0.5f);
    }

下面的程式碼將展示如何使用 Mathf.Lerp 來在兩個值之間做插值,讓一個遊戲物件來回移動。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Example : MonoBehaviour
{
// 定義遊戲物件在X軸上的移動範圍
public float minimum = -1.0F; // 最小值 -1.0
public float maximum = 1.0F; // 最大值 1.0


// Lerp 的初始值
static float t = 0.0f;

void Update()
{
// 使用 Lerp 函數在 minimum 和 maximum 之間插值,這個遊戲物件一開始將從 minimum 移動到 maximim
transform.position = new Vector3(Mathf.Lerp(minimum, maximum, t), 0, 0);

// 增加 t 的值,0.5f * Time.deltaTime 來控制插值的速度
t += 0.5f * Time.deltaTime;

// 當 t 超過 1.0 時,將 t 重置為 0 並交換 minimum 和 maximum
// 這樣物件將開始從 maximum 移動回 minimum
if (t > 1.0f)
{
float temp = maximum;
maximum = minimum;
minimum = temp;
t = 0.0f;
}
}
}

使用 0.5f * Time.deltaTime 的原因是:確保增量操作(例如移動或旋轉)在不同的幀率下保持一致。這樣,無論遊戲以每秒 30 幀還是每秒 60 幀運行,物件的移動速度都會保持一致。
舉個例子:假設你的遊戲每秒運行 60 幀,則 Update 每一秒會執行 60 次, 那麼每幀的 Time.deltaTime 約為 1/60 秒 ≈ 0.0167 秒

  • 如果想要讓每秒 t 增長的量為 0.5f , 以每一幀來看, t 增長的量要為 0.5f * 0.0167 ≈ 0.0083 , 這樣在執行 60 次 Update 後約為一秒 => 0.5f * 0.0167 * 60 ≈ 0.5f

另外還有 Color.LerpVector3.Lerp 這些方法的工作方式與 Mathf.Lerp 完全相同,差別在於輸入 ab 的類型

Reference:

ScriptableObject as delegate object

ScriptableObject 除了作為資料的容器外,還可在其內部定義方法,讓 MonoBehaviour 可以將自己傳入 ScriptableObject 的方法中,根據不同的 ScriptableObject 實體去執行不同的動作。

例如:要做增益效果,定義一個抽象的 PowerupEffect 類,它繼承 ScriptableObject ,並且只有一個 ApplyTo 方法,這個方法將增益效果應用到傳入的 GameObject 中。

1
2
3
4
public abstract class PowerupEffect : ScriptableObject 
{
public abstract void ApplyTo(GameObject object);
}

建立一個子類 HpBooster ,並 overrideApplyTo ,將增加遊戲物件的生命值。

1
2
3
4
5
6
7
8
public class HpBooster : PowerupEffect
{
public int Amount;
public override void ApplyTo(GameObject object)
{
object.GetComponent<HP>().currentValue += Amount;
}
}

在建立一個 增益物件 的腳本,這個增益物件將持有 PowerupEffect 的參考,當在觸發器中觸發時,就會呼叫它持有的 PowerupEffect 來將增益效果應用到碰到的遊戲物件。

1
2
3
4
5
6
7
8
9
public class Powerup : MonoBehaviour 
{
public PowerupEffect effect;

public void OnTriggerEnter(Collider other)
{
effect.ApplyTo(other.gameObject);
}
}

例子,使用音效

1
2
3
4
5
6
7
// 這個 struct 為最大最小值的結構
[Serializable]
public struct RangedFloat
{
public float minValue;
public float maxValue;
}

定義一個抽象的 AudioDelegateSO 並繼承 ScriptableObject。在其中含有一個 Play 抽象方法,讓傳入的 AudioSource 播放音效。

1
2
3
4
public abstract class AudioDelegateSO: ScriptableObject
{
public abstract void Play(AudioSource source);
}

定義一個 SimpleAudioDelegateSO 類,它繼承 AudioDelegateSO
這個 SimpleAudioDelegateSO 可以管理一個音效列表clips,在 Play 方法中,將隨機播放 clips 列表中的聲音。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[CreateAssetMenu(fileName ="AudioDelegate")]
public class SimpleAudioDelegateSO : AudioDelegateSO
{
public AudioClip[] clips;
public RangedFloat volume;
public RangedFloat pitch;
public override void Play(AudioSource source)
{
if (clips.Length == 0 || source == null)
{
return;
}
source.clip = clips[Random.Range(0, clips.Length)];
source.volume = Random.Range(volume.minValue, volume.maxValue);
source.pitch = Random.Range(pitch.minValue, pitch.maxValue);
source.Play();
}
}

我們可以使用這個 SimpleAudioDelegateSO 讓同一個遊戲物件隨機播放不同的聲音,例如可以儲存狗的叫聲,每當觸碰狗遊戲物件時,就隨機播放一種叫聲。


例子: 在遊戲中,會分為玩家控制,或是電腦控制的遊戲物件,如坦克遊戲中的坦克,可以由不同 AI 控制,也可以為玩家控制。這種不同的行為,我們也可以用 Delegate Object 來做。

定義一個抽象的 TankBrain 繼承 ScriptableObject。其中

  • 含有 Initialize 虛方法,不一定會實作這個方法,但是當需要做初始化的動作時可以 override 它。
  • 含有一個 Think 抽象方法,決定傳入的 TankThinker 要有什麼行為。
    1
    2
    3
    4
    public abstract class TankBrain : ScrptableObject
    {
    public abstract void Think(TankThinker tank);
    }

定義一個 PlayerControlledTank 類,它繼承 TankBrain

  • 它為玩家操控的坦克。
  • override Think 方法,在裡面取得坦克的位置,並判斷輸入來移動坦克或是發射砲彈。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    [CreateAssetMenu(menuName="Brains/Player Controlled")]
    public class PlayerControlledTank : TankBrain
    {
    public int PlayerNumber;
    private string m_MovementAxisName;
    private string m_TurnAxisName;
    private string m_FireButton;

    public void OnEnable()
    {
    m_MovementAxisName = "Vertical" + PlayerNumber;
    m_TurnAxisName = "Horizontal" + PlayerNumber;
    m_FireButton = "Fire" + PlayerNumber;
    }

    public override void Think(TankThinker tank)
    {
    // 取得坦克移動 component
    var movement = tank.GetComponent<TankMovement>();
    // 透過 Steer 與 Input 輸入來移動坦克
    movement.Steer(Input.GetAxis(m_MovementAxisName), Input.GetAxis(m_TurnAxisName));
    // 取得坦克發射砲彈 component
    var shooting = tank.GetComponent<TankShooting>();
    // 判斷 Input 輸入是否為開火按鈕
    if (Input.GetButton(m_FireButton))
    shooting.BeginChargingShot();
    else
    shooting.FireChargedShot();
    }
    }

定義一個 SimpleSniper 類,它也繼承 TankBrain ,它為電腦操控的坦克。

  • override Think 方法,在裡面判斷是否移動與開火
    • 如取得坦克的位置,並根據它的記憶(Remeber)中判斷是否看過玩家坦克
      • 如果記憶(Remeber)中沒有玩家坦克,則找最近的物件,並判斷是否為玩家坦克
      • 如果還是沒找到,則移動自己
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
[CreateAssetMenu(menuName="Brains/Simple sniper")]
public class SimpleSniper : TankBrain
{

public float aimAngleThreshold = 2f;
[MinMaxRange(0, 0.05f)]
public RangedFloat chargeTimePerDistance;
[MinMaxRange(0, 10)]
public RangedFloat timeBetweenShots;

public override void Think(TankThinker tank)
{
GameObject target = tank.Remember<GameObject>("target");
var movement = tank.GetComponent<TankMovement>();

if (!target)
{
// 如果記憶(`Remeber`)中沒有玩家坦克,則找最近的物件,並判斷是否為玩家坦克
target =
GameObject
.FindGameObjectsWithTag("Player")
.OrderBy(go => Vector3.Distance(go.transform.position, tank.transform.position))
.FirstOrDefault(go => go != tank.gameObject);
// 記住找到的目標
tank.Remember<GameObject>("target", target);
}

if (!target)
{
// 如果還是沒找到,則移動自己
movement.Steer(0.5f, 1f);
return;
}

// 瞄準目標
Vector3 desiredForward = (target.transform.position - tank.transform.position).normalized;
if (Vector3.Angle(desiredForward, tank.transform.forward) > aimAngleThreshold)
{
// 判斷轉動方向
bool clockwise = Vector3.Cross(desiredForward, tank.transform.forward).y > 0;
movement.Steer(0f, clockwise ? -1 : 1);
}
else
{
// 停止轉向
movement.Steer(0f, 0f);
}

// 獲取坦克的射擊 component
var shooting = tank.GetComponent<TankShooting>();
// 檢查是否可以開始充能射擊
if (!shooting.IsCharging)
{
if (Time.time > tank.Remember<float>("nextShotAllowedAfter"))
{
float distanceToTarget = Vector3.Distance(target.transform.position, tank.transform.position);
float timeToCharge = distanceToTarget*Random.Range(chargeTimePerDistance.minValue, chargeTimePerDistance.maxValue);
tank.Remember("fireAt", Time.time + timeToCharge);
shooting.BeginChargingShot();
}
}
else
{
float fireAt = tank.Remember<float>("fireAt");
if (Time.time > fireAt)
{
shooting.FireChargedShot();
tank.Remember("nextShotAllowedAfter", Time.time + Random.Range(timeBetweenShots.minValue, timeBetweenShots.maxValue));
}
}
}
}

返回 ScriptableObject 系列

Reference:

在 Linux 上安裝並使用 AWS CLI 將檔案上傳至 S3

在 Linux 下,使用 以下指令下載 awscliv2.zip

1
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"

解壓縮並安裝

1
2
unzip awscliv2.zip
sudo ./aws/install

使用 aws configure --profile 設定profile的 Access Key IDSecrect Access Key 等資訊。

  • aws configure --profile <你想設定的profile名稱>
    1
    2
    3
    4
    5
    root@host ~# aws configure --profile my-dev
    AWS Access Key ID [None]: ASIAIOSFODNN7EXAMPLE
    AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
    Default region name [None]: us-east-1
    Default output format [None]: json

設定完之後,便可以使用 AWS CLI 進行檔案上傳、下載或刪除等操作。
假設 Bucket 的名稱是 dev

使用 aws s3 ls 可以將 Bucket 中的檔案列出來

  • aws s3 ls s3://<你的Bucket名稱> --profile <你設定的profile>
    1
    2
    root@host ~# aws s3 ls s3://dev --profile my-dev
    2024-06-24 11:23:11 4 test.csv

使用 aws s3 cp 可以將檔案上傳到 Bucket 中或是從 Bucket 中下載檔案:

  • 上傳檔案至S3:使用 aws s3 cp <你要上傳檔案> s3://<你的Bucket名稱>/<要上傳的prefix與名稱> --profile <你設定的profile>

    1
    2
    root@host ~# aws s3 cp /tmp/test.csv s3://dev/test.csv --profile my-dev
    upload: /tmp/test.csv to s3://dev/test.csv
  • S3下載檔案:使用 aws s3 cp s3://<你的Bucket名稱>/<要下載的prefix與名稱> <你要下載的位置> --profile <你設定的profile>

    1
    2
    root@host ~# aws s3 cp s3://dev/test.csv /tmp/downloadfroms3.csv --profile my-dev
    download: s3://dev/test.csv to ../tmp/downloadfroms3.csv

使用 aws s3 rm 刪除在 S3 上面的檔案

  • aws s3 rm s3://<你的Bucket名稱>/<要刪除的prefix與名稱> --profile <你設定的profile>
    1
    2
    root@host ~# aws s3 rm s3://dev/test.csv  --profile my-dev
    delete: s3://dev/test.csv

Reference: