diff --git a/.gitignore b/.gitignore index 088ba6b..99d1388 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk + +/*.gif diff --git a/game/assets/character/player_run.png b/game/assets/character/player_run.png new file mode 100644 index 0000000..87b5997 Binary files /dev/null and b/game/assets/character/player_run.png differ diff --git a/game/assets/configs/application.json b/game/assets/configs/application.json index 0bab565..57497cf 100644 --- a/game/assets/configs/application.json +++ b/game/assets/configs/application.json @@ -41,6 +41,13 @@ 20, 20, 255 + ], + "white": [ + 188, + 188, + 188, + 188 ] - } + }, + "animation_fps": 15 } diff --git a/game/src/character/collisions.rs b/game/src/character/collisions.rs new file mode 100644 index 0000000..8b6e951 --- /dev/null +++ b/game/src/character/collisions.rs @@ -0,0 +1,35 @@ +use raylib::math::{Rectangle, Vector2}; + +use super::MainCharacter; + +const GRAVITY_PPS: f32 = 2.0; + +pub fn modify_player_based_on_forces(player: &mut MainCharacter) -> Result<(), ()> { + // Convert the player to a rectangle + let predicted_player_position = player.position + player.velocity; + let player_rect = Rectangle::new( + predicted_player_position.x - (player.size.x / 2.0), + predicted_player_position.y - (player.size.x / 2.0), + player.size.x, + player.size.y, + ); + + // Calculate a generic "floor" to always collide with + let floor_rect = Rectangle::new(f32::MIN, 0.0, f32::MAX, 1.0); + + // If the player is colliding, only apply the x force + if (floor_rect.check_collision_recs(&player_rect) || player_rect.y + player_rect.height > floor_rect.y) + && player.velocity.y > 0.0 + { + player.velocity.y = 0.0; + } + + // TODO: Error out if colliding in the X direction + + // Apply the force + player.position += player.velocity; + + // Apply gravity + player.velocity.y += GRAVITY_PPS; + Ok(()) +} diff --git a/game/src/character/mod.rs b/game/src/character/mod.rs index cd893f7..1386117 100644 --- a/game/src/character/mod.rs +++ b/game/src/character/mod.rs @@ -1,22 +1,62 @@ +pub mod collisions; pub mod render; -use raylib::math::Vector2; +use chrono::{DateTime, Utc}; +use raylib::{math::Vector2, texture::Texture2D}; -#[derive(Debug, Clone)] +use crate::utilities::anim_render::AnimatedSpriteSheet; + +use self::collisions::modify_player_based_on_forces; + +#[derive(Debug, Default)] +pub enum CharacterState { + #[default] + Running, + Jumping, + Dashing, +} + +#[derive(Debug)] pub struct MainCharacter { pub position: Vector2, + pub velocity: Vector2, pub size: Vector2, + pub sprite_sheet: AnimatedSpriteSheet, + pub current_state: CharacterState, + pub state_set_timestamp: DateTime, } impl MainCharacter { - pub fn new(position: Vector2) -> Self { + pub fn new(position: Vector2, sprite_sheet: Texture2D) -> Self { Self { position, - size: Vector2::new(60.0, 80.0), + velocity: Vector2::new(20.0, 0.0), + size: Vector2::new(100.0, 130.0), + sprite_sheet: AnimatedSpriteSheet::new( + sprite_sheet, + Vector2::new(300.0, 300.0), + 3, + 8, + 6, + ), + current_state: CharacterState::default(), + state_set_timestamp: Utc::now(), } } - pub fn apply_force(&mut self, force: Vector2) { - self.position += force; + pub fn apply_force(&mut self, force: Vector2) -> Option<()> { + self.velocity = force; + modify_player_based_on_forces(self).unwrap(); + // self.position = calculate_player_collisions(&self).unwrap(); + Some(()) + } + + pub fn update_gravity(&mut self) { + modify_player_based_on_forces(self).unwrap(); + } + + pub fn set_state(&mut self, state: CharacterState) { + self.current_state = state; + self.state_set_timestamp = Utc::now(); } } diff --git a/game/src/character/render.rs b/game/src/character/render.rs index 5b7d241..c200af8 100644 --- a/game/src/character/render.rs +++ b/game/src/character/render.rs @@ -1,15 +1,33 @@ use std::ops::{Div, Sub}; +use chrono::Utc; use raylib::prelude::*; +use tracing::log::trace; -use crate::utilities::non_ref_raylib::HackedRaylibHandle; +use crate::{utilities::non_ref_raylib::HackedRaylibHandle, GameConfig}; use super::MainCharacter; pub fn render_character_in_camera_space( raylib: &mut RaylibMode2D<'_, HackedRaylibHandle>, player: &MainCharacter, + config: &GameConfig, ) { + // Calculate the time since the start of the state + let time_since_state_change = Utc::now() - player.state_set_timestamp; - raylib.draw_rectangle_v(player.position.sub(player.size.div(2.0)), player.size, Color::WHITE); + // Calculate the number of frames since state change + let frames_since_state_change = ((time_since_state_change.num_milliseconds() as f64 / 1000.0) * config.animation_fps as f64) as f32; + + trace!( + "Rendering player frame: {} ({})", + frames_since_state_change % player.sprite_sheet.sprite_count as f32, + frames_since_state_change + ); + player.sprite_sheet.render( + raylib, + player.position.sub(player.size.div(2.0)), + Some(Vector2::new(player.size.y, player.size.y)), + Some((frames_since_state_change % player.sprite_sheet.sprite_count as f32).floor() as usize), + ); } diff --git a/game/src/scenes/ingame_scene/hud.rs b/game/src/scenes/ingame_scene/hud.rs index 51caa68..851c045 100644 --- a/game/src/scenes/ingame_scene/hud.rs +++ b/game/src/scenes/ingame_scene/hud.rs @@ -10,5 +10,8 @@ impl ScreenSpaceRender for InGameScreen { ) { // Calculate the logo position let screen_size = raylib.get_screen_size(); + + // Draw a thin glow box around the screen + raylib.draw_rectangle_lines(0, 0, screen_size.x as i32, screen_size.y as i32, config.colors.red); } } diff --git a/game/src/scenes/ingame_scene/mod.rs b/game/src/scenes/ingame_scene/mod.rs index 3d70c6f..ae1d042 100644 --- a/game/src/scenes/ingame_scene/mod.rs +++ b/game/src/scenes/ingame_scene/mod.rs @@ -1,11 +1,7 @@ use dirty_fsm::{Action, ActionFlag}; use raylib::prelude::*; -use crate::{ - character::MainCharacter, - context::GameContext, - utilities::render_layer::{FrameUpdate, ScreenSpaceRender, WorldSpaceRender}, -}; +use crate::{character::{CharacterState, MainCharacter}, context::GameContext, utilities::render_layer::{FrameUpdate, ScreenSpaceRender, WorldSpaceRender}}; use super::{Scenes, ScreenError}; use tracing::{debug, trace}; @@ -22,7 +18,7 @@ pub struct InGameScreen { impl InGameScreen { /// Construct a new `InGameScreen` - pub fn new() -> Self { + pub fn new(player_sprite_sheet: Texture2D) -> Self { Self { camera: Camera2D { offset: Vector2::zero(), @@ -30,7 +26,7 @@ impl InGameScreen { rotation: 0.0, zoom: 1.0, }, - player: MainCharacter::new(Vector2::new(0.0, -45.0)), + player: MainCharacter::new(Vector2::new(0.0, -80.0), player_sprite_sheet), } } } @@ -44,6 +40,9 @@ impl Action for InGameScreen { fn on_first_run(&mut self, _context: &GameContext) -> Result<(), ScreenError> { debug!("Running InGameScreen for the first time"); + // Set the player to running + self.player.set_state(CharacterState::Running); + Ok(()) } diff --git a/game/src/scenes/ingame_scene/update.rs b/game/src/scenes/ingame_scene/update.rs index 0670b3a..ea7cf42 100644 --- a/game/src/scenes/ingame_scene/update.rs +++ b/game/src/scenes/ingame_scene/update.rs @@ -1,27 +1,32 @@ use std::ops::Div; use super::InGameScreen; -use crate::{GameConfig, utilities::{non_ref_raylib::HackedRaylibHandle, render_layer::FrameUpdate}}; +use crate::{ + utilities::{non_ref_raylib::HackedRaylibHandle, render_layer::FrameUpdate}, + GameConfig, +}; use chrono::Duration; use raylib::prelude::*; impl FrameUpdate for InGameScreen { - fn update(&mut self, raylib: &HackedRaylibHandle, delta_seconds: &Duration, - config: &GameConfig) { + fn update( + &mut self, + raylib: &HackedRaylibHandle, + delta_seconds: &Duration, + config: &GameConfig, + ) { // Set the camera's offset based on screen size - self.camera.offset = raylib.get_screen_size().div(Vector2::new(2.0, 1.25)); - self.camera.target = Vector2::new( - self.player.position.x, - self.camera.target.y, - ); + self.camera.offset = raylib.get_screen_size().div(Vector2::new(2.0, 1.05)); + self.camera.target = Vector2::new(self.player.position.x, self.camera.target.y); // Check the only possible keyboard inputs - let is_jump = raylib.is_key_down(KeyboardKey::KEY_SPACE); + let is_jump = raylib.is_key_pressed(KeyboardKey::KEY_SPACE); let is_dash = raylib.is_key_down(KeyboardKey::KEY_LEFT_SHIFT); let is_pause = raylib.is_key_down(KeyboardKey::KEY_ESCAPE); if is_jump { - self.player.apply_force(Vector2::new(0.0, -1.0)); + self.player.apply_force(Vector2::new(0.0, -30.0)); } + self.player.update_gravity(); } } diff --git a/game/src/scenes/ingame_scene/world.rs b/game/src/scenes/ingame_scene/world.rs index a722b8b..c0ca99b 100644 --- a/game/src/scenes/ingame_scene/world.rs +++ b/game/src/scenes/ingame_scene/world.rs @@ -1,25 +1,33 @@ use std::ops::Mul; use super::InGameScreen; -use crate::{GameConfig, character::render::render_character_in_camera_space, utilities::{non_ref_raylib::HackedRaylibHandle, render_layer::WorldSpaceRender}}; +use crate::{ + character::render::render_character_in_camera_space, + utilities::{non_ref_raylib::HackedRaylibHandle, render_layer::WorldSpaceRender}, + GameConfig, +}; use raylib::prelude::*; impl WorldSpaceRender for InGameScreen { - fn render_world_space(&self, raylib: &mut RaylibMode2D<'_, HackedRaylibHandle>, - config: &GameConfig) { + fn render_world_space( + &self, + raylib: &mut RaylibMode2D<'_, HackedRaylibHandle>, + config: &GameConfig, + ) { // Render the player - render_character_in_camera_space(raylib, &self.player); + render_character_in_camera_space(raylib, &self.player, &config); // Render the floor as a line let screen_world_zero = raylib.get_screen_to_world2D(Vector2::zero(), self.camera); - let screen_world_size = raylib.get_screen_to_world2D(raylib.get_screen_size().mul(2.0), self.camera); + let screen_world_size = + raylib.get_screen_to_world2D(raylib.get_screen_size().mul(2.0), self.camera); raylib.draw_rectangle( screen_world_zero.x as i32, 0, screen_world_size.x as i32, 5, - Color::WHITE, + config.colors.white, ); } } diff --git a/game/src/scenes/mod.rs b/game/src/scenes/mod.rs index 7a0f1e9..d9ec17b 100644 --- a/game/src/scenes/mod.rs +++ b/game/src/scenes/mod.rs @@ -4,10 +4,13 @@ use self::{ }; use crate::{ context::GameContext, - utilities::{datastore::ResourceLoadError, non_ref_raylib::HackedRaylibHandle}, + utilities::{ + datastore::{load_texture_from_internal_data, ResourceLoadError}, + non_ref_raylib::HackedRaylibHandle, + }, }; use dirty_fsm::StateMachine; -use raylib::RaylibThread; +use raylib::{texture::Texture2D, RaylibThread}; pub mod fsm_error_screen; pub mod ingame_scene; @@ -40,6 +43,11 @@ pub fn build_screen_state_machine( StateMachine, ScreenError, > { + // Load the various textures needed by the states + let player_sprite_sheet = + load_texture_from_internal_data(raylib_handle, thread, "character/player_run.png").unwrap(); + + // Set up the state machine let mut machine = StateMachine::new(); machine.add_action(Scenes::FsmErrorScreen, FsmErrorScreen::new())?; machine.add_action( @@ -47,6 +55,6 @@ pub fn build_screen_state_machine( LoadingScreen::new(raylib_handle, thread)?, )?; machine.add_action(Scenes::MainMenuScreen, MainMenuScreen::new())?; - machine.add_action(Scenes::InGameScene, InGameScreen::new())?; + machine.add_action(Scenes::InGameScene, InGameScreen::new(player_sprite_sheet))?; Ok(machine) } diff --git a/game/src/utilities/anim_render.rs b/game/src/utilities/anim_render.rs new file mode 100644 index 0000000..19d4a53 --- /dev/null +++ b/game/src/utilities/anim_render.rs @@ -0,0 +1,104 @@ +use raylib::{ + color::Color, + math::{Rectangle, Vector2}, + prelude::RaylibDraw, + texture::Texture2D, +}; + +#[derive(Debug)] +pub struct AnimatedSpriteSheet { + texture: Texture2D, + sprite_size: Vector2, + sheet_width: usize, + pub sprite_count: usize, + default_sprite_id: usize, +} + +impl AnimatedSpriteSheet { + /// Construct a new AnimatedSpriteSheet + pub fn new( + texture: Texture2D, + sprite_size: Vector2, + sheet_width: usize, + sprite_count: usize, + default_sprite_id: usize, + ) -> Self { + Self { + texture, + sprite_size, + sheet_width, + sprite_count, + default_sprite_id, + } + } + + pub fn render( + &self, + raylib: &mut T, + position: Vector2, + scaled_size: Option, + sprite_id: Option, + ) where + T: RaylibDraw, + { + let sprite_id = sprite_id.unwrap_or(self.default_sprite_id); + let sprite_id = if sprite_id >= self.sprite_count { + self.default_sprite_id + } else { + sprite_id + }; + + let sprite_rect = Rectangle::new( + (sprite_id % self.sheet_width) as f32 * self.sprite_size.x, + (sprite_id / self.sheet_width) as f32 * self.sprite_size.y, + self.sprite_size.x, + self.sprite_size.y, + ); + + let scaled_size = scaled_size.unwrap_or(self.sprite_size); + + raylib.draw_texture_pro( + &self.texture, + sprite_rect, + Rectangle::new(position.x, position.y, scaled_size.x, scaled_size.y), + Vector2::zero(), + 0.0, + Color::WHITE, + ); + } + + // { + // let sprite_id = match sprite_id { + // Some(id) => { + // if id >= self.sprite_count { + // self.default_sprite_id + // } else { + // id + // } + // } + // None => self.default_sprite_id, + // }; + + // let sprite_x = sprite_id % self.sheet_width; + // let sprite_y = sprite_id / self.sheet_width; + + // raylib.draw_texture_pro( + // &self.texture, + // Rectangle { + // x: sprite_x as f32, + // y: sprite_y as f32, + // width: self.sprite_size.x, + // height: self.sprite_size.y, + // }, + // Rectangle { + // x: position.x, + // y: position.y, + // width: self.sprite_size.x, + // height: self.sprite_size.y, + // }, + // Vector2::zero(), + // 0.0, + // Color::WHITE, + // ); + // } +} diff --git a/game/src/utilities/game_config.rs b/game/src/utilities/game_config.rs index 8fc9147..d91e544 100644 --- a/game/src/utilities/game_config.rs +++ b/game/src/utilities/game_config.rs @@ -20,6 +20,7 @@ pub struct ColorTheme { pub yellow: Color, pub pink: Color, pub background: Color, + pub white: Color, } #[derive(Debug, Clone, Deserialize)] @@ -28,6 +29,7 @@ pub struct GameConfig { pub base_window_size: (i32, i32), pub sentry_dsn: String, pub colors: ColorTheme, + pub animation_fps: usize, } impl GameConfig { diff --git a/game/src/utilities/mod.rs b/game/src/utilities/mod.rs index a8e4323..9c06995 100644 --- a/game/src/utilities/mod.rs +++ b/game/src/utilities/mod.rs @@ -6,3 +6,4 @@ pub mod math; pub mod non_ref_raylib; pub mod render_layer; pub mod shaders; +pub mod anim_render;