forked from karx/catbox
395 lines
11 KiB
Rust
395 lines
11 KiB
Rust
//! 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<WindowBuildError> for CatboxError {
|
|
fn from(e: WindowBuildError) -> Self {
|
|
CatboxError(format!("{}", e))
|
|
}
|
|
}
|
|
|
|
impl From<String> for CatboxError {
|
|
fn from(e: String) -> Self {
|
|
CatboxError(e)
|
|
}
|
|
}
|
|
|
|
impl From<IntegerOrSdlError> for CatboxError {
|
|
fn from(e: IntegerOrSdlError) -> Self {
|
|
CatboxError(format!("{}", e))
|
|
}
|
|
}
|
|
|
|
impl From<TextureValueError> for CatboxError {
|
|
fn from(e: TextureValueError) -> Self {
|
|
CatboxError(format!("{}", e))
|
|
}
|
|
}
|
|
|
|
impl From<FontError> for CatboxError {
|
|
fn from(e: FontError) -> Self {
|
|
CatboxError(format!("{}", e))
|
|
}
|
|
}
|
|
|
|
pub type Result<T> = std::result::Result<T, CatboxError>;
|
|
|
|
/// Wrapper type around SDL's [`EventPump`](sdl2::EventPump). See those docs for more info.
|
|
pub struct Events {
|
|
pump: EventPump,
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
/// 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<P: AsRef<Path>>(path: P, x: i32, y: i32) -> Result<Self> {
|
|
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<Window>,
|
|
texture_creator: TextureCreator<WindowContext>,
|
|
ttf_subsystem: Sdl2TtfContext
|
|
}
|
|
|
|
impl Context {
|
|
fn new(canvas: Canvas<Window>, 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<WindowContext>, &mut Canvas<Window>) {
|
|
(&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<S: AsRef<str>>(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<bool>,
|
|
}
|
|
|
|
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<F: FnMut(&mut Context, &mut Events)>(&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);
|
|
}
|
|
}
|