在 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 ID 與 Secrect 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 設定到 Circle 的 GameObjectRuntimeSet 中,讓 Circle 知道在 OnEnable 時要放入哪個集合。之後,把 Circle 轉為 Prefab

再建立一個製造Circle的 CreateObject,這邊建立一個按鈕,當按下時,會觸發 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 物件。

總結

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

返回 ScriptableObject 系列

Reference:

偵測螢幕解析度

解析度資訊

您的螢幕解析度為:


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


User Agent:

ScriptableObject as Event

ScriptableObject 可以用來作為事件系統的一部分。這種方法可以幫助我們在不同的 MonoBehaviour 之間進行通信,而不需要它們彼此知道對方的存在,從而實現鬆耦合。

首先要建立 GameEvent 與 GameEventListener 。它們兩個互相依賴

GameEvent

  • 含有一個 GameEventListener 列表用來保存要聽取該事件的聽眾,
  • 使用 RegisterListener 來註冊聽眾
  • 使用 UnregisterListener 將聽眾移出
  • 使用 Raise 觸發聽眾註冊的事件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    [CreateAssetMenu(menuName = "ScriptableObjects/GameEvent", order = 1)]
    public class GameEvent : ScriptableObject
    {
    private readonly List<GameEventListener> eventListeners =
    new List<GameEventListener>();

    public void Raise()
    {
    for (int i = eventListeners.Count - 1; i >= 0; i--)
    eventListeners[i].OnEventRaised();
    }

    public void RegisterListener(GameEventListener listener)
    {
    if (!eventListeners.Contains(listener))
    eventListeners.Add(listener);
    }

    public void UnregisterListener(GameEventListener listener)
    {
    if (eventListeners.Contains(listener))
    eventListeners.Remove(listener);
    }
    }

GameEventListener

  • 含有一個要監聽的事件參考 Event
  • 含有一個當監聽到後,要做出的回應 Response
  • OnEnable 中將自己註冊到 Event 中
  • OnDisable 中將自己從 Event 中移出
  • OnEventRaised 中呼叫回應的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class GameEventListener : MonoBehaviour
{
[Tooltip("Event to register with.")]
public GameEvent Event;

[Tooltip("Response to invoke when Event is raised.")]
public UnityEvent Response;

private void OnEnable()
{
Event.RegisterListener(this);
}

private void OnDisable()
{
Event.UnregisterListener(this);
}

public void OnEventRaised()
{
Response.Invoke();
}
}

接著在 Unity 編輯器中建立一個 ScriptableObject 實體,並命名為 ClickEvent

建立一個 DoSomethingWhenClick 的遊戲物件(GameObject) ,並添加以下腳本,這個遊戲物件有一個公開方法 JustDoIt,當事件發生時,要被呼叫。

1
2
3
4
5
6
7
public class DoSomethingWhenClick : MonoBehaviour
{
public void JustDoIt()
{
Debug.Log("Just Do It");
}
}

建立一個 EventListener 的 GameObject,將 GameEventListener 腳本添加到這個 EventListener GameObject 中。

  • 接下來將前面建立的 ScriptableObject 實體 ClickEvent ,拖放到,EventListener 中的 Event 欄位,表示這個 EventListener 要監聽的事件是 ClickEvent
  • 然後設定聽到之後要做的事。將 DoSomethingWhenClick 遊戲物件放到 Response 欄位,並設定要執行該物件的公開方法 JustDoIt

接著建立事件發起的物件,這邊建立兩個, RaiseClickableCircle 與 RaiseClickableButWithUnityEvent

RaiseClickableCircle 的腳本如下,它主要是直接使用 GameEvent 作為參考,其缺點是只能用一個 Event

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

public GameEvent ClickGameEvent;

void OnMouseDown()
{
ClickGameEvent.Raise();
}
}

