#[macro_use] extern crate specs_derive; #[macro_use] extern crate specs_system_macro; use ggez::graphics::Drawable; use specs::prelude::*; use specs::world::WorldExt; // * constants const WIDTH: f32 = 640.0; const HEIGHT: f32 = 480.0; const MAX_HP: u32 = 100; // * utility functions fn clamp(x: f32, min: f32, max: f32) -> f32 { if x < min { min } else if x > max { max } else { x } } // * components /// This is a component for things which can appear on the screen #[derive(Component)] pub struct Drawn { sprite: Sprite, } /// We only have three things which appear: the player, the hostile /// red dots, and the green 'collision' that appears when they hit /// each other pub enum Sprite { Player, Hostile, Kaboom, } impl Sprite { /// Convert one of the pre-known sprites to is intended visual appearance fn to_mesh(&self, ctx: &mut ggez::Context) -> ggez::GameResult { match self { Sprite::Player => { ggez::graphics::Mesh::new_circle( ctx, ggez::graphics::DrawMode::fill(), [0.0, 0.0], 10.0, 0.1, ggez::graphics::WHITE, ) }, Sprite::Hostile => { ggez::graphics::Mesh::new_circle( ctx, ggez::graphics::DrawMode::fill(), [0.0, 0.0], 10.0, 0.1, (1.0, 0.0, 0.0).into(), ) }, Sprite::Kaboom => { ggez::graphics::Mesh::new_circle( ctx, ggez::graphics::DrawMode::stroke(1.0), [0.0, 0.0], 20.0, 0.1, (0.0, 1.0, 0.0).into(), ) }, } } } /// A component for things which can be positioned somewhere on the /// screen #[derive(Component, Copy, Clone)] pub struct Position { x: f32, y: f32, } impl Position { /// Lazy collision detection: returns true if the two points are /// within 20 units of each other fn close_to(&self, other: &Position) -> bool{ let dx = self.x - other.x; let dy = self.y - other.y; (dx * dx + dy * dy).sqrt() < 20.0 } } /// A component for things which move around the screen at a specific /// velocity #[derive(Component)] pub struct Velocity { dx: f32, dy: f32, } /// A component for things which are player-controlled: that is, the /// player. #[derive(Component)] pub struct Controlled; /// A component for apply damage events #[derive(Component)] pub struct AppliesDamage { target: specs::Entity, } /// A component for things which are ephemeral: when lifetime hits 0, /// the thing is removed #[derive(Component)] pub struct Lifetime { lifetime: usize, } #[derive(Component)] pub struct HP { hp: u32, } // * KeyState impl /// A KeyState contains four bools that indicate whether particular /// keys are held (the only four keys we care about, the famous /// Gamer's Square: W, A, S, and who can forget D?) pub struct KeyState { w_pressed: bool, a_pressed: bool, s_pressed: bool, d_pressed: bool, } impl KeyState { /// By default, none of them are held down fn new() -> KeyState { KeyState { w_pressed: false, a_pressed: false, s_pressed: false, d_pressed: false, } } /// On a key-down event, we might flip some to true fn handle_down(&mut self, kc: winit::VirtualKeyCode) { if kc == winit::VirtualKeyCode::W { self.w_pressed = true; } if kc == winit::VirtualKeyCode::A { self.a_pressed = true; } if kc == winit::VirtualKeyCode::S { self.s_pressed = true; } if kc == winit::VirtualKeyCode::D { self.d_pressed = true; } } /// On a key-up event, we might flip some to false fn handle_up(&mut self, kc: winit::VirtualKeyCode) { if kc == winit::VirtualKeyCode::W { self.w_pressed = false; } if kc == winit::VirtualKeyCode::A { self.a_pressed = false; } if kc == winit::VirtualKeyCode::S { self.s_pressed = false; } if kc == winit::VirtualKeyCode::D { self.d_pressed = false; } } } // * systems /// The Draw system, which needs access to the ggez context in order /// to, uh, draw stuff struct Draw<'t> { pub ctx: &'t mut ggez::Context, } impl<'a, 't> specs::System<'a> for Draw<'t> { type SystemData = ( // Position and Drawn are for the usual screen entities specs::ReadStorage<'a, Position>, specs::ReadStorage<'a, Drawn>, // HP and Controlled is so we can display the player HP specs::ReadStorage<'a, HP>, specs::ReadStorage<'a, Controlled>, ); fn run(&mut self, (pos, draw, hp, controlled): Self::SystemData) { // clear to black ggez::graphics::clear(self.ctx, ggez::graphics::BLACK); // draw all the drawable things for (pos, draw) in (&pos, &draw).join() { let param = ggez::graphics::DrawParam { dest: [pos.x, pos.y].into(), ..ggez::graphics::DrawParam::default() }; draw.sprite.to_mesh(self.ctx).unwrap().draw(self.ctx, param).unwrap(); } // find the HP of the player and print it on the screen for (hp, _) in (&hp, &controlled).join() { let player_hp = format!("HP: {}/{}", hp.hp, MAX_HP); let display = ggez::graphics::Text::new(player_hp); display.draw(self.ctx, ([50.0, 50.0],).into()).unwrap(); } // show it, yo ggez::graphics::present(self.ctx).unwrap(); } } // The Control system is for adjusting the current velocity of things // based on the current state of the keys. The longer you hold keys, // the more it'll accelerate, up to some fixed speed (20 units per // tick) system!{ Control(resource kc: KeyState, _c: Controlled, mut v: Velocity) { const SPEEDUP: f32 = 0.2; if kc.w_pressed { v.dy -= SPEEDUP; } if kc.a_pressed { v.dx -= SPEEDUP; } if kc.s_pressed { v.dy += SPEEDUP; } if kc.d_pressed { v.dx += SPEEDUP; } v.dx = clamp(v.dx, -20.0, 20.0); v.dy = clamp(v.dy, -20.0, 20.0); } } // This applies friction to components, currently by a global amount system!{ Slowdown(mut v: Velocity, _c: Controlled) { const ROUNDING: f32 = 0.01; const FRICTION: f32 = 0.95; if v.dx.abs() < ROUNDING { v.dx = 0.0; } else { v.dx *= FRICTION; } if v.dy.abs() < ROUNDING { v.dy = 0.0 } else { v.dy *= FRICTION; } } } // This moves components around and handles wrapping them system!{ Move(mut pos: Position, v: Velocity) { pos.x += v.dx; pos.y += v.dy; if pos.x < 0.0 { pos.x += WIDTH; } else if pos.x > WIDTH { pos.x -= WIDTH; } if pos.y < 0.0 { pos.y += HEIGHT; } else if pos.y > HEIGHT { pos.y -= HEIGHT; } } } // This naively handles collision detection in a shitty way that // would never scale for anything real system_impl!{ Collision( en: Entity, mut pos: Position, hp: HP, mut lifetime: Lifetime, mut drawn: Drawn, mut damage: AppliesDamage, ) { let mut seen = std::collections::HashMap::new(); // just compare every entity to every other one for (e1, p1, _) in (&en, &pos, &hp).join() { for (e2, p2, _) in (&en, &pos, &hp).join() { // don't collide with ourselves, that's stupid if e1 == e2 { continue; } // if they're close and we haven't already looked at // this one (which we might have!) then add it to the // set of collisions if p1.close_to(p2) && !seen.contains_key(&(e1, e2)) { seen.insert((e1, e2), p1.clone()); } } } // now, for each collision for ((e1, e2), p) in seen { // create the kaboom graphic! it'll only last 10 ticks, // such is the nature of kabooms. (this should probably be // split apart into another system, but whatevs) let ping = en.create(); lifetime.insert(ping, Lifetime { lifetime: 10 }).unwrap(); drawn.insert(ping, Drawn { sprite: Sprite::Kaboom }).unwrap(); pos.insert(ping, p).unwrap(); // and create the damage events let ev1 = en.create(); damage.insert(ev1, AppliesDamage { target: e1 }).unwrap(); let ev2 = en.create(); damage.insert(ev2, AppliesDamage { target: e2 }).unwrap(); } } } // This handles lifetimes ticking back down, and removing entities if // their lifetimes get too low system_impl! { TheInexorablePassageOfTime(en: Entity, mut l: Lifetime) { for (e, mut l) in (&en, &mut l).join() { l.lifetime -= 1; if l.lifetime == 0 { en.delete(e).unwrap(); } } } } // This handles applying damage to entities, and removing them if // their HP gets too low. system_impl! { TheSlingsAndArrowsOfOutrageousFortune( en: Entity, mut dmg: AppliesDamage, mut hp: HP, ) { for dmg in (&dmg).join() { if let Some(target) = hp.get_mut(dmg.target) { target.hp -= 1; if target.hp == 0 { en.delete(dmg.target).unwrap(); } } } dmg.clear(); } } // * game definition /// A game just needs a specs world! All our state is in there struct MyGame { pub world: specs::World, } impl MyGame { fn setup() -> MyGame { // this is the first step in making apple pie from scratch let mut world = specs::World::new(); // register some component types world.register::(); world.register::(); world.register::(); world.register::(); world.register::(); world.register::(); world.register::(); // create our blank key state world.insert(KeyState::new()); // create a player world.create_entity() .with(Drawn { sprite: Sprite::Player }) .with(Position { x: 200.0, y: 200.0 }) .with(Controlled) .with(Velocity { dx: 0.0, dy: 0.0 }) .with(HP { hp: MAX_HP }) .build(); // create ten ENEMY ORBS!!!!!!!11111one for _ in 0..10 { let x = rand::random::() * WIDTH; let y = rand::random::() * HEIGHT; let dx = (rand::random::() * 20.0) - 10.0; let dy = (rand::random::() * 20.0) - 10.0; world.create_entity() .with(Drawn { sprite: Sprite::Hostile }) .with(Position { x, y }) .with(Velocity { dx, dy }) .with(HP { hp: 50 }) .build(); } // this is the world I have created. observe it and despair MyGame { world } } } impl ggez::event::EventHandler for MyGame { // To draw things, we just run the Draw system fn draw(&mut self, ctx: &mut ggez::Context) -> ggez::GameResult<()> { Draw { ctx }.run_now(&self.world); Ok(()) } // To update things, we just run a bunch of these systems, and // make sure we call maintain fn update(&mut self, _ctx: &mut ggez::Context) -> ggez::GameResult<()> { TheSlingsAndArrowsOfOutrageousFortune.run_now(&self.world); Slowdown.run_now(&self.world); Control.run_now(&self.world); Move.run_now(&self.world); Collision.run_now(&self.world); TheInexorablePassageOfTime.run_now(&self.world); self.world.maintain(); Ok(()) } // to handle events, we modify the stored KeyState resource fn key_down_event( &mut self, ctx: &mut ggez::Context, keycode: winit::VirtualKeyCode, _keymod: ggez::event::KeyMods, _repeat: bool, ) { if keycode == winit::VirtualKeyCode::Escape { ggez::event::quit(ctx); } KeyState::handle_down(&mut self.world.write_resource(), keycode); } fn key_up_event( &mut self, _ctx: &mut ggez::Context, keycode: winit::VirtualKeyCode, _keymod: ggez::event::KeyMods, ) { KeyState::handle_up(&mut self.world.write_resource(), keycode); } } // * main fn main() -> ggez::GameResult<()> { // And here, we make a context and an event loop let (mut ctx, mut evloop) = ggez::ContextBuilder::new("game", "me") // make our window a, uh, size .window_mode(ggez::conf::WindowMode { width: WIDTH, height: HEIGHT, ..ggez::conf::WindowMode::default() }) // and build it .build()?; // then run the shit yo let mut my_game = MyGame::setup(); ggez::event::run(&mut ctx, &mut evloop, &mut my_game) }