From a7b75269e89e325997c5772d913b938d8602ce7b Mon Sep 17 00:00:00 2001 From: Max Bradbury Date: Wed, 2 Sep 2020 17:54:49 +0100 Subject: [PATCH] move player to this repo --- Cargo.toml | 1 + src/bin/player.rs | 302 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 303 insertions(+) create mode 100644 src/bin/player.rs diff --git a/Cargo.toml b/Cargo.toml index 79ea96c..f22a206 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +ggez = "0.5.1" toml = "^0.5.6" serde = "^1.0.114" serde_derive = "^1.0.114" diff --git a/src/bin/player.rs b/src/bin/player.rs new file mode 100644 index 0000000..4ca1bfd --- /dev/null +++ b/src/bin/player.rs @@ -0,0 +1,302 @@ +use ggez; + +// Next we need to actually `use` the pieces of ggez that we are going +// to need frequently. +use ggez::event::{KeyCode, KeyMods, EventsLoop}; +use ggez::{event, graphics, Context, GameResult}; + +// We'll bring in some things from `std` to help us in the future. +use std::time::{Duration, Instant}; + +use ggez::graphics::{Rect}; +use ggez::conf::FullscreenType; + +// The first thing we want to do is set up some constants that will help us out later. + +// Here we define the size of our game board in terms of how many grid +// cells it will take up. We choose to make a 30 x 20 game board. +const GRID_SIZE: (u8, u8) = (16, 9); +// dimension, i.e. 8×8 (square) +const GRID_CELL_SIZE: u8 = 8; + +const UPDATES_PER_SECOND: f32 = 0.4; +// And we get the milliseconds of delay that this update rate corresponds to. +const MILLIS_PER_UPDATE: u64 = (1.0 / UPDATES_PER_SECOND * 1000.0) as u64; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +struct GridPosition { x: u8, y: u8 } + +impl GridPosition { + /// We make a standard helper function so that we can create a new `GridPosition` + /// more easily. + pub fn new(x: u8, y: u8) -> Self { + GridPosition { x, y } + } + + /// We'll make another helper function that takes one grid position and returns a new one after + /// making one move in the direction of `dir`. We use our `SignedModulo` trait + /// above, which is now implemented on `i16` because it satisfies the trait bounds, + /// to automatically wrap around within our grid size if the move would have otherwise + /// moved us off the board to the top, bottom, left, or right. + pub fn new_from_move(pos: GridPosition, dir: Direction) -> Self { + match dir { + Direction::Up => GridPosition::new(pos.x, pos.y - 1), + Direction::Down => GridPosition::new(pos.x, pos.y + 1), + Direction::Left => GridPosition::new(pos.x - 1, pos.y), + Direction::Right => GridPosition::new(pos.x + 1, pos.y), + } + } +} + +/// We implement the `From` trait, which in this case allows us to convert easily between +/// a GridPosition and a ggez `graphics::Rect` which fills that grid cell. +/// Now we can just call `.into()` on a `GridPosition` where we want a +/// `Rect` that represents that grid cell. +impl From for graphics::Rect { + fn from(pos: GridPosition) -> Self { + graphics::Rect::new_i32( + pos.x as i32 * GRID_CELL_SIZE as i32, + pos.y as i32 * GRID_CELL_SIZE as i32, + GRID_CELL_SIZE as i32, + GRID_CELL_SIZE as i32, + ) + } +} + +impl From<(u8, u8)> for GridPosition { + fn from(pos: (u8, u8)) -> Self { + GridPosition { x: pos.0, y: pos.1 } + } +} + +/// Next we create an enum that will represent all the possible +/// directions that our snake could move. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Direction { + Up, + Down, + Left, + Right, +} + +impl Direction { + /// We also create a helper function that will let us convert between a + /// `ggez` `Keycode` and the `Direction` that it represents. Of course, + /// not every keycode represents a direction, so we return `None` if this + /// is the case. + pub fn from_keycode(key: KeyCode) -> Option { + match key { + KeyCode::Up => Some(Direction::Up), + KeyCode::Down => Some(Direction::Down), + KeyCode::Left => Some(Direction::Left), + KeyCode::Right => Some(Direction::Right), + _ => None, + } + } +} + +struct Avatar { + pos: GridPosition, +} + +impl Avatar { + pub fn new(pos: GridPosition) -> Self { + Avatar { + pos + } + } + + /// The main update function for our snake which gets called every time + /// we want to update the game state. + fn update(&mut self) { + + } + + /// Again, note that this approach to drawing is fine for the limited scope of this + /// example, but larger scale games will likely need a more optimized render path + /// using SpriteBatch or something similar that batches draw calls. + fn draw(&self, ctx: &mut Context, multiplier: &u8) -> GameResult<()> { + let dimension = (GRID_CELL_SIZE * multiplier) as f32; + + // And then we do the same for the head, instead making it fully red to distinguish it. + let rectangle = graphics::Mesh::new_rectangle( + ctx, + graphics::DrawMode::fill(), + ggez::graphics::Rect { + x: (self.pos.x as u16 * GRID_CELL_SIZE as u16 * *multiplier as u16) as f32, + y: (self.pos.y as u16 * GRID_CELL_SIZE as u16 * *multiplier as u16) as f32, + w: dimension, + h: dimension, + }, + [1.0, 0.5, 0.0, 1.0].into(), + )?; + + graphics::draw(ctx, &rectangle, (ggez::mint::Point2 { x: 0.0, y: 0.0 },))?; + Ok(()) + } +} + +/// Now we have the heart of our game, the GameState. This struct +/// will implement ggez's `EventHandler` trait and will therefore drive +/// everything else that happens in our game. +struct GameState { + avatar: Avatar, + /// And we track the last time we updated so that we can limit + /// our update rate. + last_update: Instant, + /// integer multiples for scaling the display + size_multiplier: u8, + fullscreen: bool, + scene_width: u8, + scene_height: u8, +} + +impl GameState { + pub fn new() -> Self { + let avatar = Avatar { + pos: (GRID_SIZE.0 / 4, GRID_SIZE.1 / 2).into(), + }; + + GameState { + avatar, + last_update: Instant::now(), + size_multiplier: 4, + fullscreen: false, + scene_width: 16, + scene_height: 9 + } + } + + fn toggle_fullscreen(&mut self) { + self.fullscreen = !self.fullscreen; + } +} + +/// Now we implement EventHandler for GameState. This provides an interface +/// that ggez will call automatically when different events happen. +impl event::EventHandler for GameState { + /// Update will happen on every frame before it is drawn. This is where we update + /// our game state to react to whatever is happening in the game world. + fn update(&mut self, _ctx: &mut Context) -> GameResult { + // First we check to see if enough time has elapsed since our last update based on + // the update rate we defined at the top. + if Instant::now() - self.last_update >= Duration::from_millis(MILLIS_PER_UPDATE) { + self.avatar.update(); + + // If we updated, we set our last_update to be now + self.last_update = Instant::now(); + } + // Finally we return `Ok` to indicate we didn't run into any errors + Ok(()) + } + + /// draw is where we should actually render the game's current state. + fn draw(&mut self, ctx: &mut Context) -> GameResult { + graphics::clear(ctx, [0.1, 0.1, 0.1, 1.0].into()); + + // todo draw the whole game, not just the avatar + self.avatar.draw(ctx, &self.size_multiplier)?; + + // Finally we call graphics::present to cycle the GPU's framebuffer and display + // the new frame we just drew. + graphics::present(ctx)?; + + // We yield the current thread until the next update + ggez::timer::yield_now(); + + Ok(()) + } + + /// key_down_event gets fired when a key gets pressed. + fn key_down_event( + &mut self, + ctx: &mut Context, + keycode: KeyCode, + modifier: KeyMods, + _repeat: bool, + ) { + if let Some(dir) = Direction::from_keycode(keycode) { + match dir { + Direction::Up => { + if self.avatar.pos.y > 0 { + self.avatar.pos.y -= 1 + } + } + Direction::Right => { + if self.avatar.pos.x < GRID_SIZE.0 - 1 { + self.avatar.pos.x += 1 + } + } + Direction::Down => { + // if y is less than 8 it's ok to increment it + if self.avatar.pos.y < GRID_SIZE.1 - 1 { + self.avatar.pos.y += 1 + } + } + Direction::Left => { + if self.avatar.pos.x > 0 { + self.avatar.pos.x -= 1 + } + } + } + } + + // todo handle plus/minus keys + if keycode == KeyCode::Add { + self.size_multiplier += 1; + } else if keycode == KeyCode::Subtract && self.size_multiplier > 1 { + self.size_multiplier -= 1; + } + + if keycode == KeyCode::F11 || (modifier == KeyMods::ALT && keycode == KeyCode::Return) { + self.toggle_fullscreen(); + + if self.fullscreen { + graphics::set_fullscreen(ctx, FullscreenType::True).unwrap(); + } else { + graphics::set_fullscreen(ctx, FullscreenType::Windowed).unwrap(); + graphics::set_drawable_size( + ctx, + (self.scene_width * self.size_multiplier) as f32, + (self.scene_height * self.size_multiplier) as f32 + ).unwrap(); + } + } + + // todo change window size + } +} + +fn window_setup(x: u8, y: u8, multiplier: u8) -> (Context, EventsLoop) { + let x = (x as u16 * GRID_CELL_SIZE as u16 * multiplier as u16) as f32; + let y = (y as u16 * GRID_CELL_SIZE as u16 * multiplier as u16) as f32; + + // Here we use a ContextBuilder to setup metadata about our game. First the title and author + let (ctx, events_loop) = ggez::ContextBuilder::new("snake", "Gray Olson") + // Next we set up the window. This title will be displayed in the title bar of the window. + .window_setup( + ggez::conf::WindowSetup::default().title("Write your game's title here") + ) + // Now we get to set the size of the window, which we use our SCREEN_SIZE constant from earlier to help with + .window_mode(ggez::conf::WindowMode::default().dimensions(x, y)) + // And finally we attempt to build the context and create the window. If it fails, we panic with the message + // "Failed to build ggez context" + .build() + .unwrap(); + + (ctx, events_loop) +} + +fn main() -> GameResult { + // Next we create a new instance of our GameState struct, which implements EventHandler + let state = &mut GameState::new(); + + let (mut ctx, mut events_loop) = window_setup( + GRID_SIZE.0, + GRID_SIZE.1, + state.size_multiplier + ); + + // And finally we actually run our game, passing in our context and state. + event::run(&mut ctx, &mut events_loop, state) +}