RaiseClickableButWithUnityEvent 幾乎與 RaiseClickableCircle 一模一樣,差別在於是使用 UnityEvent ,這樣可以放置多個 Event,當觸發 Raise 時在裡面的 Event 都會觸發。

1
2
3
4
5
6
7
8
9
public class RaiseClickableButWithUnityEvent : MonoBehaviour
{
public UnityEvent ClickEvent;

void OnMouseDown()
{
ClickEvent.Invoke();
}
}

最後執行,並點擊畫面上的圓形或是方形,可以發現都會觸發 DoSomethingWhenClick 的 JustDoIt 方法,而 RaiseClickableButWithUnityEvent , RaiseClickableCircle 與 DoSomethingWhenClick 完全不知曉對方的存在。

使用基於事件架構(event-based architecture)的好處是,它不會每一幀都在執行,只有在當事件發生時才會執行,因此效率會比在 MonoBehaviour 的 Update 方法中執行好。

返回 ScriptableObject 系列

Reference:

ScriptableObject as shared variable

ScriptableObject 可以作為遊戲變數在各個 MonoBehaviour 之間分享資料,此外,透過這種方式還可以減少 MonoBehaviour 物件之間互相參考的耦合度。

下面將使用 ScriptableObject 建立玩家生命數值,並在不同的 MonoBehaviour之間使用

首先建立一個繼承 ScriptableObject 的類 FloatVariable

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
[CreateAssetMenu(menuName = "ScriptableObjects/FloatVariable", order = 1)]
public class FloatVariable : ScriptableObject
{
#if UNITY_EDITOR
[Multiline]
public string DeveloperDescription = "";
#endif
public float Value;

public void SetValue(float value)
{
Value = value;
}

public void SetValue(FloatVariable value)
{
Value = value.Value;
}

public void ApplyChange(float amount)
{
Value += amount;
}

public void ApplyChange(FloatVariable amount)
{
Value += amount.Value;
}
}

接下來建立兩個 FloatVariable 實體, HP 與 MaxHP

然後建立繼承 MonoBehaviour 的 Player 類

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Player : MonoBehaviour
{
public FloatVariable CurrentHP;
public FloatVariable MaxHP;
public float StartingHP;
public bool ResetHP;

private void Start()
{
if (ResetHP)
{
CurrentHP.SetValue(StartingHP);
}
}

// 暴露給外部,這邊會綁定按鈕的 On Click 事件
public void Hurt()
{
CurrentHP.ApplyChange(-10);
}
}

在 Unity 編輯器中,將剛剛建立的 HP 與 MaxHP 設定到 Player 上

接著建立一個 HPView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class HPView : MonoBehaviour
{
public TextMeshProUGUI CurrentHPText;
public TextMeshProUGUI MaxHPText;

public FloatVariable CurrentHP;
public FloatVariable MaxHP;

private void Update()
{
CurrentHPText.text = CurrentHP.Value.ToString();
MaxHPText.text = MaxHP.Value.ToString();
}
}

在 Unity 編輯器中,將 HP 與 MaxHP 設定到 HPView 上,

將 Player.Hurt() 設定給按鈕的 On Click 。

最後執行,在畫面上可以按下按鈕,觸發 Player.Hurt() ,可以觀察到 CurrentHPText 的變化

並且可以發現這兩個 MonoBehaviour: Player 和 HPView 互相不知道彼此的存在,實現了鬆耦合(loosely coupled)。

返回 ScriptableObject 系列

Reference:

ScriptableObject 基礎

ScriptableObject 是一個資料容器(Data container),它透過共享通用資料的方式來減少對重複資料的記憶體佔用,進而降低你APP的記憶體使用量。

如果你的預製物件(Prefab)中儲存了一些不會改變的資料,那麼很適合把它們改為使用 ScriptableObject ,因為每當你實體化(instantiate)一個預製物件(Prefab)時,這個實體會保存自己的一份資料,當實體化很多個實體時,會有很多重複的資料佔用很多記憶體,因此你可以使用 ScriptableObject 可以把資料儲存起來,然後讓所有的預製物件(Prefab)存取這個 ScriptableObject 。

