ai to the rescue

This commit is contained in:
Mitchell Hansen
2025-11-24 22:35:45 -08:00
parent 0e66b6222e
commit efab67d027
16 changed files with 5182 additions and 3138 deletions

6750
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,32 @@
[package]
name = "amethyst-starter-2d"
name = "flappy-bird-rust"
version = "0.1.0"
authors = ["Hilmar Wiegand <me@hwgnd.de>"]
edition = "2018"
edition = "2021"
[dependencies]
#amethyst = {path ="../amethyst", version = "0.15.0"}
amethyst = "0.15.0"
log = { version = "0.4.8", features = ["serde"] }
bevy = "0.17.3"
rand = "0.9"
[features]
default = ["vulkan"]
#empty = ["amethyst/empty"]
#metal = ["amethyst/metal"]
vulkan = ["amethyst/vulkan"]
[target.wasm32-unknown-unknown.dependencies]
bevy = { version = "0.17.3", default-features = false, features = [
"bevy_asset",
"bevy_winit",
"bevy_window",
"bevy_render",
"bevy_core_pipeline",
"bevy_sprite",
"bevy_text",
"bevy_ui",
"bevy_state",
"png",
"webgl2",
] }
getrandom = { version = "0.2", features = ["js"] }
# Enable dynamic linking for faster compile times during development
[profile.dev]
opt-level = 1
[profile.dev.package."*"]
opt-level = 3

View File

@@ -1,6 +0,0 @@
(
axes: {},
actions: {
"flap": [[Key(Space)]],
},
)

View File

