Ps5手柄

14k words

简介

刚买了ps5,试试在Unity中的支持

链接

Unity-Technologies/InputSystem
DualSenseGamepadHID
InputSystem
Sony Developer
SDK
Github

常用接口

前置准备

1
2
3
4
5
6
7
DualSenseGamepadHID pad;

private void Awake()
{
pad = (DualSenseGamepadHID)Gamepad.current;
// pad.OnStateEvent
}

Image text
Image text
Image text

注意

Dualshock 4 上的功能SetMotorSpeeds(Single, Single)是使用 IOCTL 命令实现的,因此如果快速连续调用任一方法,很可能只有第一个命令会成功完成。其他命令将被删除。如果需要同时设置灯条颜色和隆隆声电机速度,请使用该SetMotorSpeedsAndLightBarColor(Single, Single, Color)方法

设置颜色

1
2

pad.SetLightBarColor(gamePadColor);

设置震动

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
/**
lowFrequency:低频
heightFrequency:高频
设置震动,范围0-1
**/
pad.SetMotorSpeeds(lowFrequency, heightFrequency);

public override void PauseHaptics()
{
if (!m_LowFrequencyMotorSpeed.HasValue && !m_HighFrequenceyMotorSpeed.HasValue)
return;

SetMotorSpeedsAndLightBarColor(0.0f, 0.0f, m_LightBarColor);
}

public override void ResetHaptics()
{
if (!m_LowFrequencyMotorSpeed.HasValue && !m_HighFrequenceyMotorSpeed.HasValue)
return;

m_HighFrequenceyMotorSpeed = null;
m_LowFrequencyMotorSpeed = null;

SetMotorSpeedsAndLightBarColor(m_LowFrequencyMotorSpeed, m_HighFrequenceyMotorSpeed, m_LightBarColor);
}

public override void ResumeHaptics()
{
if (!m_LowFrequencyMotorSpeed.HasValue && !m_HighFrequenceyMotorSpeed.HasValue)
return;
SetMotorSpeedsAndLightBarColor(m_LowFrequencyMotorSpeed, m_HighFrequenceyMotorSpeed, m_LightBarColor);
}

自适应扳机

陀螺仪

InputSystem只支持安卓和IOS的陀螺仪
索索的SDK要以公司名义申请账号,算了算了

UniSense

  1. InputSystem的拓展,可以在Unity中直接设置Ps5手柄的相关属性
    InputSystem方面的拓展

Github DualSense-Windows
2. Github上调用手柄的接口,C++库

细节

Ps5手柄库

1
2
3
4
5
6
7
8
9
10
数据的获取输出

// State object
DS5W::DS5InputState inState;
DS5W::DS5OutputState outState;
ZeroMemory(&inState, sizeof(DS5W::DS5InputState));
ZeroMemory(&outState, sizeof(DS5W::DS5OutputState));

DS5W::setDeviceOutputState(&con, &outState);

能够获取的属性

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
73
74
75
76
77
78
79
/// <summary>
/// Input state of the controler
/// </summary>
typedef struct _DS5InputState {
/// <summary>
/// Position of left stick
/// </summary>
AnalogStick leftStick;

/// <summary>
/// Posisiton of right stick
/// </summary>
AnalogStick rightStick;

/// <summary>
/// Left trigger position
/// </summary>
unsigned char leftTrigger;

/// <summary>
/// Right trigger position
/// </summary>
unsigned char rightTrigger;

/// <summary>
/// Buttons and dpad bitmask DS5W_ISTATE_BTX_?? and DS5W_ISTATE_DPAD_?? indices check with if(buttonsAndDpad & DS5W_ISTATE_DPAD_??)...
/// </summary>
unsigned char buttonsAndDpad;

/// <summary>
/// Button bitmask A (DS5W_ISTATE_BTN_A_??)
/// </summary>
unsigned char buttonsA;

/// <summary>
/// Button bitmask B (DS5W_ISTATE_BTN_B_??)
/// </summary>
unsigned char buttonsB;

/// <summary>
/// Accelerometer
/// </summary>
Vector3 accelerometer;

/// <summary>
/// Gyroscope (Currently only raw values will be dispayed! Probably needs calibration (Will be done within the lib in the future))
/// </summary>
Vector3 gyroscope;

/// <summary>
/// First touch point
/// </summary>
Touch touchPoint1;

/// <summary>
/// Second touch point
/// </summary>
Touch touchPoint2;

/// <summary>
/// Battery information
/// </summary>
Battery battery;

/// <summary>
/// Indicates the connection of headphone
/// </summary>
bool headPhoneConnected;

/// <summary>
/// EXPERIMAENTAL: Feedback of the left adaptive trigger (only when trigger effect is active)
/// </summary>
unsigned char leftTriggerFeedback;

/// <summary>
/// EXPERIMAENTAL: Feedback of the right adaptive trigger (only when trigger effect is active)
/// </summary>
unsigned char rightTriggerFeedback;
} DS5InputState;

