diff --git a/Cargo.lock b/Cargo.lock index f599a1b..8efcecc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -304,6 +304,7 @@ checksum = "342f7e9335416dc98642d5747c4ed8a6ad9f7244a36d5b2b7a1b7910e4d8f524" dependencies = [ "bevy_dylib", "bevy_internal", + "tracing", ] [[package]] @@ -369,6 +370,7 @@ dependencies = [ "downcast-rs 2.0.2", "log", "thiserror 2.0.17", + "tracing", "variadics_please", "wasm-bindgen", "web-sys", @@ -586,6 +588,7 @@ dependencies = [ "slotmap", "smallvec", "thiserror 2.0.17", + "tracing", "variadics_please", ] @@ -826,6 +829,7 @@ dependencies = [ "bevy_platform", "bevy_utils", "tracing", + "tracing-error", "tracing-log", "tracing-oslog", "tracing-subscriber", @@ -1062,6 +1066,7 @@ dependencies = [ "naga", "nonmax", "offset-allocator", + "profiling", "send_wrapper", "smallvec", "thiserror 2.0.17", @@ -3360,6 +3365,20 @@ name = "profiling" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", + "tracing", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn", +] [[package]] name = "pxfm" @@ -4047,6 +4066,16 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-error" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" +dependencies = [ + "tracing", + "tracing-subscriber", +] + [[package]] name = "tracing-log" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 78129b5..7534bde 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ bevy = { version = "0.17.2", default-features = false, features = [ "bevy_window", "bevy_winit", "bevy_state", + "multi_threaded", "png", "std", "webgl2", @@ -25,7 +26,6 @@ bevy = { version = "0.17.2", default-features = false, features = [ bevy_embedded_assets = "0.14.0" leafwing-input-manager = "0.18.0" log = { version = "*", features = [ - "max_level_debug", "release_max_level_warn", ] } rand = "0.9.2" @@ -37,12 +37,11 @@ getrandom = { version = "0.3", features = ["wasm_js"] } # Default to a native dev build. default = ["dev_native"] dev = [ - # Improve compile times for dev builds by linking Bevy as a dynamic library. "bevy/dynamic_linking", "bevy/bevy_dev_tools", "bevy/bevy_ui_debug", - # Improve error messages coming from Bevy "bevy/track_location", + "bevy/trace", ] dev_native = [ "dev", diff --git a/assets/palette.hex b/assets/palette.hex new file mode 100644 index 0000000..959726d --- /dev/null +++ b/assets/palette.hex @@ -0,0 +1,46 @@ +172038 +253a5e +3c5e8b +4f8fba +73bed3 +a4dddb +19332d +25562e +468232 +75a743 +a8ca58 +d0da91 +4d2b32 +7a4841 +ad7757 +c09473 +d7b594 +e7d5b3 +341c27 +602c2c +884b2b +be772b +de9e41 +e8c170 +241527 +411d31 +752438 +a53030 +cf573c +da863e +1e1d39 +402751 +7a367b +a23e8c +c65197 +df84a5 +090a14 +10141f +151d28 +202e37 +394a50 +577277 +819796 +a8b5b2 +c7cfcc +ebede9 diff --git a/assets/palette.png b/assets/palette.png new file mode 100644 index 0000000..3d347c3 Binary files /dev/null and b/assets/palette.png differ diff --git a/assets/sprites/asteroids/asteroid3.aseprite b/assets/sprites/asteroids/asteroid3.aseprite new file mode 100644 index 0000000..3ad60bb Binary files /dev/null and b/assets/sprites/asteroids/asteroid3.aseprite differ diff --git a/assets/sprites/asteroids/asteroid3.png b/assets/sprites/asteroids/asteroid3.png new file mode 100644 index 0000000..1539c78 Binary files /dev/null and b/assets/sprites/asteroids/asteroid3.png differ diff --git a/assets/sprites/asteroids/asteroid4.aseprite b/assets/sprites/asteroids/asteroid4.aseprite new file mode 100644 index 0000000..e414fce Binary files /dev/null and b/assets/sprites/asteroids/asteroid4.aseprite differ diff --git a/assets/sprites/asteroids/asteroid4.png b/assets/sprites/asteroids/asteroid4.png new file mode 100644 index 0000000..4a100a1 Binary files /dev/null and b/assets/sprites/asteroids/asteroid4.png differ diff --git a/assets/sprites/projectiles/projectile1.aseprite b/assets/sprites/projectiles/projectile1.aseprite new file mode 100644 index 0000000..c8eefba Binary files /dev/null and b/assets/sprites/projectiles/projectile1.aseprite differ diff --git a/assets/sprites/projectiles/projectile1.png b/assets/sprites/projectiles/projectile1.png new file mode 100644 index 0000000..becbf82 Binary files /dev/null and b/assets/sprites/projectiles/projectile1.png differ diff --git a/assets/sprites/ships/ship3.aseprite b/assets/sprites/ships/ship3.aseprite new file mode 100644 index 0000000..6faefb5 Binary files /dev/null and b/assets/sprites/ships/ship3.aseprite differ diff --git a/assets/sprites/ships/ship3.png b/assets/sprites/ships/ship3.png new file mode 100644 index 0000000..2607868 Binary files /dev/null and b/assets/sprites/ships/ship3.png differ diff --git a/src/main.rs b/src/main.rs index ff93bbd..12e257d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,11 @@ use bevy::{ asset::AssetMetaCheck, + camera::{RenderTarget, visibility::RenderLayers}, prelude::*, - window::{WindowMode, WindowResolution}, + render::render_resource::{ + Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, + }, + window::{WindowMode, WindowResized, WindowResolution}, }; use bevy_embedded_assets::{EmbeddedAssetPlugin, PluginMode}; @@ -10,6 +14,21 @@ use crate::input::InputPlugin; mod input; mod screens; +const RES_WIDTH: u32 = 853; +const RES_HEIGHT: u32 = 480; + +const PIXEL_PERFECT_LAYERS: RenderLayers = RenderLayers::layer(0); +const HIGH_RES_LAYERS: RenderLayers = RenderLayers::layer(1); + +#[derive(Component)] +struct Canvas; + +#[derive(Component)] +struct InGameCamera; + +#[derive(Component)] +struct OuterCamera; + fn main() -> AppExit { App::new().add_plugins(AppPlugin).run() } @@ -50,8 +69,9 @@ impl Plugin for AppPlugin { app.init_state::(); app.configure_sets(Update, PausableSystems.run_if(in_state(Pause(false)))); - app.insert_resource(ClearColor(Color::srgb_u8(49, 54, 56))); + app.insert_resource(ClearColor(Color::srgb_u8(9, 10, 20))); app.add_systems(Startup, spawn_camera); + app.add_systems(Update, fit_canvas.run_if(on_message::)); } } @@ -61,6 +81,58 @@ struct Pause(pub bool); #[derive(SystemSet, Copy, Clone, Eq, PartialEq, Hash, Debug)] struct PausableSystems; -fn spawn_camera(mut commands: Commands) { - commands.spawn((Name::new("Camera"), Camera2d)); +fn spawn_camera(mut commands: Commands, mut images: ResMut>) { + let canvas_size = Extent3d { + width: RES_WIDTH, + height: RES_HEIGHT, + ..default() + }; + let mut canvas = Image { + texture_descriptor: TextureDescriptor { + label: Some("Canvas Texture"), + size: canvas_size, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba8UnormSrgb, + mip_level_count: 1, + sample_count: 1, + usage: TextureUsages::COPY_DST + | TextureUsages::TEXTURE_BINDING + | TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }, + ..default() + }; + canvas.resize(canvas_size); + let image_handle = images.add(canvas); + commands.spawn(( + Camera2d, + Camera { + order: -1, + target: RenderTarget::Image(image_handle.clone().into()), + clear_color: ClearColorConfig::Custom(Color::srgb_u8(9, 10, 20)), + ..default() + }, + Msaa::Off, + InGameCamera, + PIXEL_PERFECT_LAYERS, + )); + + commands.spawn((Sprite::from_image(image_handle), Canvas, HIGH_RES_LAYERS)); + + commands.spawn((Camera2d, Msaa::Off, OuterCamera, HIGH_RES_LAYERS)); +} + +fn fit_canvas( + mut resize_messages: MessageReader, + mut projection: Single<&mut Projection, With>, +) { + let Projection::Orthographic(proj) = &mut **projection else { + return; + }; + + for msg in resize_messages.read() { + let h_scale = msg.width / RES_WIDTH as f32; + let v_scale = msg.height / RES_HEIGHT as f32; + proj.scale = 1. / h_scale.min(v_scale); + } } diff --git a/src/screens/game.rs b/src/screens/game.rs index d0dd69a..db6a9c5 100644 --- a/src/screens/game.rs +++ b/src/screens/game.rs @@ -2,7 +2,7 @@ use bevy::prelude::*; use leafwing_input_manager::prelude::*; use rand::random_range; -use crate::{input::Action, screens::Screen}; +use crate::{PIXEL_PERFECT_LAYERS, input::Action, screens::Screen}; #[derive(States, Copy, Clone, Eq, PartialEq, Hash, Debug, Default)] pub enum GameState { @@ -57,22 +57,21 @@ fn spawn_game_screen( ) { commands.spawn(( Name::new("Player Ship"), - Player, + Player { + can_shoot: true, + timer: Timer::from_seconds(0.15, TimerMode::Once), + }, DespawnOnExit(Screen::Game), Sprite { - image: asset_server.load("sprites/ships/ship1.png"), - custom_size: Some(Vec2::splat(64.0)), + image: asset_server.load("sprites/ships/ship3.png"), + custom_size: Some(Vec2::splat(32.0)), ..default() }, - PlayerShotTimer(Timer::from_seconds(0.15, TimerMode::Repeating)), - children![( - Name::new("Player Hitbox"), - Sprite { - image: asset_server.load("sprites/ships/ship1-hitbox.png"), - custom_size: Some(Vec2::splat(64.0)), - ..default() - } - )], + Transform { + translation: Vec3::new(0.0, -200.0, 0.0), + ..default() + }, + PIXEL_PERFECT_LAYERS, )); time.unpause(); @@ -104,14 +103,14 @@ enum GameSystems { } #[derive(Component)] -struct Player; +struct Player { + pub can_shoot: bool, + pub timer: Timer, +} #[derive(Component)] struct Projectile; -#[derive(Component)] -struct PlayerShotTimer(Timer); - #[derive(Component)] struct Asteroid { pub speed: f32, @@ -124,13 +123,17 @@ struct AsteroidSpawnTimer(Timer); fn player_input( mut commands: Commands, input_query: Query<&ActionState>, - mut player: Query<(&mut Transform, &mut PlayerShotTimer), With>, + mut player_query: Query<(&mut Transform, &mut Player)>, time: Res>, + asset_server: Res, ) { let action_state = input_query.single().unwrap(); - let (mut player_transform, mut shot_timer) = player.single_mut().unwrap(); + let (mut player_transform, mut player) = player_query.single_mut().unwrap(); - shot_timer.0.tick(time.delta()); + player.timer.tick(time.delta()); + if player.timer.is_finished() { + player.can_shoot = true; + } let player_speed = 6.0; @@ -150,11 +153,15 @@ fn player_input( player_transform.translation += Vec3::X * player_speed; } - if action_state.pressed(&Action::Shoot) && shot_timer.0.is_finished() { + player_transform.translation.x = player_transform.translation.x.clamp(-400.0, 400.0); + player_transform.translation.y = player_transform.translation.y.clamp(-220.0, 220.0); + + if action_state.pressed(&Action::Shoot) && player.can_shoot { commands.spawn(( Name::new("Player Projectile"), + PIXEL_PERFECT_LAYERS, Sprite { - custom_size: Some(Vec2::splat(16.0)), + image: asset_server.load("sprites/projectiles/projectile1.png"), ..default() }, Transform { @@ -164,6 +171,8 @@ fn player_input( Projectile, DespawnOnExit(Screen::Game), )); + player.can_shoot = false; + player.timer.reset(); } } @@ -180,7 +189,7 @@ fn pause(mut time: ResMut>, input_query: Query<&ActionState>) { for mut projectile in projectiles.iter_mut() { - projectile.translation += Vec3::Y * 20.0; + projectile.translation += Vec3::Y * 15.0; } } @@ -195,32 +204,30 @@ fn spawn_asteroids( return; } - let count = random_range(1..=5); + let count = random_range(3..=9); - let lanes = 7; - let lane_width = 1400.0 / lanes as f32; + let lanes = 10; + let lane_width = 750.0 / lanes as f32; let mut used_lanes = Vec::new(); - if count == 5 { + let mut spawned_count = 0; + if count > 6 { let big_spawn = random_range(0..4) == 0; if big_spawn { let lane = random_range(1..(lanes - 1)); used_lanes.push(lane); - used_lanes.push(lane - 1); - used_lanes.push(lane + 1); let lane_variance = random_range(-lane_width / 4.0..lane_width / 4.0); let x = lane_width * lane as f32 + lane_width / 2.0 + lane_variance; spawn_asteroid( &mut commands, &asset_server, - Vec3::new(x - 700.0, 720.0, -10.0), - Vec2::splat(256.0), + Vec3::new(x - 350.0, 360.0, -10.0), + Vec2::splat(64.0), ); - return; } + spawned_count += 2; } - let mut spawned_count = 0; while spawned_count < count { let lane = random_range(0..lanes); if used_lanes.contains(&lane) { @@ -231,8 +238,8 @@ fn spawn_asteroids( spawn_asteroid( &mut commands, &asset_server, - Vec3::new(x - 700.0, 720.0, -10.0), - Vec2::splat(128.0), + Vec3::new(x - 350.0, 360.0, -10.0), + Vec2::splat(32.0), ); spawned_count += 1; used_lanes.push(lane); @@ -259,7 +266,7 @@ fn spawn_asteroid( position: Vec3, size: Vec2, ) { - let image_index = random_range(1..=2); + let image_index = random_range(3..=4); let image = format!("sprites/asteroids/asteroid{image_index}.png"); commands.spawn(( Name::new("Asteroid"), @@ -276,7 +283,7 @@ fn spawn_asteroid( ..default() }, Asteroid { - speed: random_range(1.0..4.0), + speed: random_range(1.0..2.0), rotation_speed: random_range(-0.02..0.02), }, DespawnOnExit(Screen::Game), @@ -290,13 +297,13 @@ fn collide_with_asteroid_check( mut state: ResMut>, ) { let player_transform = player.single().unwrap(); - let hitbox_size = 23.0; + let hitbox_size = 8.0; for asteroid_transform in asteroids.iter() { let distance = player_transform .translation .distance(asteroid_transform.translation); - if distance < (hitbox_size / 2.0) + (110.0 / 2.0) { + if distance < (hitbox_size / 2.0) + (30.0 / 2.0) { println!("Player hit by an asteroid!"); time.pause(); state.set(GameState::GameOver); @@ -314,7 +321,7 @@ fn projectile_asteroid_collision( let distance = projectile_transform .translation .distance(asteroid_transform.translation); - if distance < (16.0 / 2.0) + (110.0 / 2.0) { + if distance < (16.0 / 2.0) + (30.0 / 2.0) { commands.entity(projectile_entity).despawn(); commands.entity(asteroid_entity).despawn(); } @@ -332,6 +339,7 @@ fn spawn_game_over(mut commands: Commands) { Node::default(), DespawnOnExit(Screen::Game), DespawnOnExit(GameState::GameOver), + PIXEL_PERFECT_LAYERS, children![ ( Name::new("Game Over Text"),