From 7c8e1dc21cb418bc6aaad47be85157953edafc07 Mon Sep 17 00:00:00 2001 From: gered Date: Sun, 15 May 2022 16:34:25 -0400 Subject: [PATCH] add balls_v2 example --- examples/balls_v2/Cargo.toml | 10 ++ examples/balls_v2/assets/balls.pcx | Bin 0 -> 1466 bytes examples/balls_v2/src/entities.rs | 252 +++++++++++++++++++++++++++++ examples/balls_v2/src/main.rs | 47 ++++++ examples/balls_v2/src/states.rs | 106 ++++++++++++ 5 files changed, 415 insertions(+) create mode 100644 examples/balls_v2/Cargo.toml create mode 100644 examples/balls_v2/assets/balls.pcx create mode 100644 examples/balls_v2/src/entities.rs create mode 100644 examples/balls_v2/src/main.rs create mode 100644 examples/balls_v2/src/states.rs diff --git a/examples/balls_v2/Cargo.toml b/examples/balls_v2/Cargo.toml new file mode 100644 index 0000000..4fde2f2 --- /dev/null +++ b/examples/balls_v2/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "balls_v2" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.55" +libretrogd = { path = "../../libretrogd", features = [] } +sdl2 = { version = "0.34.5", features = ["static-link", "bundled", "unsafe_textures" ] } + diff --git a/examples/balls_v2/assets/balls.pcx b/examples/balls_v2/assets/balls.pcx new file mode 100644 index 0000000000000000000000000000000000000000..e07f63302758ea0b6e079d5edda046218ae8d64d GIT binary patch literal 1466 zcmeH_p-;m=6vp4uE^Ww(CD$#5t?Zg05QvJ2i3tQ^a$*93n3$-TKp+q#AP@-T#3X+J z0)apv$m@k5kcbHc0*OE(nw0Nu3n74kfa8+)zRSD!@@u}ng>uO!oPF|Wy>w%)XGBW1 zHdZ^QNiu9FNvGZE?Ch)`^|~vY{oZlEJ?#&Y!NJRry6GB~4$RDtPeBDSWyFVJi}@W^#it4ZJF8< zwdJX-X?WVww1q7ywlHl`=EDq21Z-haN#TC9(V8~55u`6|-O|_F6aLM literal 0 HcmV?d00001 diff --git a/examples/balls_v2/src/entities.rs b/examples/balls_v2/src/entities.rs new file mode 100644 index 0000000..cecf095 --- /dev/null +++ b/examples/balls_v2/src/entities.rs @@ -0,0 +1,252 @@ +use libretrogd::*; +use libretrogd::entities::*; +use libretrogd::events::*; +use libretrogd::utils::rnd_value; + +use crate::states::*; + +pub const BALL_BASE_SPEED: i32 = 8; +pub const BOUNCE_PARTICLE_COLOR: u8 = 32; +pub const BOUNCE_PARTICLE_LIFETIME: f32 = 0.5; +pub const BOUNCE_PARTICLE_SPEED: i32 = 32; +pub const BALL_TRAIL_PARTICLE_INTERVAL: f32 = 0.25; +pub const TRAIL_PARTICLE_LIFETIME: f32 = 2.0; + +pub struct Position(Vector2); + +pub struct Velocity(Vector2); + +pub struct SpriteIndex(usize); + +pub struct BouncesAgainstEdge; + +pub struct Particle; + +pub struct Color(u8); + +pub struct LifeLeft { + pub life: f32, + pub initial: f32, +} + +pub struct LeavesTrail { + pub timer: f32, +} + +pub struct ColorByLifeTime(u8, u8, u8, u8, u8); + +pub enum Event { + CollideAgainstEdge(EntityId), + Kill(EntityId), + LeaveTrail(Vector2), +} + +fn new_basic_particle_entity(entities: &mut Entities, x: f32, y: f32, color: u8, lifetime: f32, angle: f32, speed: i32) { + let id = entities.new_entity(); + entities.add_component(id, Particle); + entities.add_component(id, Color(color)); + entities.add_component(id, LifeLeft { life: lifetime, initial: lifetime }); + entities.add_component(id, Position(Vector2::new(x, y))); + entities.add_component(id, Velocity(Vector2::from_angle(angle) * speed as f32)); +} + +fn new_trail_particle_entity(entities: &mut Entities, x: f32, y: f32, lifetime: f32) { + let id = entities.new_entity(); + entities.add_component(id, Particle); + entities.add_component(id, ColorByLifeTime(33, 26, 21, 16, 10)); + entities.add_component(id, LifeLeft { life: lifetime, initial: lifetime }); + entities.add_component(id, Position(Vector2::new(x, y))); +} + +fn new_bounce_particles(entities: &mut Entities, x: f32, y: f32) { + for direction in 0..6 { + let angle = direction as f32 * (RADIANS_360 / 6.0); + new_basic_particle_entity( + entities, + x, + y, + BOUNCE_PARTICLE_COLOR, + BOUNCE_PARTICLE_LIFETIME, + angle, + BOUNCE_PARTICLE_SPEED + ); + } +} + +fn new_ball_entity(entities: &mut Entities) { + let id = entities.new_entity(); + + let x: i32 = rnd_value(SCREEN_LEFT as i32 + 1, SCREEN_RIGHT as i32 - BALL_SIZE - 1); + let y: i32 = rnd_value(SCREEN_TOP as i32 + 1, SCREEN_BOTTOM as i32 - BALL_SIZE - 1); + + let speed = rnd_value(1, 3) * BALL_BASE_SPEED; + let vx = if rnd_value(0, 1) == 0 { -speed } else { speed }; + let vy = if rnd_value(0, 1) == 0 { -speed } else { speed }; + + let sprite_index = rnd_value(0, NUM_BALL_SPRITES - 1); + + entities.add_component(id, Position(Vector2::new(x as f32, y as f32))); + entities.add_component(id, Velocity(Vector2::new(vx as f32, vy as f32))); + entities.add_component(id, SpriteIndex(sprite_index)); + entities.add_component(id, BouncesAgainstEdge); + entities.add_component(id, LeavesTrail { timer: BALL_TRAIL_PARTICLE_INTERVAL }); +} + +fn update_system_movement(context: &mut Context) { + if let Some(mut positions) = context.entities.components_mut::() { + let velocities = context.entities.components::(); + + for (entity, position) in positions.iter_mut() { + if let Some(velocity) = velocities.get(&entity) { + position.0 += velocity.0 * context.delta; + } + } + } +} + +fn update_system_collision(context: &mut Context) { + if let Some(bounceable) = context.entities.components::() { + let mut positions = context.entities.components_mut::(); + let mut velocities = context.entities.components_mut::(); + + for (entity, _) in bounceable.iter() { + let mut position = positions.get_mut(&entity).unwrap(); + let mut velocity = velocities.get_mut(&entity).unwrap(); + + let mut bounced = false; + if position.0.x as i32 <= SCREEN_LEFT as i32 || position.0.x as i32 + BALL_SIZE >= SCREEN_RIGHT as i32 { + position.0.x -= velocity.0.x * context.delta; + velocity.0.x = -velocity.0.x; + bounced = true; + } + if position.0.y as i32 <= SCREEN_TOP as i32 || position.0.y as i32 + BALL_SIZE >= SCREEN_BOTTOM as i32 { + position.0.y -= velocity.0.y * context.delta; + velocity.0.y = -velocity.0.y; + bounced = true; + } + if bounced { + context.event_publisher.queue(Event::CollideAgainstEdge(*entity)); + } + } + } +} + +fn update_system_lifetime(context: &mut Context) { + if let Some(mut lifetimes) = context.entities.components_mut::() { + for (entity, lifetime) in lifetimes.iter_mut() { + lifetime.life -= context.delta; + if lifetime.life < 0.0 { + context.event_publisher.queue(Event::Kill(*entity)); + } + } + } +} + +fn update_system_leave_particle_trail(context: &mut Context) { + if let Some(mut leaves_trails) = context.entities.components_mut::() { + let positions = context.entities.components::(); + for (entity, leaves_trail) in leaves_trails.iter_mut() { + leaves_trail.timer -= context.delta; + if leaves_trail.timer <= 0.0 { + leaves_trail.timer = BALL_TRAIL_PARTICLE_INTERVAL; + let position = positions.get(&entity).unwrap(); + let mut trail_position = position.0; + trail_position.x += (BALL_SIZE / 2) as f32; + trail_position.y += (BALL_SIZE / 2) as f32; + context.event_publisher.queue(Event::LeaveTrail(trail_position)); + } + } + } +} + +fn render_system_sprites(context: &mut Context) { + if let Some(sprite_indices) = context.entities.components::() { + let positions = context.entities.components::(); + for (entity, sprite_index) in sprite_indices.iter() { + let position = positions.get(&entity).unwrap(); + context.system.video.blit( + BlitMethod::Transparent(0), + &context.sprites[sprite_index.0], + position.0.x as i32, + position.0.y as i32 + ); + } + } +} + +fn render_system_particles(context: &mut Context) { + if let Some(particles) = context.entities.components::() { + let positions = context.entities.components::(); + let colors = context.entities.components::(); + let colors_by_lifetime = context.entities.components::(); + let lifetimes = context.entities.components::(); + for (entity, _) in particles.iter() { + let position = positions.get(&entity).unwrap(); + + let pixel_color; + if let Some(color) = colors.get(&entity) { + pixel_color = Some(color.0); + } else if let Some(color_by_lifetime) = colors_by_lifetime.get(&entity) { + let lifetime = lifetimes.get(&entity).unwrap(); + let percent_life = lifetime.life / lifetime.initial; + pixel_color = Some(if percent_life >= 0.8 { + color_by_lifetime.0 + } else if percent_life >= 0.6 { + color_by_lifetime.1 + } else if percent_life >= 0.4 { + color_by_lifetime.2 + } else if percent_life >= 0.2 { + color_by_lifetime.3 + } else { + color_by_lifetime.4 + }); + } else { + pixel_color = None; + } + + if let Some(color) = pixel_color { + context.system.video.set_pixel(position.0.x as i32, position.0.y as i32, color); + } + } + } +} + +fn event_handler(event: &Event, context: &mut Context) -> bool { + match event { + Event::Kill(entity) => { + context.entities.remove_entity(*entity); + }, + Event::CollideAgainstEdge(entity) => { + let positions = context.entities.components::(); + let position = positions.get(entity).unwrap(); + let x = position.0.x + (BALL_SIZE / 2) as f32; + let y = position.0.y + (BALL_SIZE / 2) as f32; + drop(positions); + new_bounce_particles(&mut context.entities, x, y); + }, + Event::LeaveTrail(position) => { + new_trail_particle_entity(&mut context.entities, position.x, position.y, TRAIL_PARTICLE_LIFETIME); + } + } + false +} + +pub fn init_entities(entities: &mut Entities) { + entities.remove_all_entities(); + for _ in 0..NUM_BALLS { + new_ball_entity(entities); + } +} + +pub fn init_component_system(cs: &mut ComponentSystems) { + cs.add_update_system(update_system_movement); + cs.add_update_system(update_system_collision); + cs.add_update_system(update_system_lifetime); + cs.add_update_system(update_system_leave_particle_trail); + cs.add_render_system(render_system_particles); + cs.add_render_system(render_system_sprites); +} + +pub fn init_event_listeners(event_listeners: &mut EventListeners) { + event_listeners.add(event_handler); +} diff --git a/examples/balls_v2/src/main.rs b/examples/balls_v2/src/main.rs new file mode 100644 index 0000000..f76c596 --- /dev/null +++ b/examples/balls_v2/src/main.rs @@ -0,0 +1,47 @@ +use std::path::Path; + +use anyhow::Result; + +use libretrogd::*; +use libretrogd::events::*; +use libretrogd::states::*; + +use crate::entities::*; +use crate::states::*; + +mod entities; +mod states; + +fn main() -> Result<()> { + let system = SystemBuilder::new().window_title("Flying Balls").vsync(true).build()?; + let mut game = Game::new(system)?; + let mut states = States::new(); + states.push(SimulationState)?; + + let mut is_running = true; + + let tick_frequency = game.context.system.tick_frequency(); + let mut last_ticks = game.context.system.ticks(); + + while is_running && !states.is_empty() { + game.context.system.do_events_with(|event| { + if let sdl2::event::Event::Quit { .. } = event { + is_running = false; + } + }); + + let ticks = game.context.system.ticks(); + let elapsed = ticks - last_ticks; + last_ticks = ticks; + game.context.delta = (elapsed as f64 / tick_frequency as f64) as f32; + + states.update(&mut game)?; + + game.context.system.video.clear(0); + states.render(&mut game); + + game.context.system.display()?; + } + + Ok(()) +} diff --git a/examples/balls_v2/src/states.rs b/examples/balls_v2/src/states.rs new file mode 100644 index 0000000..4b06fb8 --- /dev/null +++ b/examples/balls_v2/src/states.rs @@ -0,0 +1,106 @@ +use sdl2::keyboard::Scancode; + +use libretrogd::entities::*; +use libretrogd::states::*; + +use crate::*; + +pub const BALL_SIZE: i32 = 8; +pub const NUM_BALLS: usize = 32; +pub const NUM_BALL_SPRITES: usize = 16; + +pub struct Context { + pub delta: f32, + pub system: System, + pub font: BitmaskFont, + pub sprites: Vec, + pub entities: Entities, + pub event_publisher: EventPublisher, +} + +pub struct Game { + pub context: Context, + pub component_systems: ComponentSystems, + pub event_listeners: EventListeners, +} + +impl Game { + pub fn new(mut system: System) -> Result { + let font = BitmaskFont::new_vga_font()?; + + let (balls_bmp, balls_palette) = Bitmap::load_pcx_file(Path::new("./assets/balls.pcx"))?; + system.palette = balls_palette.clone(); + + let mut sprites = Vec::new(); + for i in 0..NUM_BALL_SPRITES { + let mut sprite = Bitmap::new(BALL_SIZE as u32, BALL_SIZE as u32)?; + sprite.blit_region( + BlitMethod::Solid, + &balls_bmp, + &Rect::new(i as i32 * BALL_SIZE as i32, 0, BALL_SIZE as u32, BALL_SIZE as u32), + 0, + 0 + ); + sprites.push(sprite); + } + + let entities = Entities::new(); + let mut component_systems = ComponentSystems::new(); + let event_publisher = EventPublisher::new(); + let mut event_listeners = EventListeners::new(); + + init_component_system(&mut component_systems); + init_event_listeners(&mut event_listeners); + + Ok(Game { + context: Context { + delta: 0.0, + system, + font, + sprites, + entities, + event_publisher + }, + component_systems, + event_listeners + }) + } + + pub fn do_events(&mut self) { + self.event_listeners.take_queue_from(&mut self.context.event_publisher); + self.event_listeners.dispatch_queue(&mut self.context); + } +} + +pub struct SimulationState; + +impl GameState for SimulationState { + fn update(&mut self, _state: State, context: &mut Game) -> Option> { + if context.context.system.keyboard.is_key_up(Scancode::S) { + context.do_events(); + context.component_systems.update(&mut context.context); + } + + if context.context.system.keyboard.is_key_pressed(Scancode::Escape) { + return Some(StateChange::Pop); + } + + None + } + + fn render(&mut self, _state: State, context: &mut Game) { + context.context.system.video.clear(2); + context.component_systems.render(&mut context.context); + context.context.system.video.print_string("hello, world!", 10, 10, 15, &context.context.font); + } + + fn transition(&mut self, _state: State, _context: &mut Game) -> bool { + true + } + + fn state_change(&mut self, new_state: State, _old_state: State, context: &mut Game) { + if new_state == State::Pending { + init_entities(&mut context.context.entities); + } + } +} \ No newline at end of file