能够设置的属性

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

typedef struct _DS5OutputState {
/// <summary>
/// Left / Hard rumbel motor
/// </summary>
unsigned char leftRumble;

/// <summary>
/// Right / Soft rumbel motor
/// </summary>
unsigned char rightRumble;

/// <summary>
/// State of the microphone led
/// </summary>
MicLed microphoneLed;

/// <summary>
/// Diables all leds
/// </summary>
bool disableLeds;

/// <summary>
/// Player leds
/// </summary>
PlayerLeds playerLeds;

/// <summary>
/// Color of the lightbar
/// </summary>
Color lightbar;

/// <summary>
/// Effect of left trigger
/// </summary>
TriggerEffect leftTriggerEffect;

/// <summary>
/// Effect of right trigger
/// </summary>
TriggerEffect rightTriggerEffect;

} DS5OutputState;

判断当前连接方式

1
2
3
4
5
6
7
8
builder << L"DS5 (";
if (con._internal.connection == DS5W::DeviceConnection::BT) {
builder << L"BT";
}
else {
builder << L"USB";
}

设备的获取和释放

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
// Enum all controllers presentf
DS5W::DeviceEnumInfo infos[16];
unsigned int controllersCount = 0;
DS5W_ReturnValue rv = DS5W::enumDevices(infos, 16, &controllersCount);

// check size
if (controllersCount == 0) {
console.writeLine(L"No DualSense controller found!");
system("pause");
return -1;
}


if (DS5W_SUCCESS(DS5W::initDeviceContext(&infos[0], &con))) {
console.writeLine(L"DualSense controller connected");

builder << L") Press L1 and R1 to exit";
// Application infinity loop
while (!(inState.buttonsA & DS5W_ISTATE_BTN_A_LEFT_BUMPER && inState.buttonsA & DS5W_ISTATE_BTN_A_RIGHT_BUMPER)) {
// Get input state
if (DS5W_SUCCESS(DS5W::getDeviceInputState(&con, &inState))) {

DS5W::setDeviceOutputState(&con, &outState);
}
else {
// Device disconnected show error and try to reconnect
console.writeLine(L"Device removed!");
DS5W::reconnectDevice(&con);
}
}

// Free state
DS5W::freeDeviceContext(&con);
}

触摸板

1
2
3
4
5
6
7
8
9
10
11
//触摸板当前是否被按下
inState.buttonsB & DS5W_ISTATE_BTN_B_PAD_BUTTON


//在触摸板上的位置(好像最多支持两个位置)
inState.touchPoint1.x
inState.touchPoint1.y

inState.touchPoint2.x
inState.touchPoint2.x

自适应扳机

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
//值
(int)inState.leftTrigger
(int)inState.rightTrigger

扳机是否被按下
inState.buttonsA & DS5W_ISTATE_BTN_A_LEFT_TRIGGER
inState.buttonsA & DS5W_ISTATE_BTN_A_RIGHT_TRIGGER

肩键是否按下
inState.buttonsA & DS5W_ISTATE_BTN_A_LEFT_BUMPER
inState.buttonsA & DS5W_ISTATE_BTN_A_RIGHT_BUMPER

自适应扳机类型
//截面阻力
DS5W::TriggerEffectType::SectionResitance
outState.leftTriggerEffect.Section.startPosition = 0x00;
outState.leftTriggerEffect.Section.endPosition = 0x60;
设置截面的值

//连贯的阻力,力的大小,开始的位置
DS5W::TriggerEffectType::ContinuousResitance;
outState.rightTriggerEffect.Continuous.force = 0xFF;
outState.rightTriggerEffect.Continuous.startPosition = 0x00;

//没有阻力
DS5W::TriggerEffectType::NoResitance


outState.leftTriggerEffect.effectType = DS5W::TriggerEffectType::SectionResitance;

LED

1
2
3
4
5
6
7
8

