ScriptableObject as Event

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

首先要建立 GameEventGameEventListener 。它們兩個互相依賴

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");
}
}

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

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

接著建立事件發起的物件,這邊建立兩個, RaiseClickableCircleRaiseClickableButWithUnityEvent

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();
}
}

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

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

返回 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 實體, HPMaxHP

然後建立繼承 MonoBehaviourPlayer

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 編輯器中,將剛剛建立的 HPMaxHP 設定到 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 編輯器中,將 HPMaxHP 設定到 HPView 上,

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

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

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

返回 ScriptableObject 系列

Reference:

ScriptableObject 基礎

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

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

ScriptableObjectMonoBehaviour 一樣,繼承了 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>();

MonoBehaviourScriptableObject 的比較

MonoBehaviour ScriptableObject
MonoBehaviour 接收所有來自 Unity 的 callback,像是 StartAwakeUpdateOnEnable ,OnDisablOnCollisonEnter ScriptableObject 只接收一些: AwakeOnEnableOnDestroyOnDisable 。 在 Editor 中的話還有 OnValidateReset
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)

關閉 Chrome的 Web Security

在前後端分離的狀況下,很容易就會遇到跨域的問題,為了開發測試方便,我們可以暫時禁用Chrome的安全設定,以方便我們測試開發。

在Mac上使用以下指令來關閉 Chrome的安全設定

1
open -n -a "Google Chrome" --args --user-data-dir=/tmp/temp_chrome_user_data_dir http://localhost:8080/ --disable-web-security

當出現 你正在使用不受支援的命令列標嫩:--disable-web-security•這可能會危及穩定性與安全性。 字樣,表示你已經成功關閉Chrome的安全設定了。
你正在使用不受支援的命令列標嫩:--disable-web-security•這可能會危及穩定性與安全性。

Reference: https://stackoverflow.com/questions/3102819/disable-same-origin-policy-in-chrome

在前端直接將檔案上傳到AWS S3

在自己架設的伺服器中,如果要把檔案上傳到AWS S3,一種常見的做法是先將檔案上傳到自身伺服器然後再上傳到AWS S3,但是這樣太耗費資源了。
幸好AWS S3有方式可以讓你在前端直接將檔案上傳到S3中,以下說明如何設定前端的HTML,讓檔案直接上傳。

首先你要有可以存取BucketAWSAccessKeyIdAWSSecretAccessKey

以下資料皆來自https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-post-example.html

AWSAccessKeyId AKIAIOSFODNN7EXAMPLE
AWSSecretAccessKey wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Bucket sigv4examplebucket

HTML FORM

以下是HTML Form的例子

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
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>

<!-- 將form的 action ,設為 action="https://<你的Bucket名稱>.s3.amazonaws.com/" , 如下-->
<form action="http://sigv4examplebucket.s3.amazonaws.com/" method="post" enctype="multipart/form-data">
Key to upload:
<!-- 這個 key 將設定 S3 Object Key, 例如設為: user/user1/myfile.csv,之後在AWS S3上面看到的就是 user/user1/myfile.csv -->
<input type="input" name="key" value="user/user1/myfile.csv" /><br />
<!-- 這個 acl 將設定 S3 ACL, 如設為:public-read 表示可以被公開讀取,也可以設為 private ,表示是私有的-->
<input type="hidden" name="acl" value="public-read" />
<!-- 這個 success_action_redirect 將設定 上傳成功之後,要導向到哪個網頁,如設為:http://localhost:8080/uploaded.html -->
<input type="hidden" name="success_action_redirect" value="http://localhost:8080/uploaded.html" />
Content-Type:
<!-- 這個 Content-Type 將限定要上傳的檔案類型,可以不設定 -->
<!-- <input type="input" name="Content-Type" value="image/jpeg" /><br /> -->
<!-- 這個 x-amz-meta-uuid 應由後端產生一組隨機的字串,讓AWS S3做驗證 -->
<input type="hidden" name="x-amz-meta-uuid" value="14365123651274" />
<!-- 這個 x-amz-server-side-encryption 說明使用哪種加密方式,例如 AES256 -->
<input type="hidden" name="x-amz-server-side-encryption" value="AES256" />

