catbox3d/src/lib.rs

555 lines
15 KiB
Rust
Raw Normal View History

//! Work in progress game engine, inspired by [arcade](https://arcade.academy/).
2022-03-13 13:21:13 -05:00
//!
//! ```no_run
//! use cat_box::{draw_text, Game, Sprite, SpriteCollection, get_mouse_state, get_keyboard_state};
//! use sdl2::keyboard::Scancode;
2022-03-11 15:02:38 -06:00
//!
//! fn main() {
//! let game = Game::new("catbox demo", 1000, 800);
2022-04-12 13:09:28 -05:00
//!
//! let mut i = 0u8;
2022-03-11 15:02:38 -06:00
//! let mut s = Sprite::new("duck.png", 500, 400).unwrap();
//! let mut s2 = Sprite::new("duck.png", 400, 500).unwrap();
2022-04-12 13:09:28 -05:00
//!
//! let mut coll = SpriteCollection::new();
//! for n in 0..10 {
//! for o in 0..8 {
//! let x = Sprite::new("duck.png", n * 100, o * 100).unwrap();
//! coll.push(x);
//! }
2022-04-12 13:09:28 -05:00
//! }
//! game.run(|ctx| {
//! i = (i + 1) % 255;
2022-03-16 12:01:38 -05:00
//! ctx.set_background_colour(i as u8, 64, 255);
2022-04-12 13:09:28 -05:00
//!
//! draw_text(
//! ctx,
//! format!("i is {}", i),
//! "MesloLGS NF Regular.ttf",
//! 72,
//! (300, 300),
//! cat_box::TextMode::Shaded {
//! foreground: (255, 255, 255),
//! background: (0, 0, 0),
//! },
//! )
//! .unwrap();
2022-04-12 13:09:28 -05:00
//!
//! let (start_x, start_y) = s.position().into();
//! let m = get_mouse_state(ctx);
//! let x_diff = m.x - start_x;
//! let y_diff = m.y - start_y;
2022-04-12 13:09:28 -05:00
//!
2022-03-11 15:02:38 -06:00
//! let angle = (y_diff as f64).atan2(x_diff as f64);
//! s.set_angle(angle.to_degrees());
2022-04-12 13:09:28 -05:00
//!
//! for spr in coll.iter() {
//! let (start_x, start_y) = spr.position().into();
//! let m = get_mouse_state(ctx);
//! let x_diff = m.x - start_x;
//! let y_diff = m.y - start_y;
2022-04-12 13:09:28 -05:00
//!
//! let angle = (y_diff as f64).atan2(x_diff as f64);
//! spr.set_angle(angle.to_degrees());
//! }
2022-04-12 13:09:28 -05:00
//!
//! let keys = get_keyboard_state(ctx).keys;
2022-04-12 13:09:28 -05:00
//!
//! for key in keys {
//! let offset = match key {
//! Scancode::Escape => {
//! game.terminate();
//! (0, 0)
//! },
//! Scancode::W | Scancode::Up => (0, 5),
//! Scancode::S | Scancode::Down => (0, -5),
//! Scancode::A | Scancode::Left => (-5, 0),
//! Scancode::D | Scancode::Right => (5, 0),
//! _ => (0, 0),
//! };
2022-04-12 13:09:28 -05:00
//!
//! s.translate(offset);
2022-04-12 13:09:28 -05:00
//!
//! for spr in coll.iter() {
//! spr.translate(offset);
2022-03-11 15:02:38 -06:00
//! }
//! }
2022-04-12 13:09:28 -05:00
//!
//! s2.draw(ctx).unwrap();
//! s.draw(ctx).unwrap();
//! coll.draw(ctx).unwrap();
2022-03-11 15:02:38 -06:00
//! })
//! .unwrap();
//! }
//! ```
#![warn(clippy::pedantic)]
#![allow(
clippy::similar_names,
clippy::needless_doctest_main,
clippy::module_name_repetitions,
clippy::missing_errors_doc
)]
2022-10-06 16:09:53 -05:00
#![cfg_attr(docsrs, feature(doc_cfg))]
pub mod math;
2023-04-25 09:21:59 -05:00
pub mod sprite;
2023-08-04 21:02:49 -05:00
use sdl2::sys::SDL_Window;
2023-04-25 09:21:59 -05:00
pub use sprite::physics::*;
2023-08-04 21:02:49 -05:00
pub use sprite::sprite::{Sprite, SpriteCollection};
2022-04-12 13:09:28 -05:00
2022-10-06 08:50:47 -05:00
#[cfg(feature = "audio")]
use rodio::{self, source::Source, Decoder, OutputStream};
use sdl2::{
2022-04-12 13:09:28 -05:00
mouse::MouseButton,
rect::Rect,
render::{Canvas, TextureCreator, TextureValueError},
ttf::{FontError, InitError, Sdl2TtfContext},
2022-03-14 12:32:42 -05:00
video::{Window, WindowBuildError, WindowContext},
2022-04-12 13:09:28 -05:00
EventPump, IntegerOrSdlError,
2022-03-06 13:08:05 -06:00
};
2023-08-04 21:02:49 -05:00
use std::{cell::Cell, path::Path, time::Instant};
use math::vec2::Vec2Int;
2022-03-11 15:02:38 -06:00
#[doc(no_inline)]
pub use sdl2::{self, event::Event, keyboard::Scancode, pixels::Color};
2022-03-11 15:02:38 -06:00
/// Utility macro for cloning things into closures.
///
/// Temporary workaround for [Rust RFC 2407](https://github.com/rust-lang/rfcs/issues/2407)
2022-03-06 14:23:10 -06:00
#[macro_export]
macro_rules! cloned {
($thing:ident => $e:expr) => {
let $thing = $thing.clone();
$e
};
($($thing:ident),* => $e:expr) => {
$( let $thing = $thing.clone(); )*
$e
}
}
macro_rules! error_from_format {
($($t:ty),+) => {
$(
impl From<$t> for CatboxError {
fn from(e: $t) -> Self {
CatboxError(format!("{}", e))
}
}
)+
};
}
2022-06-21 05:05:56 -05:00
#[derive(Clone, Debug)]
2022-03-06 10:55:10 -06:00
pub struct CatboxError(String);
impl From<String> for CatboxError {
fn from(e: String) -> Self {
CatboxError(e)
}
}
error_from_format! {
WindowBuildError,
IntegerOrSdlError,
TextureValueError,
FontError,
InitError
2022-03-16 11:26:25 -05:00
}
#[cfg(feature = "audio")]
error_from_format! {
rodio::StreamError,
std::io::Error,
rodio::decoder::DecoderError,
rodio::PlayError
}
2022-06-21 05:05:56 -05:00
impl std::fmt::Display for CatboxError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
2022-03-06 10:55:10 -06:00
pub type Result<T> = std::result::Result<T, CatboxError>;
/// Wrapper type around SDL's [`EventPump`](sdl2::EventPump). See those docs for more info.
2022-03-07 10:20:05 -06:00
pub struct Events {
2022-03-07 13:00:51 -06:00
pump: EventPump,
2022-03-07 10:20:05 -06:00
}
impl AsRef<EventPump> for Events {
fn as_ref(&self) -> &EventPump {
&self.pump
}
}
impl AsMut<EventPump> for Events {
fn as_mut(&mut self) -> &mut EventPump {
&mut self.pump
}
}
impl Iterator for Events {
type Item = Event;
fn next(&mut self) -> Option<Event> {
self.pump.poll_event()
}
}
/// Game context.
///
2022-03-15 11:36:33 -05:00
/// In most cases, this should never actually be used; instead, just pass it around to the various cat-box functions such as [`Sprite::draw()`].
2022-03-14 12:32:42 -05:00
pub struct Context {
canvas: Canvas<Window>,
event_pump: EventPump,
2022-03-14 12:32:42 -05:00
texture_creator: TextureCreator<WindowContext>,
2022-03-16 11:54:51 -05:00
ttf_subsystem: Sdl2TtfContext,
2022-03-14 12:32:42 -05:00
}
impl Context {
fn new(canvas: Canvas<Window>, pump: EventPump, ttf_subsystem: Sdl2TtfContext) -> Self {
2022-03-14 12:32:42 -05:00
let creator = canvas.texture_creator();
Self {
canvas,
event_pump: pump,
2022-03-14 12:32:42 -05:00
texture_creator: creator,
2022-03-16 11:54:51 -05:00
ttf_subsystem,
2022-03-14 12:32:42 -05:00
}
}
/// Get the inner [`Canvas`](sdl2::render::Canvas) and [`TextureCreator`](sdl2::render::TextureCreator).
///
/// Only use this method if you know what you're doing.
2022-04-12 13:09:28 -05:00
pub fn inner(
&mut self,
) -> (
&TextureCreator<WindowContext>,
&mut Canvas<Window>,
&mut EventPump,
) {
(
&self.texture_creator,
&mut self.canvas,
&mut self.event_pump,
)
2022-03-14 12:32:42 -05:00
}
fn update(&mut self) {
2022-03-14 12:32:42 -05:00
self.canvas.present();
}
fn clear(&mut self) {
self.canvas.clear();
}
2022-03-15 11:36:33 -05:00
fn check_for_quit(&mut self) -> bool {
let (_, _, pump) = self.inner();
for event in pump.poll_iter() {
if let Event::Quit { .. } = event {
return true;
}
2022-04-12 13:09:28 -05:00
}
false
}
2022-03-15 11:36:33 -05:00
/// 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));
}
2022-03-14 12:32:42 -05:00
}
2022-03-16 11:53:26 -05:00
/// Set the mode for drawing text.
#[derive(Clone, Copy, Debug)]
2022-03-16 11:53:26 -05:00
pub enum TextMode {
/// Render the text transparently.
2022-03-16 11:54:51 -05:00
Transparent { colour: (u8, u8, u8) },
2022-03-16 11:53:26 -05:00
/// Render the text with a foreground and a background colour.
2022-03-16 11:54:51 -05:00
///
2022-03-16 11:53:26 -05:00
/// This creates a box around the text.
Shaded {
foreground: (u8, u8, u8),
2022-03-16 11:54:51 -05:00
background: (u8, u8, u8),
},
2022-03-16 11:53:26 -05:00
}
/// Draw text to the screen.
2022-03-16 11:54:51 -05:00
///
2022-03-16 11:53:26 -05:00
/// This loads a font from the current directory, case sensitive.
2022-03-16 11:54:51 -05:00
///
2022-03-16 11:53:26 -05:00
/// `pos` refers to the *center* of the rendered text.
2022-03-16 11:54:51 -05:00
///
2022-03-16 11:53:26 -05:00
/// Refer to [`TextMode`] for information about colouring.
2022-03-16 11:54:51 -05:00
///
2022-03-16 11:53:26 -05:00
/// ``` no_run
/// # use cat_box::*;
/// # let game = Game::new("", 100, 100);
/// # game.run(|ctx| {
2022-03-16 11:53:26 -05:00
/// let mode = TextMode::Shaded {
/// foreground: (255, 255, 255),
/// background: (0, 0, 0)
/// };
/// draw_text(ctx, "text to draw", "arial.ttf", 72, (300, 300), mode);
/// # });
pub fn draw_text<S: AsRef<str>, I: Into<Vec2Int>>(
2022-03-16 11:54:51 -05:00
ctx: &mut Context,
text: S,
font: &str,
size: u16,
pos: I,
2022-03-16 11:54:51 -05:00
mode: TextMode,
) -> Result<()> {
let font = ctx.ttf_subsystem.load_font(font, size)?;
2022-03-16 11:33:12 -05:00
let renderer = font.render(text.as_ref());
2022-03-16 11:26:25 -05:00
let surf = match mode {
TextMode::Transparent { colour: (r, g, b) } => renderer.solid(Color::RGB(r, g, b)),
2022-03-16 11:54:51 -05:00
TextMode::Shaded {
foreground: (fr, fg, fb),
background: (br, bg, bb),
} => renderer.shaded(Color::RGB(fr, fg, fb), Color::RGB(br, bg, bb)),
2022-03-16 11:26:25 -05:00
}?;
drop(font);
let (creator, canvas, _) = ctx.inner();
2022-03-16 11:26:25 -05:00
let texture = creator.create_texture_from_surface(&surf)?;
let pos = pos.into();
2022-03-16 11:26:25 -05:00
let srect = surf.rect();
let dest_rect: Rect = Rect::from_center((pos.x, pos.y), srect.width(), srect.height());
2022-03-16 11:26:25 -05:00
canvas.copy_ex(&texture, None, dest_rect, 0.0, None, false, false)?;
Ok(())
}
/// Representation of the mouse state.
pub struct MouseRepr {
pub buttons: Vec<MouseButton>,
2023-08-04 21:02:49 -05:00
pub pos: Vec2Int,
}
/// Representation of the keyboard state.
pub struct KeyboardRepr {
2022-04-12 13:09:28 -05:00
pub keys: Vec<Scancode>,
}
/// Get the mouse state.
/// ```no_run
/// # use cat_box::*;
/// # let game = Game::new("catbox-demo", 10, 10);
/// # game.run(|ctx| {
/// let m = get_mouse_state(ctx);
/// println!("({}, {})", m.x, m.y);
/// # });
pub fn get_mouse_state(ctx: &mut Context) -> MouseRepr {
let (_, _, pump) = ctx.inner();
let mouse = pump.mouse_state();
MouseRepr {
buttons: mouse.pressed_mouse_buttons().collect(),
2023-08-04 21:02:49 -05:00
pos: Vec2Int::new(mouse.x(), mouse.y()),
}
}
/// Get the keyboard state.
/// ```no_run
/// # use cat_box::*;
/// # let game = Game::new("catbox-demo", 10, 10);
/// # game.run(|ctx| {
/// let k = get_keyboard_state(ctx);
/// for code in k.keys {
/// println!("{}", code);
/// }
/// # });
pub fn get_keyboard_state(ctx: &mut Context) -> KeyboardRepr {
let (_, _, pump) = ctx.inner();
let keyboard = pump.keyboard_state();
2022-04-12 13:09:28 -05:00
KeyboardRepr {
2022-04-12 13:09:28 -05:00
keys: keyboard.pressed_scancodes().collect(),
}
}
2022-03-11 15:02:38 -06:00
/// Representation of the game.
2022-03-06 10:55:10 -06:00
pub struct Game {
2022-03-11 15:02:38 -06:00
/// The title that the window displays.
pub title: String,
2022-03-11 15:02:38 -06:00
/// The width of the opened window
pub width: u32,
2022-03-11 15:02:38 -06:00
/// The height of the opened window
pub height: u32,
2023-03-09 22:54:28 -06:00
pub time: Cell<Instant>,
2022-03-06 13:08:05 -06:00
stopped: Cell<bool>,
2022-03-06 10:55:10 -06:00
}
impl Game {
2022-03-11 15:02:38 -06:00
/// Creates a new Game struct.
///
/// Make sure to use [`Self::run()`] to actually begin the game logic.
///
/// ```
2022-03-13 13:21:13 -05:00
/// # use cat_box::Game;
2022-03-11 15:02:38 -06:00
/// Game::new("cool game", 1000, 1000);
/// ```
///
#[must_use]
2022-03-06 10:55:10 -06:00
pub fn new(title: &str, width: u32, height: u32) -> Self {
Self {
title: title.to_string(),
width,
height,
2023-03-09 22:54:28 -06:00
time: Instant::now().into(),
2022-03-06 13:08:05 -06:00
stopped: Cell::new(false),
2022-03-06 10:55:10 -06:00
}
}
2023-03-28 11:37:33 -05:00
///Gets time elapsed since last timer reset in milliseconds
///
///Run this within the game loop
///```
2023-03-28 11:44:51 -05:00
///# use cat_box::Game;
2023-03-28 12:05:38 -05:00
///# let game = Game::new("wacky game", 1000, 1000);
2023-03-28 11:37:33 -05:00
///# game.run(|ctx| {
/// if game.step() >= 1000
/// {
/// println!("A second has passed approx!");
/// game.t_reset();
/// }
///}).unwrap();
///```
2023-03-09 22:54:28 -06:00
pub fn step(&self) -> u128 {
2023-04-09 21:39:26 -05:00
self.time.get().elapsed().as_millis()
2023-03-09 22:54:28 -06:00
}
2023-03-28 11:37:33 -05:00
///Resets in-game timer
2023-03-09 22:54:28 -06:00
pub fn t_reset(&self) {
self.time.set(Instant::now());
}
2022-03-06 10:55:10 -06:00
2022-03-11 15:02:38 -06:00
/// Runs the game. Note: this method blocks, as it uses an infinite loop.
///
/// ```no_run
2022-03-13 13:21:13 -05:00
/// # use cat_box::Game;
2022-03-11 15:02:38 -06:00
/// # let game = Game::new("Cool game", 1000, 1000);
/// game.run(|ctx| {
2022-03-11 15:02:38 -06:00
/// // Game logic goes here
/// });
/// ```
pub fn run<F: FnMut(&mut Context)>(&self, mut func: F) -> Result<()> {
2022-03-06 10:55:10 -06:00
let sdl_context = sdl2::init()?;
let video_subsystem = sdl_context.video()?;
let mut window_build = video_subsystem.window(&self.title, self.width, self.height);
//init window
let window = if cfg!(feature = "opengl") {
window_build.opengl().build()?
} else if cfg!(feature = "vulkan") {
window_build.vulkan().build()?
} else {
window_build.build()?
};
2022-03-06 10:55:10 -06:00
2022-03-14 12:32:42 -05:00
let canvas = window.into_canvas().build()?;
let s = sdl2::ttf::init()?;
2022-03-06 10:55:10 -06:00
2022-03-07 12:58:02 -06:00
let event_pump = sdl_context.event_pump()?;
2022-04-12 13:09:28 -05:00
let mut ctx = Context::new(canvas, event_pump, s);
2022-03-14 12:32:42 -05:00
2022-03-06 10:55:10 -06:00
loop {
if self.stopped.get() || ctx.check_for_quit() {
break;
}
ctx.clear();
func(&mut ctx);
2022-03-14 12:32:42 -05:00
ctx.update();
2022-03-06 10:55:10 -06:00
}
Ok(())
}
2022-10-06 08:50:47 -05:00
2023-08-04 21:02:49 -05:00
/// Runs the game from a raw pointer to a Window (blocks)
pub unsafe fn run_from_ll<F: FnMut(&mut Context)>(
&self,
win: *mut SDL_Window,
mut func: F,
) -> Result<()> {
unsafe {
let sdl_context = sdl2::init()?;
let video_subsystem = sdl_context.video()?;
let window = Window::from_ll(video_subsystem, win);
let canvas = window.into_canvas().build()?;
let s = sdl2::ttf::init()?;
let event_pump = sdl_context.event_pump()?;
let mut ctx = Context::new(canvas, event_pump, s);
loop {
if self.stopped.get() || ctx.check_for_quit() {
break;
}
ctx.clear();
func(&mut ctx);
ctx.update();
}
Ok(())
}
}
2022-10-02 11:12:05 -05:00
/// 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);
}
}
2022-10-06 08:50:47 -05:00
2022-10-06 16:09:53 -05:00
#[cfg(feature = "audio")]
#[cfg_attr(docsrs, doc(cfg(feature = "audio")))]
2022-10-02 11:12:05 -05:00
/// Plays an audio file given the path of file and plays it for y seconds
2022-10-06 08:50:47 -05:00
/// ```no_run
/// # use cat_box::play;
/// play("/path/to/song.mp71", 15);
2022-10-02 11:12:05 -05:00
/// ```
2022-10-06 16:12:07 -05:00
pub fn play<P: AsRef<Path> + Send + 'static>(
path: P,
time: u64,
) -> std::thread::JoinHandle<Result<()>> {
2022-10-06 16:09:53 -05:00
use std::fs::File;
use std::io::BufReader;
use std::thread;
2022-10-06 08:50:47 -05:00
thread::spawn(move || {
let (_stream, stream_handle) = OutputStream::try_default()?;
2022-10-06 08:50:47 -05:00
// Load a sound from a file, using a path relative to Cargo.toml
2022-10-06 16:12:07 -05:00
let file = BufReader::new(File::open(path)?);
2022-10-06 08:50:47 -05:00
// Decode that sound file into a source
let source = Decoder::new(file)?;
2022-10-06 08:50:47 -05:00
// Play the sound directly on the device
stream_handle.play_raw(source.convert_samples())?;
2022-10-06 08:50:47 -05:00
// The sound plays in a separate audio thread,
// so we need to keep the main thread alive while it's playing.
std::thread::sleep(std::time::Duration::from_secs(time));
Ok(())
2022-10-06 08:50:47 -05:00
})
}