2D車輪關節(Wheel Joint 2D)

2D車輪關節(Wheel Joint 2D):是一種物理關節,可以用它來模擬轉動的車輪,

  • 可以為它施加馬達動力(motor power),
  • 它使用了懸吊彈簧(suspension spring)來保持與車輛主體(main body of the vehicle)的距離。

這種關節可用來模擬車輪(wheels)和懸掛系統(suspension)。其目的是保持兩個點在一條延伸到無限遠的直線上,同時使它們重疊(overlap)。這兩個點可以是兩個Rigidbody2D組件,也可以是一個Rigidbody2D組件和世界中的一個固定位置(通過將Connected Rigidbody設置為None來連接到世界中的固定位置)。

  • 簡單的說:2D車輪關節(Wheel Joint 2D),讓兩個點保持在同一條直線上,而這條直線是可以延伸到無限遠的。同時,當沒有外力作用時,這兩個點會保持在一起(即重疊)。

Wheel Joint 2D類似於Slider Joint 2D(無馬達動力或限制約束)和Hinge Joint 2D(無限制約束)的組合。
這種關節對兩個連接的剛體(rigid body)物件施加線性力(linear force),使它們保持在線上,並施加角度馬達(angular motor)以使物件在線上旋轉(rotate),同時用彈簧(spring)來模擬車輪懸掛(wheel suspension)。

設置
Maximum Motor Speed(最大馬達速度)與Maximum Motor Force(最大馬達動力)控制角馬達速度,讓兩個剛體物體旋轉。
Maximum Motor Force(最大馬達動力):設置角馬達的最大扭矩。

  • 即馬達所能施加的最大旋轉力。這個最大扭矩的設置對於控制車輪的轉動速度和力量非常重要。例如:
    • 如果 Maximum Motor Force 設置得太小,馬達可能無法提供足夠的扭矩來驅動車輪,特別是在遇到障礙或需要加速的情況下。
    • 如果 Maximum Motor Force 設置得太大,馬達可能會施加過大的扭矩,導致車輪快速旋轉,可能會影響車輛的穩定性
      模擬懸掛
      可以設置車輪懸掛的硬度和運動,以模擬不同程度的懸掛效果。例如,模擬硬且幾乎不動的懸掛:
  • 設置高頻率(Frequency)(1,000,000是最高值)== 硬懸掛。
  • 設置高阻尼比(Damping Ratio)(1是最高值)== 幾乎不動的懸掛。

若要模擬有彈性且自由運動的懸掛,可以使用以下設置:

  • 設置低頻率 == 鬆弛的懸掛。
  • 設置低阻尼比 == 可動的懸掛。

保持零相對線性距離(zero relative linear distance):在兩個剛體物體的錨點之間指定的線上保持零相對線性距離。
保持角速度(angular speed):在兩個剛體物體的錨點之間保持角速度(通過Maximum Motor Speed選項設置速度,通過Maximum Motor Force設置最大扭矩)。

應用場景

  • 這種關節適用於構建需要像是通過旋轉樞軸連接,但不能脫離指定線的物理對象。例如:模擬具有馬達驅動的車輪。
屬性 功能
Enable Collision 啟用這個屬性後,可以偵測碰撞。
Connected Rigid Body 指定此關節連接到的其他遊戲物件(GameObject)。如果將此設置為「None」,則關節的另一端固定在由「Connected Anchor」設置定義的空間點。
Auto Configure Connected Anchor 啟用此屬性以自動設置此關節連接到的遊戲物件的錨點(Anchor)位置。如果啟用此屬性,則不需要為「Connected Anchor」屬性輸入座標。
Anchor 定義此遊戲物件的2D剛體(Rigidbody2D)上的關節(joint)端點連接的位置(以x、y座標表示)。
Connected Anchor 定義這個關節(joint)端點要連接到另一個遊戲物件的2D剛體(Rigidbody2D)上的位置(以x、y座標表示)。
Suspension 選擇此選項以展開此屬性的設置。
Damping Ratio(阻尼比) 控制彈簧(spring)振盪(oscillation)的關節設置。較高的阻尼比意味著彈簧會更快停止振盪
Frequency 設定遊戲物件接近所需分離距離時彈簧振盪的頻率(以每秒週期數為單位)。範圍為0到1,000,000,值越高,彈簧越硬,也就是說越不容易來回振動。注意:將頻率設置為零會創建最硬的彈簧類型關節(即不振動)
Angle 設定懸掛系統(suspension)的世界(World)運動角度
Use Motor 啟用此選項以對關節應用馬達動力。
Motor 選擇此選項以展開此屬性的設置。
Motor Speed 設定馬達要達到的目標速度(每秒度數)。
Maximum Motor Force 設定馬達在嘗試達到目標速度時可以施加的最大扭矩(torque)(或旋轉力(rotation))。
Break Action 設置當超過力(force)臨界值或達到扭矩臨界值(torque threshold)時採取的動作。
Break Force 設置力的臨界值,當超過時這個關節就會執行選擇的Break Action。預設值為Infinity,表示不執行Break Action
Break Torque 設置扭矩的臨界值,當超過時這個關節就會執行選擇的Break Action。預設值為Infinity,表示不執行Break Action

