Skip to main content

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}