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}