ScriptableObject 和 MonoBehaviour 一樣,繼承了 UnityEngine.Object ,但是與 MonoBehaviour 不同在於你不能把 ScriptableObject 附加(attach)到一個 GameObject 上,也不能進行 GatComponent 等對 GameObject 的操作,相反你需要把它儲存為資源(Asset)。

在使用Unity編輯器時,你可以把資料放在 ScriptableObject ,在執行時也可以調整其中的資料,但是你不可以把它當作遊戲存檔的工具。

使用 ScriptableObject 主要情況是在編輯時期修改 ScriptableObject 的資料,在執行時這些資料會被當作資源(Asset)使用。

使用方式

建立一個類並繼承 ScriptableObject,你可以使用 CreateAssetMenu 屬性(attribute),讓你方便在編輯器中使用,以下是一個範例:

1
2
3
4
5
6
7
8
[CreateAssetMenu(fileName = "Data", menuName = "ScriptableObjects/SpawnManagerScriptableObject", order = 1)]
public class SpawnManagerScriptableObject : ScriptableObject
{
public string prefabName;

public int numberOfPrefabsToCreate;
public Vector3[] spawnPoints;
}

之後,你可以在 Assets -> Create -> ScriptableObjects -> SpawnManagerScriptableObject 中建立新的 ScriptableObject 實體。

你可以更改該實體的名稱與它的屬性。

接下來你可以在 MonoBehaviour 中使用這個 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
29
30
31
32
33
34
35
using UnityEngine;

public class Spawner : MonoBehaviour
{
// 要實體化 Prefab 的參考
public GameObject entityToSpawn;

// 一個 ScriptableObject 的參考
public SpawnManagerScriptableObject spawnManagerValues;

// 記錄了實體化了多少個實體
int instanceNumber = 1;

void Start()
{
SpawnEntities();
}

void SpawnEntities()
{
int currentSpawnPointIndex = 0;

for (int i = 0; i < spawnManagerValues.numberOfPrefabsToCreate; i++)
{
// 使用 ScriptableObject 中的資料
GameObject currentEntity = Instantiate(entityToSpawn, spawnManagerValues.spawnPoints[currentSpawnPointIndex], Quaternion.identity);

currentEntity.name = spawnManagerValues.prefabName + instanceNumber;

currentSpawnPointIndex = (currentSpawnPointIndex + 1) % spawnManagerValues.spawnPoints.Length;

instanceNumber++;
}
}
}

在Unity編輯器中,使用拖拉的方式將剛剛建立好的 ScriptableObject 實體拖拉到對應的位置上,另外 Box Prefab 是一個簡單的Square

最後在執行Unity,就可以看到Unity使用剛剛建立出來 ScriptableObject 資料去產生 Box 物件。

此外除了在編輯器中建立 實體,在執行期間也可以透過 CreateInstance 來建立。

1
ScriptableObject.CreateInstance<MyScriptableObjectClass>();

MonoBehaviour 與 ScriptableObject 的比較

MonoBehaviour ScriptableObject
MonoBehaviour 接收所有來自 Unity 的 callback,像是 Start , Awake , Update , OnEnable ,OnDisabl , OnCollisonEnter 等 ScriptableObject 只接收一些: Awake , OnEnable , OnDestroy 與 OnDisable 。 在 Editor 中的話還有 OnValidate 與 Reset
MonoBehaviour 必須要 附加(attach)到 GameObject 上 ScriptableObject 不能附加到 GameObject 上,需要在專案(Project Level)中將它們儲存為 asset 檔案,在其他腳本中參考這些 ScriptableObject asset
當儲存 MonoBehaviour 時,會將它們的資料儲存到 Scenes 與 Prefabs 中 每個 ScriptableObject 實體都會被保存為專案層級(Project level)的獨立檔案中
一般在 Play Mode 中修改 MonoBehaviour 中的值之後離開 Play Mode 的話,這些值會被重新設定(Reset) 當離開 Play Mode 後, ScriptableObject 則不會重新設定。此外當編譯發布之後,執行期間 ScriptableObject 修改的值不會被保存,而是為發布時的值

