固定時間步長(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:

ScriptableObject as Enum

ScriptableObject 作為 Enum 可以有很多好處,像是可含有自己的屬性和方法等。另外還不具 Enum 的一些缺陷

Enum 的缺陷有

  1. 它會隱含(implicitly)的為所有元素賦予一個整數值,這造成刪改 Enum 中的值很不方便,若隨意刪除一個元素,那它之後元素的值都會變

    • 下面例子,沒有明確指定值,因此會自動從0開始為每個元素賦值
    1
    2
    3
    4
    5
    6
    enum Skill 
    {
    SpeedUp,
    PowerUp,
    DefenseUp
    }

    預設會自動幫你的賦值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    enum Skill 
    {
    SpeedUp = 0,
    PowerUp = 1,
    DefenseUp = 2
    }

    // 假設之後要刪除 `PowerUp` ,那麼在它之後元素的值都會變
    enum Skill
    {
    SpeedUp,
    DefenseUp // DefenseUp 的值會變為 1
    }
  2. 除非使用 Nullable 否則它不可以為 NULL ,因此一般常會自訂一個元素來代表空值

    • 自訂一個元素來代表空值
      1
      2
      3
      4
      5
      6
      7
      enum Skill 
      {
      NONE = 0,
      SpeedUp = 1,
      PowerUp = 2,
      DefenseUp = 3
      }
  3. C#enum 中不允許有方法或是屬性,需要使用擴展方法來完成

    • 不可以有方法或屬性
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      enum Skill 
      {
      NONE = 0,
      SpeedUp = 1,
      PowerUp = 2,
      DefenseUp = 3
      // 不可以
      // private float bonusRatio;
      // 不可以
      // public float GetBonusRatio(){ return bonusRatio; }
      }
      需要使用擴展方法來完成
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      public static class SkillExtensions
      {
      public static double GetBonusRatio(this Skill skill)
      {
      switch (skill)
      {
      case Skill.SpeedUp:
      return 1.2;
      case Skill.PowerUp:
      return 1.5;
      case Skill.DefenseUp:
      return 1.3;
      default:
      return 1.0;
      }
      }
      }

因此將 ScriptableObject 作為 Enum 來使用也是一種好選擇,使用的方式為

1
2
3
4
5
[CreateAssetMenu(menuName = "ScriptableObjects/SkillSO")]
public class SkillSO : ScriptableObject
{

}

然後在透過 Unity 編輯器建立需要的元素即可

使用上也和 Enum 類似

返回 ScriptableObject 系列

Reference:

ScriptableObject as Runtime Set

RuntimeSet 的做法是將 ScriptableObject 作為一個分享集合容器,在執行期間,將物件放入,讓其他要使用它的物件可以透過這個分享容器拿到。

以下的例子中,將建立一個 GameObjectRuntimeSet ,這個 GameObjectRuntimeSet 將會在執行時持有 Circle 物件集合,而 Circle 物件將由 CreateObject 建立,而 ObjectMonitor 負責查看 GameObjectRuntimeSet 中有多少 Circle 物件

以下建立一個通用抽象類 RuntimeSet 它繼承 ScriptableObject

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
public abstract class RuntimeSet<T> : ScriptableObject
{
protected readonly List<T> Items = new();

public int Count => Items.Count;

public void Add(T thing)
{
if (!Items.Contains(thing))
Items.Add(thing);
}

public bool Remove(T thing)
{
return Items.Remove(thing);
}

public T this[int index]
{
get => Items[index];
set => Items[index] = value;
}

public void Clear()
{
Items.Clear();
}
}

接著繼承 RuntimeSet 建立一個 GameObjectRuntimeSet , 這個 GameObjectRuntimeSet 將為 GameObject 的分享集合容器

1
2
3
4
[CreateAssetMenu(menuName = "ScriptableObjects/GameObjectRuntimeSet")]
public class GameObjectRuntimeSet : RuntimeSet<GameObject>
{
}

在 Unity 編輯器中建立一個 ScriptableObject 實體 CircleRuntimeSet

接著建立要放到這個集合的物件 Circle ,在 OnEnable 時將自己加入集合, OnDisable 時將自己從集合中移除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Circle : MonoBehaviour
{
public GameObjectRuntimeSet GameObjectRuntimeSet;
private void OnEnable()
{
GameObjectRuntimeSet.Add(this.gameObject);
}

private void OnDisable()
{
GameObjectRuntimeSet.Remove(this.gameObject);
}

}

ScriptableObject 實體 CircleRuntimeSet 設定到 CircleGameObjectRuntimeSet 中,讓 Circle 知道在 OnEnable 時要放入哪個集合。之後,把 Circle 轉為 Prefab

再建立一個製造CircleCreateObject,這邊建立一個按鈕,當按下時,會觸發 CreateObject.Create()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CreateObject : MonoBehaviour
{

public GameObject circle;

public void Create()
{
float randomX = Random.Range(-6f, 6f);
float randomY = Random.Range(-6f, 6f);
Vector3 randomPosition = new Vector3(randomX, randomY, 0f);

Instantiate(circle, randomPosition, Quaternion.identity);
}

}

最後建立一個觀看這個集合的物件

1
2
3
4
5
6
7
8
9
10
11
public class ObjectMonitor : MonoBehaviour
{

public GameObjectRuntimeSet GameObjectRuntimeSet;

public void Update()
{
Debug.Log("Current objects in the set " + GameObjectRuntimeSet.Count);
}

}

觀察結果
每按下按鈕一次,所產生的 Circle 物件都會加入 GameObjectRuntimeSet ,而需要知道有多少個 Circle 的物件,它只要從 GameObjectRuntimeSet 裏面就可以找到目前有多少 Circle 物件。

總結

  • RuntimeSetScriptableObject 作為分享的集合容器。
  • 它可以避免使用 Singleton 來管理物件群
  • 它避免了使用 Object.FindObjectOfTypeGameObject.FindWithTag 等開銷可能會比較大的方法來搜尋物件

返回 ScriptableObject 系列

Reference:

偵測螢幕解析度

解析度資訊

您的螢幕解析度為:


您的瀏覽器內部目前寬高為:


User Agent: