Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/tools/gamepad_viewer.rs
6595 views
1
//! Shows a visualization of gamepad buttons, sticks, and triggers
2
3
use std::f32::consts::PI;
4
5
use bevy::{
6
input::gamepad::{GamepadAxisChangedEvent, GamepadButtonChangedEvent, GamepadConnectionEvent},
7
prelude::*,
8
sprite::Anchor,
9
};
10
11
const BUTTON_RADIUS: f32 = 25.;
12
const BUTTON_CLUSTER_RADIUS: f32 = 50.;
13
const START_SIZE: Vec2 = Vec2::new(30., 15.);
14
const TRIGGER_SIZE: Vec2 = Vec2::new(70., 20.);
15
const STICK_BOUNDS_SIZE: f32 = 100.;
16
17
const BUTTONS_X: f32 = 150.;
18
const BUTTONS_Y: f32 = 80.;
19
const STICKS_X: f32 = 150.;
20
const STICKS_Y: f32 = -135.;
21
22
const NORMAL_BUTTON_COLOR: Color = Color::srgb(0.3, 0.3, 0.3);
23
const ACTIVE_BUTTON_COLOR: Color = Color::srgb(0.5, 0., 0.5);
24
const LIVE_COLOR: Color = Color::srgb(0.4, 0.4, 0.4);
25
const DEAD_COLOR: Color = Color::srgb(0.13, 0.13, 0.13);
26
27
#[derive(Component, Deref)]
28
struct ReactTo(GamepadButton);
29
#[derive(Component)]
30
struct MoveWithAxes {
31
x_axis: GamepadAxis,
32
y_axis: GamepadAxis,
33
scale: f32,
34
}
35
#[derive(Component)]
36
struct TextWithAxes {
37
x_axis: GamepadAxis,
38
y_axis: GamepadAxis,
39
}
40
#[derive(Component, Deref)]
41
struct TextWithButtonValue(GamepadButton);
42
43
#[derive(Component)]
44
struct ConnectedGamepadsText;
45
46
#[derive(Resource)]
47
struct ButtonMaterials {
48
normal: MeshMaterial2d<ColorMaterial>,
49
active: MeshMaterial2d<ColorMaterial>,
50
}
51
52
impl FromWorld for ButtonMaterials {
53
fn from_world(world: &mut World) -> Self {
54
Self {
55
normal: world.add_asset(NORMAL_BUTTON_COLOR).into(),
56
active: world.add_asset(ACTIVE_BUTTON_COLOR).into(),
57
}
58
}
59
}
60
#[derive(Resource)]
61
struct ButtonMeshes {
62
circle: Mesh2d,
63
triangle: Mesh2d,
64
start_pause: Mesh2d,
65
trigger: Mesh2d,
66
}
67
68
impl FromWorld for ButtonMeshes {
69
fn from_world(world: &mut World) -> Self {
70
Self {
71
circle: world.add_asset(Circle::new(BUTTON_RADIUS)).into(),
72
triangle: world
73
.add_asset(RegularPolygon::new(BUTTON_RADIUS, 3))
74
.into(),
75
start_pause: world.add_asset(Rectangle::from_size(START_SIZE)).into(),
76
trigger: world.add_asset(Rectangle::from_size(TRIGGER_SIZE)).into(),
77
}
78
}
79
}
80
81
#[derive(Bundle)]
82
struct GamepadButtonBundle {
83
mesh: Mesh2d,
84
material: MeshMaterial2d<ColorMaterial>,
85
transform: Transform,
86
react_to: ReactTo,
87
}
88
89
impl GamepadButtonBundle {
90
pub fn new(
91
button_type: GamepadButton,
92
mesh: Mesh2d,
93
material: MeshMaterial2d<ColorMaterial>,
94
x: f32,
95
y: f32,
96
) -> Self {
97
Self {
98
mesh,
99
material,
100
transform: Transform::from_xyz(x, y, 0.),
101
react_to: ReactTo(button_type),
102
}
103
}
104
105
pub fn with_rotation(mut self, angle: f32) -> Self {
106
self.transform.rotation = Quat::from_rotation_z(angle);
107
self
108
}
109
}
110
111
fn main() {
112
App::new()
113
.add_plugins(DefaultPlugins)
114
.init_resource::<ButtonMaterials>()
115
.init_resource::<ButtonMeshes>()
116
.add_systems(
117
Startup,
118
(setup, setup_sticks, setup_triggers, setup_connected),
119
)
120
.add_systems(
121
Update,
122
(
123
update_buttons,
124
update_button_values,
125
update_axes,
126
update_connected,
127
),
128
)
129
.run();
130
}
131
132
fn setup(mut commands: Commands, meshes: Res<ButtonMeshes>, materials: Res<ButtonMaterials>) {
133
commands.spawn(Camera2d);
134
135
// Buttons
136
137
commands.spawn((
138
Transform::from_xyz(BUTTONS_X, BUTTONS_Y, 0.),
139
Visibility::default(),
140
children![
141
GamepadButtonBundle::new(
142
GamepadButton::North,
143
meshes.circle.clone(),
144
materials.normal.clone(),
145
0.,
146
BUTTON_CLUSTER_RADIUS,
147
),
148
GamepadButtonBundle::new(
149
GamepadButton::South,
150
meshes.circle.clone(),
151
materials.normal.clone(),
152
0.,
153
-BUTTON_CLUSTER_RADIUS,
154
),
155
GamepadButtonBundle::new(
156
GamepadButton::West,
157
meshes.circle.clone(),
158
materials.normal.clone(),
159
-BUTTON_CLUSTER_RADIUS,
160
0.,
161
),
162
GamepadButtonBundle::new(
163
GamepadButton::East,
164
meshes.circle.clone(),
165
materials.normal.clone(),
166
BUTTON_CLUSTER_RADIUS,
167
0.,
168
),
169
],
170
));
171
172
// Start and Pause
173
174
commands.spawn(GamepadButtonBundle::new(
175
GamepadButton::Select,
176
meshes.start_pause.clone(),
177
materials.normal.clone(),
178
-30.,
179
BUTTONS_Y,
180
));
181
182
commands.spawn(GamepadButtonBundle::new(
183
GamepadButton::Start,
184
meshes.start_pause.clone(),
185
materials.normal.clone(),
186
30.,
187
BUTTONS_Y,
188
));
189
190
// D-Pad
191
192
commands.spawn((
193
Transform::from_xyz(-BUTTONS_X, BUTTONS_Y, 0.),
194
Visibility::default(),
195
children![
196
GamepadButtonBundle::new(
197
GamepadButton::DPadUp,
198
meshes.triangle.clone(),
199
materials.normal.clone(),
200
0.,
201
BUTTON_CLUSTER_RADIUS,
202
),
203
GamepadButtonBundle::new(
204
GamepadButton::DPadDown,
205
meshes.triangle.clone(),
206
materials.normal.clone(),
207
0.,
208
-BUTTON_CLUSTER_RADIUS,
209
)
210
.with_rotation(PI),
211
GamepadButtonBundle::new(
212
GamepadButton::DPadLeft,
213
meshes.triangle.clone(),
214
materials.normal.clone(),
215
-BUTTON_CLUSTER_RADIUS,
216
0.,
217
)
218
.with_rotation(PI / 2.),
219
GamepadButtonBundle::new(
220
GamepadButton::DPadRight,
221
meshes.triangle.clone(),
222
materials.normal.clone(),
223
BUTTON_CLUSTER_RADIUS,
224
0.,
225
)
226
.with_rotation(-PI / 2.),
227
],
228
));
229
230
// Triggers
231
232
commands.spawn(GamepadButtonBundle::new(
233
GamepadButton::LeftTrigger,
234
meshes.trigger.clone(),
235
materials.normal.clone(),
236
-BUTTONS_X,
237
BUTTONS_Y + 115.,
238
));
239
240
commands.spawn(GamepadButtonBundle::new(
241
GamepadButton::RightTrigger,
242
meshes.trigger.clone(),
243
materials.normal.clone(),
244
BUTTONS_X,
245
BUTTONS_Y + 115.,
246
));
247
}
248
249
fn setup_sticks(
250
mut commands: Commands,
251
meshes: Res<ButtonMeshes>,
252
materials: Res<ButtonMaterials>,
253
) {
254
// NOTE: This stops making sense because in entities because there isn't a "global" default,
255
// instead each gamepad has its own default setting
256
let gamepad_settings = GamepadSettings::default();
257
let dead_upper =
258
STICK_BOUNDS_SIZE * gamepad_settings.default_axis_settings.deadzone_upperbound();
259
let dead_lower =
260
STICK_BOUNDS_SIZE * gamepad_settings.default_axis_settings.deadzone_lowerbound();
261
let dead_size = dead_lower.abs() + dead_upper.abs();
262
let dead_mid = (dead_lower + dead_upper) / 2.0;
263
264
let live_upper =
265
STICK_BOUNDS_SIZE * gamepad_settings.default_axis_settings.livezone_upperbound();
266
let live_lower =
267
STICK_BOUNDS_SIZE * gamepad_settings.default_axis_settings.livezone_lowerbound();
268
let live_size = live_lower.abs() + live_upper.abs();
269
let live_mid = (live_lower + live_upper) / 2.0;
270
271
let mut spawn_stick = |x_pos, y_pos, x_axis, y_axis, button| {
272
let style = TextFont {
273
font_size: 13.,
274
..default()
275
};
276
commands.spawn((
277
Transform::from_xyz(x_pos, y_pos, 0.),
278
Visibility::default(),
279
children![
280
Sprite::from_color(DEAD_COLOR, Vec2::splat(STICK_BOUNDS_SIZE * 2.),),
281
(
282
Sprite::from_color(LIVE_COLOR, Vec2::splat(live_size)),
283
Transform::from_xyz(live_mid, live_mid, 2.),
284
),
285
(
286
Sprite::from_color(DEAD_COLOR, Vec2::splat(dead_size)),
287
Transform::from_xyz(dead_mid, dead_mid, 3.),
288
),
289
(
290
Text2d::default(),
291
Transform::from_xyz(0., STICK_BOUNDS_SIZE + 2., 4.),
292
Anchor::BOTTOM_CENTER,
293
TextWithAxes { x_axis, y_axis },
294
children![
295
(TextSpan(format!("{:.3}", 0.)), style.clone()),
296
(TextSpan::new(", "), style.clone()),
297
(TextSpan(format!("{:.3}", 0.)), style),
298
]
299
),
300
(
301
meshes.circle.clone(),
302
materials.normal.clone(),
303
Transform::from_xyz(0., 0., 5.).with_scale(Vec2::splat(0.15).extend(1.)),
304
MoveWithAxes {
305
x_axis,
306
y_axis,
307
scale: STICK_BOUNDS_SIZE,
308
},
309
ReactTo(button),
310
),
311
],
312
));
313
};
314
315
spawn_stick(
316
-STICKS_X,
317
STICKS_Y,
318
GamepadAxis::LeftStickX,
319
GamepadAxis::LeftStickY,
320
GamepadButton::LeftThumb,
321
);
322
spawn_stick(
323
STICKS_X,
324
STICKS_Y,
325
GamepadAxis::RightStickX,
326
GamepadAxis::RightStickY,
327
GamepadButton::RightThumb,
328
);
329
}
330
331
fn setup_triggers(
332
mut commands: Commands,
333
meshes: Res<ButtonMeshes>,
334
materials: Res<ButtonMaterials>,
335
) {
336
let mut spawn_trigger = |x, y, button_type| {
337
commands.spawn((
338
GamepadButtonBundle::new(
339
button_type,
340
meshes.trigger.clone(),
341
materials.normal.clone(),
342
x,
343
y,
344
),
345
children![(
346
Transform::from_xyz(0., 0., 1.),
347
Text(format!("{:.3}", 0.)),
348
TextFont {
349
font_size: 13.,
350
..default()
351
},
352
TextWithButtonValue(button_type),
353
)],
354
));
355
};
356
357
spawn_trigger(-BUTTONS_X, BUTTONS_Y + 145., GamepadButton::LeftTrigger2);
358
spawn_trigger(BUTTONS_X, BUTTONS_Y + 145., GamepadButton::RightTrigger2);
359
}
360
361
fn setup_connected(mut commands: Commands) {
362
// This is UI text, unlike other text in this example which is 2d.
363
commands.spawn((
364
Text::new("Connected Gamepads:\n"),
365
Node {
366
position_type: PositionType::Absolute,
367
top: px(12),
368
left: px(12),
369
..default()
370
},
371
ConnectedGamepadsText,
372
children![TextSpan::new("None")],
373
));
374
}
375
376
fn update_buttons(
377
gamepads: Query<&Gamepad>,
378
materials: Res<ButtonMaterials>,
379
mut query: Query<(&mut MeshMaterial2d<ColorMaterial>, &ReactTo)>,
380
) {
381
for gamepad in &gamepads {
382
for (mut handle, react_to) in query.iter_mut() {
383
if gamepad.just_pressed(**react_to) {
384
*handle = materials.active.clone();
385
}
386
if gamepad.just_released(**react_to) {
387
*handle = materials.normal.clone();
388
}
389
}
390
}
391
}
392
fn update_button_values(
393
mut events: EventReader<GamepadButtonChangedEvent>,
394
mut query: Query<(&mut Text2d, &TextWithButtonValue)>,
395
) {
396
for button_event in events.read() {
397
for (mut text, text_with_button_value) in query.iter_mut() {
398
if button_event.button == **text_with_button_value {
399
**text = format!("{:.3}", button_event.value);
400
}
401
}
402
}
403
}
404
405
fn update_axes(
406
mut axis_events: EventReader<GamepadAxisChangedEvent>,
407
mut query: Query<(&mut Transform, &MoveWithAxes)>,
408
text_query: Query<(Entity, &TextWithAxes)>,
409
mut writer: Text2dWriter,
410
) {
411
for axis_event in axis_events.read() {
412
let axis_type = axis_event.axis;
413
let value = axis_event.value;
414
for (mut transform, move_with) in query.iter_mut() {
415
if axis_type == move_with.x_axis {
416
transform.translation.x = value * move_with.scale;
417
}
418
if axis_type == move_with.y_axis {
419
transform.translation.y = value * move_with.scale;
420
}
421
}
422
for (text, text_with_axes) in text_query.iter() {
423
if axis_type == text_with_axes.x_axis {
424
*writer.text(text, 1) = format!("{value:.3}");
425
}
426
if axis_type == text_with_axes.y_axis {
427
*writer.text(text, 3) = format!("{value:.3}");
428
}
429
}
430
}
431
}
432
433
fn update_connected(
434
mut connected: EventReader<GamepadConnectionEvent>,
435
gamepads: Query<(Entity, &Name), With<Gamepad>>,
436
text: Single<Entity, With<ConnectedGamepadsText>>,
437
mut writer: TextUiWriter,
438
) {
439
if connected.is_empty() {
440
return;
441
}
442
connected.clear();
443
444
let formatted = gamepads
445
.iter()
446
.map(|(entity, name)| format!("{entity} - {name}"))
447
.collect::<Vec<_>>()
448
.join("\n");
449
450
*writer.text(*text, 1) = if !formatted.is_empty() {
451
formatted
452
} else {
453
"None".to_string()
454
}
455
}
456
457