在 ScriptableObject 中只有以下的 callback 會被呼叫

  • Awake : 與 MonoBehaviour 的類似,只有在 ScriptableObject 開始時會被呼叫。當遊戲執行(launched)或是有參考到這個 ScriptableObject asset 的 Scene 被載入時會呼叫。
  • OnEnable : 在 Awake 之後呼叫,當 ScriptableObject 被載入(Loaded)或是實體化(Instantiated)時會被呼叫。當 ScriptableObject.CreateInstance 或是 script 被重新編譯(recompilation)時 OnEnable 會被呼叫。
  • OnDisable : 當載入的 Scene 不再參考這個 ScriptableObject asset 時,就會呼叫 OnDisable , 它會在 OnDestory 之前被呼叫。此外在 script 重新編譯進入 Play Mode 時,也會呼叫,此時會出現 OnDisable 出現在 OnEnable 的情況。
  • OnDestory : 當 ScriptableObject 在 編輯器中刪除,或是重程式碼中刪除時,會呼叫 OnDestory 。如果是在執行期間建立的 ScriptableObject 在APP離開或是離開 Play Mode 時也會呼叫。

以下 callback 只在編輯器中呼叫

  • OnValidate : 當值在 Inspector 中改變值,才會呼叫。可以在這邊確保你的編輯器輸入的資料是合適的範圍(如 0 ~ 1)。
  • Reset : 當點擊 Inspector 中的 Reset 按鈕時,會呼叫。

返回 ScriptableObject 系列

Reference:

預製物件(Prefab)

Prefab

Unity 中的預製物件(Prefab),讓你可以儲存一個遊戲物件(GameObject),這個預製物件(Prefab)會包含該遊戲物件的所有組件(Component),屬性值(property values),以及它的子遊戲物件,並且可以建立,更改它的設定,就像是一個可重用的資源(Asset)。預製物件資源(Prefab Asset)就像是一個模板(template),讓你在場景中(Scene)添加預製物件實體(Prefab instance)

  • 任何在 Prefab Asset 的更改,都會自動反應(reflected)到該預製物件的實體上。
  • 預製物件中也可以含有預製物件,這稱為 Nest Prefabs
    • Unity 2018.3b 以後的版本支援 Nest Prefabs
  • 預製物件的實體值可以被覆蓋(override),讓每個預製物件的實體可以有不同的行為
  • 如果在執行期間(runtime)才加入的遊戲物件,那麼這個遊戲物件可以把它變為預製物件,例如子彈,NPC等只會在遊戲期間出現的物件

當你想要在你的遊戲專案中重用遊戲物件(GameObject),例如非玩家角色(non-player character (NPC)),或是一些道具,應該把這個遊戲物件變為預製物件(Prefab),因為Unity的Prefab System會自動幫你同步遊戲物件的資料,讓這些有相同預製物件的實體有同樣的資料。以下常見使用預製物件的例子

  • 環境資源(Environmental Assets):例如在關卡中心顯示多次的樹。
  • 非玩家角色(NPCs):例如某種機器人,出現在關卡中多次,他們以透過覆蓋(override)在移動速度,發出聲音上有所不同。
  • 拋射物(Projectiles):如子彈,箭等,像是海盜船的砲台開火時,要產生一個砲彈實體
  • 主要玩家角色:玩家在每個不同的場景中都會出現在一個開始點上,因此很適將它變為預製物件

建立 Prefab Asset