Damping Ratio(阻尼比):描述系統在受到擾動後振盪及衰減的情形。阻尼比越大,物件受力振盪後越快恢復原狀。

  • 無阻尼:= 0 , 對應沒有阻尼的簡諧運動。
  • 欠阻尼:< 1 , 指數遞減且振盪
  • 過阻尼:> 1 , 沒有振盪的指數遞減
  • 臨界阻尼:= 1 , 介於過阻尼及欠阻尼之間,是許多工程應用想要的結果
  • https://zh.wikipedia.org/zh-tw/%E9%98%BB%E5%B0%BC%E6%AF%94

Reference:https://docs.unity3d.com/Manual/class-WheelJoint2D.html

Runtime DataBinding(使用UI Builder)

以下將示範如何使用UI BuilderScriptableObject來建立DataBinding

  1. 在Unity編輯器中,建立一個Script
  2. 建立一個名為ExampleObjectScriptableObject(檔案會是 ExampleObject.cs),它包含了
    • 一個string vector3Label,
    • 一個Vector3 vector3Value
    • 一個float sumOfVector3Properties,這個sumOfVector3Properties是一個readonly的屬性其值來自於vector3Label中的xyz之和,
    • 一個float dangerLevel是一個 0 ~ 1之間的數值。之後在UI Builder中更改使用Value To Progress設定,讓它可以根據ConverterGroup顯示對應的值
      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
      using Unity.Properties;
      using UnityEditor;
      using UnityEngine;
      using UnityEngine.UIElements;

      [CreateAssetMenu]
      public class ExampleObject : ScriptableObject
      {
      [InitializeOnLoadMethod]
      public static void RegisterConverters()
      {

      // Create local Converters
      var group = new ConverterGroup("Value To Progress");

      // Converter groups can have multiple converters. This example converts a float to both a color and a string.
      group.AddConverter((ref float v) => new StyleColor(Color.Lerp(Color.red, Color.green, v)));
      group.AddConverter((ref float value) =>
      {
      return value switch
      {
      >= 0 and < 1.0f / 3.0f => "Danger",
      >= 1.0f / 3.0f and < 2.0f / 3.0f => "Neutral",
      _ => "Good"
      };
      });

      // Register the converter group in InitializeOnLoadMethod to make it accessible from the UI Builder.
      ConverterGroups.RegisterConverterGroup(group);
      }

      [Header("Bind to multiple properties")]
      public string vector3Label;
      public Vector3 vector3Value;

      [CreateProperty]
      public float sumOfVector3Properties => vector3Value.x + vector3Value.y + vector3Value.z;

      [Header("Binding using a converter group")]
      [Range(0, 1)] public float dangerLevel;
      }
  3. 在Unity編輯器中,選擇 Create > Example Object ,建立一個Example Object並命名為Object1 (檔案會是 Object1.asset)
  4. 建立一個UXML,命名為ExampleObject(檔案會是ExampleObject.uxml)
  5. 點擊剛剛建立的ExampleObject.uxml,開啟UI Builder
  6. 在Hierarchy panel中,加入一個VisualElement
  7. 在剛剛建立的VisualElement中加入Vector3FieldFloatFieldLabel
  8. 最後Hierarchy panel畫面為
  9. 選擇最上層的VisualElement UI Element,在右邊找到Data Source,選擇之前建立的Object1.asset,這樣做,會將它的Child Elements都預設綁定Object1
  10. 選擇Vector3Field UI Element,在右邊找到Label,對Label按一下滑鼠右鍵,點選Add binding...,在Data Source Path中找到並選擇vector3Label,Binding Mode選擇To Target
  11. 接下來,對Vector3FieldValue按一下滑鼠右鍵,點選Add binding...,在Data Source Path中找到並選擇vector3Value,Binding Mode選擇To Target
  12. 選擇Float UI Element,在右邊找到Value按一下滑鼠右鍵,點選Add binding...,在Data Source Path中找到並選擇sumOfVector3Properties,Binding Mode選擇To Target
  13. 選擇Label UI Element,
    • 在右邊找到Text,對Text按一下滑鼠右鍵,點選Add binding...,在Data Source Path中找到並選擇dangerLevel,Binding Mode選擇To Target
    • 接著打開Advanced Settings 找到 Converters > To target property (UI) 選擇 Value To Progress
    • 接下來,在右邊找到Color,對Color按一下滑鼠右鍵,點選Add binding...,在Data Source Path勾選Show only compatible之後才會出現可以選擇的相容屬性,選擇dangerLevel,Binding Mode選擇To Target
    • 接著打開Advanced Settings 找到 Converters > To target property (UI) 選擇 Value To Progress
  14. 至此,這個UI與 ExampleObject的綁定就算完成了,UXML可能會為
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <engine:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:engine="UnityEngine.UIElements" xmlns:editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
    <engine:VisualElement data-source="project://database/Assets/UI/Custom/Object1.asset?fileID=11400000&amp;guid=a0e72bbf894f04543b805067235fe91c&amp;type=2#Object1" style="flex-grow: 1;">
    <engine:Vector3Field name="Vector3Field" focusable="false">
    <Bindings>
    <engine:DataBinding property="label" binding-mode="ToTarget" data-source-path="vector3Label" />
    <engine:DataBinding property="value" binding-mode="ToTarget" data-source-path="vector3Value" />
    </Bindings>
    </engine:Vector3Field>
    <engine:FloatField label="Float Field" name="FloatField">
    <Bindings>
    <engine:DataBinding property="value" binding-mode="ToTarget" data-source-path="sumOfVector3Properties" />
    <engine:DataBinding property="text" binding-mode="ToTarget" data-source-path="dangerLevel" source-to-ui-converters="Value To Progress" />
    <engine:DataBinding property="text" binding-mode="ToTarget" data-source-path="dangerLevel" source-to-ui-converters="Value To Progress" />
    </Bindings>
    </engine:FloatField>
    <engine:Label text="Label" name="Label" enable-rich-text="false" parse-escape-sequences="true">
    <Bindings>
    <engine:DataBinding property="value" binding-mode="ToTarget" data-source-path="sumOfVector3Properties" />
    <engine:DataBinding property="text" binding-mode="ToTarget" data-source-path="dangerLevel" source-to-ui-converters="Value To Progress" />
    <engine:DataBinding property="text" binding-mode="ToTarget" data-source-path="dangerLevel" source-to-ui-converters="Value To Progress" />
    </Bindings>
    </engine:Label>
    </engine:VisualElement>
    </engine:UXML>
  15. 可以調整Object1來觀察UI的變化
  16. 結果

