implementation
This commit is contained in:
+25
@@ -5,3 +5,28 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
bevy = "0.19.0"
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
|
||||
# Enable more optimization in the release profile at the cost of compile time.
|
||||
[profile.release]
|
||||
# Do a second optimization pass over the entire program, including dependencies.
|
||||
# Slows compile times, marginal improvements.
|
||||
lto = "thin"
|
||||
# Compile the entire crate as one unit.
|
||||
# Slows compile times, marginal improvements.
|
||||
codegen-units = 1
|
||||
|
||||
# Optimize for size in the wasm-release profile to reduce load times and bandwidth usage on web.
|
||||
[profile.wasm-release]
|
||||
# Default to release profile values.
|
||||
inherits = "release"
|
||||
# Optimize with size in mind (also try "z", sometimes it is better).
|
||||
# Slightly slows compile times, great improvements to file size and runtime performance.
|
||||
opt-level = "s"
|
||||
# Strip all debugging information from the binary to slightly reduce file size.
|
||||
strip = "debuginfo"
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
|
||||
#import bevy_sprite::mesh2d_view_bindings::globals
|
||||
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(0) var base_color_texture: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(1) var base_color_sampler: sampler;
|
||||
|
||||
@fragment
|
||||
fn fragment(mesh: VertexOutput) -> @location(0) vec4<f32> {
|
||||
return textureSample(base_color_texture, base_color_sampler, mesh.uv + vec2(globals.time / 10., 0.));
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
+110
@@ -0,0 +1,110 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use bevy::{image::ImageLoaderSettings, prelude::*, time::common_conditions::on_timer};
|
||||
|
||||
pub const CANVAS_SIZE: Vec2 = Vec2::new(480., 270.);
|
||||
pub const PLAYER_SIZE: f32 = 25.0;
|
||||
pub const PIPE_SPEED: f32 = 200.0;
|
||||
const PIPE_SIZE: Vec2 = Vec2::new(32., CANVAS_SIZE.y);
|
||||
const GAP_SIZE: f32 = 100.0;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Pipe;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct PipeTop;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct PipeBottom;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct PointsGate;
|
||||
|
||||
pub struct PipePlugin;
|
||||
|
||||
impl Plugin for PipePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(
|
||||
FixedUpdate,
|
||||
(
|
||||
despawn_pipes,
|
||||
shift_pipes_to_the_left,
|
||||
spawn_pipes.run_if(on_timer(Duration::from_secs(1))),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_pipes(mut commands: Commands, asset_server: Res<AssetServer>, time: Res<Time>) {
|
||||
let image = asset_server
|
||||
.load_builder()
|
||||
.with_settings(|settings: &mut ImageLoaderSettings| {
|
||||
settings
|
||||
.sampler
|
||||
.get_or_init_descriptor()
|
||||
.set_filter(bevy::image::ImageFilterMode::Nearest);
|
||||
})
|
||||
.load("pipe.png");
|
||||
|
||||
let image_mode = SpriteImageMode::Sliced(TextureSlicer {
|
||||
border: BorderRect::axes(8., 19.),
|
||||
center_scale_mode: SliceScaleMode::Stretch,
|
||||
..default()
|
||||
});
|
||||
|
||||
let transform = Transform::from_xyz(CANVAS_SIZE.x / 2., 0., 1.);
|
||||
let gap_y_position = (time.elapsed_secs() * 4.2309875).sin() * CANVAS_SIZE.y / 4.;
|
||||
let pipe_offset = PIPE_SIZE.y / 2.0 + GAP_SIZE / 2.0;
|
||||
|
||||
commands.spawn((
|
||||
transform,
|
||||
Visibility::Visible,
|
||||
Pipe,
|
||||
children![
|
||||
(
|
||||
Sprite {
|
||||
image: image.clone(),
|
||||
image_mode: image_mode.clone(),
|
||||
custom_size: Some(PIPE_SIZE),
|
||||
..default()
|
||||
},
|
||||
Transform::from_xyz(0.0, pipe_offset + gap_y_position, 1.0),
|
||||
PipeTop
|
||||
),
|
||||
(
|
||||
Visibility::Hidden,
|
||||
Sprite {
|
||||
color: Color::WHITE,
|
||||
custom_size: Some(Vec2::new(10.0, GAP_SIZE)),
|
||||
..default()
|
||||
},
|
||||
Transform::from_xyz(0.0, gap_y_position, 1.0),
|
||||
PointsGate,
|
||||
),
|
||||
(
|
||||
Sprite {
|
||||
image,
|
||||
image_mode,
|
||||
custom_size: Some(PIPE_SIZE),
|
||||
..default()
|
||||
},
|
||||
Transform::from_xyz(0.0, -pipe_offset + gap_y_position, 1.0),
|
||||
PipeBottom
|
||||
)
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
pub fn shift_pipes_to_the_left(mut pipes: Query<&mut Transform, With<Pipe>>, time: Res<Time>) {
|
||||
for mut pipe in &mut pipes {
|
||||
pipe.translation.x = PIPE_SPEED.mul_add(-time.delta_secs(), pipe.translation.x);
|
||||
}
|
||||
}
|
||||
|
||||
fn despawn_pipes(mut commands: Commands, pipes: Query<(Entity, &Transform), With<Pipe>>) {
|
||||
for (entity, transform) in pipes.iter() {
|
||||
if transform.translation.x < -(CANVAS_SIZE.x / 2.0 + PIPE_SIZE.x) {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
}
|
||||
+260
-2
@@ -1,3 +1,261 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
use bevy::{
|
||||
camera::ScalingMode,
|
||||
color::palettes::tailwind::RED_400,
|
||||
image::{ImageAddressMode, ImageLoaderSettings},
|
||||
math::bounding::{Aabb2d, BoundingCircle, IntersectsVolume},
|
||||
prelude::*,
|
||||
render::render_resource::AsBindGroup,
|
||||
shader::ShaderRef,
|
||||
sprite_render::{Material2d, Material2dPlugin},
|
||||
};
|
||||
use flappy_bird::*;
|
||||
|
||||
// #[derive(Component)] generates an implementation for the
|
||||
// Component trait (trait is like an interface) at compile time
|
||||
#[derive(Component)]
|
||||
#[require(Gravity(1000.), Velocity)]
|
||||
struct Player;
|
||||
|
||||
#[derive(Component)]
|
||||
struct Gravity(f32);
|
||||
|
||||
#[derive(Component, Default)]
|
||||
struct Velocity(f32);
|
||||
|
||||
#[derive(Event)]
|
||||
struct EndGame;
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
struct Score(u32);
|
||||
|
||||
#[derive(Event)]
|
||||
pub struct ScorePoint;
|
||||
|
||||
#[derive(Component)]
|
||||
struct ScoreText;
|
||||
|
||||
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
|
||||
pub struct BackgroundMaterial {
|
||||
#[texture(0)]
|
||||
#[sampler(1)]
|
||||
pub color_texture: Handle<Image>,
|
||||
}
|
||||
|
||||
impl Material2d for BackgroundMaterial {
|
||||
fn fragment_shader() -> ShaderRef {
|
||||
"background.wgsl".into()
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> AppExit {
|
||||
App::new()
|
||||
.init_resource::<Score>()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_plugins((
|
||||
PipePlugin,
|
||||
Material2dPlugin::<BackgroundMaterial>::default(),
|
||||
))
|
||||
.add_systems(Startup, startup)
|
||||
.add_systems(
|
||||
FixedUpdate,
|
||||
(gravity, check_in_bounds, check_collisions).chain(),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
controls,
|
||||
score_update.run_if(resource_changed::<Score>),
|
||||
enforce_bird_direction,
|
||||
),
|
||||
)
|
||||
.add_observer(respawn_on_endgame)
|
||||
.add_observer(|_trigger: On<ScorePoint>, mut score: ResMut<Score>| {
|
||||
score.0 += 1;
|
||||
})
|
||||
.run()
|
||||
}
|
||||
|
||||
fn startup(
|
||||
mut commands: Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
mut config_store: ResMut<GizmoConfigStore>,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<BackgroundMaterial>>,
|
||||
) {
|
||||
// Control wether gizmos are drawn
|
||||
let (config, _) = config_store.config_mut::<DefaultGizmoConfigGroup>();
|
||||
config.enabled = false;
|
||||
|
||||
// Pass a tuple of components
|
||||
commands.spawn((
|
||||
Camera2d,
|
||||
Projection::Orthographic(OrthographicProjection {
|
||||
scaling_mode: ScalingMode::AutoMax {
|
||||
max_width: CANVAS_SIZE.x,
|
||||
max_height: CANVAS_SIZE.y,
|
||||
},
|
||||
..OrthographicProjection::default_2d()
|
||||
}),
|
||||
));
|
||||
|
||||
commands.spawn((
|
||||
Mesh2d(meshes.add(Rectangle::new(CANVAS_SIZE.x, CANVAS_SIZE.x))),
|
||||
MeshMaterial2d(
|
||||
materials.add(BackgroundMaterial {
|
||||
color_texture: asset_server
|
||||
.load_builder()
|
||||
.with_settings(|settings: &mut ImageLoaderSettings| {
|
||||
settings
|
||||
.sampler
|
||||
.get_or_init_descriptor()
|
||||
.set_address_mode(ImageAddressMode::Repeat);
|
||||
})
|
||||
.load("background_color_grass.png"),
|
||||
}),
|
||||
),
|
||||
));
|
||||
|
||||
commands.spawn((
|
||||
Player,
|
||||
Sprite {
|
||||
// Use Some(...) because custom_size expects an Option<Vec2>
|
||||
custom_size: Some(Vec2::splat(PLAYER_SIZE)),
|
||||
image: asset_server.load("bevy-bird.png"),
|
||||
color: Srgba::hex("#282828").unwrap().into(),
|
||||
// The .. fills the remaining fields from another Sprite struct instance,
|
||||
// default() in this case
|
||||
..default()
|
||||
},
|
||||
Transform::from_xyz(-CANVAS_SIZE.x / 4.0, 0.0, 1.0),
|
||||
));
|
||||
|
||||
commands.spawn((
|
||||
Node {
|
||||
width: percent(100.),
|
||||
margin: px(20.).top(),
|
||||
..default()
|
||||
},
|
||||
Text::new("0"),
|
||||
TextLayout::new(Justify::Center, LineBreak::WordBoundary),
|
||||
TextFont {
|
||||
font_size: FontSize::Px(33.0),
|
||||
..default()
|
||||
},
|
||||
TextColor(Srgba::hex("#282828").unwrap().into()),
|
||||
ScoreText,
|
||||
));
|
||||
}
|
||||
|
||||
fn gravity(mut transforms: Query<(&mut Transform, &mut Velocity, &Gravity)>, time: Res<Time>) {
|
||||
for (mut transform, mut velocity, gravity) in &mut transforms {
|
||||
// Velocity is a tuple struct: struct Velocity(f32) in contrast
|
||||
// to a named struct struct: struct Velocity { value: f32 }.
|
||||
// The elements are accessed by index with .0, .1, etc.
|
||||
velocity.0 = gravity.0.mul_add(-time.delta_secs(), velocity.0);
|
||||
|
||||
transform.translation.y = velocity
|
||||
.0
|
||||
.mul_add(time.delta_secs(), transform.translation.y);
|
||||
}
|
||||
}
|
||||
|
||||
fn controls(
|
||||
mut velocity: Single<&mut Velocity, With<Player>>,
|
||||
buttons: Res<ButtonInput<MouseButton>>,
|
||||
) {
|
||||
if buttons.any_just_pressed([MouseButton::Left, MouseButton::Right]) {
|
||||
velocity.0 = 400.;
|
||||
}
|
||||
}
|
||||
|
||||
fn check_in_bounds(player: Single<&Transform, With<Player>>, mut commands: Commands) {
|
||||
if player.translation.y < -CANVAS_SIZE.y / 2.0 - PLAYER_SIZE
|
||||
|| player.translation.y > CANVAS_SIZE.y / 2.0 + PLAYER_SIZE
|
||||
{
|
||||
commands.trigger(EndGame);
|
||||
}
|
||||
}
|
||||
|
||||
fn respawn_on_endgame(
|
||||
_: On<EndGame>,
|
||||
mut commands: Commands,
|
||||
player: Single<Entity, With<Player>>,
|
||||
mut score: ResMut<Score>,
|
||||
) {
|
||||
score.0 = 0;
|
||||
commands.entity(*player).insert((
|
||||
Transform::from_xyz(-CANVAS_SIZE.x / 4.0, 0.0, 1.0),
|
||||
Velocity(0.),
|
||||
));
|
||||
}
|
||||
|
||||
fn check_collisions(
|
||||
mut commands: Commands,
|
||||
player: Single<(&Sprite, Entity), With<Player>>,
|
||||
pipe_segments: Query<(&Sprite, Entity), Or<(With<PipeTop>, With<PipeBottom>)>>,
|
||||
pipe_gaps: Query<(&Sprite, Entity), With<PointsGate>>,
|
||||
mut gizmos: Gizmos,
|
||||
transform_helper: TransformHelper,
|
||||
) -> Result<()> {
|
||||
let player_transform = transform_helper.compute_global_transform(player.1)?;
|
||||
let player_collider =
|
||||
BoundingCircle::new(player_transform.translation().xy(), PLAYER_SIZE / 2.);
|
||||
|
||||
gizmos.circle_2d(
|
||||
player_transform.translation().xy(),
|
||||
PLAYER_SIZE / 2.,
|
||||
RED_400,
|
||||
);
|
||||
|
||||
for (sprite, entity) in &pipe_segments {
|
||||
let pipe_transform = transform_helper.compute_global_transform(entity)?;
|
||||
let pipe_collider = Aabb2d::new(
|
||||
pipe_transform.translation().xy(),
|
||||
sprite.custom_size.unwrap() / 2.,
|
||||
);
|
||||
|
||||
gizmos.rect_2d(
|
||||
pipe_transform.translation().xy(),
|
||||
sprite.custom_size.unwrap(),
|
||||
RED_400,
|
||||
);
|
||||
if player_collider.intersects(&pipe_collider) {
|
||||
commands.trigger(EndGame);
|
||||
// Despawn so we don't reset multiple times while stuck on the pipe
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
|
||||
for (sprite, entity) in &pipe_gaps {
|
||||
let gap_transform = transform_helper.compute_global_transform(entity)?;
|
||||
let gap_collider = Aabb2d::new(
|
||||
gap_transform.translation().xy(),
|
||||
sprite.custom_size.unwrap() / 2.,
|
||||
);
|
||||
|
||||
gizmos.rect_2d(
|
||||
gap_transform.translation().xy(),
|
||||
sprite.custom_size.unwrap().xy(),
|
||||
RED_400,
|
||||
);
|
||||
|
||||
if player_collider.intersects(&gap_collider) {
|
||||
commands.trigger(ScorePoint);
|
||||
// Despawn so we only count the score once
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn score_update(mut query: Query<&mut Text, With<ScoreText>>, score: Res<Score>) {
|
||||
for mut span in &mut query {
|
||||
span.0 = score.0.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
fn enforce_bird_direction(mut player: Single<(&mut Transform, &Velocity), With<Player>>) {
|
||||
let calculated_velocity = Vec2::new(PIPE_SPEED, player.1.0);
|
||||
player.0.rotation = Quat::from_rotation_z(calculated_velocity.to_angle())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user