<!-- 這個 X-Amz-Credential 由 <AWSAccessKeyId>/<日期>/<Bucket-Region>/<RegionService>/<Signing> 組成,RegionService和Signing是固定的就是s3和aws4_request-->
<input type="text" name="X-Amz-Credential" value="AKIAIOSFODNN7EXAMPLE/20151229/us-east-1/s3/aws4_request" />
<!-- 這個 X-Amz-Algorithm說明建立這個Request的簽名使用哪種演算法,例如 AWS4-HMAC-SHA256 -->
<input type="text" name="X-Amz-Algorithm" value="AWS4-HMAC-SHA256" />
<!-- 這個 X-Amz-Date 說明建立這個Request的簽名的日期,為可選的,需要為 ISO 8601 basic format (YYYYMMDD'T'HHMMSS'Z') 格式 -->
<input type="text" name="X-Amz-Date" value="20151229T000000Z" />

Tags for File:
<!-- 這個 x-amz-meta-tag 是可選的 -->
<!-- <input type="input" name="x-amz-meta-tag" value="" /><br /> -->
<!-- 這個 Policy 為Base64編碼的字串,在下面說明 -->
<input type="hidden" name="Policy" value='<Base64-encoded policy string>' />
<!-- 這個 X-Amz-Signature 是簽名,包含AWSSecretAccessKey的值,應由後端產生,在下面說明 -->
<input type="hidden" name="X-Amz-Signature" value="<Signature-value>" />
File:
<input type="file" name="file" /> <br />
<input type="submit" name="submit" value="Upload to Amazon S3" />
</form>

</html>

Base64-encoded policy string

Policy 應由後端產生,他是一個JSON物件,以下是一個例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{ "expiration": "2015-12-30T12:00:00.000Z",
"conditions": [
{"bucket": "sigv4examplebucket"},
["starts-with", "$key", "user/user1/"],
{"acl": "public-read"},
{"success_action_redirect": "http://localhost:8080/uploaded.html"},
// ["starts-with", "$Content-Type", "image/"],
{"x-amz-meta-uuid": "14365123651274"},
{"x-amz-server-side-encryption": "AES256"},
// ["starts-with", "$x-amz-meta-tag", ""],
{"x-amz-credential": "AKIAIOSFODNN7EXAMPLE/20151229/us-east-1/s3/aws4_request"},
{"x-amz-algorithm": "AWS4-HMAC-SHA256"},
{"x-amz-date": "20151229T000000Z" }
]
}
expiration 表示這個上傳的Request可用的期限
conditions 設定條件,在檔案上傳前AWS S3會檢查一下,是否和HTML Form中的相符合
bucket Bucket 名稱,此範例為 sigv4examplebucket
“starts-with”, “$key” 要放在這個Bucket的 Prefix,此範例為user/user1/ ,在HTML Form中的 key 需要符合在此設定的前綴祠才可以,如 user/user1/myfile.csv
acl 設定 S3 ACL, 如設為:public-read 表示可以被公開讀取,也可以設為 private ,表示是私有的
success_action_redirect 上傳成功之後,要導向到哪個網頁,如設為:http://localhost:8080/uploaded.html
x-amz-meta-uuid 後端產生的UUID
x-amz-server-side-encryption 說明使用哪種加密方式, AES256
“starts-with”, “$Content-Type” 上傳的 content type,可以不設定
x-amz-credential /<日期>/// 組成,RegionService和Signing是固定的就是s3和aws4_request
x-amz-algorithm 說明建立這個Request的簽名使用哪種演算法,例如 AWS4-HMAC-SHA256
x-amz-date 建立這個Request的簽名的日期,為可選的,需要為 ISO 8601 basic format (YYYYMMDD’T’HHMMSS’Z’) 格式

注意,如果你在HTML Form中沒有設定某些參數,那麼在Policy中也不要設定,例如我在HTML Form中沒有設定 ["starts-with", "$Content-Type", "image/"]["starts-with", "$x-amz-meta-tag", ""],那麼在 Policy中也不要設定。