Reference: https://docs.unity3d.com/2023.2/Documentation/Manual/UIE-get-started-runtime-binding.html

暴露自訂control給UXML

  1. 自訂control需要繼承VisualElement並且這個自訂control需要有一個預設constructor
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class StatusBar : VisualElement
    {
    // 需要有一個預設constructor
    public StatusBar()
    {
    m_Status = String.Empty;
    }

    string m_Status;
    public string status { get; set; }
    }
  2. 為了讓你自訂的control可以在UXML中使用,你需要定義一個factory class,如果沒有特殊的需求,可以直接繼承UxmlFactory<T>
    1
    2
    3
    4
    5
    6
    7
    class StatusBar : VisualElement
    {
    // 建議直接將factory class放在自訂的control中
    public new class UxmlFactory : UxmlFactory<StatusBar> {}

    // ...
    }
  3. UxmlTraits:定義在UXML中可以使用的特徵(UXML traits),
    • UxmlTraits的目的有
      • factory建立新物件時使用它們。
      • 在schema產生時,會分析它們以取得關於該Element的資訊,之後將這些資訊轉換為XML schema directive。
    • 以下範例
      • 宣告了一個m_Status,用來定義一個XML attribute status
      • uxmlChildElementsDescription回傳一個空IEnumerable,用來表明這個自訂的StatusBar沒有child。
      • Init()中,XML parser從property bag讀出的值設定給StatusBar.status
      • UxmlTraits class定義在StatusBar class中,讓Init()可以存取StatusBar的私有變數。
      • UxmlTraits繼承了VisualElement.UxmlTraits,它也擁有VisualElement.UxmlTraits的屬性
      • Init()會呼叫base.Init()以初始化base class的屬性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class StatusBar : VisualElement
{
public new class UxmlFactory : UxmlFactory<StatusBar, UxmlTraits> {}

public new class UxmlTraits : VisualElement.UxmlTraits
{
UxmlStringAttributeDescription m_Status = new UxmlStringAttributeDescription { name = "status" };

public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
{
get { yield break; }
}

public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
base.Init(ve, bag, cc);
((StatusBar)ve).status = m_Status.GetValueFromBag(bag, cc);
}
}

// ...
}
- 注意:在UI Builder中創作時,UI Builder可能會多次呼叫`UxmlTraits.Init()`來同步UXML檔案中的值,建議使用`GetValueFromBag`而不是`TryGetValueFromBag`以確保當UI Builder取消設定(unset)element的值時,這個element不會殘留先前的值。
  1. 若想要有child的話,需要override uxmlChildElementsDescription
    1
    2
    3
    4
    5
    6
    7
    public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
    {
    get
    {
    yield return new UxmlChildElementDescription(typeof(VisualElement));
    }
    }
  2. 可以將你自訂的control放到namespace中來分類它們
    1
    2
    [assembly: UxmlNamespacePrefix("My.First.Namespace", "first")]
    [assembly: UxmlNamespacePrefix("My.Second.Namespace", "second")]

Reference:https://docs.unity3d.com/Manual/UIE-expose-custom-control-to-uxml.html#define-a-factory