在 Unity 中建立預製物件資源(Prefab Asset)的方式非常簡單,在Unity編輯器中,將在 Hierarchy window 中的物件使用滑鼠左鍵拖曳到下方的 Project window 即可。操作完成的同時會發現原先的遊戲物件變為淡藍色的,表示它變為預製物件(Prefab instance)了

Reference:

FormerlySerializedAs

FormerlySerializedAs 是 Unity 中的一個屬性(attribute),用於處理欄位的重新命名。當重新命名一個序列化的欄位時,Unity 通常會失去對舊名的參考,導致序列化出現問題。
使用 FormerlySerializedAs 可以告诉 Unity 欄位之前的名稱,從而正確地反序列化。

以下為修改範例:
在 BaseEntity 類別中有一個 HP 欄位以及一個 ItemData 欄位,並且已經在 Inspector 上更改過它們的值

1
2
3
4
5
6
7
8
9
10
11
public class BaseEntity : MonoBehaviour
{
public int HP = 0;
public Item ItemData;
}


public class Item : MonoBehaviour
{
// ...
}
  • HP 與 ItemData 欄位,已經在 `Inspector` 上更改過它們的值

此時,想要更改欄位的名稱,如果直接重新命名這些欄位的話,在 Inspector 會丟失這些值,

  • 直接重新命名這些欄位的話,在 `Inspector` 會丟失這些值

使用 FormerlySerializedAs 告訴 Unity 欄位之前叫什麼。當編譯後,Unity 會幫我們將值對應到對應到新的欄位上。

1
2
3
4
5
6
7
8
public class BaseEntity : MonoBehaviour
{
[FormerlySerializedAs("HP")]
public int Hp = 0;

[FormerlySerializedAs("ItemData")]
public Item Item;
}

確認更新之後沒問題,可將 FormerlySerializedAs 刪除,但記得要在大家都更新完之後再做刪除的動作。

Reference:

UV坐標

UV 是一種紋理坐標系統,與物體的空間坐標 (x, y, z) 屬於不同的坐標系。 UV 座標是用來映射紋理到物體表面,它與物體坐標之間存在映射關係,但這種關係不一定是線性或唯一的。

  • (x, y, z) 是世界或局部空間座標,表示物體在 3D 空間中的位置(Transform 座標),使用笛卡兒坐標系
  • (U, V) 是 2D 紋理座標,通常在 0~1 範圍內,用來對應貼圖的像素

假設紋理大小是 512x512 但我們不會用 512x512 去做映射,因為 UV 座標的範圍是 0 到 1 之間,所以 512x512 大小的圖片就會被轉換為 uv(0/512,0/512) 至 (512/512,512/512)。這麼做的話即使你的紋理不是 512x512,而是 1024x1024 或其他大小,UV 仍然適用,因為它們代表的是 相對位置,而不是像素座標。

當我們想要檢查 Mesh 的頂點索引、 UV 座標,或三角形組成方式,我們可以使用 MeshFilter 來取得 Mesh

  • 使用 mesh.triangles 可以查看三角形索引
    • mesh.triangles 是一個 整數陣列,每 3 個數字代表一個三角形的 3 個頂點索引。
    • 這些索引對應到 mesh.vertices 陣列中的頂點。
  • 使用 mesh.vertices 找到 頂點的 3D 位置
    • mesh.vertices 是一個 Vector3[] 陣列,存放 Mesh 的每個頂點在 世界空間中的 3D 座標。
    • 這些頂點的 順序對應 mesh.triangles 的索引
  • 使用 mesh.uv 來查看 UV 座標
    • mesh.uv 是一個 Vector2[] 陣列,存放 Mesh 每個頂點對應的 UV 座標。
    • UV 是 貼圖座標,範圍通常是 (0,0) 到 (1,1),但如果是 貼圖 Atlas 或特殊映射方式,可能會超出這個範圍。