// 麦克风LED Mic led
if (inState.buttonsB & DS5W_ISTATE_BTN_B_MIC_BUTTON) {
outState.microphoneLed = DS5W::MicLed::ON;
}
else if (inState.buttonsB & DS5W_ISTATE_BTN_B_PLAYSTATION_LOGO) {
outState.microphoneLed = DS5W::MicLed::OFF;
}

Unity中检测当前平台是否支持陀螺仪功能

1
2
3
4
5
6
7
8
9
10
11
12
13
//检查是否支持陀螺仪
if (SystemInfo.supportsGyroscope)
{
//启用陀螺仪
Input.gyro.enabled = true;
//获取陀螺仪的信息
//...
}
else
{
//提示不支持陀螺仪
Debug.Log("Gyroscope is not supported on this platform.");
}

摇杆输入获取

根据左右摇杆控制镜头旋转和位移

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
public InputAction moveAction;
public InputAction lookAction;


protected new void OnEnable()
{
if (m_Visualization == Mode.None)
return;

if (s_EnabledInstances == null)
s_EnabledInstances = new List<InputControlVisualizer>();
if (s_EnabledInstances.Count == 0)
{
//注册设备改动监听
InputSystem.onDeviceChange += OnDeviceChange;
//注册输入事件监听
InputSystem.onEvent += OnEvent;
}
s_EnabledInstances.Add(this);

ResolveControl();

base.OnEnable();
}

protected new void OnDisable()
{
if (m_Visualization == Mode.None)
return;

s_EnabledInstances.Remove(this);
if (s_EnabledInstances.Count == 0)
{
//移除监听
InputSystem.onDeviceChange -= OnDeviceChange;
InputSystem.onEvent -= OnEvent;
}

m_Control = null;

base.OnDisable();
}

private static void OnDeviceChange(InputDevice device, InputDeviceChange change)
{
if (change != InputDeviceChange.Added && change != InputDeviceChange.Removed)
return;

for (var i = 0; i < s_EnabledInstances.Count; ++i)
{
var component = s_EnabledInstances[i];
if (change == InputDeviceChange.Removed && component.m_Control != null &&
component.m_Control.device == device)
component.ResolveControl();
else if (change == InputDeviceChange.Added)
component.ResolveControl();
}
}

private static void OnEvent(InputEventPtr eventPtr, InputDevice device)
{
// Ignore very first update as we usually get huge lag spikes and event count
// spikes in it from stuff that has accumulated while going into play mode or
// starting up the player.
if (InputState.updateCount <= 1)
return;

if (InputState.currentUpdateType == InputUpdateType.Editor)
return;

if (!eventPtr.IsA<StateEvent>() && !eventPtr.IsA<DeltaStateEvent>())
return;

for (var i = 0; i < s_EnabledInstances.Count; ++i)
{
var component = s_EnabledInstances[i];
if (component.m_Control?.device != device || component.m_Visualizer == null)
continue;

component.OnEventImpl(eventPtr, device);
}
}

//值的获取方式
private unsafe void OnEventImpl(InputEventPtr eventPtr, InputDevice device)
{
switch (m_Visualization)
{
case Mode.Value:
{
var statePtr = m_Control.GetStatePtrFromStateEvent(eventPtr);
if (statePtr == null)
return; // No value for control in event.
var value = m_Control.ReadValueFromStateAsObject(statePtr);
m_Visualizer.AddSample(value, eventPtr.time);
break;
}

case Mode.Events:
{
var visualizer = (VisualizationHelpers.TimelineVisualizer)m_Visualizer;
var frame = (int)InputState.updateCount;
ref var valueRef = ref visualizer.GetOrCreateSample(0, frame);
var value = valueRef.ToInt32() + 1;
valueRef = value;
visualizer.limitsY =
new Vector2(0, Mathf.Max(value, visualizer.limitsY.y));
break;
}

case Mode.MaximumLag:
{
var visualizer = (VisualizationHelpers.TimelineVisualizer)m_Visualizer;
var lag = (Time.realtimeSinceStartup - eventPtr.time) * 1000; // In milliseconds.
var frame = (int)InputState.updateCount;
ref var valueRef = ref visualizer.GetOrCreateSample(0, frame);

if (lag > valueRef.ToDouble())
{
valueRef = lag;
if (lag > visualizer.limitsY.y)
visualizer.limitsY = new Vector2(0, Mathf.Ceil((float)lag));
}
break;
}

case Mode.Bytes:
{
var visualizer = (VisualizationHelpers.TimelineVisualizer)m_Visualizer;
var frame = (int)InputState.updateCount;
ref var valueRef = ref visualizer.GetOrCreateSample(0, frame);
var value = valueRef.ToInt32() + eventPtr.sizeInBytes;
valueRef = value;
visualizer.limitsY =
new Vector2(0, Mathf.Max(value, visualizer.limitsY.y));
break;
}

case Mode.DeviceCurrent:
{
m_Visualizer.AddSample(device, eventPtr.time);
break;
}
}
}


