ScriptableObject 是一個資料容器(Data container),用於儲存資料並作為資源(Asset)在執行時使用。除了基本的資料儲存功能外,它還有多種特殊用法。
ScriptableObject 是一個資料容器(Data container),用於儲存資料並作為資源(Asset)在執行時使用。除了基本的資料儲存功能外,它還有多種特殊用法。
錄製遊戲影片是時間管理中的一個特例。保存螢幕圖片的操作需要耗費相當多的時間,這會導致遊戲的正常幀率降低,錄製出的影片無法反映遊戲的實際表現。為了解決這個問題,可以使用 captureFramerate 屬性。該屬性的預設值為 0,表示未錄製遊戲。如果要進行錄製,則需要將該屬性的值設為非零值,此時遊戲時間會變慢,幀更新會在精確的間隔內發生。
幀之間的間隔等於 1/Time.captureFramerate,所以如果將值設置為5.0,則更新每五分之一秒發生一次。由於幀率需求有效降低,在 Update 方法中有時間保存螢幕截圖或執行其他操作。
1 | public class ExampleScript : MonoBehaviour { |
返回 時間與幀 大綱
以下將說明各種 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 幀中,
time 和 unscaledTime 可以發現這些值穩定的增加,約為 60 幀, timeScale 設為 1 。在第 8 幀中,出現了超過一秒的大延遲,
maximumDeltaTime 的值時,Unity 會限制 deltaTime 回報的值以及增加到目前時間的量。這個限制的目的是避免如果時間步長超過這個數值可能會發生的不良副作用。deltaTime 的值變得非常大。例如,如果延遲超過一秒,deltaTime 可能會超過 1.0 秒。如果一個物體的移動量是透過 deltaTime 縮放的,這樣大的 deltaTime 值會導致物體在一幀內移動非常大的距離,可能直接穿過遊戲中的牆壁或其他障礙物,導致遊戲中不希望看到的「穿牆」現象。下圖表中可以看到在第 8 幀時, unscaledDeltaTime 超過一秒(1.016),但是 deltaTime 回報的是 0.333 ,這是因為 deltaTime 被 maximumDeltaTime 限制。
在第 8 幀時, unscaledTime 增加將近一秒(1.123)的時間,但是 time 只增加較小的時間(0.440),這是因為 time 加上的是被限制的 deltaTime (即 maximumDeltaTime 的值)。簡單的說:
unscaledTime 代表從遊戲開始到目前幀的真實經過時間,不受限制。time 代表遊戲開始到目前幀的經過時間,但是受 maximumDeltaTime 限制。Time.smoothDeltaTime 提供了最近一段時間內 deltaTime 的平滑近似值,這種平滑處理可以避免在計算中突然出現的波動,這種平滑的演算法無法預測未來的變化,但是可以讓回報的時間較平滑,因此可以比較符合實際預期的時間。
maximumDeltaTime 也會影響物理系統,物理系統使用 fixedTimestep 值來決定每個固定步長中模擬的時間量。Unity 嘗試保持物理模擬與實際經過的時間同步,有時會在一幀內執行多次物理更新。然而,如果由於某些重計算或延遲導致物理模擬落後太多,系統可能需要多次步驟來追趕當前時間。這種大量的步驟可能進一步導致性能下降。
maximumDeltaTime 值還作為物理系統在任何給定兩幀之間模擬時間的上限。maximumDeltaTime,物理引擎將不會試圖模擬超過 maximumDeltaTime 的時間,而是讓幀處理趕上。一旦幀更新完成,物理模擬將恢復,就像它停止後沒有時間流逝一樣。這樣做的結果是,物理物體將不會像通常那樣以完全實時移動,而是會略微減速。然而,物理的「時鐘」仍然會跟踪它們,就好像它們在正常移動一樣。物理時間的減速通常不容易察覺,並且通常是在遊戲性能與實時物理模擬之間的可接受折衷。返回 時間與幀 大綱
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 。
Time.time 的值會增加 1。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.unscaledTime和Time.unscaledDeltaTime)會忽略這些變化和限制,並回報實際經過的時間。對於需要在遊戲慢動作播放時仍以固定速度響應的任何事物都很有用。例如,用於 UI 交互動畫。返回 時間與幀 大綱
Reference:
在 Unity 中,它的物理系統使用固定時間步長(Fixed Timestep),Unity 提供 FixedUpdate 作為我們程式碼的入口(entry point),你可以在這邊執行與物理相關的程式碼,例如向 Rigidbody 施加力。
fixedDeltaTime 是 Unity 中控制固定時間步長循環間隔的屬性,以秒為單位。
fixedDeltaTime為 0.01 時意味著每個固定時間步長持續 1/100 秒,因此每秒會執行 100 次固定更新。當你的APP執行的幀率高於每秒固定時間步長(Fixed Timestep)時,這表示每個幀的持續時間會比單個固定時間步長的持續時間還要短。
fixedDeltaTime 設為 0.02,那麼通常每秒會有 50 次固定更新。如果你的 APP 的幀率低於固定時間步長值,這意味著每個幀的持續時間比單個固定時間步長還要長。為了解決這個問題,Unity 每幀會執行一個或多個固定更新,以便物理模擬能夠趕上自上一幀以來流逝的時間。
fixedDeltaTime 設定的越低,表示越頻繁的執行物理更新,模擬的越精確,但是會加大 CPU 的負擔。
返回 時間與幀 大綱
在 Unity 中, APP 的每一幀所執行的時間會因為程式碼複雜度或是顯示畫面的複雜度而有所不同,此外也會受到裝置設備的效能影響。
FPS 。在 Unity 中,提供了 Update 方法作為每一幀執行我們程式碼的入口(entry point),例如在 Update 中取得使用者的輸入讓遊戲角色向前移動。但是要記住遊戲的幀率是可變的,因此Update調用之間的時間間隔也會變化。
下面的程式碼想做的事是把這個遊戲物件平移一段距離,但是幀率(frame rate)是會變動的,因此這個遊戲物件在移動時,它的速度也會變化。
例如:
1 | public class ExampleScript : MonoBehaviour { |
大部分的情況下,這是不好的,因為通常在遊戲中會希望物體是以穩定且可預測的速度移動而不受到幀率影響。
Time.deltaTime 確保一致性Time.deltaTime 為從上一幀到目前幀之間的時間間隔,以秒為單位。這個屬性通常用於在 Update 方法中進行時間相關的計算
例如,你想要從現開始,經過一秒後做某些動作,可以這樣操作:
1 | public class ExampleScript : MonoBehaviour { |
因此解決上面位移問題的方式是根據每一幀經過的時間來縮放每一幀的移動量,這樣無論幀率如何變化,物體每秒移動的總距離都會保持一致。程式碼如下:
1 | public class ExampleScript : MonoBehaviour { |
返回 時間與幀 大綱
以下將建立一個簡單的技能冷卻計時器
1 | public class SkillTimer : MonoBehaviour |
timer 變數用於累計經過的時間。
skillCooldown 用於設定技能冷卻時間為 3 秒。
每次 Update 被呼叫時,timer 會不斷的加上 Time.deltaTime
當 timer 超過 skillCooldown 時,就會重置計時器,並施放技能
返回 時間與幀 大綱
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:
Mathf.Lerp 是 Unity 中的一個數學函數,用於在兩個值之間進行線性插值(Linear Interpolation)。
這個函數可以幫助你製作平滑的效果,像是顏色轉換,動畫轉換等。
語法為 float Mathf.Lerp(float a, float b, float t)
a :起始值b :結束值t :代表 a 與 b 之間的插值比例,範圍在 0 到 1 之間。如果 t 為 0,則返回 a;如果 t 為 1,則返回 b;如果 t 為 0.5,則返回 a 和 b 之間的中間值(0.5)。我們來看一個例子,假設 a=0 , b=20 , t=0.5 。 每一秒執行一次,並將結果儲存回 a
Mathf.Lerp (0f, 20f, 0.5f) = 10Mathf.Lerp (10f, 20f, 0.5f) = 15Mathf.Lerp (15f, 20f, 0.5f) = 17.5將結果畫為一個座標圖,其中 X 軸代表時間(每一秒), Y 軸代表每一秒執行之後的 a
可得出以下簡單的結論
a 會從開始快速增加,但是當越接近 b 時,成長的速度就會越慢。下面是一個使用 Mathf.Lerp 調整光強度的例子。
Mathf.Lerp(0f, 8f, 0.5f) 得到的值為 4Mathf.Lerp(4f, 8f, 0.5f) 得到的值為 6Mathf.Lerp(6f, 8f, 0.5f) 得到的值為 7Mathf.Lerp(7f, 8f, 0.5f) 得到的值為 7.5a 的值接近 b ,其變化的速率將減慢。1 | void Start() |
下面的程式碼將展示如何使用 Mathf.Lerp 來在兩個值之間做插值,讓一個遊戲物件來回移動。
1 | public class Example : MonoBehaviour |
使用 0.5f * Time.deltaTime 的原因是:確保增量操作(例如移動或旋轉)在不同的幀率下保持一致。這樣,無論遊戲以每秒 30 幀還是每秒 60 幀運行,物件的移動速度都會保持一致。
舉個例子:假設你的遊戲每秒運行 60 幀,則 Update 每一秒會執行 60 次, 那麼每幀的 Time.deltaTime 約為 1/60 秒 ≈ 0.0167 秒
Update 後約為一秒 => 0.5f * 0.0167 * 60 ≈ 0.5f另外還有 Color.Lerp 和 Vector3.Lerp 這些方法的工作方式與 Mathf.Lerp 完全相同,差別在於輸入 a 和 b 的類型
Reference:
ScriptableObject as delegate object
ScriptableObject 除了作為資料的容器外,還可在其內部定義方法,讓 MonoBehaviour 可以將自己傳入 ScriptableObject 的方法中,根據不同的 ScriptableObject 實體去執行不同的動作。
例如:要做增益效果,定義一個抽象的 PowerupEffect 類,它繼承 ScriptableObject ,並且只有一個 ApplyTo 方法,這個方法將增益效果應用到傳入的 GameObject 中。
1 | public abstract class PowerupEffect : ScriptableObject |
建立一個子類 HpBooster ,並 override 了 ApplyTo ,將增加遊戲物件的生命值。
1 | public class HpBooster : PowerupEffect |
在建立一個 增益物件 的腳本,這個增益物件將持有 PowerupEffect 的參考,當在觸發器中觸發時,就會呼叫它持有的 PowerupEffect 來將增益效果應用到碰到的遊戲物件。
1 | public class Powerup : MonoBehaviour |
例子,使用音效
1 | // 這個 struct 為最大最小值的結構 |
定義一個抽象的 AudioDelegateSO 並繼承 ScriptableObject。在其中含有一個 Play 抽象方法,讓傳入的 AudioSource 播放音效。
1 | public abstract class AudioDelegateSO: ScriptableObject |
定義一個 SimpleAudioDelegateSO 類,它繼承 AudioDelegateSO ,
這個 SimpleAudioDelegateSO 可以管理一個音效列表clips,在 Play 方法中,將隨機播放 clips 列表中的聲音。
1 | [] |
我們可以使用這個 SimpleAudioDelegateSO 讓同一個遊戲物件隨機播放不同的聲音,例如可以儲存狗的叫聲,每當觸碰狗遊戲物件時,就隨機播放一種叫聲。
例子: 在遊戲中,會分為玩家控制,或是電腦控制的遊戲物件,如坦克遊戲中的坦克,可以由不同 AI 控制,也可以為玩家控制。這種不同的行為,我們也可以用 Delegate Object 來做。
定義一個抽象的 TankBrain 繼承 ScriptableObject。其中
Initialize 虛方法,不一定會實作這個方法,但是當需要做初始化的動作時可以 override 它。Think 抽象方法,決定傳入的 TankThinker 要有什麼行為。1 | public abstract class TankBrain : ScrptableObject |
定義一個 PlayerControlledTank 類,它繼承 TankBrain
override Think 方法,在裡面取得坦克的位置,並判斷輸入來移動坦克或是發射砲彈。1 | [] |
定義一個 SimpleSniper 類,它也繼承 TankBrain ,它為電腦操控的坦克。
override Think 方法,在裡面判斷是否移動與開火Remeber)中判斷是否看過玩家坦克Remeber)中沒有玩家坦克,則找最近的物件,並判斷是否為玩家坦克1 | [] |
Reference: