//! Work in progress game engine, inspired by [arcade](arcade.academy/). //! //! ```no_run //! use cat_box::{Event, Game, Keycode, Sprite}; //! //! fn main() { //! let game = Game::new("cat_box demo", 1000, 800); //! //! let mut i = 0.0; //! let mut s = Sprite::new("duck.png", 500, 400).unwrap(); //! game.run(|ctx, event_pump| { //! i = (i + 1.0) % 360.0; //! //! let (start_x, start_y) = s.position(); //! let m = sdl2::mouse::MouseState::new(event_pump.as_ref()); //! let x_diff = m.x() - start_x; //! let y_diff = m.y() - start_y; //! //! let angle = (y_diff as f64).atan2(x_diff as f64); //! s.set_angle(angle.to_degrees()); //! //! for event in event_pump { //! match event { //! Event::Quit { .. } //! | Event::KeyDown { //! keycode: Some(Keycode::Escape), //! .. //! } => game.terminate(), //! //! Event::KeyDown { keycode, .. } => { //! let offset = match keycode.unwrap() { //! Keycode::W | Keycode::Up => (0, 5), //! Keycode::S | Keycode::Down => (0, -5), //! Keycode::A | Keycode::Left => (-5, 0), //! Keycode::D | Keycode::Right => (5, 0), //! _ => (0, 0), //! }; //! //! s.translate(offset); //! } //! _ => {} //! } //! } //! //! s.draw(ctx).unwrap(); //! }) //! .unwrap(); //! } //! ``` use std::{cell::Cell, path::Path}; use sdl2::{ image::ImageRWops, rect::Rect, render::{Canvas, TextureCreator, TextureValueError}, rwops::RWops, surface::Surface, video::{Window, WindowBuildError, WindowContext}, EventPump, IntegerOrSdlError, ttf::{Sdl2TtfContext, Font, FontError}, }; #[doc(no_inline)] pub use sdl2::event::Event; #[doc(no_inline)] pub use sdl2::keyboard::Keycode; #[doc(no_inline)] pub use sdl2::pixels::Color; /// Utility macro for cloning things into closures. /// /// Temporary workaround for [Rust RFC 2407](https://github.com/rust-lang/rfcs/issues/2407) #[macro_export] macro_rules! cloned { ($thing:ident => $e:expr) => { let $thing = $thing.clone(); $e }; ($($thing:ident),* => $e:expr) => { $( let $thing = $thing.clone(); )* $e } } #[derive(Debug)] pub struct CatboxError(String); impl From for CatboxError { fn from(e: WindowBuildError) -> Self { CatboxError(format!("{}", e)) } } impl From for CatboxError { fn from(e: String) -> Self { CatboxError(e) } } impl From for CatboxError { fn from(e: IntegerOrSdlError) -> Self { CatboxError(format!("{}", e)) } } impl From for CatboxError { fn from(e: TextureValueError) -> Self { CatboxError(format!("{}", e)) } } impl From for CatboxError { fn from(e: FontError) -> Self { CatboxError(format!("{}", e)) } } pub type Result = std::result::Result; /// Wrapper type around SDL's [`EventPump`](sdl2::EventPump). See those docs for more info. pub struct Events { pump: EventPump, } impl AsRef for Events { fn as_ref(&self) -> &EventPump { &self.pump } } impl AsMut for Events { fn as_mut(&mut self) -> &mut EventPump { &mut self.pump } } impl Iterator for Events { type Item = Event; fn next(&mut self) -> Option { self.pump.poll_event() } } /// Representation of a sprite. pub struct Sprite { rect: Rect, surf: Surface<'static>, angle: f64, } impl Sprite { /// Create a new Sprite. The `path` is relative to the current directory while running. /// /// Don't forget to call [`draw()`](Self::draw()) after this. /// ``` /// # use cat_box::*; /// let s = Sprite::new("duck.png", 500, 400).unwrap(); /// ``` pub fn new>(path: P, x: i32, y: i32) -> Result { let ops = RWops::from_file(path, "r")?; let surf = ops.load()?; let srect = surf.rect(); let dest_rect: Rect = Rect::from_center((x, y), srect.width(), srect.height()); Ok(Self { rect: dest_rect, surf, angle: 0.0, }) } /// Draws the sprite to the window. This should only be called inside your main event loop. /// /// ```no_run /// # use cat_box::*; /// # let mut s = Sprite::new("duck.png", 500, 400).unwrap(); /// # let game = Game::new("sprite demo", 1000, 1000); /// # game.run(|ctx, _| { /// s.draw(ctx); /// # }); /// ``` pub fn draw(&mut self, ctx: &mut Context) -> Result<()> { let (creator, canvas) = ctx.inner(); let text = creator.create_texture_from_surface(&self.surf)?; canvas.copy_ex(&text, None, self.rect, self.angle, None, false, false)?; Ok(()) } /// Translate the sprite, in the form of (delta x, delta y) /// /// ``` /// # use cat_box::*; /// # let mut s = Sprite::new("duck.png", 500, 400).unwrap(); /// s.translate((5, 10)); /// ``` pub fn translate(&mut self, position: (i32, i32)) { let new_x = self.rect.x() + position.0; let new_y = self.rect.y() - position.1; self.rect.set_x(new_x); self.rect.set_y(new_y); } /// Set the angle of the sprite, in degrees of clockwise rotation. /// /// ``` /// # use cat_box::*; /// # let mut s = Sprite::new("duck.png", 500, 400).unwrap(); /// s.set_angle(45.0); /// ``` pub fn set_angle(&mut self, angle: f64) { self.angle = angle; } /// Get the angle of the sprite, in degrees of clockwise rotation. /// /// ``` /// # use cat_box::*; /// # let s = Sprite::new("duck.png", 500, 400).unwrap(); /// let angle = s.angle(); /// ``` pub fn angle(&self) -> f64 { self.angle } /// Get the x and y coordinates of the center of the sprite, in the form of (x, y). /// /// ``` /// # use cat_box::*; /// # let s = Sprite::new("duck.png", 500, 400).unwrap(); /// let (x, y) = s.position(); /// ``` pub fn position(&self) -> (i32, i32) { self.rect.center().into() } } pub enum TextMode { Transparent { colour: (u8, u8, u8) }, Shaded { foreground: (u8, u8, u8), background: (u8, u8, u8) } } /// Game context. /// /// In most cases, this should never actually be used; instead, just pass it around to the various cat-box functions such as [`Sprite::draw()`]. pub struct Context { canvas: Canvas, texture_creator: TextureCreator, ttf_subsystem: Sdl2TtfContext } impl Context { fn new(canvas: Canvas, ttf_subsystem: Sdl2TtfContext) -> Self { let creator = canvas.texture_creator(); Self { canvas, texture_creator: creator, ttf_subsystem } } /// Get the inner [`Canvas`](sdl2::render::Canvas) and [`TextureCreator`](sdl2::render::TextureCreator). /// /// Only use this method if you know what you're doing. pub fn inner(&mut self) -> (&TextureCreator, &mut Canvas) { (&self.texture_creator, &mut self.canvas) } fn update(&mut self) { self.canvas.present(); } fn clear(&mut self) { self.canvas.clear(); } /// Set the background colour. See [`Canvas::set_draw_color()`](sdl2::render::Canvas::set_draw_color()) for more info. pub fn set_background_colour(&mut self, r: u8, g: u8, b: u8) { self.canvas.set_draw_color(Color::RGB(r, g, b)); } } pub fn draw_text>(ctx: &mut Context, text: S, font: &str, size: u16, pos: (i32, i32), mode: TextMode) -> Result<()> { let font = ctx.ttf_subsystem.load_font(font, size)?; let renderer = font.render(text.as_ref()); let surf = match mode { TextMode::Transparent { colour: (r, g, b) } => renderer.solid(Color::RGB(r, g, b)), TextMode::Shaded { foreground: (fr, fg, fb), background: (br, bg, bb) } => renderer.shaded(Color::RGB(fr, fg, fb), Color::RGB(br, bg, bb)), }?; drop(font); let (creator, canvas) = ctx.inner(); let texture = creator.create_texture_from_surface(&surf)?; let srect = surf.rect(); let dest_rect: Rect = Rect::from_center(pos, srect.width(), srect.height()); canvas.copy_ex(&texture, None, dest_rect, 0.0, None, false, false)?; Ok(()) } /// Representation of the game. pub struct Game { /// The title that the window displays. pub title: String, /// The width of the opened window pub width: u32, /// The height of the opened window pub height: u32, stopped: Cell, } impl Game { /// Creates a new Game struct. /// /// Make sure to use [`Self::run()`] to actually begin the game logic. /// /// ``` /// # use cat_box::Game; /// Game::new("cool game", 1000, 1000); /// ``` /// pub fn new(title: &str, width: u32, height: u32) -> Self { Self { title: title.to_string(), width, height, stopped: Cell::new(false), } } /// Runs the game. Note: this method blocks, as it uses an infinite loop. /// /// ```no_run /// # use cat_box::Game; /// # let game = Game::new("Cool game", 1000, 1000); /// game.run(|ctx, events| { /// // Game logic goes here /// }); /// ``` pub fn run(&self, mut func: F) -> Result<()> { let sdl_context = sdl2::init()?; let video_subsystem = sdl_context.video()?; let window = video_subsystem .window(&self.title, self.width, self.height) .position_centered() // .opengl() .vulkan() .build()?; let canvas = window.into_canvas().build()?; let s = sdl2::ttf::init().unwrap(); let event_pump = sdl_context.event_pump()?; let mut events = Events { pump: event_pump }; let mut ctx = Context::new(canvas, s); loop { if self.stopped.get() { break; } ctx.clear(); func(&mut ctx, &mut events); ctx.update(); } Ok(()) } /// Stops the game loop. This method should be called inside the closure that you passed to [`Self::run()`]. /// ``` /// # use cat_box::Game; /// # let game = Game::new("asjdhfkajlsdh", 0, 0); /// // ... in the game loop: /// game.terminate(); /// ``` pub fn terminate(&self) { self.stopped.set(true); } }