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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
use bevy::{
input::gamepad::{
GamepadConnection, GamepadConnectionEvent, GamepadInput, RawGamepadAxisChangedEvent,
RawGamepadButtonChangedEvent, RawGamepadEvent,
},
prelude::*,
};
use block2::StackBlock;
use crossbeam::channel::{Receiver, unbounded};
use objc2::rc::Retained;
use objc2_foundation::{NSNotification, NSNotificationCenter, NSObjectNSScriptClassDescription};
use objc2_game_controller::{
GCController, GCControllerDidConnectNotification, GCControllerDidDisconnectNotification,
GCControllerPlayerIndex, GCDevice, GCDevicePhysicalInput as _, GCExtendedGamepad,
};
use std::{
ptr::NonNull,
sync::{Arc, atomic::AtomicUsize},
};
use crate::{GamepadId, GamepadIndex};
#[derive(Event)]
pub(crate) enum AppleGameControllerEvent {
Connected {
id: GamepadId,
connection: GamepadConnection,
},
Disconnected {
id: GamepadId,
},
}
#[derive(Resource)]
pub(crate) struct ConnectionEventChannel<T: Event> {
rx: Receiver<T>,
}
pub struct AppleGameController;
impl AppleGameController {
pub fn gamepad_connection_system(
mut commands: Commands,
channel: Res<ConnectionEventChannel<AppleGameControllerEvent>>,
mut writer: EventWriter<GamepadConnectionEvent>,
mut gamepad_index: ResMut<GamepadIndex>,
) {
while let Ok(event) = channel.rx.try_recv() {
match event {
AppleGameControllerEvent::Connected { id, connection } => {
let entity = commands.spawn_empty().id();
gamepad_index.gamepad.insert(id, entity);
writer.send(GamepadConnectionEvent {
gamepad: entity,
connection,
});
}
AppleGameControllerEvent::Disconnected { id } => {
if let Some(entity) = gamepad_index.gamepad.remove(&id) {
writer.send(GamepadConnectionEvent {
gamepad: entity,
connection: GamepadConnection::Disconnected,
});
}
}
}
}
}
unsafe fn button_value(pad: &Retained<GCExtendedGamepad>, button: &GamepadButton) -> f32 {
unsafe {
match button {
GamepadButton::South => pad.buttonB().value(),
GamepadButton::East => pad.buttonA().value(),
GamepadButton::North => pad.buttonX().value(),
GamepadButton::West => pad.buttonY().value(),
GamepadButton::C => 0.0,
GamepadButton::Z => 0.0,
GamepadButton::LeftTrigger => pad.leftTrigger().value(),
GamepadButton::LeftTrigger2 => pad.leftShoulder().value(),
GamepadButton::RightTrigger => pad.rightTrigger().value(),
GamepadButton::RightTrigger2 => pad.rightShoulder().value(),
GamepadButton::Select => pad.buttonOptions().map(|b| b.value()).unwrap_or(0.0),
GamepadButton::Start => pad.buttonMenu().value(),
GamepadButton::Mode => pad.buttonHome().map(|b| b.value()).unwrap_or(0.0),
GamepadButton::LeftThumb => pad.leftThumbstickButton().unwrap().value(),
GamepadButton::RightThumb => pad.rightThumbstickButton().unwrap().value(),
GamepadButton::DPadUp => pad.dpad().up().value(),
GamepadButton::DPadDown => pad.dpad().down().value(),
GamepadButton::DPadLeft => pad.dpad().left().value(),
GamepadButton::DPadRight => pad.dpad().right().value(),
GamepadButton::Other(other) => unimplemented!("Other button {other}"),
}
}
}
unsafe fn axis_value(pad: &Retained<GCExtendedGamepad>, axis: &GamepadAxis) -> f32 {
unsafe {
match axis {
GamepadAxis::LeftStickX => pad.leftThumbstick().xAxis().value(),
GamepadAxis::LeftStickY => pad.leftThumbstick().yAxis().value(),
GamepadAxis::LeftZ => pad.leftShoulder().value(),
GamepadAxis::RightStickX => pad.rightThumbstick().xAxis().value(),
GamepadAxis::RightStickY => pad.rightThumbstick().yAxis().value(),
GamepadAxis::RightZ => pad.rightShoulder().value(),
GamepadAxis::Other(other) => unimplemented!("Other axis {other}"),
}
}
}
pub fn poll_system(
gamepad_index: Res<GamepadIndex>,
query: Query<&Gamepad>,
mut events: EventWriter<RawGamepadEvent>,
) {
unsafe {
for controller in GCController::controllers() {
if let Some(_state) = controller.input().nextInputState() {
// Get the ID of this controller
let id = controller.playerIndex().0 as GamepadId;
let Some(pad) = controller.extendedGamepad() else {
return;
};
// Lookup the GamepadId in the index to get Entity of the Gamepad component
if let Some(gamepad_entity) = gamepad_index.gamepad.get(&id) {
match query.get(*gamepad_entity) {
Ok(gamepad) => {
// Iterate each axis, and get the current value from the bevy Gamepad Component
// which will be compared against the new GCControllerInputState.
//
// Emit gamepad events for any axis which differs
for axis in gamepad.get_analog_axes() {
if let Some(value) = gamepad.get(*axis) {
match axis {
GamepadInput::Axis(gamepad_axis) => {
let new_value =
Self::axis_value(&pad, gamepad_axis);
if new_value != value {
let axis_event = RawGamepadAxisChangedEvent {
gamepad: *gamepad_entity,
axis: *gamepad_axis,
value: new_value,
};
events.send(RawGamepadEvent::Axis(axis_event));
}
}
GamepadInput::Button(gamepad_button) => {
let new_value =
Self::button_value(&pad, gamepad_button);
if new_value != value {
let button_event =
RawGamepadButtonChangedEvent {
gamepad: *gamepad_entity,
button: *gamepad_button,
value: new_value,
};
events.send(RawGamepadEvent::Button(
button_event,
));
}
}
}
}
}
}
Err(_e) => {
// Events may arrive before the Gamepad component is spawned.
// For now, just silently ignore the events
//error!(id, "Failed to get Gamepad component. {e}");
}
}
} else {
error!(
id,
"Received GCControllerInputState but controller does not exist in GamepadIndex resource"
);
}
}
}
}
}
/// This must be called from the main thread or it will crash.
pub fn setup(app: &mut App) {
let (tx, rx) = unbounded();
app.insert_resource(ConnectionEventChannel { rx });
// Atomic to track the next controller ID. This will be assigned as the playerIndex of the Controller
// and incremented in the connection callbacks. IDs are not reused on disconnect/reconnect
// and may have an effect on controllers with LED displays that indicate the player number.
let next_controller_id = Arc::new(AtomicUsize::new(0));
unsafe {
#[cfg(debug_assertions)]
let value_changed = StackBlock::new(
|gamepad: NonNull<GCExtendedGamepad>,
event: NonNull<objc2_game_controller::GCControllerElement>| {
let gamepad = gamepad.as_ref();
let event = event.as_ref();
debug!("Value changed callback {:?} {:?}", gamepad, event);
},
);
#[cfg(debug_assertions)]
let input_changed = StackBlock::new(
|device: NonNull<
objc2::runtime::ProtocolObject<
dyn objc2_game_controller::GCDevicePhysicalInput,
>,
>,
element: NonNull<
objc2::runtime::ProtocolObject<
dyn objc2_game_controller::GCPhysicalInputElement,
>,
>| {
debug!(
"Input changed {:#?} {:#?}",
device.as_ref(),
element.as_ref()
);
},
);
let connect_tx = tx.clone();
let notification_center = NSNotificationCenter::defaultCenter();
notification_center.addObserverForName_object_queue_usingBlock(
Some(GCControllerDidConnectNotification),
None,
None,
&StackBlock::new(move |notification: NonNull<NSNotification>| {
let Some(object) = notification.as_ref().object() else {
return;
};
if let Some(controller) = object.downcast_ref::<GCController>() {
// On debug builds, register a callback to log element value changes
#[cfg(debug_assertions)]
{
// Regester a change handler block on GCControllerLiveInput
let input = controller.input();
input.setElementValueDidChangeHandler(Some(&input_changed));
controller
.extendedGamepad()
.unwrap()
.setValueChangedHandler(&*value_changed as *const _ as *mut _);
}
let class_name = controller.className().to_string();
let vendor_name = controller
.vendorName()
.map(|name| name.to_string())
.unwrap_or(String::from("Unknown Gamepad"));
let next_index =
next_controller_id.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
controller.setPlayerIndex(GCControllerPlayerIndex(next_index as isize));
let event = AppleGameControllerEvent::Connected {
id: controller.playerIndex().0 as GamepadId,
connection: GamepadConnection::Connected {
name: format!(
"{class_name} {vendor_name} {}",
controller.playerIndex().0 as GamepadId
),
vendor_id: None,
product_id: None,
},
};
if let Err(e) = connect_tx.send(event) {
error!("Failed to send to controller event channel: {e}");
}
}
}),
);
let disconnect_tx = tx.clone();
notification_center.addObserverForName_object_queue_usingBlock(
Some(GCControllerDidDisconnectNotification),
None,
None,
&StackBlock::new(move |notification: NonNull<NSNotification>| {
let Some(object) = notification.as_ref().object() else {
return;
};
if let Some(controller) = object.downcast_ref::<GCController>() {
let id = controller.playerIndex().0 as GamepadId;
if let Err(e) =
disconnect_tx.send(AppleGameControllerEvent::Disconnected { id })
{
error!("Failed to send to controller event channel: {e}");
}
}
}),
);
GCController::setShouldMonitorBackgroundEvents(true);
GCController::startWirelessControllerDiscoveryWithCompletionHandler(None);
}
}
}