ai to the rescue
This commit is contained in:
6750
Cargo.lock
generated
6750
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
36
Cargo.toml
36
Cargo.toml
@@ -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
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
(
|
||||
axes: {},
|
||||
actions: {
|
||||
"flap": [[Key(Space)]],
|
||||
},
|
||||
)
|
||||
@@ -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 |
@@ -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 |
@@ -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,
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -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;
|
||||
|
||||
116
src/main.rs
116
src/main.rs
@@ -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]),
|
||||
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())
|
||||
)
|
||||
.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(())
|
||||
.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();
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
impl<'a, 'b> PlayState<'a, 'b> {
|
||||
// Initialize score and score timer
|
||||
commands.insert_resource(Score::default());
|
||||
commands.insert_resource(ScoreTimer {
|
||||
timer: Timer::from_seconds(1.0, TimerMode::Repeating),
|
||||
});
|
||||
|
||||
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 {
|
||||
// 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
|
||||
})
|
||||
.with(transform)
|
||||
.build());
|
||||
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,
|
||||
));
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
Trans::None
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
fn update(&mut self, data: &mut StateData<'_, GameData<'_, '_>>) -> SimpleTrans {
|
||||
let Ok(window) = window_query.single() else {
|
||||
return;
|
||||
};
|
||||
let width = window.width();
|
||||
let height = window.height();
|
||||
|
||||
if let Some(dispatcher) = self.dispatcher.as_mut() {
|
||||
dispatcher.dispatch(&data.world);
|
||||
// Despawn existing score digits
|
||||
for entity in existing_digits.iter() {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
Trans::None
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct ReadyScreen;
|
||||
|
||||
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();
|
||||
|
||||
use log::info;
|
||||
use crate::components::*;
|
||||
use std::collections::HashMap;
|
||||
use crate::systems::{BirbGravity, ScrollScrollables};
|
||||
use crate::play_state::PlayState;
|
||||
// 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,
|
||||
));
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ReadyState {
|
||||
sprites: Vec<Entity>,
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct SpriteHandles {
|
||||
pub texture: Handle<Image>,
|
||||
pub layout: Handle<TextureAtlasLayout>,
|
||||
}
|
||||
|
||||
// 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 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;
|
||||
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
|
||||
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");
|
||||
|
||||
// Create texture atlas layout based on the spritesheet
|
||||
// Coordinates from flappy.ron
|
||||
let mut layout = TextureAtlasLayout::new_empty(UVec2::new(512, 512));
|
||||
|
||||
// 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
|
||||
|
||||
// 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
|
||||
|
||||
// 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 layout_handle = texture_atlases.add(layout);
|
||||
|
||||
commands.insert_resource(SpriteHandles {
|
||||
texture: texture.clone(),
|
||||
layout: layout_handle,
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// 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,
|
||||
)
|
||||
};
|
||||
// Setup camera
|
||||
commands.spawn((Camera2d, Transform::from_xyz(width * 0.5, height * 0.5, 0.0)));
|
||||
|
||||
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_map.iter()
|
||||
.map(|i| (i.0.clone(), SpriteRender {
|
||||
sprite_sheet: sheet_handle.clone(),
|
||||
sprite_number: i.1,
|
||||
}))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn init_camera(world: &mut World) {
|
||||
|
||||
let dimensions = (*world.read_resource::<ScreenDimensions>()).clone();
|
||||
|
||||
// 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.);
|
||||
|
||||
world
|
||||
.create_entity()
|
||||
.with(Camera::standard_2d(dimensions.width(), dimensions.height()))
|
||||
.with(transform)
|
||||
.build();
|
||||
}
|
||||
|
||||
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 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();
|
||||
|
||||
|
||||
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);
|
||||
|
||||
self.persistent_sprites.push(world
|
||||
.create_entity()
|
||||
.with(background_sprite.clone()) // Sprite Render
|
||||
.with(TiledScroller {
|
||||
speed: -75.0,
|
||||
// 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,
|
||||
})
|
||||
.with(transform.clone())
|
||||
.build());
|
||||
},
|
||||
PersistentSprite,
|
||||
));
|
||||
|
||||
transform.set_translation_xyz(3.0*143.0/2.0*3.0, 3.0*256.0/2.0, 0.0);
|
||||
|
||||
self.persistent_sprites.push(world
|
||||
.create_entity()
|
||||
.with(background_sprite.clone()) // Sprite Render
|
||||
.with(TiledScroller {
|
||||
speed: -75.0,
|
||||
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,
|
||||
})
|
||||
.with(transform.clone())
|
||||
.build());
|
||||
},
|
||||
PersistentSprite,
|
||||
));
|
||||
|
||||
transform.set_translation_xyz(3.0*168.0/2.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,
|
||||
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: 56.0 * 3.0,
|
||||
})
|
||||
.with(transform.clone())
|
||||
.build());
|
||||
height: ground_height,
|
||||
},
|
||||
Ground,
|
||||
PersistentSprite,
|
||||
));
|
||||
|
||||
transform.set_translation_xyz(3.0*168.0/2.0*3.0, 3.0*56.0/2.0, 0.1);
|
||||
|
||||
self.persistent_sprites.push(world
|
||||
.create_entity()
|
||||
.with(ground_sprite.clone()) // Sprite Render
|
||||
.with(TiledScroller {
|
||||
speed: -100.0,
|
||||
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: 56.0 * 3.0,
|
||||
})
|
||||
.with(transform.clone())
|
||||
.build());
|
||||
height: ground_height,
|
||||
},
|
||||
Ground,
|
||||
PersistentSprite,
|
||||
));
|
||||
|
||||
transform.set_translation_xyz(dimensions.width()*0.5, dimensions.height()*0.8, 0.2);
|
||||
// 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,
|
||||
));
|
||||
|
||||
self.sprites.push(world
|
||||
.create_entity()
|
||||
.with(flappy_bird_text_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_PLAY_BUTTON,
|
||||
}),
|
||||
..default()
|
||||
},
|
||||
Transform {
|
||||
translation: Vec3::new(width * 0.25, height * 0.4, 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_LEADERBOARD_BUTTON,
|
||||
}),
|
||||
..default()
|
||||
},
|
||||
Transform {
|
||||
translation: Vec3::new(width * 0.75, 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());
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
334
src/systems.rs
334
src/systems.rs
@@ -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 bevy::prelude::*;
|
||||
use crate::components::*;
|
||||
use crate::splash_state::*;
|
||||
use rand::Rng;
|
||||
|
||||
// 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();
|
||||
|
||||
// Reset position when off screen
|
||||
if transform.translation.x + scroller.width / 2.0 < 0.0 {
|
||||
transform.translation.x = scroller.width / 2.0 * 3.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Apply gravity
|
||||
birb.vertical_speed -= 1500.0 * time.delta_secs();
|
||||
transform.translation.y += birb.vertical_speed * time.delta_secs();
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
};
|
||||
|
||||
use log::info;
|
||||
use crate::components::*;
|
||||
|
||||
pub struct ScrollScrollables;
|
||||
|
||||
/*
|
||||
Pausable systems
|
||||
https://book.amethyst.rs/stable/controlling_system_execution/pausable_systems.html
|
||||
*/
|
||||
|
||||
// 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>,
|
||||
);
|
||||
|
||||
fn run(&mut self, (mut transforms, mut scrolling, time): Self::SystemData) {
|
||||
for (mut transform, mut object) in (&mut transforms, &mut scrolling).join() {
|
||||
|
||||
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);
|
||||
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;
|
||||
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,
|
||||
));
|
||||
}
|
||||
object.vertical_speed -= 1500.0 * time.delta_seconds();
|
||||
transform.prepend_translation_y(object.vertical_speed * time.delta_seconds());
|
||||
}
|
||||
|
||||
// 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,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user