之後由後端使用Base64將這段字串編碼,這個Base64字串編碼就是要放在 HTML Form 中的 Policy,以下是Java產生的範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
JSONObject j = new JSONObject();

j.put("expiration", "2015-12-30T12:00:00.000Z");
j.put("conditions",
new JSONArray()
.put(new JSONObject().put("bucket", "sigv4examplebucket"))
.put(new JSONArray().put("starts-with").put("$key").put("user/user1/"))
.put(new JSONObject().put("acl", "private"))
.put(new JSONObject().put("success_action_redirect", "http://localhost:8080"))
.put(new JSONObject().put("x-amz-meta-uuid", "14365123651274"))
.put(new JSONObject().put("x-amz-server-side-encryption", "AES256"))
.put(new JSONObject().put("x-amz-credential", "AKIAIOSFODNN7EXAMPLE/20151229/us-east-1/s3/aws4_request"))
.put(new JSONObject().put("x-amz-algorithm", "AWS4-HMAC-SHA256"))
.put(new JSONObject().put("x-amz-date", "20151229T000000Z"))
);
// 這個 Policy 就是要放在 HTML Form 中的 Policy
String policy = Base64.getEncoder().encodeToString(j.toString().getBytes());

Signature-value

最後產生 Signature ,Java 可以直接使用有人提供的產生方式,來自 https://gist.github.com/phstudy/3523576726d74a0410f8

1
2
3
4
5
6
7
8
9
10
// 在 AccessSecretKey前面要加上 AWS4
String accessSecretKey = "AWS4" + "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
String date = "20151229";
String region = "us-east-1";
String regionService = "s3";
String signing = "aws4_request";
String stringToSign = policy;

// 這個 signature 字串,就是要放在 HTML Form 中的 X-Amz-Signature
String signature = getSignatureV4(accessSecretKey, date, region, regionService, signing, stringToSign);

以上動作完成之後,還需要為你的Bucket設定 Cross-origin resource sharing (CORS)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"POST",
"GET",
"PUT"
],
"AllowedOrigins": [
"http://www.mywebsite.com/"
],
"ExposeHeaders": []
}
]

Bucket沒有設定Cross-origin resource sharing (CORS)會出現 CORS policy 錯誤

1
Access to XMLHttpRequest at 'https://<你的bucket>.s3.amazonaws.com/' from origin 'https://你的網站.com ' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

如果是還在測試開發的話,可以先將Chrome的web-security關閉,Mac使用以下指令,其他設定可以參考: disable-same-origin-policy-in-chrome

1
open -n -a "Google Chrome" --args --user-data-dir=/tmp/temp_chrome_user_data_dir http://localhost:8080/ --disable-web-security

Reference:

2D樞紐關節 (Hinge Joint 2D)

2D樞紐/鉸鏈關節(Hinge Joint 2D) 是 Unity 中的一個 2D 物理組件。

  • 它可以讓遊戲物件(GameObject)圍繞一個特定的點旋轉
  • 這個關節可以讓兩個點重疊,這兩個點可以是
    • 兩個2D剛體(Rigidbody 2D)物件,或是
    • 一個2D剛體物件另一個為一個世界空間中固定的點。若要連接到世界空間中固定的點可以透過將 Connected Rigidbody 設為 None
  • 這個關節可以向連接的兩個點施加一個線性力(linear force)
  • 通常用於模擬門、槓桿或鐘擺等旋轉機制。

2D樞紐/鉸鏈關節(Hinge Joint 2D)具有三個約束條件,這些條件都是可選的

  1. 保持兩個2D剛體(Rigidbody 2D)物件上的兩個錨點(anchor point)之間的相對線性距離(relative linear distance)
    • 此約束確保錨點之間的距離保持不變。
  2. 保持兩個2D剛體(Rigidbody 2D)遊戲物件上的兩個錨點之間的角速度(angular speed)
    • 可以使用Maximum Motor Force屬性限制最大扭矩(maximum torque)來限制角速度。
  3. 保持角度(angle)在指定的弧度(arc)範圍內
    • 此約束確保角度在一定範圍內。

