macroquad_virtual_joystick/lib.rs
1//! A simple joystick for macroquad games
2//!
3//! The joystick can be updated by touches or mouse
4//!
5//! # Example
6//! ```
7//! use macroquad::prelude::*;
8//! use macroquad_virtual_joystick::Joystick;
9//!
10//! #[macroquad::main("Simple Joystick")]
11//! async fn main() {
12//! const SPEED: f32 = 2.5;
13//! let mut position = Vec2::new(screen_width() / 2.0, screen_height() / 4.);
14//! let mut joystick = Joystick::new(100.0, 200.0, 50.0);
15//! loop {
16//! clear_background(WHITE);
17//!
18//! let joystick_event = joystick.update();
19//! position += joystick_event.direction.to_local() * joystick_event.intensity * SPEED;
20//!
21//! draw_circle(position.x, position.y, 50., YELLOW);
22//!
23//! joystick.render();
24//! next_frame().await
25//! }
26//! }
27//! ```
28#![warn(missing_docs)]
29
30use macroquad::prelude::{
31 color_u8, draw_circle, is_mouse_button_down, mouse_position, touches, Color, MouseButton,
32 TouchPhase, Vec2,
33};
34
35static BACKGROUND_COLOR: Color = color_u8!(96, 128, 144, 128);
36static KNOB_COLOR: Color = color_u8!(96, 128, 144, 168);
37
38/// The joystick component
39///
40/// # Examples
41/// ```no_run
42/// use macroquad_virtual_joystick::Joystick;
43/// let center_x = 100.0;
44/// let center_y = 50.0;
45/// let size = 50.0;
46/// // create a new joystick
47/// let mut joystick = Joystick::new(center_x, center_y, size);
48/// // render the joystick and determine the action
49/// let joystick_action = joystick.update();
50/// ```
51pub struct Joystick {
52 center: Vec2,
53 size: f32,
54 background: JoystickElement,
55 knob: JoystickElement,
56 dragging: bool,
57 touch_id: u64,
58 event: JoystickEvent,
59}
60
61impl Joystick {
62 /// create a new joystick
63 ///
64 /// # Arguments
65 /// * `x`, `y`: center of the joystick
66 /// * `size`: diameter of the joystick
67 ///
68 /// # Examples
69 /// ```
70 /// use macroquad_virtual_joystick::Joystick;
71 /// let center_x = 100.0;
72 /// let center_y = 50.0;
73 /// let size = 50.0;
74 /// let joystick = Joystick::new(center_x, center_y, size);
75 /// ```
76 pub fn new(x: f32, y: f32, size: f32) -> Self {
77 let background_fn = Box::new(|center_x: f32, center_y: f32, radius: f32| {
78 draw_circle(center_x, center_y, radius, BACKGROUND_COLOR);
79 });
80 let background = JoystickElement::new(x, y, size / 2., background_fn);
81 let knob_fn = Box::new(|center_x: f32, center_y: f32, radius: f32| {
82 draw_circle(center_x, center_y, radius, KNOB_COLOR);
83 });
84 let knob = JoystickElement::new(x, y, size / 4., knob_fn);
85
86 Self {
87 center: Vec2::new(x, y),
88 size,
89 background,
90 knob,
91 dragging: false,
92 touch_id: 0,
93 event: JoystickEvent::default(),
94 }
95 }
96
97 /// create a new [`Joystick`] with custom elements for background and knob
98 ///
99 /// # Arguments
100 /// * `x`, `y`: center of the joystick
101 /// * `size`: diameter of the joystick, should have the same size as the background element
102 /// * `knob_size`: diameter of the knob, should have the same size as the background element
103 /// * `render_background`, `render_knob`: custom drawing functions with the following
104 /// arguments:
105 /// * `x` the x coordinate of the center of the component
106 /// * `y` the y coordinate of the center of the component
107 /// * `radius` the radius used for mouse/ touch collision
108 /// for good UX this should also be the size of the drawing
109 ///
110 /// # Examples
111 /// ```
112 /// use macroquad::prelude::*;
113 /// use macroquad_virtual_joystick::Joystick;
114 ///
115 /// fn render_background(x: f32, y: f32, radius: f32) {
116 /// draw_circle(x, y, radius, RED);
117 /// }
118 ///
119 /// fn render_knob(x: f32, y: f32, radius: f32) {
120 /// draw_circle(x, y, radius, GREEN);
121 /// }
122 ///
123 /// #[macroquad::main("Custom Joystick")]
124 /// async fn main() {
125 /// const SPEED: f32 = 2.5;
126 /// let mut position = Vec2::new(screen_width() / 2.0, screen_height() / 4.0);
127 ///
128 /// let background_size = 50.0;
129 /// let knob_size = 32.0;
130 ///
131 /// let mut joystick = Joystick::from_custom_elements(
132 /// 100.0,
133 /// 200.0,
134 /// background_size,
135 /// knob_size,
136 /// Box::new(render_background),
137 /// Box::new(render_knob),
138 /// );
139 /// loop {
140 /// clear_background(WHITE);
141 ///
142 /// let joystick_event = joystick.update();
143 /// position += joystick_event.direction.to_local() * joystick_event.intensity * SPEED;
144 ///
145 /// draw_circle(position.x, position.y, 50.0, YELLOW);
146 ///
147 /// joystick.render();
148 /// next_frame().await
149 /// }
150 /// }
151 /// ```
152 pub fn from_custom_elements(
153 x: f32,
154 y: f32,
155 size: f32,
156 knob_size: f32,
157 render_background: Box<fn(f32, f32, f32)>,
158 render_knob: Box<fn(f32, f32, f32)>,
159 ) -> Self {
160 let center = Vec2::new(x, y);
161 let background = JoystickElement::new(x, y, size / 2., render_background);
162 let knob = JoystickElement::new(x, y, knob_size / 2., render_knob);
163
164 Self {
165 center,
166 size,
167 background,
168 knob,
169 dragging: false,
170 touch_id: 0,
171 event: JoystickEvent::default(),
172 }
173 }
174
175 /// render the joystick
176 ///
177 /// renders the background and knob
178 ///
179 /// call [`macroquad::prelude::set_default_camera()`] before!
180 pub fn render(&self) {
181 self.background.render();
182 self.knob.render();
183 }
184
185 /// update the joystick from touch
186 fn update_touch(&mut self) {
187 for touch in touches() {
188 match touch.phase {
189 TouchPhase::Started => {
190 // a touch starts in the joystick
191 if (touch.position - self.center).length() < (self.size / 2.) {
192 self.dragging = true;
193 self.touch_id = touch.id;
194 self.moving(touch.position);
195 }
196 }
197 TouchPhase::Moved => {
198 if self.dragging && touch.id == self.touch_id {
199 self.moving(touch.position);
200 }
201 }
202 TouchPhase::Ended | TouchPhase::Cancelled => {
203 if self.dragging && touch.id == self.touch_id {
204 self.reset();
205 }
206 }
207 _ => {}
208 }
209 }
210 }
211
212 /// update the joystick from mouse drag
213 fn update_mouse(&mut self) {
214 let (mouse_x, mouse_y) = mouse_position();
215 let mouse = Vec2::new(mouse_x, mouse_y);
216 let mouse_down = is_mouse_button_down(MouseButton::Left);
217 if self.dragging {
218 if mouse_down {
219 self.moving(mouse)
220 } else {
221 self.reset();
222 }
223 } else if mouse_down && (self.center - mouse).length() < (self.size / 2.) {
224 self.dragging = true;
225 self.moving(mouse)
226 }
227 }
228
229 /// reset the joystick
230 fn reset(&mut self) {
231 self.dragging = false;
232 self.knob.x = self.center.x;
233 self.knob.y = self.center.y;
234 self.event = JoystickEvent::default();
235 }
236
237 /// update the joystick
238 ///
239 /// this updates the joystick and returns the current [`JoystickEvent`]
240 ///
241 /// # Examples
242 /// see [`Joystick`]
243 pub fn update(&mut self) -> JoystickEvent {
244 if touches().is_empty() {
245 self.update_mouse();
246 } else {
247 self.update_touch();
248 }
249 self.event
250 }
251
252 /// move the knob according to the drag position and update the [`self.event`]
253 fn moving(&mut self, position: Vec2) {
254 let radius = self.size / 2.;
255 let delta = position - self.center;
256 let angle = delta.y.atan2(delta.x);
257 let angle_degrees = angle.to_degrees();
258
259 // maximum distance for the knob is the radius of the background
260 let dist = f32::min(delta.length(), radius);
261
262 self.knob.x = self.center.x + dist * angle.cos();
263 self.knob.y = self.center.y + dist * angle.sin();
264
265 let intensity = dist / radius;
266 let direction = if intensity == 0. {
267 JoystickDirection::Idle
268 } else {
269 JoystickDirection::from_degrees(angle_degrees as f64)
270 };
271 self.event = JoystickEvent::new(direction, intensity, angle);
272 }
273}
274
275/// element of the [`Joystick`]
276///
277/// can be used for the background or the knob
278struct JoystickElement {
279 x: f32,
280 y: f32,
281 radius: f32,
282 drawable: Box<dyn Fn(f32, f32, f32)>,
283}
284
285impl JoystickElement {
286 fn new(x: f32, y: f32, radius: f32, drawable: Box<dyn Fn(f32, f32, f32)>) -> Self {
287 Self {
288 x,
289 y,
290 radius,
291 drawable,
292 }
293 }
294
295 /// render the element
296 pub fn render(&self) {
297 (self.drawable)(self.x, self.y, self.radius);
298 }
299}
300
301#[allow(missing_docs)]
302/// different directions of the [`Joystick`]
303#[derive(Clone, Copy, Debug, Eq, PartialEq)]
304pub enum JoystickDirection {
305 Up,
306 UpLeft,
307 Left,
308 DownLeft,
309 Down,
310 DownRight,
311 Right,
312 UpRight,
313 Idle,
314}
315
316impl JoystickDirection {
317 /// calculate a JoystickDirection from degrees
318 ///
319 /// 0 degrees are on the positive X-Axis and then it rotates clockwise
320 ///
321 /// # Examples
322 /// ```
323 /// use macroquad_virtual_joystick::JoystickDirection;
324 ///
325 /// let degrees = 153.5;
326 /// let direction = JoystickDirection::from_degrees(degrees);
327 ///
328 /// assert_eq!(direction, JoystickDirection::DownLeft);
329 /// ```
330 pub fn from_degrees(degrees: f64) -> Self {
331 if degrees > -22.5 && degrees <= 22.5 {
332 Self::Right
333 } else if degrees > 22.5 && degrees <= 67.5 {
334 Self::DownRight
335 } else if degrees > 67.5 && degrees <= 112.5 {
336 Self::Down
337 } else if degrees > 112.5 && degrees <= 157.5 {
338 Self::DownLeft
339 } else if degrees > 157.5 && degrees <= 180. {
340 Self::Left
341 } else if degrees > -157.5 && degrees <= -112.5 {
342 Self::UpLeft
343 } else if degrees > -112.5 && degrees <= -67.5 {
344 Self::Up
345 } else if degrees > -67.5 && degrees <= -22.5 {
346 Self::UpRight
347 } else {
348 Self::Idle
349 }
350 }
351
352 /// convert the direction to a Vec2 with x and y
353 ///
354 /// x and y are both one of these: [-1.0, 0.0, 1.0]
355 ///
356 /// # Examples
357 /// ```
358 /// use macroquad::prelude::Vec2;
359 /// use macroquad_virtual_joystick::JoystickDirection;
360 ///
361 /// let direction = JoystickDirection::Up;
362 /// assert_eq!(direction.to_local(), Vec2::new(0.0, -1.0))
363 /// ```
364 pub fn to_local(&self) -> Vec2 {
365 let (x, y) = match self {
366 Self::Right => (1., 0.),
367 Self::DownRight => (1., 1.),
368 Self::Down => (0., 1.),
369 Self::DownLeft => (-1., 1.),
370 Self::Left => (-1., 0.),
371 Self::UpLeft => (-1., -1.),
372 Self::Up => (0., -1.),
373 Self::UpRight => (1., -1.),
374 Self::Idle => (0., 0.),
375 };
376 Vec2::new(x, y)
377 }
378}
379
380/// the event of the [`Joystick`]
381///
382/// call [`Joystick::update`] to get the current event
383#[derive(Clone, Copy, Debug)]
384pub struct JoystickEvent {
385 /// the direction to which the knob was moved
386 pub direction: JoystickDirection,
387
388 /// the intensity of the knob move, from 0 (center) to 1 (edge)
389 pub intensity: f32,
390
391 /// the angle of the knob (in radians)
392 ///
393 /// starting on the positive x-axis and rotating counter-clockwise
394 pub angle: f32,
395}
396
397impl JoystickEvent {
398 fn new(direction: JoystickDirection, intensity: f32, angle: f32) -> Self {
399 Self {
400 direction,
401 intensity,
402 angle,
403 }
404 }
405}
406
407impl Default for JoystickEvent {
408 fn default() -> Self {
409 Self {
410 direction: JoystickDirection::Idle,
411 intensity: 0.,
412 angle: 0.,
413 }
414 }
415}