implementation
This commit is contained in:
+25
@@ -5,3 +5,28 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy = "0.19.0"
|
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() {
|
use bevy::{
|
||||||
println!("Hello, world!");
|
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