public void Update()
{
var look = lookAction.ReadValue<Vector2>();
var move = moveAction.ReadValue<Vector2>();

// Update orientation first, then move. Otherwise move orientation will lag
// behind by one frame.
Look(look);
Move(move);
}

private void Move(Vector2 direction)
{
if (direction.sqrMagnitude < 0.01)
return;
var scaledMoveSpeed = moveSpeed * Time.deltaTime;
// For simplicity's sake, we just keep movement in a single plane here. Rotate
// direction according to world Y rotation of player.
var move = Quaternion.Euler(0, transform.eulerAngles.y, 0) * new Vector3(direction.x, 0, direction.y);
transform.position += move * scaledMoveSpeed;
}

private void Look(Vector2 rotate)
{
if (rotate.sqrMagnitude < 0.01)
return;
var scaledRotateSpeed = rotateSpeed * Time.deltaTime;
m_Rotation.y += rotate.x * scaledRotateSpeed;
m_Rotation.x = Mathf.Clamp(m_Rotation.x - rotate.y * scaledRotateSpeed, -89, 89);
transform.localEulerAngles = m_Rotation;
}

按钮输入获取

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

public InputAction fireAction;

public void Awake()
{
// We could use `fireAction.triggered` in Update() but that makes it more difficult to
// implement the charging mechanism. So instead we use the `started`, `performed`, and
// `canceled` callbacks to run the firing logic right from within the action.

fireAction.performed +=
ctx =>
{
if (ctx.interaction is SlowTapInteraction)
{
StartCoroutine(BurstFire((int)(ctx.duration * burstSpeed)));
}
else
{
Fire();
}
m_Charging = false;
};
fireAction.started +=
ctx =>
{
if (ctx.interaction is SlowTapInteraction)
m_Charging = true;
};
fireAction.canceled +=
ctx =>
{
m_Charging = false;
};
}

private IEnumerator BurstFire(int burstAmount)
{
for (var i = 0; i < burstAmount; ++i)
{
Fire();
yield return new WaitForSeconds(0.1f);
}
}

private void Fire()
{
var transform = this.transform;
var newProjectile = Instantiate(projectile);
newProjectile.transform.position = transform.position + transform.forward * 0.6f;
newProjectile.transform.rotation = transform.rotation;
var size = 1;
newProjectile.transform.localScale *= size;
newProjectile.GetComponent<Rigidbody>().mass = Mathf.Pow(size, 3);
newProjectile.GetComponent<Rigidbody>().AddForce(transform.forward * 20f, ForceMode.Impulse);
newProjectile.GetComponent<MeshRenderer>().material.color =
new Color(Random.value, Random.value, Random.value, 1.0f);
}

模拟鼠标输入

VirtualMouseInput.cs
定义 InputActionProperty 行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public InputActionProperty scrollWheelAction
{
get => m_ScrollWheelAction;
set => SetAction(ref m_ScrollWheelAction, value);
}

private static void SetAction(ref InputActionProperty field, InputActionProperty value)
{
var oldValue = field;
field = value;

if (oldValue.reference == null)
{
var oldAction = oldValue.action;
if (oldAction != null && oldAction.enabled)
{
oldAction.Disable();
if (value.reference == null)
value.action?.Enable();
}
}
}

轮询

在 Windows(仅限 XInput 控制器)、通用 Windows 平台 (UWP) 和 Switch 上,Unity 显式轮询游戏手柄,而不是将更新作为事件传递。
可以手动控制轮询频率。默认轮询频率为 60 Hz。用于InputSystem.pollingFrequency获取或设置频率

1
2
3
// Poll gamepads at 120 Hz.
InputSystem.pollingFrequency = 120;

增加频率会导致相应设备上的事件数量增加。事件上提供的时间戳应大致遵循轮询频率规定的间隔。但是请注意,异步后台轮询取决于操作系统线程调度,并且可能会有所不同。