cat_box/
lib.rs

1//! Work in progress game engine, inspired by [arcade](https://arcade.academy/).
2//!
3//! ```no_run
4//! use cat_box::{draw_text, Game, Sprite, SpriteCollection, get_mouse_state, get_keyboard_state};
5//! use sdl2::keyboard::Scancode;
6//!
7//! fn main() {
8//!    let game = Game::new("catbox demo", 1000, 800);
9//!
10//!     let mut i = 0u8;
11//!     let mut s = Sprite::new("duck.png", 500, 400).unwrap();
12//!     let mut s2 = Sprite::new("duck.png", 400, 500).unwrap();
13//!
14//!     let mut coll = SpriteCollection::new();
15//!     for n in 0..10 {
16//!         for o in 0..8 {
17//!             let x = Sprite::new("duck.png", n * 100, o * 100).unwrap();
18//!             coll.push(x);
19//!         }
20//!     }
21//!     game.run(|ctx| {
22//!         i = (i + 1) % 255;
23//!         ctx.set_background_colour(i as u8, 64, 255);
24//!
25//!         draw_text(
26//!             ctx,
27//!             format!("i is {}", i),
28//!             "MesloLGS NF Regular.ttf",
29//!             72,
30//!             (300, 300),
31//!             cat_box::TextMode::Shaded {
32//!                 foreground: (255, 255, 255),
33//!                 background: (0, 0, 0),
34//!             },
35//!         )
36//!         .unwrap();
37//!
38//!         let (start_x, start_y) = s.position().into();
39//!         let m = get_mouse_state(ctx);
40//!         let x_diff = m.x - start_x;
41//!         let y_diff = m.y - start_y;
42//!
43//!         let angle = (y_diff as f64).atan2(x_diff as f64);
44//!         s.set_angle(angle.to_degrees());
45//!
46//!         for spr in coll.iter() {
47//!             let (start_x, start_y) = spr.position().into();
48//!             let m = get_mouse_state(ctx);
49//!             let x_diff = m.x - start_x;
50//!             let y_diff = m.y - start_y;
51//!
52//!             let angle = (y_diff as f64).atan2(x_diff as f64);
53//!             spr.set_angle(angle.to_degrees());
54//!         }
55//!
56//!         let keys = get_keyboard_state(ctx).keys;
57//!
58//!         for key in keys {
59//!             let offset = match key {
60//!                 Scancode::Escape => {
61//!                     game.terminate();
62//!                     (0, 0)
63//!                 },
64//!                 Scancode::W | Scancode::Up => (0, 5),
65//!                 Scancode::S | Scancode::Down => (0, -5),
66//!                 Scancode::A | Scancode::Left => (-5, 0),
67//!                 Scancode::D | Scancode::Right => (5, 0),
68//!                 _ => (0, 0),
69//!             };
70//!
71//!             s.translate(offset);
72//!
73//!             for spr in coll.iter() {
74//!                 spr.translate(offset);
75//!             }
76//!         }
77//!
78//!         s2.draw(ctx).unwrap();
79//!         s.draw(ctx).unwrap();
80//!         coll.draw(ctx).unwrap();
81//!     })
82//!     .unwrap();
83//! }
84//! ```
85
86#![warn(clippy::pedantic)]
87#![allow(
88    clippy::similar_names,
89    clippy::needless_doctest_main,
90    clippy::module_name_repetitions,
91    clippy::missing_errors_doc
92)]
93
94pub mod physics;
95pub mod vec2;
96
97use std::{
98    cell::Cell,
99    ops::{Deref, DerefMut},
100    path::Path,
101    slice::IterMut,
102};
103
104use sdl2::{
105    image::ImageRWops,
106    mouse::MouseButton,
107    rect::Rect,
108    render::{Canvas, TextureCreator, TextureValueError},
109    rwops::RWops,
110    surface::Surface,
111    ttf::{FontError, InitError, Sdl2TtfContext},
112    video::{Window, WindowBuildError, WindowContext},
113    EventPump, IntegerOrSdlError,
114};
115
116use vec2::Vec2Int;
117
118#[doc(no_inline)]
119pub use sdl2::{self, event::Event, keyboard::Scancode, pixels::Color};
120
121/// Utility macro for cloning things into closures.
122///
123/// Temporary workaround for [Rust RFC 2407](https://github.com/rust-lang/rfcs/issues/2407)
124#[macro_export]
125macro_rules! cloned {
126    ($thing:ident => $e:expr) => {
127        let $thing = $thing.clone();
128        $e
129    };
130    ($($thing:ident),* => $e:expr) => {
131        $( let $thing = $thing.clone(); )*
132        $e
133    }
134}
135
136macro_rules! error_from_format {
137    ($($t:ty),+) => {
138        $(
139        impl From<$t> for CatboxError {
140            fn from(e: $t) -> Self {
141                CatboxError(format!("{}", e))
142            }
143        }
144        )+
145    };
146}
147
148#[derive(Clone, Debug)]
149pub struct CatboxError(String);
150
151impl From<String> for CatboxError {
152    fn from(e: String) -> Self {
153        CatboxError(e)
154    }
155}
156
157error_from_format! {
158    WindowBuildError,
159    IntegerOrSdlError,
160    TextureValueError,
161    FontError,
162    InitError
163}
164
165impl std::fmt::Display for CatboxError {
166    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167        self.0.fmt(f)
168    }
169}
170
171pub type Result<T> = std::result::Result<T, CatboxError>;
172
173/// Wrapper type around SDL's [`EventPump`](sdl2::EventPump). See those docs for more info.
174pub struct Events {
175    pump: EventPump,
176}
177
178impl AsRef<EventPump> for Events {
179    fn as_ref(&self) -> &EventPump {
180        &self.pump
181    }
182}
183
184impl AsMut<EventPump> for Events {
185    fn as_mut(&mut self) -> &mut EventPump {
186        &mut self.pump
187    }
188}
189
190impl Iterator for Events {
191    type Item = Event;
192
193    fn next(&mut self) -> Option<Event> {
194        self.pump.poll_event()
195    }
196}
197
198/// Representation of a sprite.
199pub struct Sprite {
200    pub rect: Rect,
201    surf: Surface<'static>,
202    angle: f64,
203}
204
205impl Sprite {
206    /// Create a new Sprite. The `path` is relative to the current directory while running.
207    ///
208    /// Don't forget to call [`draw()`](Self::draw()) after this.
209    /// ```
210    /// # use cat_box::*;
211    /// let s = Sprite::new("duck.png", 500, 400).unwrap();
212    /// ```
213    pub fn new<P: AsRef<Path>>(path: P, x: i32, y: i32) -> Result<Self> {
214        let ops = RWops::from_file(path, "r")?;
215        let surf = ops.load()?;
216
217        let srect = surf.rect();
218        let dest_rect: Rect = Rect::from_center((x, y), srect.width(), srect.height());
219
220        Ok(Self {
221            rect: dest_rect,
222            surf,
223            angle: 0.0,
224        })
225    }
226
227    /// Create a new sprite using a slice of bytes, like what is returned from `include_bytes!`
228    ///
229    /// Don't forget to call [`draw()`](Self::draw()) after this.
230    /// ```
231    /// # use cat_box::*;
232    /// let bytes = include_bytes!("../duck.png");
233    /// let s = Sprite::from_bytes(bytes, 500, 400).unwrap();
234    /// ```
235    pub fn from_bytes<B: AsRef<[u8]>>(bytes: B, x: i32, y: i32) -> Result<Self> {
236        let ops = RWops::from_bytes(bytes.as_ref())?;
237        let surf = ops.load()?;
238
239        let srect = surf.rect();
240        let dest_rect: Rect = Rect::from_center((x, y), srect.width(), srect.height());
241
242        Ok(Self {
243            rect: dest_rect,
244            surf,
245            angle: 0.0,
246        })
247    }
248
249    /// Draws the sprite to the window. This should only be called inside your main event loop.
250    ///
251    /// ```no_run
252    /// # use cat_box::*;
253    /// # let mut s = Sprite::new("duck.png", 500, 400).unwrap();
254    /// # let game = Game::new("sprite demo", 1000, 1000);
255    /// # game.run(|ctx| {
256    /// s.draw(ctx);
257    /// # });
258    /// ```
259    pub fn draw(&mut self, ctx: &mut Context) -> Result<()> {
260        let (creator, canvas, _) = ctx.inner();
261        let text = creator.create_texture_from_surface(&self.surf)?;
262
263        canvas.copy_ex(&text, None, self.rect, self.angle, None, false, false)?;
264
265        Ok(())
266    }
267
268    /// Translate the sprite, in the form of (delta x, delta y)
269    ///
270    /// ```
271    /// # use cat_box::*;
272    /// # let mut s = Sprite::new("duck.png", 500, 400).unwrap();
273    /// s.translate((5, 10));
274    /// ```
275    pub fn translate<I: Into<Vec2Int>>(&mut self, position: I) {
276        let position = position.into();
277        let new_x = self.rect.x() + position.x;
278        let new_y = self.rect.y() - position.y;
279
280        self.rect.set_x(new_x);
281        self.rect.set_y(new_y);
282    }
283
284    /// Reposition the center of the sprite in the form of (x, y)
285    ///
286    /// ```
287    /// # use cat_box::*;
288    /// # let mut s = Sprite::new("duck.png", 500, 400).unwrap();
289    /// s.set_position((5, 10));
290    /// ```
291    pub fn set_position<I: Into<Vec2Int>>(&mut self, position: I) {
292        let position = position.into();
293        self.rect.center_on((position.x, position.y));
294    }
295
296    /// Set the angle of the sprite, in degrees of clockwise rotation.
297    ///
298    /// ```
299    /// # use cat_box::*;
300    /// # let mut s = Sprite::new("duck.png", 500, 400).unwrap();
301    /// s.set_angle(45.0);
302    /// ```
303    pub fn set_angle(&mut self, angle: f64) {
304        self.angle = angle;
305    }
306
307    /// Get the angle of the sprite, in degrees of clockwise rotation.
308    ///
309    /// ```
310    /// # use cat_box::*;
311    /// # let s = Sprite::new("duck.png", 500, 400).unwrap();
312    /// let angle = s.angle();
313    /// ```
314    #[must_use]
315    pub fn angle(&self) -> f64 {
316        self.angle
317    }
318
319    /// Get the x and y coordinates of the center of the sprite, in the form of (x, y).
320    ///
321    /// ```
322    /// # use cat_box::*;
323    /// # let s = Sprite::new("duck.png", 500, 400).unwrap();
324    /// let (x, y) = s.position().into();
325    /// ```
326    #[must_use]
327    pub fn position(&self) -> Vec2Int {
328        self.rect.center().into()
329    }
330}
331
332/// Manages a collection of [`Sprite`]s.
333///
334/// Technically, this is a thin wrapper around a simple [`Vec`] of sprites,
335/// although with some convenience methods.
336#[derive(Default)]
337pub struct SpriteCollection {
338    v: Vec<Sprite>,
339}
340
341impl SpriteCollection {
342    /// Creates a new [`SpriteCollection`].
343    ///
344    /// See [`Vec::new()`] for more information.
345    /// ```
346    /// # use cat_box::*;
347    /// let sprites = SpriteCollection::new();
348    /// ```
349    #[must_use]
350    pub fn new() -> Self {
351        Self { v: Vec::new() }
352    }
353
354    /// Creates a new [`SpriteCollection`] with the specified capacity.
355    ///
356    /// The collection will be able to hold exactly `capacity` items without reallocating.
357    /// ```
358    /// # use cat_box::*;
359    /// let sprites = SpriteCollection::with_capacity(10);
360    /// ```
361    #[must_use]
362    pub fn with_capacity(cap: usize) -> Self {
363        Self {
364            v: Vec::with_capacity(cap),
365        }
366    }
367
368    /// Draw all the sprites in this collection to the window.
369    /// This should only be called inside the main event loop.
370    /// ```no_run
371    /// # use cat_box::*;
372    /// # let mut sprites = SpriteCollection::new();
373    /// # let mut game = Game::new("asjdfhalksjdf", 1, 1);
374    /// # game.run(|ctx| {
375    /// sprites.draw(ctx);
376    /// # });
377    /// ```
378    pub fn draw(&mut self, ctx: &mut Context) -> Result<()> {
379        for s in &mut self.v {
380            s.draw(ctx)?;
381        }
382
383        Ok(())
384    }
385
386    /// Add a new [`Sprite`] to the end of this collection.
387    /// ```
388    /// # use cat_box::*;
389    /// let mut sprites = SpriteCollection::new();
390    /// let s = Sprite::new("duck.png", 500, 400).unwrap();
391    /// sprites.push(s);
392    /// ```
393    pub fn push(&mut self, s: Sprite) {
394        self.v.push(s);
395    }
396
397    /// Inserts an element at position `index` within the collection.
398    /// Shifts all elements after it to the right.
399    /// ```
400    /// # use cat_box::*;
401    /// let mut sprites = SpriteCollection::new();
402    /// let s = Sprite::new("duck.png", 500, 400).unwrap();
403    /// sprites.insert(s, 0);
404    /// ```
405    pub fn insert(&mut self, s: Sprite, index: usize) {
406        self.v.insert(index, s);
407    }
408
409    /// Removes and returns the last element, or `None` if the collection is empty.
410    /// ```
411    /// # use cat_box::*;
412    /// let mut sprites = SpriteCollection::new();
413    /// let s = sprites.pop();
414    /// ```
415    pub fn pop(&mut self) -> Option<Sprite> {
416        self.v.pop()
417    }
418
419    /// Removes and returns the element at `index`.
420    /// Shifts all elements after it to the left.
421    /// This method will panic if the index is out of bounds.
422    /// ```
423    /// # use cat_box::*;
424    /// let mut sprites = SpriteCollection::new();
425    /// # let s = Sprite::new("duck.png", 500, 400).unwrap();
426    /// # sprites.push(s);
427    /// sprites.remove(0);
428    /// ```
429    pub fn remove(&mut self, index: usize) -> Sprite {
430        self.v.remove(index)
431    }
432
433    /// Return an iterator over the sprites in this collection.
434    /// Use this to modify the sprites themselves, for example to set their position or angle.
435    pub fn iter(&mut self) -> IterMut<'_, Sprite> {
436        self.v.iter_mut()
437    }
438
439    /// Clears the collection, without touching the allocated capacity.
440    /// ```
441    /// # use cat_box::*;
442    /// let mut sprites = SpriteCollection::new();
443    /// # let s = Sprite::new("duck.png", 500, 400).unwrap();
444    /// # sprites.push(s);
445    /// sprites.clear();
446    /// ```
447    pub fn clear(&mut self) {
448        self.v.clear();
449    }
450
451    /// Move all the elements of `other` into `Self`.
452    /// ```
453    /// # use cat_box::*;
454    /// let mut sprites = SpriteCollection::new();
455    /// let mut sprites2 = SpriteCollection::new();
456    /// # let s = Sprite::new("duck.png", 500, 400).unwrap();
457    /// # let s2 = Sprite::new("duck.png", 400, 500).unwrap();
458    /// # sprites.push(s);
459    /// # sprites2.push(s2);
460    /// sprites.concat(sprites2);
461    /// ```
462    pub fn concat(&mut self, mut other: SpriteCollection) {
463        self.v.append(&mut *other);
464    }
465
466    /// Returns the length of this vector.
467    #[must_use]
468    pub fn len(&self) -> usize {
469        self.v.len()
470    }
471
472    /// Get a reference to the element at `index`, or `None` if it doesn't exist.
473    /// ```
474    /// # use cat_box::*;
475    /// let mut sprites = SpriteCollection::new();
476    /// # let s = Sprite::new("duck.png", 500, 400).unwrap();
477    /// # sprites.push(s);
478    /// let s = sprites.get(0);
479    /// ```
480    #[must_use]
481    pub fn get(&self, index: usize) -> Option<&Sprite> {
482        self.v.get(index)
483    }
484
485    /// Return the inner Vec. Only use this method if you know what you're doing.
486    #[must_use]
487    pub fn inner(&self) -> &Vec<Sprite> {
488        &self.v
489    }
490
491    #[must_use]
492    pub fn is_empty(&self) -> bool {
493        self.v.is_empty()
494    }
495}
496
497impl Deref for SpriteCollection {
498    type Target = Vec<Sprite>;
499
500    fn deref(&self) -> &Self::Target {
501        &self.v
502    }
503}
504
505impl DerefMut for SpriteCollection {
506    fn deref_mut(&mut self) -> &mut Self::Target {
507        &mut self.v
508    }
509}
510
511/// Game context.
512///
513/// In most cases, this should never actually be used; instead, just pass it around to the various cat-box functions such as [`Sprite::draw()`].
514pub struct Context {
515    canvas: Canvas<Window>,
516    event_pump: EventPump,
517    texture_creator: TextureCreator<WindowContext>,
518    ttf_subsystem: Sdl2TtfContext,
519}
520
521impl Context {
522    fn new(canvas: Canvas<Window>, pump: EventPump, ttf_subsystem: Sdl2TtfContext) -> Self {
523        let creator = canvas.texture_creator();
524        Self {
525            canvas,
526            event_pump: pump,
527            texture_creator: creator,
528            ttf_subsystem,
529        }
530    }
531
532    /// Get the inner [`Canvas`](sdl2::render::Canvas) and [`TextureCreator`](sdl2::render::TextureCreator).
533    ///
534    /// Only use this method if you know what you're doing.
535    pub fn inner(
536        &mut self,
537    ) -> (
538        &TextureCreator<WindowContext>,
539        &mut Canvas<Window>,
540        &mut EventPump,
541    ) {
542        (
543            &self.texture_creator,
544            &mut self.canvas,
545            &mut self.event_pump,
546        )
547    }
548
549    fn update(&mut self) {
550        self.canvas.present();
551    }
552
553    fn clear(&mut self) {
554        self.canvas.clear();
555    }
556
557    fn check_for_quit(&mut self) -> bool {
558        let (_, _, pump) = self.inner();
559
560        for event in pump.poll_iter() {
561            if let Event::Quit { .. } = event {
562                return true;
563            }
564        }
565
566        false
567    }
568
569    /// Set the background colour. See [`Canvas::set_draw_color()`](sdl2::render::Canvas::set_draw_color()) for more info.
570    pub fn set_background_colour(&mut self, r: u8, g: u8, b: u8) {
571        self.canvas.set_draw_color(Color::RGB(r, g, b));
572    }
573}
574
575/// Set the mode for drawing text.
576#[derive(Clone, Copy, Debug)]
577pub enum TextMode {
578    /// Render the text transparently.
579    Transparent { colour: (u8, u8, u8) },
580    /// Render the text with a foreground and a background colour.
581    ///
582    /// This creates a box around the text.
583    Shaded {
584        foreground: (u8, u8, u8),
585        background: (u8, u8, u8),
586    },
587}
588
589/// Draw text to the screen.
590///
591/// This loads a font from the current directory, case sensitive.
592///
593/// `pos` refers to the *center* of the rendered text.
594///
595/// Refer to [`TextMode`] for information about colouring.
596///
597/// ``` no_run
598/// # use cat_box::*;
599/// # let game = Game::new("", 100, 100);
600/// # game.run(|ctx| {
601/// let mode = TextMode::Shaded {
602///     foreground: (255, 255, 255),
603///     background: (0, 0, 0)
604/// };
605/// draw_text(ctx, "text to draw", "arial.ttf", 72, (300, 300), mode);
606/// # });
607pub fn draw_text<S: AsRef<str>, I: Into<Vec2Int>>(
608    ctx: &mut Context,
609    text: S,
610    font: &str,
611    size: u16,
612    pos: I,
613    mode: TextMode,
614) -> Result<()> {
615    let font = ctx.ttf_subsystem.load_font(font, size)?;
616    let renderer = font.render(text.as_ref());
617
618    let surf = match mode {
619        TextMode::Transparent { colour: (r, g, b) } => renderer.solid(Color::RGB(r, g, b)),
620        TextMode::Shaded {
621            foreground: (fr, fg, fb),
622            background: (br, bg, bb),
623        } => renderer.shaded(Color::RGB(fr, fg, fb), Color::RGB(br, bg, bb)),
624    }?;
625
626    drop(font);
627    let (creator, canvas, _) = ctx.inner();
628    let texture = creator.create_texture_from_surface(&surf)?;
629
630    let pos = pos.into();
631
632    let srect = surf.rect();
633    let dest_rect: Rect = Rect::from_center((pos.x, pos.y), srect.width(), srect.height());
634
635    canvas.copy_ex(&texture, None, dest_rect, 0.0, None, false, false)?;
636
637    Ok(())
638}
639
640/// Representation of the mouse state.
641pub struct MouseRepr {
642    pub buttons: Vec<MouseButton>,
643    pub x: i32,
644    pub y: i32,
645}
646
647/// Representation of the keyboard state.
648pub struct KeyboardRepr {
649    pub keys: Vec<Scancode>,
650}
651
652/// Get the mouse state.
653/// ```no_run
654/// # use cat_box::*;
655/// # let game = Game::new("catbox-demo", 10, 10);
656/// # game.run(|ctx| {
657/// let m = get_mouse_state(ctx);
658/// println!("({}, {})", m.x, m.y);
659/// # });
660pub fn get_mouse_state(ctx: &mut Context) -> MouseRepr {
661    let (_, _, pump) = ctx.inner();
662
663    let mouse = pump.mouse_state();
664
665    MouseRepr {
666        buttons: mouse.pressed_mouse_buttons().collect(),
667        x: mouse.x(),
668        y: mouse.y(),
669    }
670}
671
672/// Get the keyboard state.
673/// ```no_run
674/// # use cat_box::*;
675/// # let game = Game::new("catbox-demo", 10, 10);
676/// # game.run(|ctx| {
677/// let k = get_keyboard_state(ctx);
678/// for code in k.keys {
679///     println!("{}", code);
680/// }
681/// # });
682pub fn get_keyboard_state(ctx: &mut Context) -> KeyboardRepr {
683    let (_, _, pump) = ctx.inner();
684
685    let keyboard = pump.keyboard_state();
686
687    KeyboardRepr {
688        keys: keyboard.pressed_scancodes().collect(),
689    }
690}
691
692/// Representation of the game.
693pub struct Game {
694    /// The title that the window displays.
695    pub title: String,
696    /// The width of the opened window
697    pub width: u32,
698    /// The height of the opened window
699    pub height: u32,
700    stopped: Cell<bool>,
701}
702
703impl Game {
704    /// Creates a new Game struct.
705    ///
706    /// Make sure to use [`Self::run()`] to actually begin the game logic.
707    ///
708    /// ```
709    /// # use cat_box::Game;
710    /// Game::new("cool game", 1000, 1000);
711    /// ```
712    ///
713    #[must_use]
714    pub fn new(title: &str, width: u32, height: u32) -> Self {
715        Self {
716            title: title.to_string(),
717            width,
718            height,
719            stopped: Cell::new(false),
720        }
721    }
722
723    /// Runs the game. Note: this method blocks, as it uses an infinite loop.
724    ///
725    /// ```no_run
726    /// # use cat_box::Game;
727    /// # let game = Game::new("Cool game", 1000, 1000);
728    /// game.run(|ctx| {
729    ///     // Game logic goes here
730    /// });
731    /// ```
732    pub fn run<F: FnMut(&mut Context)>(&self, mut func: F) -> Result<()> {
733        let sdl_context = sdl2::init()?;
734        let video_subsystem = sdl_context.video()?;
735
736        let window = video_subsystem
737            .window(&self.title, self.width, self.height)
738            .position_centered()
739            // .opengl()
740            .vulkan()
741            .build()?;
742
743        let canvas = window.into_canvas().build()?;
744        let s = sdl2::ttf::init()?;
745
746        let event_pump = sdl_context.event_pump()?;
747
748        let mut ctx = Context::new(canvas, event_pump, s);
749
750        loop {
751            if self.stopped.get() || ctx.check_for_quit() {
752                break;
753            }
754            ctx.clear();
755            func(&mut ctx);
756            ctx.update();
757        }
758
759        Ok(())
760    }
761
762    /// Stops the game loop. This method should be called inside the closure that you passed to [`Self::run()`].
763    /// ```
764    /// # use cat_box::Game;
765    /// # let game = Game::new("asjdhfkajlsdh", 0, 0);
766    /// // ... in the game loop:
767    /// game.terminate();
768    /// ```
769    pub fn terminate(&self) {
770        self.stopped.set(true);
771    }
772}