自訂control

建立自訂的Control需要建立一個control C# class,然後將它暴露給UXML

  1. 建立一個繼承VisualElement的Class

  2. 若是要建立可綁定(Binding)的自訂Control,可以繼承自BaseField基底類別,而非 BindableElement。繼承BaseField有以下好處

    • 有實作一個泛型的INotifyValueChanged
    • 預設focusable
    • 提供一個水平佈局(horizontal layout),左側為label element,右側為input element
  3. VisualElement不會綁定到GameObject的生命週期函數,也就是說它不會收到以下callbacks

    • Awake()
    • OnEnable()
    • OnDisable()
    • OnDestroy()
  4. 可以在自訂control中的constructor做初始化的動作,但是如果你想要將初始化的動作延遲到這個自訂的control被加到UI之後,可以把初始化的動作加到AttachToPanelEvent callback

  5. 使用DetachFromPanelEvent callback可以偵測你自訂的control是否已經從UI上移除

    1
    2
    3
    4
    5
    6
    7
    8
    public CustomControl()
    {
    var myCustomElement = rootVisualElement.Q(className: "my-custom-element");
    myCustomElement.RegisterCallback<AttachToPanelEvent>(e =>
    { /* 此處放置當這個elemenet被加入到UI上要做的事 */ });
    myCustomElement.RegisterCallback<DetachFromPanelEvent>(e =>
    { /* 此處放置當這個elemenet從UI上移除後要做的事 */ });
    }
  6. UI Toolkit會分派這兩個事件(AttachToPanelEvent與DetachFromPanelEvent)給所有element

  7. 要讓UXMLUI Builder使用你自訂的control,需要定義一個繼承UxmlFactory<T>factory class將你自訂的control暴露給他們。

  8. 綁定資料需要實作INotifyValueChanged並根據需要監聽ChangeEvent。繼承BindableElement或是實作IBindable

  9. 你可以創建USS custom properties以設定自訂控制項的樣式。

Reference: https://docs.unity3d.com/Manual/UIE-create-custom-controls.html

使用Action

有兩種方式回應Action

  1. Polling(輪詢):Polling方法是不斷的檢查你感興趣Action目前的狀態,通常會在MonoBehaviour script中的Update()方法去做輪詢。
  2. Event-driven(事件驅動):Event-driven則是建立要執行的方法,當Action執行時,就會自動呼叫並執行。
Polling

在大部分的場景,特別是動作遊戲,使用者的輸入必須平滑順暢的控制遊戲中的角色,使用Polling通常是比較容易去實作。

使用InputAction提供的ReadValue<>()便可以取得目前Action的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using UnityEngine;
using UnityEngine.InputSystem;

public class Example : MonoBehaviour
{
InputAction moveAction;

private void Start()
{
moveAction = InputSystem.actions.FindAction("Move");
}

void Update()
{
// 取得action的值
Vector2 moveValue = moveAction.ReadValue<Vector2>();
// 取得值之後,便可以將其應用到GameObject上,讓其移動。
}
}
方法 描述
InputAction.WasPerformedThisFrame() 在當前幀的任何時間點,如果這個Action在InputAction.phase曾經變為「已執行」(Performed),則為True。
InputAction.WasCompletedThisFrame() 在當前幀的任何時間點,如果這個Action在InputAction.phase曾經從「已執行」(Performed)更改為任何其他階段(phase),則為True。這對於按鈕那些具有「按下」(Press) 或「按住」(Hold)等動作非常有用,當按住時,返回的為False。Pass-Through

以下程式碼使用了預設的Interact Action,它包含一個「按住」(Hold)交互(interaction),使得只有綁定的控件被按住一段時間 (例如 0.4 秒) 後才會執行該動作。

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
using UnityEngine;
using UnityEngine.InputSystem;

public class Example : MonoBehaviour
{
InputAction interactAction;

private void Start()
{
interactAction = InputSystem.actions.FindAction("Interact");
}

void Update()
{
if (interactAction.WasPerformedThisFrame())
{
// 在此執行的程式碼滿足:Interact action按住足夠長時間的第一幀
}

if (interactAction.WasCompletedThisFrame())
{
// 在此執行的程式碼滿足:Interact action被按住足夠長時間後釋放的那一幀
}
}
}

以下方法可以用來輪詢按鈕是否被按下或是釋放

方法 描述
InputAction.IsPressed() 如果Action的「動作水平(the level of actuation)」已經超過「按下壓力點(press point)」,並且尚未下降或低於「釋放臨界點(release threshold)」,則為True。
InputAction.WasPressedThisFrame() 如果Action的「動作水平(the level of actuation)」在當前幀的任何時間點達到或超過「按下壓力點(press point)」,則為True。
InputAction.WasReleasedThisFrame() 如果Action的「動作水平(the level of actuation)」在當前幀的任何時間點,在「按下壓力點(press point)」以上或是或低於「釋放臨界點(release threshold)」,則為True。