可以使用這個關節來建構需要像旋轉樞軸行為的物理遊戲物件。例如:

  • 翹翹板的樞軸(see-saw pivot):水平部分連接到基座。使用關節的角度(Angle)限制來模擬翹翹板的最高點和最低點。
  • 剪刀(scissors)的樞軸:剪刀用鉸鏈連接在一起。使用關節的角度限制來模擬剪刀的閉合和最大打開。
  • 簡單的車輪連接到車體:車輪的樞軸點在車輪的中心與車體相連。你可以使用2D樞紐/鉸鏈關節(Hinge Joint 2D) 的馬達(motor)來旋轉車輪。
屬性 功能
Enable Collision 啟用這個屬性後,可以偵測碰撞。
Connected Rigidbody 指定此關節連接到的其他遊戲物件(GameObject)。如果將此設置為「None」,則關節的另一端固定在由「Connected Anchor」設置定義的空間點。
Auto Configure Connected Anchor 啟用此屬性以自動設置此2D鉸鏈關節(Hinge Joint 2D)連接到的遊戲物件的錨點(Anchor)位置。如果啟用此屬性,則不需要為「Connected Anchor」屬性輸入座標。
Anchor 定義此遊戲物件的2D剛體(Rigidbody2D)上的關節(joint)端點連接的位置(以x、y座標表示)。
Connected Anchor 定義這個關節(joint)端點要連接到另一個遊戲物件的2D剛體(Rigidbody2D)上的位置(以x、y座標表示)。
Use Motor 啟用此選項以對關節應用馬達動力。
Motor 選擇此選項以展開此屬性的設置。
Motor Speed 設定馬達要達到的目標速度(每秒度數)。
Maximum Motor Force 設定馬達在嘗試達到目標速度時可以施加的最大扭矩(torque)(或旋轉力(rotation))。
Use Limits 啟用此選項以限制旋轉的角度(rotation angle)。
Angle Limits 選擇此選項以展開對角度限制的設置。當Use Limits啟用時,就會應用這些限制。
Lower Angle 設定旋轉圓弧(rotation arc)下端(lower end)允許的極限。
Upper Angle 設定旋轉圓弧(rotation arc)上端(upper end)允許的極限。
Break Action 設置當超過力(force)臨界值或達到扭矩臨界值(torque threshold)時採取的動作。
Break Force 設置力的臨界值,當超過時這個關節就會執行選擇的Break Action。預設值為Infinity,表示不執行Break Action
Break Torque 設置扭矩的臨界值,當超過時這個關節就會執行選擇的Break Action。預設值為Infinity,表示不執行Break Action

Reference

Unity MonoBehavior

MonoBehaviour 是 Unity 中最重要的基類之一,用於控制遊戲物件(GameObject)的行為。並提供了一系列生命週期方法,這些方法在遊戲物件生命週期的不同階段會被調用。此外,還提供了一些輔助方法,用於執行常見任務,例如訪問遊戲物件和組件(Component)。


生命週期方法 (Lifecycle methods)