接下來我們建立一個 Quad 並設定他的 Materail 試試看。

  1. 建立一個 512x512 的圖片
  2. 建立一個 Material , 將其設為 Unlit/Texture ,在把 512x512 的圖片放到這個 Material 中。
  3. 建立一個 Quad , 並將剛剛建立的 Material 賦給這個 Quad
    • Unity 中 Quad(矩形)通常有 4 個頂點,在 Unity 內部,它其實是由兩個三角形組成的。
  4. 在 Quad GameObject 上建立一個 Script
    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
    public class Test : MonoBehaviour
    {

    Mesh mesh;
    public Vector2[] uvs;

    private void Awake()
    {
    uvs = new Vector2[4];

    uvs[0] = new Vector2(0f, 0.75f); // 左下
    uvs[1] = new Vector2(0.25f, 0.75f); // 右下
    uvs[2] = new Vector2(0f, 1); // 左上
    uvs[3] = new Vector2(0.25f, 1); // 右上

    mesh = GetComponent<MeshFilter>().mesh;

    for (int i = 0; i < mesh.vertexCount; i++)
    {
    Debug.Log($"頂點索引 {i} 的 3D 位置: {mesh.vertices[i]}");
    }

    for (int i = 0; i < mesh.triangles.Length; i += 3)
    {
    Debug.Log($"三角形 {i / 3}: {mesh.triangles[i]}, {mesh.triangles[i + 1]}, {mesh.triangles[i + 2]}");
    }
    }


    private void LateUpdate()
    {

    mesh.SetUVs(0, uvs);
    }

    }
  5. 執行,會發現顯示的是紅色1的區塊,這是因為我們設定了以下 UV 座標,對照下圖可以清楚看到 (0,0.75) , (0.25,0.75) , (0,1) , (0.25,1) 就是 紅色1 的區塊。
    • uvs[0] = new Vector2(0f, 0.75f); // 左下
    • uvs[1] = new Vector2(0.25f, 0.75f); // 右下
    • uvs[2] = new Vector2(0f, 1); // 左上
    • uvs[3] = new Vector2(0.25f, 1); // 右上
  6. 使用 mesh.triangles 可以查看三角形索引
    1
    2
    3
    4
    for (int i = 0; i < mesh.triangles.Length; i += 3)
    {
    Debug.Log($"三角形 {i / 3}: {mesh.triangles[i]}, {mesh.triangles[i + 1]}, {mesh.triangles[i + 2]}");
    }
    會印出
  • 三角形 0: 0, 3, 1
  • 三角形 1: 3, 0, 2
    這表示有兩個三角形,
  • 第一個三角形 0 的頂點索引是 0, 3, 1
  • 第二個三角形 0 的頂點索引是 3, 0, 2
  1. 之後我們可以使用 mesh.vertices 找到 頂點的 3D 位置
    1
    2
    3
    4
    for (int i = 0; i < mesh.vertexCount; i++)
    {
    Debug.Log($"頂點索引 {i} 的 3D 位置: {mesh.vertices[i]}");
    }
    會印出
  • 頂點索引 0 的 3D 位置: (-0.50, -0.50, 0.00)
  • 頂點索引 1 的 3D 位置: (0.50, -0.50, 0.00)
  • 頂點索引 2 的 3D 位置: (-0.50, 0.50, 0.00)
  • 頂點索引 3 的 3D 位置: (0.50, 0.50, 0.00)
  1. 將 Quad 放到坐標系上看就很清楚了

參考


Power 對 UV 的影響
當你對 UV 使用 Power,會導致貼圖的變形,因為 UV 不是線性的了。
舉例:UV 使用 Power

UV.xy → Power(UV.xy, 2)
Power(UV, 1) 👉 不變
Power(UV, 2) 👉 使貼圖集中在 (0,0),朝右上角壓縮

  • (0.5,0.2) 經過 Power(UV, 2) 之後,變為 (0.025,0.04)
    Power(UV, 0.5) 👉 擴展貼圖,使細節集中在 (1,1)