以下範例有三個Actions,分別為ShieldTeleportSubmit(它們不是預設的action)

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
using UnityEngine;
using UnityEngine.InputSystem;

public class Example : MonoBehaviour
{
InputAction shieldAction;
InputAction teleportAction;
InputAction submitAction;

private void Start()
{
shieldAction = InputSystem.actions.FindAction("Shield");
teleportAction = InputSystem.actions.FindAction("Teleport");
submitAction = InputSystem.actions.FindAction("Submit");
}

void Update()
{
if (shieldAction.IsPressed())
{
// shield會在shield action被按下的每一幀都處於激活狀態

if (teleportAction.WasPressedThisFrame())
{
// teleport會在 按下動作的第一幀 發生,直到 按鈕釋放 後才會再次生效。
}

if (submit.WasReleasedThisFrame())
{
// submit會在 動作釋放的幀 發生,這是一種常用于 UI 控制按鈕的常見技巧。
}
}
}
Event-driven

可以為Action註冊一些callback,當某些輸入發生時,Action就會通知你,讓你可以做出相應的回應。
有以下方式可以使用callback

  1. 透過PlayerInput Component註冊callback
  2. 使用Actionstartedperformedcanceled callback
  3. 使用ActionMapactionTriggered callback
  4. 使用Input System的全域InputSystem.onActionChange callback
  5. InputActionTrace可以記錄Actions上的改變
PlayerInput Component

使用PlayerInput component可以在inspector中直接設定callback。此外也可以透過程式碼來設定

Phase(階段) 描述
Disabled 此Action已停用且無法接收輸入
Waiting 此Action已啟用且等待輸入
Started Input System已接收到輸入並開始與Action互動
Performed 與Action的互動已經完成
Canceled 與Action的互動已經被取消
Action callbacks

可以透過 InputAction.phase 來取得目前的階段,Started,Performed與Canceled階段各有與之關聯的callback函數

1
2
3
4
5
var action = new InputAction();

action.started += context => {/* Action已經開始 */};
action.performed += context => {/* Action已經執行 */};
action.canceled += context => {/* Action已經取消 */};

每個callback都會接收一個InputAction.CallbackContext結構,該結構包含上下文信息,您可以利用它來查詢Action的當前狀態,以及讀取觸發該動作的裝置控制的值。

  • 注意:此結構的內容只在callback期間中有效,請勿把它暫存並在callback之外使用

callback函數的觸發時機和方式取決於綁定中存在的交互 (Interactions)。如果綁定沒有適用的交互,則會應用預設的交互(Default Interaction)。

除了監聽個別Action,你也可以使用InputActionMap.actionTriggered監聽該Map中的全部Action,使用這種方式,該單一個callback會收到started,performed與canceled,且其InputAction.CallbackContext結構與使用個別started,performed與canceled的結構相同。

1
2
3
4
5
6
var actionMap = new InputActionMap();
actionMap.AddAction("action1", "<Gamepad>/buttonSouth");
actionMap.AddAction("action2", "<Gamepad>/buttonNorth");

actionMap.actionTriggered +=
context => { ... };

InputSystem.onActionChangeInputSystem.onDeviceChange類似,讓你可以全域(globally)監聽任何與Action相關的變更

1
2
3
4
5
6
7
8
9
10
11
12
13
InputSystem.onActionChange +=
(obj, change) =>
{
// obj可能是InputAction或是InputMap,需要依靠change去判斷
switch (change)
{
case InputActionChange.ActionStarted:
case InputActionChange.ActionPerformed:
case InputActionChange.ActionCanceled:
Debug.Log($"{((InputAction)obj).name} {change}");
break;
}
}
InputActionTrace

,你可以使用InputActionTrace為某些特定Action集合建立日誌(log),讓你可以追蹤它們。