MonoBehaviour 提供了一系列生命週期方法,這些方法在遊戲物件生命週期的不同階段會被調用。這些方法包括

  • Awake:此方法在遊戲物件(GameObject)實體被建立時呼叫一次。
    • 通常用來:在APP開始前初始化遊戲物件狀態屬性,例如設置變量、添加組件(Component)等。
    • Unity會在所有啟用的遊戲物件都實體化之後,才呼叫Awake,所以可以呼叫FindWithTag,而不用擔心找不到啟用的遊戲物件。
    • Awake不可以使用協程(Awake cannot act as a coroutine)
    • 注意:Unity 不會保證用特定的順序調用每個遊戲物件的 Awake 方法。因此,不可以假設一個遊戲物件的 Awake 方法會在另一個遊戲物件的 Awake方法之前或之後調用。
      • 正確的方式是在 Awake 中設置自身的屬性和取得其他物件的引用,而在 Start 方法中處理需要依賴其他遊戲物件的屬性和引用。
    • 注意:如果如果遊戲物件是啟用(Enable)但是腳本(Script)並沒有被啟用,Awake依然會被執行。
      • 如果如果遊戲物件是啟用但是腳本並沒有被啟用,Awake依然會被執行
  • OnEnable:啟用時才會呼叫,在Awake之後執行,每次啟用都會呼叫
    • 通常在此註冊事件
  • Start:此方法在遊戲物件的所有組件都已初始化且第一幀渲染之前調用一次。
    • 當這個遊戲物件處於 Disabled 就不會呼叫,直到該物件第一次被啟用時,才呼叫。
    • 通常用來:處理開始遊戲物件開始的遊戲邏輯。
    • 是在AwakeOnEnable之後呼叫
    • Start可以使用協程 (Coroutine)
  • Update:此方法在遊戲的每一幀調用。
    • 當物件為啟用時才會呼叫Update
    • 通常用來:更新遊戲物件的狀態和處理使用者輸入。
  • FixedUpdate:此方法在固定的時間間隔調用,通常每 0.02 秒調用一次。
    • 通常用來:在此方法更新遊戲物件的物理模擬
    • 設定FixedUpdate的時間間隔
  • LateUpdate:此方法在所有的 Update 方法之後調用。
    • 它是更新依賴於 Update 方法結果的遊戲邏輯的理想場所。
    • 例如:攝影機跟隨(follow camera)就很適合放在此,因為它需要跟隨那些可能在 Update 中已經移動過的物件。
  • OnDisable:與OnEnable類似,但是是非啟用時才會呼叫,每次非啟用都會呼叫
    • 通常在此取消註冊事件
    • 當遊戲物件為disable時,不會呼叫UpdateFixedUpdateLateUpdate等相關更新方法。
    • 當遊戲物件為disable時,物件仍然在場景(Scene)中,該物件仍不會被釋放。
  • OnDestroy:此方法在遊戲物件被銷毀時調用一次。物件從場景(Scene)中被移除了,該物件所佔用的記憶體可以被釋放。
    • 當原先場景關閉,載入新的場景時,也會觸發OnDestroy方法
    • 通常用來:清理遊戲物件使用的資源。
    • 如果如果遊戲物件是啟用(Enable)但是腳本(Script)並沒有被啟用,移除該物件時,OnDestroy依然會被執行。
    • 注意:如果使用者是在手機平台上,那麼當使用者暫停你的APP時,作業系統(OS)可能會終止你的APP,而不會觸發 OnDestroy 方法,因此不要依賴此方法來保存APP的狀態。請將每次應用程序失去焦點視為APP退出,並使用 MonoBehaviour.OnApplicationFocus 來保存資料。

注意:
在Unity中預設是不會控制Script的執行順序,Unity的執行會先將所有腳本的Awake執行完之後,才去執行所有腳本的Start,如果真的需要依賴順序你需要到ProjectSetting中的Script Exceution Order去設定順序,數字越小的越先執行。

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class A : MonoBehaviour
{
// 最先呼叫
private void Awake()
{
Debug.Log($"{nameof(A)} is Awake");
}

// 啟用時才會呼叫,在Awake之後執行,每次啟用都會呼叫
private void OnEnable()
{
Debug.Log($"{nameof(A)} is OnEnable");
}

// 在第一幀開始前執行
void Start()
{
Debug.Log($"{nameof(A)} is Start");
}

// 每一幀執行
void Update()
{
Debug.Log($"{nameof(A)} is Update");
}

// 在 Update 之後執行,也是每一幀執行
private void LateUpdate()
{
Debug.Log($"{nameof(A)} is LateUpdate");
}

// 固定時間呼叫
private void FixedUpdate()
{
Debug.Log($"{nameof(A)} is FixedUpdate");
}

// 非啟用時才會呼叫,每次非啟用都會呼叫
private void OnDisable()
{
Debug.Log($"{nameof(A)} is OnDisable");
}

// 目前Component銷毀時才會呼叫
private void OnDestroy()
{
Debug.Log($"{nameof(A)} is OnDestroy");
}
}

Reference: