diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..3393fda --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,3 @@ +newline_style = "Unix" +# The "Default" setting has a heuristic which splits lines too aggresively. +use_small_heuristics = "Max" diff --git a/Cargo.toml b/Cargo.toml index c9c421d..307f148 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,11 @@ repository = "https://github.com/o2sh/teki" [dependencies] +clap = "2.33.3" +specs = "0.16.1" +specs-derive = "0.4" + +[dependencies.sdl2] +version = "0.34.3" +default-features = false +features = ["image"] \ No newline at end of file diff --git a/assets/neko.png b/assets/neko.png new file mode 100644 index 0000000..db4ef63 Binary files /dev/null and b/assets/neko.png differ diff --git a/src/components.rs b/src/components.rs new file mode 100644 index 0000000..1d6320c --- /dev/null +++ b/src/components.rs @@ -0,0 +1,33 @@ +use sdl2::rect::{Point, Rect}; +use specs::prelude::*; +use specs_derive::Component; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Direction { + Up, + Down, + Left, + Right, +} + +#[derive(Component, Debug, Default)] +#[storage(NullStorage)] +pub struct KeyboardControlled; + +#[derive(Component, Debug)] +#[storage(VecStorage)] +pub struct Position(pub Point); + +#[derive(Component, Debug)] +#[storage(VecStorage)] +pub struct Velocity { + pub speed: i32, + pub direction: Direction, +} + +#[derive(Component, Debug)] +#[storage(VecStorage)] +pub struct Sprite { + pub spritesheet: usize, + pub region: Rect, +} diff --git a/src/keyboard.rs b/src/keyboard.rs new file mode 100644 index 0000000..9367ffe --- /dev/null +++ b/src/keyboard.rs @@ -0,0 +1,34 @@ +use specs::prelude::*; + +use crate::components::*; + +use super::MovementCommand; + +const PLAYER_MOVEMENT_SPEED: i32 = 20; + +pub struct Keyboard; + +impl<'a> System<'a> for Keyboard { + type SystemData = ( + ReadExpect<'a, Option>, + ReadStorage<'a, KeyboardControlled>, + WriteStorage<'a, Velocity>, + ); + + fn run(&mut self, mut data: Self::SystemData) { + let movement_command = match &*data.0 { + Some(movement_command) => movement_command, + None => return, // no change + }; + + for (_, vel) in (&data.1, &mut data.2).join() { + match movement_command { + &MovementCommand::Move(direction) => { + vel.speed = PLAYER_MOVEMENT_SPEED; + vel.direction = direction; + } + MovementCommand::Stop => vel.speed = 0, + } + } + } +} diff --git a/src/main.rs b/src/main.rs index e7a11a9..941df48 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,123 @@ -fn main() { - println!("Hello, world!"); +mod components; +mod keyboard; +mod physics; +mod renderer; + +use clap::{crate_description, crate_name, crate_version, App}; + +use sdl2::event::Event; +use sdl2::image::{self, InitFlag, LoadTexture}; +use sdl2::keyboard::Keycode; +use sdl2::pixels::Color; +use sdl2::rect::{Point, Rect}; +use specs::prelude::*; +use std::time::Duration; + +use crate::components::*; + +pub enum MovementCommand { + Stop, + Move(Direction), +} + +fn main() -> Result<(), String> { + App::new(crate_name!()).version(crate_version!()).about(crate_description!()); + + let sdl_context = sdl2::init()?; + let video_subsystem = sdl_context.video()?; + let _image_context = image::init(InitFlag::PNG)?; + let window = video_subsystem + .window("game tutorial", 800, 600) + .position_centered() + .build() + .expect("could not initialize video subsystem"); + + let mut canvas = window.into_canvas().build().expect("could not make a canvas"); + + let texture_creator = canvas.texture_creator(); + + let mut dispatcher = DispatcherBuilder::new() + .with(keyboard::Keyboard, "Keyboard", &[]) + .with(physics::Physics, "Physics", &["Keyboard"]) + .build(); + + let mut world = World::new(); + dispatcher.setup(&mut world); + renderer::SystemData::setup(&mut world); + + let movement_command: Option = None; + world.insert(movement_command); + + let textures = [texture_creator.load_texture("assets/neko.png")?]; + + let spritesheet = 0; + let player_top_left_frame = Rect::new(0, 0, 45, 45); + let (frame_width, frame_height) = player_top_left_frame.size(); + + let sprite = Sprite { + spritesheet, + region: Rect::new( + player_top_left_frame.x(), + player_top_left_frame.y(), + frame_width, + frame_height, + ), + }; + + world + .create_entity() + .with(KeyboardControlled) + .with(Position(Point::new(0, 0))) + .with(Velocity { speed: 0, direction: Direction::Right }) + .with(sprite) + .build(); + + let mut event_pump = sdl_context.event_pump()?; + let mut i = 0; + 'running: loop { + let mut movement_command = None; + + // Handle events + for event in event_pump.poll_iter() { + match event { + Event::Quit { .. } | Event::KeyDown { keycode: Some(Keycode::Escape), .. } => { + break 'running; + } + Event::KeyDown { keycode: Some(Keycode::Left), repeat: false, .. } => { + movement_command = Some(MovementCommand::Move(Direction::Left)); + } + Event::KeyDown { keycode: Some(Keycode::Right), repeat: false, .. } => { + movement_command = Some(MovementCommand::Move(Direction::Right)); + } + Event::KeyDown { keycode: Some(Keycode::Up), repeat: false, .. } => { + movement_command = Some(MovementCommand::Move(Direction::Up)); + } + Event::KeyDown { keycode: Some(Keycode::Down), repeat: false, .. } => { + movement_command = Some(MovementCommand::Move(Direction::Down)); + } + Event::KeyUp { keycode: Some(Keycode::Left), repeat: false, .. } + | Event::KeyUp { keycode: Some(Keycode::Right), repeat: false, .. } + | Event::KeyUp { keycode: Some(Keycode::Up), repeat: false, .. } + | Event::KeyUp { keycode: Some(Keycode::Down), repeat: false, .. } => { + movement_command = Some(MovementCommand::Stop); + } + _ => {} + } + } + + *world.write_resource() = movement_command; + + // Update + i = (i + 1) % 255; + dispatcher.dispatch(&mut world); + world.maintain(); + + // Render + renderer::render(&mut canvas, Color::RGB(i, 64, 255 - i), &textures, world.system_data())?; + + // Time management! + ::std::thread::sleep(Duration::new(0, 1_000_000_000u32 / 20)); + } + + Ok(()) } diff --git a/src/physics.rs b/src/physics.rs new file mode 100644 index 0000000..b863faf --- /dev/null +++ b/src/physics.rs @@ -0,0 +1,29 @@ +use specs::prelude::*; + +use crate::components::*; + +pub struct Physics; + +impl<'a> System<'a> for Physics { + type SystemData = (WriteStorage<'a, Position>, ReadStorage<'a, Velocity>); + + fn run(&mut self, mut data: Self::SystemData) { + use self::Direction::*; + for (pos, vel) in (&mut data.0, &data.1).join() { + match vel.direction { + Left => { + pos.0 = pos.0.offset(-vel.speed, 0); + } + Right => { + pos.0 = pos.0.offset(vel.speed, 0); + } + Up => { + pos.0 = pos.0.offset(0, -vel.speed); + } + Down => { + pos.0 = pos.0.offset(0, vel.speed); + } + } + } + } +} diff --git a/src/renderer.rs b/src/renderer.rs new file mode 100644 index 0000000..1c72c89 --- /dev/null +++ b/src/renderer.rs @@ -0,0 +1,35 @@ +use sdl2::pixels::Color; +use sdl2::rect::{Point, Rect}; +use sdl2::render::{Texture, WindowCanvas}; +use specs::prelude::*; + +use crate::components::*; + +// Type alias for the data needed by the renderer +pub type SystemData<'a> = (ReadStorage<'a, Position>, ReadStorage<'a, Sprite>); + +pub fn render( + canvas: &mut WindowCanvas, + background: Color, + textures: &[Texture], + data: SystemData, +) -> Result<(), String> { + canvas.set_draw_color(background); + canvas.clear(); + + let (width, height) = canvas.output_size()?; + + for (pos, sprite) in (&data.0, &data.1).join() { + let current_frame = sprite.region; + + // Treat the center of the screen as the (0, 0) coordinate + let screen_position = pos.0 + Point::new(width as i32 / 2, height as i32 / 2); + let screen_rect = + Rect::from_center(screen_position, current_frame.width(), current_frame.height()); + canvas.copy(&textures[sprite.spritesheet], current_frame, screen_rect)?; + } + + canvas.present(); + + Ok(()) +}