  • 注意:InputActionTrace會使用不受管理的記憶體,因此在使用後要去處理(disposed)它,避免造成記憶體洩漏(memory leak)
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
// 實體化一個 InputActionTrace
var trace = new InputActionTrace();

// 為這個Action訂閱trace。
// 取消訂閱的話,使用UnsubscribeFrom方法
trace.SubscribeTo(myAction);

// 為整個Action Map訂閱trace
// 取消訂閱的話,使用UnsubscribeFrom方法
trace.SubscribeTo(myActionMap);

// 為所有在系統中的Action訂閱trace
trace.SubscribeToAll();

// 為這個Action紀錄一個觸發
myAction.performed +=
ctx =>
{
if (ctx.ReadValue<float>() > 0.5f)
trace.RecordAction(ctx);
};

// 輸出這個trace
Debug.Log(string.Join(",\n", trace));

// 拜訪所有Action的trace紀錄,之後清除trace。
foreach (var record in trace)
{
Debug.Log($"{record.action} was {record.phase} by control {record.control}");

// 讀取這個紀錄的值,你需要知道它的類型是什麼,否則你可以把它讀為一般的Byte。此處假設他是float
Debug.Log("Value: " + record.ReadValue<float>());

// 如果願意接受GC負擔,也可以將值讀取為物件(object)。
// 此時,你可以不需要知道它的類型
Debug.Log("Value: " + record.ReadValueAsObject());
}
trace.Clear();

// 取消所有追蹤的訂閱。
trace.UnsubscribeFromAll();

// 釋放trace使用的記憶體
trace.Dispose();

一旦紀錄完成,只要不同時進行寫入操作,並且主線程上不會同時變更Action設置,則trace可以安全地從多個線程讀取。

Action types

Action有三種類型,這些類型會影響Input System如何處理操作Action的狀態。

Action Type 描述
Value 預設的Action Type。適用於任何需要追踪控制項(Control)狀態連續變化的輸入。Value action Type 會持續監控所有綁定到該動作(Action)的控制項(Control),然後選擇最活躍(actuated)的控制項作為驅動該動作的控制項,並在值發生變化時觸發回調函式(callback)報告該控制項的值。如果另一個綁定控制項的活躍程度更高,那麼該控制項就會成為驅動動作的控制項,動作會開始報告該控制項的值。這個過程稱為 衝突解決 (conflict resolution)。如果您希望允許遊戲中不同的控制項控制一個動作,但僅同時從一個控制項接收輸入,那麼這將非常有用。當動作(action)首次啟用時,它會對所有綁定控制項(Control)執行 初始狀態檢查(initial state check)。如果任何一個控制項被激活,則動作會觸發一個回調,傳遞當前值。
Button Button類型非常類似於Value類型,但有以下幾點不同:Button類型的動作只能綁定到「按鈕控制項」(ButtonControl)。與Value類型的動作不同,Button類型的動作不會執行初始狀態檢查。「按鈕」類型的動作適用於每次按下時觸發一次操作的輸入。在這種情況下,初始狀態檢查通常沒有用,因為它可能會在啟用動作時,因之前按住按鈕而觸發動作 (即按鈕仍然處於按下狀態)。
PassThrough PassThrough類型的動作與上面描述的Value動作不同,它繞過了衝突解決。PassThrough類型的動作不會選擇一個特定的控制項作為驅動動作的來源。
相反,任何綁定的控制項會變更都會觸發一個回調,並將該控制項的目前值傳遞給回調函數。PassThrough類型的動作適用於您想要處理來自一組控制項的所有輸入的情況。

使用Input Debugger,可以觀察目前啟用的Action以及與他們綁定的Control。
也可以使用InputActionVisualizer在螢幕上即時可視化動作的值和交互狀態。


上一篇:Actions概念

Reference: https://docs.unity3d.com/Packages/com.unity.inputsystem@1.8/manual/RespondingToActions.html

Actions

Action是將裝置控制(device control)與輸入(input)分開的一個重要概念。舉例來說,某個遊戲中某些輸入(input)的目的是讓遊戲角色移動,而與該動作(Action)相關的裝置控制(device control)可能是左搖桿。將動作(action)與執行這個輸入的裝置控制(左搖桿)關聯起來的稱作綁定(Binding)。你可以在Action Editor中建立這種綁定,當在程式碼中使用Actons時,你不需要去指定特定的裝置,因為Binding定義了哪些裝置可以執行這個Action。

透過Action Editor你可以為Action建立多個設備的對應,例如下圖,

Move對應了鍵盤與遊戲搖桿,之後你就可以在程式碼中取得這個Action的參考,並可以檢查它的值,或是為它附加callback方法

1
2
// 透過InputSystem API可以找到Move,注意不要在Update loop中使用,因為它是基於字串搜尋,因此會影響效能
InputAction moveAction = InputSystem.actions.FindAction("Move");

注意:

  • Action只可在runtime使用,不可以在Edit Window code中使用。
  • 也可以不使用Action和Binding來直接取得裝置控制的值var gamepad = Gamepad.current;,但是彈性會比較差。
  • Action Editor 中顯示的動作順序僅供視覺參考,並不代表實際程式碼執行的順序。多個動作可能在同一幀中執行,Input system的動作順序是不確定的。為了避免潛在問題,請勿在程式碼中假設動作會按特定順序執行。
當您在InputSystem中使用Action進行Script編寫時,可以利用以下一些重要的API,
API 名稱 描述
InputAction 一個命名Action,它可以回傳與其綁定的裝置控制的值,或是觸發callback。這個API就是Action Editor中Action那一欄的值
InputActionMap 命名的Action集合。這個API持有的集合就是Action Editor中Action Map那一欄的值
InputSystem.actions 是一個專案範圍內動作集合(ProjectWideActions)的引用(reference)
InputBinding 動作(Action)與其接收輸入的特定設備控制(device control)之間的關係,ActionBindings
  • Action:每個Action都有一個名稱(InputAction.name),在一個Action Map中,這個Action的名稱必須是獨一無二的,每個Action也會有一個獨一無二的ID(InputAction.id),可以用它們來找到這個Action的引用。當ID產生之後,就算名稱改變ID也不會變。
  • Action Map:每個Action Map都有一個名稱(InputActionMap.name),這個Action Map的名稱必須是獨一無二的,每個Action Map也會有一個獨一無二的ID(InputActionMap.id),可以用它們來找到這個Action Map的引用。當ID產生之後,就算名稱改變ID也不會變。
建立Action

建立Action有好幾種方式