@@ -1,4 +0,0 @@
(
title: "test",
dimensions: Some((432, 768)),
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -1,73 +0,0 @@
List((
texture_width: 512,
texture_height: 512,
sprites: [
( // Daytime background
x: 0,
y: 0,
width: 144,
height: 256,
),
( // Nighttime background
x: 146,
y: 0,
width: 144,
height: 256,
),
( // Down Pipe
x: 56,
y: 323,
width: 26,
height: 160,
),
( // Up Pipe
x: 84,
y: 323,
width: 26,
height: 160,
),
( // Ground
x: 292,
y: 0,
width: 168,
height: 56,
),
( // Floppy
x: 3,
y: 490,
width: 17,
height: 13,
),
( // Tap Tap Dialogue
x: 292,
y: 91,
width: 56,
height: 48,
),
( // Play Button
x: 354,
y: 118,
width: 52,
height: 29,
),
( // Leaderboard button
x: 414,
y: 118,
width: 52,
height: 29,
),
( // Get Ready
x: 295,
y: 59,
width: 91,
height: 24,
),
( // Flappy Bird Text
x: 351,
y: 91,
width: 90,
height: 25,
)
]
))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -1,24 +0,0 @@
(
texture_width: 690,
texture_height: 230,
sprites: [
(
x: 0,
y: 0,
width: 230,
height: 230,
),
(
x: 230,
y: 0,
width: 230,
height: 230,
),
(
x: 460,
y: 0,
width: 230,
height: 230,
)
]
)

View File

@@ -1,38 +1,80 @@
use amethyst::{
core::transform::TransformBundle,
core::transform::Transform,
prelude::*,
renderer::{
plugins::{RenderFlat2D, RenderToWindow},
types::DefaultBackend,
RenderingBundle,
},
utils::application_root_dir,
core::SystemDesc,
derive::SystemDesc,
ecs::prelude::{Component, DenseVecStorage, Entity},
ecs::prelude::{Join, ReadStorage, System, SystemData, WriteStorage},
};
use bevy::prelude::*;
// Falling object component to bucket us into something the system can manipulate
#[derive(Clone)]
// Scrolling background/ground component
#[derive(Component, Clone)]
pub struct TiledScroller {
pub speed: f32,
pub width: f32,
pub height: f32,
pub position: f32,
}
impl Component for TiledScroller {
type Storage = DenseVecStorage<Self>;
}
// Falling object component to bucket us into something the system can manipulate
// Bird component
#[derive(Component)]
pub struct Birb {
pub vertical_speed: f32,
pub starting_height: f32,
pub position: f32,
}
impl Component for Birb {
type Storage = DenseVecStorage<Self>;
// Bird animation component
#[derive(Component)]
pub struct BirdAnimation {
pub timer: Timer,
pub current_frame: usize,
}
// Pipe component
#[derive(Component)]
pub struct Pipe {
pub speed: f32,
}
// Collider component for collision detection
#[derive(Component)]
pub struct Collider {
pub width: f32,
pub height: f32,
}
// Ground marker component
#[derive(Component)]
pub struct Ground;
// Timer resource for pipe spawning
#[derive(Resource)]
pub struct PipeSpawnTimer {
pub timer: Timer,
}
// Score resource
#[derive(Resource, Default)]
pub struct Score {
pub value: u32,
}
// High score resource
#[derive(Resource, Default)]
pub struct HighScore {
pub value: u32,
}
// Timer resource for score incrementing
#[derive(Resource)]
pub struct ScoreTimer {
pub timer: Timer,
}
// Score display marker component
#[derive(Component)]
pub struct ScoreDisplay;
// Debug hitbox visualization resource
#[derive(Resource, Default)]
pub struct DebugHitboxes {
pub enabled: bool,
}
// Marker for hitbox debug sprites
#[derive(Component)]
pub struct HitboxDebugSprite;

View File

@@ -3,63 +3,67 @@ mod systems;
mod ready_state;
mod play_state;
mod splash_state;
mod gameover_state;
use amethyst::{
input::{InputBundle, StringBindings},
core::transform::TransformBundle,
core::transform::Transform,
prelude::*,
renderer::{
plugins::{RenderFlat2D, RenderToWindow},
types::DefaultBackend,
RenderingBundle,
},
utils::application_root_dir,
core::SystemDesc,
derive::SystemDesc,
ecs::prelude::{Component, DenseVecStorage, Entity},
ecs::prelude::{Join, ReadStorage, System, SystemData, WriteStorage},
};
use bevy::prelude::*;
use bevy::window::WindowResolution;
use systems::*;
use splash_state::*;
use ready_state::*;
use play_state::*;
use gameover_state::*;
use components::*;
use crate::components::*;
use crate::systems::*;
use std::path::PathBuf;
use std::str::FromStr;
use crate::splash_state::SplashState;
fn main() -> amethyst::Result<()> {
amethyst::start_logger(Default::default());
// Gets the root directory of the application
let mut app_root = PathBuf::from_str("/home/mrh/source/flappy-bird-rust/")?;
// join on the resources path, and the config.
let resources = app_root.join("resources");
let display_config = resources.join("display_config.ron");
let binding_path = resources.join("bindings.ron");
let input_bundle = InputBundle::<StringBindings>::new()
.with_bindings_from_file(binding_path)?;
let game_data = GameDataBuilder::default()
.with_bundle(TransformBundle::new())?
.with_bundle(input_bundle)?
// .with(System, "system", &["required_things"])
.with(ScrollScrollables, "scroll", &[])
.with_bundle(
RenderingBundle::<DefaultBackend>::new()
.with_plugin(
RenderToWindow::from_config_path(display_config).unwrap()
.with_clear([0.34, 0.36, 0.52, 1.0]),
)
.with_plugin(RenderFlat2D::default()),
)?;
// Creates the app with the startup state and bound game data
let mut game = Application::new(resources, SplashState::default(), game_data)?;
game.run();
Ok(())
fn main() {
App::new()
.add_plugins(DefaultPlugins
.set(WindowPlugin {
primary_window: Some(Window {
title: "Flappy Bird - Rust Edition".to_string(),
resolution: WindowResolution::new(429, 768),
..default()
}),
..default()
})
.set(ImagePlugin::default_nearest())
)
.init_state::<GameState>()
.init_resource::<HighScore>()
.init_resource::<DebugHitboxes>()
.init_resource::<DebugScoreCard>()
.add_systems(Startup, (load_sprites, setup_splash).chain())
.add_systems(Update, splash_input.run_if(in_state(GameState::Splash)))
.add_systems(OnExit(GameState::Splash), cleanup_splash)
.add_systems(OnEnter(GameState::Ready), setup_ready)
.add_systems(Update, ready_input.run_if(in_state(GameState::Ready)))
.add_systems(OnExit(GameState::Ready), cleanup_ready)
.add_systems(OnEnter(GameState::Playing), setup_play)
.add_systems(Update, (
bird_gravity,
animate_bird,
rotate_bird,
spawn_pipes,
move_pipes,
check_collisions,
play_input,
update_score,
render_score,
).run_if(in_state(GameState::Playing)))
.add_systems(OnExit(GameState::Playing), cleanup_play)
.add_systems(OnEnter(GameState::GameOver), setup_gameover)
.add_systems(Update, (
gameover_input,
animate_score_card,
render_tiny_score,
render_tiny_high_score,
toggle_scorecard_debug,
render_scorecard_debug,
).run_if(in_state(GameState::GameOver)))
.add_systems(OnExit(GameState::GameOver), cleanup_gameover)
.add_systems(Update, (
scroll_scrollables,
toggle_hitbox_debug,
render_hitbox_debug,
))
.run();
}

View File

@@ -1,104 +1,143 @@
use amethyst::{
assets::{AssetStorage, Loader},
core::transform::Transform,
core::math::Vector3,
input::{get_key, is_close_requested, is_key_down, VirtualKeyCode},
prelude::*,
renderer::{Camera, ImageFormat, SpriteRender, SpriteSheet, SpriteSheetFormat, Texture},
window::ScreenDimensions,
ecs::prelude::{Dispatcher, DispatcherBuilder, Component, DenseVecStorage, Entity},
};
use log::info;
use bevy::prelude::*;
use crate::components::*;
use std::collections::HashMap;
use crate::systems::{BirbGravity, ScrollScrollables};
use crate::splash_state::*;
#[derive(Default)]
pub struct PlayState<'a, 'b> {
#[derive(Component)]
pub struct PlayScreen;
// Custom dispatch systems for this state
dispatcher: Option<Dispatcher<'a, 'b>>,
pub fn setup_play(
mut commands: Commands,
sprite_handles: Res<SpriteHandles>,
window_query: Query<&Window>,
) {
let Ok(window) = window_query.single() else {
return;
};
let width = window.width();
let height = window.height();
sprites: Vec<Entity>,
// Initialize pipe spawn timer
commands.insert_resource(PipeSpawnTimer {
timer: Timer::from_seconds(2.0, TimerMode::Repeating),
});
// Initialize score and score timer
commands.insert_resource(Score::default());
commands.insert_resource(ScoreTimer {
timer: Timer::from_seconds(1.0, TimerMode::Repeating),
});
// Spawn the bird
commands.spawn((
Sprite {
image: sprite_handles.texture.clone(),
texture_atlas: Some(TextureAtlas {
layout: sprite_handles.layout.clone(),
index: SPRITE_BIRD_ANIM_1,
}),
..default()
},
Transform {
translation: Vec3::new(width / 2.0, height / 2.0, 0.2),
scale: Vec3::splat(3.0),
..default()
},
Birb {
vertical_speed: 0.0,
position: 0.0,
starting_height: 0.0,
},
BirdAnimation {
timer: Timer::from_seconds(0.1, TimerMode::Repeating),
current_frame: 0,
},
Collider {
width: BIRD_WIDTH * 0.6, // Reduce to 60% for more forgiving hitbox
height: BIRD_HEIGHT * 0.6,
},
PlayScreen,
));
}
impl<'a, 'b> PlayState<'a, 'b> {
fn init_sprites(&mut self, world: &mut World) {
let sprites = world.try_fetch_mut::<HashMap<String, SpriteRender>>().unwrap().clone();
let dimensions = (*world.read_resource::<ScreenDimensions>()).clone();
let birb_sprite = sprites
.get("floppy").unwrap();
let mut transform = Transform::default();
transform.set_scale(Vector3::new(3.0, 3.0, 3.0));
transform.set_translation_xyz(dimensions.width()/2.0, dimensions.height()/2.0, 0.2);
self.sprites.push(world
.create_entity()
.with(birb_sprite.clone()) // Sprite Render
.with(Birb {
vertical_speed: 0.0,
position: 0.0,
starting_height: 0.0
})
.with(transform)
.build());
pub fn play_input(
keyboard: Res<ButtonInput<KeyCode>>,
mut next_state: ResMut<NextState<GameState>>,
) {
if keyboard.just_pressed(KeyCode::KeyP) {
next_state.set(GameState::Splash);
}
}
impl<'a, 'b> SimpleState for PlayState<'a, 'b> {
fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
let world = data.world;
let dimensions = (*world.read_resource::<ScreenDimensions>()).clone();
// Create the `DispatcherBuilder` and register some `System`s that should only run for this `State`.
let mut dispatcher_builder = DispatcherBuilder::new();
dispatcher_builder.add(BirbGravity { fired: false }, "gravity", &[]);
let mut dispatcher = dispatcher_builder.build();
dispatcher.setup(world);
self.dispatcher = Some(dispatcher);
PlayState::init_sprites(self, world);
}
fn handle_event(
&mut self,
mut data: StateData<'_, GameData<'_, '_>>,
event: StateEvent,
) -> SimpleTrans {
if let StateEvent::Window(event) = &event {
// Check if the window should be closed
if is_close_requested(&event) || is_key_down(&event, VirtualKeyCode::Escape) {
return Trans::Quit;
}
if is_key_down(&event, VirtualKeyCode::P) {
let world = data.world;
for i in &self.sprites {
world.delete_entity(*i);
}
self.sprites.clear();
return Trans::Pop;
}
}
Trans::None
}
fn update(&mut self, data: &mut StateData<'_, GameData<'_, '_>>) -> SimpleTrans {
if let Some(dispatcher) = self.dispatcher.as_mut() {
dispatcher.dispatch(&data.world);
}
Trans::None
pub fn update_score(
time: Res<Time>,
mut score_timer: ResMut<ScoreTimer>,
mut score: ResMut<Score>,
) {
if score_timer.timer.tick(time.delta()).just_finished() {
score.value += 1;
}
}
pub fn render_score(
mut commands: Commands,
score: Res<Score>,
sprite_handles: Res<SpriteHandles>,
window_query: Query<&Window>,
existing_digits: Query<Entity, With<ScoreDisplay>>,
) {
// Only update if score changed
if !score.is_changed() {
return;
}
let Ok(window) = window_query.single() else {
return;
};
let width = window.width();
let height = window.height();
// Despawn existing score digits
for entity in existing_digits.iter() {
commands.entity(entity).despawn();
}
// Convert score to digits
let score_string = score.value.to_string();
let num_digits = score_string.len() as f32;
let digit_spacing = 25.0;
let total_width = num_digits * digit_spacing;
let start_x = (width - total_width) / 2.0;
// Spawn digit sprites
for (i, digit_char) in score_string.chars().enumerate() {
let digit = digit_char.to_digit(10).unwrap() as usize;
let sprite_index = SPRITE_NUMBER_0 + digit;
commands.spawn((
Sprite {
image: sprite_handles.texture.clone(),
texture_atlas: Some(TextureAtlas {
layout: sprite_handles.layout.clone(),
index: sprite_index,
}),
..default()
},
Transform {
translation: Vec3::new(start_x + (i as f32 * digit_spacing), height - 50.0, 0.3),
scale: Vec3::splat(2.5),
..default()
},
ScoreDisplay,
PlayScreen,
));
}
}
pub fn cleanup_play(
mut commands: Commands,
query: Query<Entity, With<PlayScreen>>,
) {
for entity in query.iter() {
commands.entity(entity).despawn();
}
}

View File

@@ -1,104 +1,75 @@
use amethyst::{
assets::{AssetStorage, Loader},
core::transform::Transform,
core::math::Vector3,
input::{get_mouse_button, get_key, is_close_requested, is_key_down, VirtualKeyCode},
prelude::*,
renderer::{Camera, ImageFormat, SpriteRender, SpriteSheet, SpriteSheetFormat, Texture},
window::ScreenDimensions,
ecs::prelude::{Dispatcher, DispatcherBuilder, Component, DenseVecStorage, Entity},
};
use bevy::prelude::*;
use crate::splash_state::*;
use log::info;
use crate::components::*;
use std::collections::HashMap;
use crate::systems::{BirbGravity, ScrollScrollables};
use crate::play_state::PlayState;
#[derive(Component)]
pub struct ReadyScreen;
#[derive(Default)]
pub struct ReadyState {
sprites: Vec<Entity>,
pub fn setup_ready(
mut commands: Commands,
sprite_handles: Res<SpriteHandles>,
window_query: Query<&Window>,
) {
let Ok(window) = window_query.single() else {
return;
};
let width = window.width();
let height = window.height();
// Get ready text
commands.spawn((
Sprite {
image: sprite_handles.texture.clone(),
texture_atlas: Some(TextureAtlas {
layout: sprite_handles.layout.clone(),
index: SPRITE_GET_READY_TEXT,
}),
..default()
},
Transform {
translation: Vec3::new(width * 0.5, height * 0.8, 0.2),
scale: Vec3::splat(3.0),
..default()
},
ReadyScreen,
));
// Tap tap dialogue
commands.spawn((
Sprite {
image: sprite_handles.texture.clone(),
texture_atlas: Some(TextureAtlas {
layout: sprite_handles.layout.clone(),
index: SPRITE_TAP_TAP_DIALOGUE,
}),
..default()
},
Transform {
translation: Vec3::new(width * 0.5, height * 0.5, 0.2),
scale: Vec3::splat(3.0),
..default()
},
ReadyScreen,
));
}
impl ReadyState {
fn init_sprites(&mut self, world: &mut World) {
let dimensions = (*world.read_resource::<ScreenDimensions>()).clone();
let sprites = world.try_fetch_mut::<HashMap<String, SpriteRender>>().unwrap().clone();
let get_ready_text_sprite = sprites
.get("get-ready-text").unwrap().clone();
let tap_tap_dialogue_sprite = sprites
.get("tap-tap-dialogue").unwrap().clone();
let mut transform = Transform::default();
transform.set_scale(Vector3::new(3.0, 3.0, 3.0));
transform.set_translation_xyz(dimensions.width()*0.5, dimensions.height()*0.8, 0.2);
self.sprites.push(world
.create_entity()
.with(get_ready_text_sprite.clone()) // Sprite Render
.with(transform.clone())
.build());
transform.set_translation_xyz(dimensions.width()*0.5, dimensions.height()*0.5, 0.2);
self.sprites.push(world
.create_entity()
.with(tap_tap_dialogue_sprite.clone()) // Sprite Render
.with(transform.clone())
.build());
pub fn ready_input(
keyboard: Res<ButtonInput<KeyCode>>,
mouse: Res<ButtonInput<MouseButton>>,
touches: Res<Touches>,
mut next_state: ResMut<NextState<GameState>>,
) {
if keyboard.just_pressed(KeyCode::Space)
|| mouse.just_pressed(MouseButton::Left)
|| touches.any_just_pressed() {
next_state.set(GameState::Playing);
}
}
impl SimpleState for ReadyState {
fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
let world = data.world;
ReadyState::init_sprites(self, world);
}
fn on_resume(&mut self, data: StateData<'_, GameData<'_, '_>>) {
}
fn handle_event(
&mut self,
mut data: StateData<'_, GameData<'_, '_>>,
event: StateEvent,
) -> SimpleTrans {
if let StateEvent::Ui(event) = &event {
// if event.event_type == UiEventType::Click {
//
// }
}
if let StateEvent::Window(event) = &event {
// Check if the window should be closed
if is_close_requested(&event) || is_key_down(&event, VirtualKeyCode::Escape) {
return Trans::Quit;
}
// Check if the window should be closed
if is_key_down(&event, VirtualKeyCode::Space) {
let world = data.world;
for i in &self.sprites {
world.delete_entity(*i);
}
self.sprites.clear();
return Trans::Push(Box::new(PlayState::default()));
}
}
Trans::None
}
fn update(&mut self, data: &mut StateData<'_, GameData<'_, '_>>) -> SimpleTrans {
Trans::None
pub fn cleanup_ready(
mut commands: Commands,
query: Query<Entity, With<ReadyScreen>>,
) {
for entity in query.iter() {
commands.entity(entity).despawn();
}
}

View File

@@ -1,241 +1,332 @@
use amethyst::{
assets::{AssetStorage, Loader},
core::transform::Transform,
core::math::Vector3,
input::{get_key, is_close_requested, is_key_down, VirtualKeyCode},
prelude::*,
renderer::{Camera, ImageFormat, SpriteRender, SpriteSheet, SpriteSheetFormat, Texture},
window::ScreenDimensions,
ecs::prelude::{Dispatcher, DispatcherBuilder, Component, DenseVecStorage, Entity},
};
use log::info;
use bevy::prelude::*;
use crate::components::*;
use std::collections::HashMap;
use crate::systems::{BirbGravity, ScrollScrollables};
use crate::ready_state::ReadyState;
#[derive(Default)]
pub struct SplashState {
sprites: Vec<Entity>,
persistent_sprites: Vec<Entity>,
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
pub enum GameState {
#[default]
Splash,
Ready,
Playing,
GameOver,
}
impl SplashState {
#[derive(Component)]
pub struct SplashScreen;
fn load_sprites(world: &mut World) -> HashMap<String, SpriteRender> {
// Load the texture for our sprites. We'll later need to
// add a handle to this texture to our `SpriteRender`s, so
// we need to keep a reference to it.
let texture_handle = {
let loader = world.read_resource::<Loader>();
let texture_storage = world.read_resource::<AssetStorage<Texture>>();
loader.load(
"sprites/flappy.png",
ImageFormat::default(),
(),
&texture_storage,
)
};
#[derive(Component)]
pub struct PersistentSprite;
// Load the spritesheet definition file, which contains metadata on our
// spritesheet texture.
let sheet_handle = {
let loader = world.read_resource::<Loader>();
let sheet_storage = world.read_resource::<AssetStorage<SpriteSheet>>();
loader.load(
"sprites/flappy.ron",
SpriteSheetFormat(texture_handle),
(),
&sheet_storage,
)
};
#[derive(Resource)]
pub struct SpriteHandles {
pub texture: Handle<Image>,
pub layout: Handle<TextureAtlasLayout>,
}
let sprite_map = vec![
("day-background".to_string(), 0),
("night-background".to_string(), 1),
("down-pipe".to_string(), 2),
("up-pipe".to_string(), 3),
("ground".to_string(), 4),
("floppy".to_string(), 5),
("tap-tap-dialogue".to_string(), 6),
("play-button".to_string(), 7),
("leaderboard-button".to_string(), 8),
("get-ready-text".to_string(), 9),
("flappy-bird-text".to_string(), 10),
];
// Sprite indices from the spritesheet
pub const SPRITE_DAY_BACKGROUND: usize = 0;
pub const SPRITE_NIGHT_BACKGROUND: usize = 1;
pub const SPRITE_DOWN_PIPE: usize = 2;
pub const SPRITE_UP_PIPE: usize = 3;
pub const SPRITE_GROUND: usize = 4;
pub const SPRITE_FLOPPY: usize = 5;
sprite_map.iter()
.map(|i| (i.0.clone(), SpriteRender {
sprite_sheet: sheet_handle.clone(),
sprite_number: i.1,
}))
.collect()
}
// Sprite dimensions (from RON file)
pub const PIPE_WIDTH: f32 = 26.0;
pub const PIPE_HEIGHT: f32 = 160.0;
pub const BIRD_WIDTH: f32 = 17.0;
pub const BIRD_HEIGHT: f32 = 13.0;
pub const SPRITE_TAP_TAP_DIALOGUE: usize = 6;
pub const SPRITE_PLAY_BUTTON: usize = 7;
pub const SPRITE_LEADERBOARD_BUTTON: usize = 8;
pub const SPRITE_GET_READY_TEXT: usize = 9;
pub const SPRITE_FLAPPY_BIRD_TEXT: usize = 10;
pub const SPRITE_GAME_OVER: usize = 11;
pub const SPRITE_NUMBER_0: usize = 12;
pub const SPRITE_NUMBER_1: usize = 13;
pub const SPRITE_NUMBER_2: usize = 14;
pub const SPRITE_NUMBER_3: usize = 15;
pub const SPRITE_NUMBER_4: usize = 16;
pub const SPRITE_NUMBER_5: usize = 17;
pub const SPRITE_NUMBER_6: usize = 18;
pub const SPRITE_NUMBER_7: usize = 19;
pub const SPRITE_NUMBER_8: usize = 20;
pub const SPRITE_NUMBER_9: usize = 21;
fn init_camera(world: &mut World) {
// Game over screen sprites
pub const SPRITE_MEDAL_PLATINUM: usize = 22;
pub const SPRITE_SCORE_CARD: usize = 23;
pub const SPRITE_MEDAL_GOLD: usize = 24;
pub const SPRITE_MEDAL_SILVER: usize = 25;
pub const SPRITE_MEDAL_BRONZE: usize = 26;
let dimensions = (*world.read_resource::<ScreenDimensions>()).clone();
// Tiny number sprites for score card
pub const SPRITE_TINY_NUMBER_0: usize = 27;
pub const SPRITE_TINY_NUMBER_1: usize = 28;
pub const SPRITE_TINY_NUMBER_2: usize = 29;
pub const SPRITE_TINY_NUMBER_3: usize = 30;
pub const SPRITE_TINY_NUMBER_4: usize = 31;
pub const SPRITE_TINY_NUMBER_5: usize = 32;
pub const SPRITE_TINY_NUMBER_6: usize = 33;
pub const SPRITE_TINY_NUMBER_7: usize = 34;
pub const SPRITE_TINY_NUMBER_8: usize = 35;
pub const SPRITE_TINY_NUMBER_9: usize = 36;
// Center the camera in the middle of the screen, and let it cover
// the entire screen
let mut transform = Transform::default();
transform.set_translation_xyz(dimensions.width() * 0.5, dimensions.height() * 0.5, 1.);
// Bird animation sprites
pub const SPRITE_BIRD_ANIM_1: usize = 37;
pub const SPRITE_BIRD_ANIM_2: usize = 38;
pub const SPRITE_BIRD_ANIM_3: usize = 39;
world
.create_entity()
.with(Camera::standard_2d(dimensions.width(), dimensions.height()))
.with(transform)
.build();
}
pub fn load_sprites(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
) {
let texture = asset_server.load("sprites/flappy.png");
fn init_sprites(&mut self, world: &mut World) {
// Create texture atlas layout based on the spritesheet
// Coordinates from flappy.ron
let mut layout = TextureAtlasLayout::new_empty(UVec2::new(512, 512));
let sprites = world.try_fetch_mut::<HashMap<String, SpriteRender>>().unwrap().clone();
// Add sprite rectangles using correct coordinates from the RON file
layout.add_texture(URect::new(0, 0, 144, 256)); // 0: day-background
layout.add_texture(URect::new(146, 0, 290, 256)); // 1: night-background
layout.add_texture(URect::new(56, 323, 82, 483)); // 2: down-pipe
layout.add_texture(URect::new(84, 323, 110, 483)); // 3: up-pipe
layout.add_texture(URect::new(292, 0, 460, 56)); // 4: ground
layout.add_texture(URect::new(3, 490, 20, 503)); // 5: floppy
layout.add_texture(URect::new(292, 91, 348, 139)); // 6: tap-tap-dialogue
layout.add_texture(URect::new(354, 118, 406, 147)); // 7: play-button
layout.add_texture(URect::new(414, 118, 466, 147)); // 8: leaderboard-button
layout.add_texture(URect::new(295, 59, 386, 83)); // 9: get-ready-text
layout.add_texture(URect::new(351, 91, 441, 116)); // 10: flappy-bird-text
layout.add_texture(URect::new(395, 58, 495, 80)); // 11: game-over
layout.add_texture(URect::new(496, 60, 508, 78)); // 12: number 0
layout.add_texture(URect::new(136, 455, 144, 473)); // 13: number 1
layout.add_texture(URect::new(292, 160, 304, 178)); // 14: number 2
layout.add_texture(URect::new(306, 160, 318, 178)); // 15: number 3
layout.add_texture(URect::new(320, 160, 332, 178)); // 16: number 4
layout.add_texture(URect::new(334, 160, 346, 178)); // 17: number 5
layout.add_texture(URect::new(292, 184, 304, 202)); // 18: number 6
layout.add_texture(URect::new(306, 184, 318, 202)); // 19: number 7
layout.add_texture(URect::new(320, 184, 332, 202)); // 20: number 8
layout.add_texture(URect::new(334, 184, 346, 202)); // 21: number 9
let dimensions = (*world.read_resource::<ScreenDimensions>()).clone();
// Game over screen sprites
layout.add_texture(URect::new(121, 258, 143, 280)); // 22: medal platinum
layout.add_texture(URect::new(3, 259, 116, 316)); // 23: score card
layout.add_texture(URect::new(121, 282, 143, 304)); // 24: medal gold
layout.add_texture(URect::new(112, 453, 134, 475)); // 25: medal silver
layout.add_texture(URect::new(112, 477, 134, 499)); // 26: medal bronze
let flappy_bird_text_sprite = sprites
.get("flappy-bird-text").unwrap().clone();
let play_button_sprite = sprites
.get("play-button").unwrap().clone();
let leaderboard_button_sprite = sprites
.get("leaderboard-button").unwrap().clone();
let background_sprite = sprites
.get("day-background").unwrap().clone();
let night_background_sprite = sprites
.get("night-background").unwrap().clone();
let ground_sprite = sprites
.get("ground").unwrap().clone();
// Tiny number sprites for score card
layout.add_texture(URect::new(138, 323, 144, 330)); // 27: tiny number 0
layout.add_texture(URect::new(140, 332, 144, 344)); // 28: tiny number 1
layout.add_texture(URect::new(138, 349, 144, 356)); // 29: tiny number 2
layout.add_texture(URect::new(138, 358, 144, 365)); // 30: tiny number 3
layout.add_texture(URect::new(138, 375, 144, 382)); // 31: tiny number 4
layout.add_texture(URect::new(138, 384, 144, 391)); // 32: tiny number 5
layout.add_texture(URect::new(138, 401, 144, 408)); // 33: tiny number 6
layout.add_texture(URect::new(138, 410, 144, 417)); // 34: tiny number 7
layout.add_texture(URect::new(138, 427, 144, 434)); // 35: tiny number 8
layout.add_texture(URect::new(138, 436, 144, 443)); // 36: tiny number 9
// Bird animation sprites
layout.add_texture(URect::new(3, 491, 20, 503)); // 37: bird animation 1
layout.add_texture(URect::new(31, 491, 48, 503)); // 38: bird animation 2
layout.add_texture(URect::new(59, 491, 76, 503)); // 39: bird animation 3
let mut transform = Transform::default();
transform.set_scale(Vector3::new(3.0, 3.0, 3.0));
transform.set_translation_xyz(3.0*143.0/2.0, 3.0*256.0/2.0, 0.0);
let layout_handle = texture_atlases.add(layout);
self.persistent_sprites.push(world
.create_entity()
.with(background_sprite.clone()) // Sprite Render
.with(TiledScroller {
speed: -75.0,
position: 1.0,
width: 143.0 * 3.0,
height: 256.0 * 3.0,
})
.with(transform.clone())
.build());
commands.insert_resource(SpriteHandles {
texture: texture.clone(),
layout: layout_handle,
});
}
transform.set_translation_xyz(3.0*143.0/2.0*3.0, 3.0*256.0/2.0, 0.0);
pub fn setup_splash(
mut commands: Commands,
sprite_handles: Res<SpriteHandles>,
window_query: Query<&Window>,
) {
let Ok(window) = window_query.single() else {
return;
};
let width = window.width();
let height = window.height();
self.persistent_sprites.push(world
.create_entity()
.with(background_sprite.clone()) // Sprite Render
.with(TiledScroller {
speed: -75.0,
position: 2.0,
width: 143.0 * 3.0,
height: 256.0 * 3.0,
})
.with(transform.clone())
.build());
// Setup camera
commands.spawn((Camera2d, Transform::from_xyz(width * 0.5, height * 0.5, 0.0)));
transform.set_translation_xyz(3.0*168.0/2.0, 3.0*56.0/2.0, 0.1);
// Background sprites (persistent across states)
commands.spawn((
Sprite {
image: sprite_handles.texture.clone(),
texture_atlas: Some(TextureAtlas {
layout: sprite_handles.layout.clone(),
index: SPRITE_DAY_BACKGROUND,
}),
..default()
},
Transform {
translation: Vec3::new(3.0 * 143.0 / 2.0, 3.0 * 256.0 / 2.0, 0.0),
scale: Vec3::splat(3.0),
..default()
},
TiledScroller {
speed: -120.0,
position: 1.0,
width: 143.0 * 3.0,
height: 256.0 * 3.0,
},
PersistentSprite,
));
self.persistent_sprites.push(world
.create_entity()
.with(ground_sprite.clone()) // Sprite Render
.with(TiledScroller {
speed: -100.0,
position: 2.0,
width: 167.0 * 3.0,
height: 56.0 * 3.0,
})
.with(transform.clone())
.build());
commands.spawn((
Sprite {
image: sprite_handles.texture.clone(),
texture_atlas: Some(TextureAtlas {
layout: sprite_handles.layout.clone(),
index: SPRITE_DAY_BACKGROUND,
}),
..default()
},
Transform {
translation: Vec3::new(3.0 * 143.0 / 2.0 * 3.0, 3.0 * 256.0 / 2.0, 0.0),
scale: Vec3::splat(3.0),
..default()
},
TiledScroller {
speed: -120.0,
position: 2.0,
width: 143.0 * 3.0,
height: 256.0 * 3.0,
},
PersistentSprite,
));
transform.set_translation_xyz(3.0*168.0/2.0*3.0, 3.0*56.0/2.0, 0.1);
// Ground sprites - positioned at bottom of screen
let ground_height = 56.0 * 3.0; // 168px
let ground_y = ground_height / 2.0; // Center the sprite vertically at half its height
self.persistent_sprites.push(world
.create_entity()
.with(ground_sprite.clone()) // Sprite Render
.with(TiledScroller {
speed: -100.0,
position: 2.0,
width: 167.0 * 3.0,
height: 56.0 * 3.0,
})
.with(transform.clone())
.build());
commands.spawn((
Sprite {
image: sprite_handles.texture.clone(),
texture_atlas: Some(TextureAtlas {
layout: sprite_handles.layout.clone(),
index: SPRITE_GROUND,
}),
..default()
},
Transform {
translation: Vec3::new(3.0 * 168.0 / 2.0, ground_y, 0.1),
scale: Vec3::splat(3.0),
..default()
},
TiledScroller {
speed: -120.0,
position: 2.0,
width: 167.0 * 3.0,
height: ground_height,
},
Ground,
PersistentSprite,
));
transform.set_translation_xyz(dimensions.width()*0.5, dimensions.height()*0.8, 0.2);
commands.spawn((
Sprite {
image: sprite_handles.texture.clone(),
texture_atlas: Some(TextureAtlas {
layout: sprite_handles.layout.clone(),
index: SPRITE_GROUND,
}),
..default()
},
Transform {
translation: Vec3::new(3.0 * 168.0 / 2.0 * 3.0, ground_y, 0.1),
scale: Vec3::splat(3.0),
..default()
},
TiledScroller {
speed: -120.0,
position: 2.0,
width: 167.0 * 3.0,
height: ground_height,
},
Ground,
PersistentSprite,
));
self.sprites.push(world
.create_entity()
.with(flappy_bird_text_sprite.clone())
.with(transform.clone())
.build());
// Splash screen specific sprites
commands.spawn((
Sprite {
image: sprite_handles.texture.clone(),
texture_atlas: Some(TextureAtlas {
layout: sprite_handles.layout.clone(),
index: SPRITE_FLAPPY_BIRD_TEXT,
}),
..default()
},
Transform {
translation: Vec3::new(width * 0.5, height * 0.8, 0.2),
scale: Vec3::splat(3.0),
..default()
},
SplashScreen,
));
transform.set_translation_xyz(dimensions.width()*0.25, dimensions.height()*0.4, 0.2);
commands.spawn((
Sprite {
image: sprite_handles.texture.clone(),
texture_atlas: Some(TextureAtlas {
layout: sprite_handles.layout.clone(),
index: SPRITE_PLAY_BUTTON,
}),
..default()
},
Transform {
translation: Vec3::new(width * 0.25, height * 0.4, 0.2),
scale: Vec3::splat(3.0),
..default()
},
SplashScreen,
));
self.sprites.push(world
.create_entity()
.with(play_button_sprite.clone())
.with(transform.clone())
.build());
commands.spawn((
Sprite {
image: sprite_handles.texture.clone(),
texture_atlas: Some(TextureAtlas {
layout: sprite_handles.layout.clone(),
index: SPRITE_LEADERBOARD_BUTTON,
}),
..default()
},
Transform {
translation: Vec3::new(width * 0.75, height * 0.4, 0.2),
scale: Vec3::splat(3.0),
..default()
},
SplashScreen,
));
}
transform.set_translation_xyz(dimensions.width()*0.75, dimensions.height()*0.4, 0.2);
self.sprites.push(world
.create_entity()
.with(leaderboard_button_sprite.clone())
.with(transform.clone())
.build());
pub fn splash_input(
keyboard: Res<ButtonInput<KeyCode>>,
mouse: Res<ButtonInput<MouseButton>>,
touches: Res<Touches>,
mut next_state: ResMut<NextState<GameState>>,
) {
if keyboard.just_pressed(KeyCode::Space)
|| mouse.just_pressed(MouseButton::Left)
|| touches.any_just_pressed() {
next_state.set(GameState::Ready);
}
}
impl SimpleState for SplashState {
fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
let world = data.world;
// Load the sprites. Insert them into the world as this is the first function to be called
let sprites = SplashState::load_sprites(world);
world.insert(sprites.clone());
SplashState::load_sprites(world);
SplashState::init_camera(world);
SplashState::init_sprites(self, world);
}
fn handle_event(
&mut self,
mut data: StateData<'_, GameData<'_, '_>>,
event: StateEvent,
) -> SimpleTrans {
if let StateEvent::Window(event) = &event {
// Check if the window should be closed
if is_close_requested(&event) || is_key_down(&event, VirtualKeyCode::Escape) {
return Trans::Quit;
}
// Check if the window should be closed
if is_key_down(&event, VirtualKeyCode::Space) {
let world = data.world;
for i in &self.sprites {
world.delete_entity(*i);
}
self.sprites.clear();
return Trans::Push(Box::new(ReadyState::default()));
}
}
Trans::None
}
fn update(&mut self, data: &mut StateData<'_, GameData<'_, '_>>) -> SimpleTrans {
Trans::None
pub fn cleanup_splash(
mut commands: Commands,
query: Query<Entity, With<SplashScreen>>,
) {
for entity in query.iter() {
commands.entity(entity).despawn();
}
}

View File

@@ -1,68 +1,296 @@
use amethyst::{
core::SystemDesc,
core::timing::Time,
core::transform::{Transform, TransformBundle},
derive::SystemDesc,
ecs::prelude::{},
ecs::prelude::{
Component, DenseVecStorage, Entity, Join, Read,
ReadStorage, System, SystemData, WriteStorage, Write
},
input::{InputHandler, StringBindings},
};
use log::info;
use bevy::prelude::*;
use crate::components::*;
use crate::splash_state::*;
use rand::Rng;
pub struct ScrollScrollables;
// System to scroll backgrounds and ground
pub fn scroll_scrollables(
mut query: Query<(&mut Transform, &mut TiledScroller)>,
time: Res<Time>,
) {
for (mut transform, scroller) in query.iter_mut() {
transform.translation.x += scroller.speed * time.delta_secs();
/*
Pausable systems
https://book.amethyst.rs/stable/controlling_system_execution/pausable_systems.html
*/
// Reset position when off screen
if transform.translation.x + scroller.width / 2.0 < 0.0 {
transform.translation.x = scroller.width / 2.0 * 3.0;
}
}
}
// This system iterates all the objects with transform (and falling object) component
impl<'a> System<'a> for ScrollScrollables {
type SystemData = (
WriteStorage<'a, Transform>,
WriteStorage<'a, TiledScroller>,
Read<'a, Time>,
);
// System to apply gravity to the bird
pub fn bird_gravity(
mut query: Query<(&mut Transform, &mut Birb)>,
time: Res<Time>,
keyboard: Res<ButtonInput<KeyCode>>,
mouse: Res<ButtonInput<MouseButton>>,
touches: Res<Touches>,
) {
for (mut transform, mut birb) in query.iter_mut() {
// Flap on space key, mouse click, or touch
if keyboard.just_pressed(KeyCode::Space)
|| mouse.just_pressed(MouseButton::Left)
|| touches.any_just_pressed() {
birb.vertical_speed = 600.0;
}
fn run(&mut self, (mut transforms, mut scrolling, time): Self::SystemData) {
for (mut transform, mut object) in (&mut transforms, &mut scrolling).join() {
// Apply gravity
birb.vertical_speed -= 1500.0 * time.delta_secs();
transform.translation.y += birb.vertical_speed * time.delta_secs();
}
}
transform.prepend_translation_x(object.speed * time.delta_seconds());
if transform.translation().x+object.width/2.0 < 0.0 {
transform.set_translation_x(object.width/2.0*3.0);
// System to animate the bird sprite
pub fn animate_bird(
time: Res<Time>,
mut query: Query<(&mut Sprite, &mut BirdAnimation)>,
) {
for (mut sprite, mut anim) in query.iter_mut() {
anim.timer.tick(time.delta());
if anim.timer.just_finished() {
anim.current_frame = (anim.current_frame + 1) % 3;
let frame_index = match anim.current_frame {
0 => SPRITE_BIRD_ANIM_1,
1 => SPRITE_BIRD_ANIM_2,
2 => SPRITE_BIRD_ANIM_3,
_ => SPRITE_BIRD_ANIM_1,
};
if let Some(ref mut atlas) = sprite.texture_atlas {
atlas.index = frame_index;
}
}
}
}
#[derive(Default)]
pub struct BirbGravity {
pub fired: bool,
// System to rotate bird based on velocity
pub fn rotate_bird(
mut query: Query<(&mut Transform, &Birb)>,
) {
for (mut transform, birb) in query.iter_mut() {
// Map velocity to rotation angle
// Upward velocity (600) -> 45 degrees upward (-45° or -π/4 rad)
// Downward velocity -> 45 degrees downward (45° or π/4 rad)
// Clamp velocity between -600 and 600 for smooth rotation
let clamped_velocity = birb.vertical_speed.clamp(-600.0, 600.0);
// Map velocity to angle: -600 (falling) = -45°, +600 (rising) = 45°
let angle = (clamped_velocity / 600.0) * (std::f32::consts::PI / 4.0);
transform.rotation = Quat::from_rotation_z(angle);
}
}
// This system iterates all the objects with transform (and falling object) component
impl<'a> System<'a> for BirbGravity {
type SystemData = (
WriteStorage<'a, Transform>,
WriteStorage<'a, Birb>,
Read<'a, Time>,
Read<'a, InputHandler<StringBindings>>,
);
// System to spawn pipes
pub fn spawn_pipes(
mut commands: Commands,
mut timer: ResMut<PipeSpawnTimer>,
time: Res<Time>,
sprite_handles: Res<SpriteHandles>,
window_query: Query<&Window>,
) {
timer.timer.tick(time.delta());
fn run(&mut self, (mut transforms, mut scrolling, time, input): Self::SystemData) {
for (mut transform, mut object) in (&mut transforms, &mut scrolling).join() {
if timer.timer.just_finished() {
let Ok(window) = window_query.single() else {
return;
};
//match game.current_state
if input.action_is_down("flap").expect("No action") {
object.vertical_speed = 600.0;
}
object.vertical_speed -= 1500.0 * time.delta_seconds();
transform.prepend_translation_y(object.vertical_speed * time.delta_seconds());
let mut rng = rand::rng();
let gap_size = 180.0;
// Ground sprite is centered at y = 56.0 * 3.0 / 2.0 = 84px
// Top of ground is at y = 84 + (168/2) = 168px
let ground_height = 56.0 * 3.0; // 168px
let ground_y = ground_height / 2.0; // 84px (center of ground sprite)
let ground_top = ground_y + ground_height / 2.0; // 168px (top edge of ground)
// Calculate gap center position
// Ensure minimum clearance above ground and below ceiling
let min_gap_y = ground_top + gap_size / 2.0 + 50.0; // 50px clearance above ground
let max_gap_y = window.height() - gap_size / 2.0 - 50.0; // 50px clearance below ceiling
let gap_y = rng.random_range(min_gap_y..max_gap_y);
let pipe_speed = -120.0; // Match background/ground speed
// Calculate actual pipe positions for debugging
let top_pipe_y = gap_y + gap_size / 2.0 + PIPE_HEIGHT * 3.0 / 2.0;
let bottom_pipe_y = gap_y - gap_size / 2.0 - PIPE_HEIGHT * 3.0 / 2.0;
let bottom_pipe_bottom = bottom_pipe_y - PIPE_HEIGHT * 3.0 / 2.0;
println!("Ground top: {}, Bottom pipe bottom edge: {}, Overlap: {}",
ground_top, bottom_pipe_bottom, ground_top - bottom_pipe_bottom);
// Spawn top pipe (down-pipe)
commands.spawn((
Sprite {
image: sprite_handles.texture.clone(),
texture_atlas: Some(TextureAtlas {
layout: sprite_handles.layout.clone(),
index: SPRITE_DOWN_PIPE,
}),
..default()
},
Transform {
translation: Vec3::new(window.width() + 50.0, gap_y + gap_size / 2.0 + PIPE_HEIGHT * 3.0 / 2.0, 0.05),
scale: Vec3::splat(3.0),
..default()
},
Pipe { speed: pipe_speed },
Collider {
width: PIPE_WIDTH * 0.7, // Reduce to 70% for more forgiving hitbox
height: PIPE_HEIGHT * 0.9, // Slightly reduce height too
},
crate::play_state::PlayScreen,
));
// Spawn bottom pipe (up-pipe)
commands.spawn((
Sprite {
image: sprite_handles.texture.clone(),
texture_atlas: Some(TextureAtlas {
layout: sprite_handles.layout.clone(),
index: SPRITE_UP_PIPE,
}),
..default()
},
Transform {
translation: Vec3::new(window.width() + 50.0, gap_y - gap_size / 2.0 - PIPE_HEIGHT * 3.0 / 2.0, 0.05),
scale: Vec3::splat(3.0),
..default()
},
Pipe { speed: pipe_speed },
Collider {
width: PIPE_WIDTH * 0.7, // Reduce to 70% for more forgiving hitbox
height: PIPE_HEIGHT * 0.9, // Slightly reduce height too
},
crate::play_state::PlayScreen,
));
}
}
// System to move pipes
pub fn move_pipes(
mut commands: Commands,
mut query: Query<(Entity, &mut Transform, &Pipe)>,
time: Res<Time>,
) {
for (entity, mut transform, pipe) in query.iter_mut() {
transform.translation.x += pipe.speed * time.delta_secs();
// Despawn pipes that are off screen
if transform.translation.x < -100.0 {
commands.entity(entity).despawn();
}
}
}
// System to check collisions
pub fn check_collisions(
bird_query: Query<(&Transform, &Collider), With<Birb>>,
pipe_query: Query<(&Transform, &Collider), With<Pipe>>,
ground_query: Query<&Transform, With<Ground>>,
mut next_state: ResMut<NextState<GameState>>,
window_query: Query<&Window>,
) {
let Ok((bird_transform, bird_collider)) = bird_query.single() else {
return;
};
let bird_pos = bird_transform.translation;
let bird_scale = bird_transform.scale;
let bird_half_width = (bird_collider.width * bird_scale.x) / 2.0;
let bird_half_height = (bird_collider.height * bird_scale.y) / 2.0;
// Check collision with pipes
for (pipe_transform, pipe_collider) in pipe_query.iter() {
let pipe_pos = pipe_transform.translation;
let pipe_scale = pipe_transform.scale;
let pipe_half_width = (pipe_collider.width * pipe_scale.x) / 2.0;
let pipe_half_height = (pipe_collider.height * pipe_scale.y) / 2.0;
// AABB collision detection
if (bird_pos.x - bird_half_width < pipe_pos.x + pipe_half_width)
&& (bird_pos.x + bird_half_width > pipe_pos.x - pipe_half_width)
&& (bird_pos.y - bird_half_height < pipe_pos.y + pipe_half_height)
&& (bird_pos.y + bird_half_height > pipe_pos.y - pipe_half_height)
{
next_state.set(GameState::GameOver);
return;
}
}
// Check collision with ground
for ground_transform in ground_query.iter() {
let ground_height = 56.0 * 3.0; // 168px
let ground_y = ground_transform.translation.y + ground_height / 2.0; // Ground top edge
if bird_pos.y - bird_half_height <= ground_y {
next_state.set(GameState::GameOver);
return;
}
}
// Check if bird went above screen
if let Ok(window) = window_query.single() {
if bird_pos.y + bird_half_height >= window.height() {
next_state.set(GameState::GameOver);
return;
}
}
}
// System to toggle hitbox debug visualization
pub fn toggle_hitbox_debug(
keyboard: Res<ButtonInput<KeyCode>>,
mut debug: ResMut<DebugHitboxes>,
) {
if keyboard.just_pressed(KeyCode::KeyH) {
debug.enabled = !debug.enabled;
println!("Hitbox debug: {}", if debug.enabled { "ON" } else { "OFF" });
}
}
// System to render hitbox debug sprites
pub fn render_hitbox_debug(
mut commands: Commands,
debug: Res<DebugHitboxes>,
collider_query: Query<(&Transform, &Collider), (Without<HitboxDebugSprite>, Without<Ground>)>,
existing_debug: Query<Entity, With<HitboxDebugSprite>>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
// Clean up existing debug sprites
for entity in existing_debug.iter() {
commands.entity(entity).despawn();
}
// If debug is enabled, spawn new debug sprites
if debug.enabled {
for (transform, collider) in collider_query.iter() {
let scale = transform.scale;
let width = collider.width * scale.x;
let height = collider.height * scale.y;
// Create a rectangular mesh for the hitbox
let mesh = Mesh::from(Rectangle::new(width, height));
commands.spawn((
Mesh2d::from(meshes.add(mesh)),
MeshMaterial2d(materials.add(ColorMaterial::from(Color::srgba(1.0, 0.0, 0.0, 0.3)))),
Transform {
translation: Vec3::new(
transform.translation.x,
transform.translation.y,
0.9, // High z-order to render on top
),
..default()
},
HitboxDebugSprite,
));
}
}
}