  • 方法一:使用Action Editor建立Action:最簡單的方式是使用Action Editor建立
  • 方法二:在MonoBehaviours中宣告Action,此方法與方法一類似,差異在於它將Actions定義在GameObject的屬性中,並儲存為Scene或是Prefab。並且它不是project-wide action,因此在使用時需要手動的啟用,停用這些Action。
    1
    2
    3
    4
    5
    6
    7
    8
    using UnityEngine;
    using UnityEngine.InputSystem;

    public class ExampleScript : MonoBehaviour
    {
    public InputAction move;
    public InputAction jump;
    }
  • 方法三:在JSON中載入
    1
    2
    3
    4
    5
    // Load a set of action maps from JSON.
    var maps = InputActionMap.FromJson(json);

    // Load an entire InputActionAsset from JSON.
    var asset = InputActionAsset.FromJson(json);
  • 方法四:使用程式碼
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // Create free-standing Actions.
    var lookAction = new InputAction("look", binding: "<Gamepad>/leftStick");
    var moveAction = new InputAction("move", binding: "<Gamepad>/rightStick");

    lookAction.AddBinding("<Mouse>/delta");
    moveAction.AddCompositeBinding("Dpad")
    .With("Up", "<Keyboard>/w")
    .With("Down", "<Keyboard>/s")
    .With("Left", "<Keyboard>/a")
    .With("Right", "<Keyboard>/d");

    // Create an Action Map with Actions.
    var map = new InputActionMap("Gameplay");
    var lookAction = map.AddAction("look");
    lookAction.AddBinding("<Gamepad>/leftStick");

    // Create an Action Asset.
    var asset = ScriptableObject.CreateInstance<InputActionAsset>();
    var gameplayMap = new InputActionMap("gameplay");
    asset.AddActionMap(gameplayMap);
    var lookAction = gameplayMap.AddAction("look", "<Gamepad>/leftStick");

對於非專案範圍(project-wide)的Actions,在使用前需要先啟用(Enable)

1
2
3
4
5
// 啟用單一action
lookAction.Enable();

// 啟用整個action map.
gameplayActions.Enable();

當您啟用一個「動作」(Action)時,InputSystem會解析其綁定(bindings)。啟用後,「動作」會積極監控其綁定的「控件」(Control(s))。如果綁定的「控件」狀態改變,「動作」就會處理該變化。如果「控件」的變化代表「交互」(Interaction) 變化,則「動作」會創建一個響應。所有這些都發生在輸入系統的更新邏輯中。取決於輸入設置中選擇的「更新模式」,這可能每幀發生一次,每固定更新一次,或者如果更新設置為手動,則手動發生一次。

在「動作」啟用的情況下,有些配置無法更改,例如「動作綁定」。要停止ActionAction Map響應輸入,可以使用「停用」(Disable)方法。


上一篇:使用Action Editor編輯Action

下一篇:使用Action

Reference: https://docs.unity3d.com/Packages/com.unity.inputsystem@1.8/manual/Actions.html

使用Action Editor編輯Input

  1. Project-Wide Actions中可以建立一個整個專案可用的Action Asset,此外你也可以自己再額外建立更多的Action Asset,在編輯器中,選擇Create > Input Actions 便可建立一個Action Asset。

  2. 滑鼠左鍵點擊兩次剛剛建立的Action檔案,便可以打開編輯畫面(Actions Editor)

    Name 描述
    Action Maps 顯示目前定義的Action Maps
    Actions 顯示目前選中的Action Map擁有的Action以及Binding
    Properties 顯示目前選中Action或是Binding的屬性(properties)
  3. 設定Action Maps:在Action Maps旁邊的+可以添加Action Map,對新增的Action Map按下兩次滑鼠左鍵可以對它命名,

    • 注意Action Map的名稱不可以包含 / (slashes),
  4. 設定Action:在Action旁邊的+可以添加Action,對新增的Action按下兩次滑鼠左鍵可以對它命名

    • Action的屬性
      • Action Type:可以選擇ButtonValue或是PassThrough
        • 若你的Action為鍵盤按鈕,滑鼠點擊或是搖桿按鈕控制的話,選擇Button,如果有多個裝置接上,那麼只會選擇一個最活躍的輸入裝置的輸入(稱為Conflicting inputs)
        • 若是滑鼠移動或是搖桿移動這種屬於連續不斷改變的輸入的話,選擇Value
        • PassThrough和Value相同,差別在於不處理衝突,即會發送所有綁定此Action裝置的輸入。
      • Control Type:讓你選擇此Action期望的控制類型,這可以限制哪些控制設備可以顯示在UI上,例如選擇了2D axis,那麼在選擇綁定時只有那些支援2D vector的控制設備會顯示在選項上。
      • Binding:在添加的自訂Action旁邊的+可以為這個Action新增Binding。可以在一個Action上添加多個Binding以支援多個類型的輸入裝置。
      • Composite Bindings:為多個Binding組成,
        • 例如Up/Down/Left/Right Composite就是在模擬2D搖桿(2D stick input)的輸入
        • 可以透過Duplicate添加不同的 Binding
  5. 在Action Editor左上角可以找到Control Schemes,讓你可以根據不同的裝置啟用或是停用Action的Bindings


上一篇:將Action設為整個專案可用(Project-Wide Actions)

下一篇:Actions概念

Project-Wide Actions

Input System的project-wide actions功能讓你可以設定一個能夠在整個專案中使用的Action Asset,當Action Asset設為project-wide actions之後,那這個action就是個preloaded asset,也就是說當你的App啟動後就會載入,並且一直保持可用直到App關閉為止。

建立Project-Wide Actions Asset
  1. 找到 Edit > Project Settings > Input System Package 並按下 Create a new project-wide Action Asset
  2. 在專案中會建立一個名為InputSystem_Actions的Action Asset。
  3. 這個InputSystem_Actions中已經有一些預設的Action,像是MoveJump等,符合大部分遊戲,並預設綁定大部分裝置如鍵盤,滑鼠,遊戲搖桿,觸控螢幕,XR等
  4. 在程式碼中使用InputSystem.actions去找到Action
    1
    InputSystem.actions.FindAction("Move");

上一篇:安裝Input System

下一篇:使用Action Editor編輯Action

Reference: https://docs.unity3d.com/Packages/com.unity.inputsystem@1.8/manual/ProjectWideActions.html

安裝Input System

Input System需要Unity 2019.4更高版本以及.NET 4 runtime

安裝Input System package
  1. 在編輯器中,找到 Window > Package Manager 打開 Package Manager
  2. 更改為Unity Registry,並搜尋 Input System
  3. 安裝Input System
  4. 選擇是否在後端啟用Input System。Unity預設啟用的是InputManager(UnityEngine.Input),在安裝Input System時會詢問你是否要啟用Input System,選擇Yes,這會讓編輯器重開。
  5. 此外你也可以在Edit > Project Settings > Player 中找到 Active Input Handling來更改
  6. 在C# script中,當Input System在後端被啟用時,C# #define 會加入一個ENABLE_INPUT_SYSTEM=1的定義;當原先的Input Manager在後端被啟用時,C# #define會加入一個ENABLE_LEGACY_INPUT_MANAGER=1,如果是同時啟用則ENABLE_INPUT_SYSTEMENABLE_LEGACY_INPUT_MANAGER皆會設為1。

上一篇:Input System基本概念

下一篇:將Action設為整個專案可用(Project-Wide Actions)

Reference: https://docs.unity3d.com/Packages/com.unity.inputsystem@1.8/manual/ActionsEditor.html

輸入事件(Input events)

當字串透過使用者輸入到Text field時,便會觸發InputEvent,與觸碰螢幕的PointerCaptureOutEvent類似。對於預設的鍵盤輸入,每次按一下鍵(keystroke)都會觸發InputEvent。但是對於間接來源(indirect source)的輸入,將不會觸發InputEvent,例如自動化腳本(automated script)。

專屬於Input Event的屬性

  • previousData: 存放先前的資料
  • newData: 存放新的資料

當資料輸入到繼承了TextInputBaseField的control便會發送InputEventInputEvent不同於ChangeEvent的地方在於,即使輸入到control的資料沒改變,它也會發送InputEvent

例子:下面例子為一個TextField註冊一個InputEvent,每當觸發便會印出輸出的的值
  1. 在Assets > Scripts > Editor 下建立一個 C# Script InputEventExample.cs
  2. 將以下程式碼複製到剛剛建立的C# 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
    using UnityEditor;
    using UnityEngine;
    using UnityEngine.UIElements;

    public class InputEventExample : EditorWindow
    {
    // 這個Attributes會在 Windows -> UI Tollkit 下加入一個 InputEventExample
    [MenuItem("Window/UI Toolkit/InputEventExample")]
    public static void ShowExample()
    {
    var wnd = GetWindow<InputEventExample>();
    wnd.titleContent = new GUIContent("Input Event Example");
    }

    public void CreateGUI()
    {
    // 建立 TextField
    TextField textField = new TextField();
    rootVisualElement.Add(textField);

    // 註冊InputEvent
    textField.RegisterCallback<InputEvent>(OnInput);
    }

    private void OnInput(InputEvent evt)
    {
    // 印出InputEvent中的newData與previousData
    Debug.Log("newData=" + evt.newData + " previousData=" + evt.previousData);
    }
    }
  3. 在Unity編輯器中找到 Window > UI Toolkit > InputEventExample
  4. 顯示結果

Reference: https://docs.unity3d.com/Manual/UIE-Input-Events.html