Initial working commit

master
Wynd 2025-01-01 18:52:19 +02:00
commit cb75e8cf8c
23 changed files with 5246 additions and 0 deletions

1
.gitignore vendored 100644
View File

@ -0,0 +1 @@
/target

4312
Cargo.lock generated 100644

File diff suppressed because it is too large Load Diff

21
Cargo.toml 100644
View File

@ -0,0 +1,21 @@
cargo-features = ["codegen-backend"]
[package]
name = "avoid-rs"
version = "0.1.0"
edition = "2021"
[dependencies]
bevy = { version = "0.14", features = ["wav"] }
fastrand = "*"
[profile.dev]
codegen-backend = "cranelift"
lto = false
incremental = true
[profile.release]
codegen-units = 1
lto = true
strip = true
opt-level = 3

Binary file not shown.

BIN
assets/enemy1.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
assets/enemy2.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
assets/enemy3.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
assets/gameover.wav 100644

Binary file not shown.

BIN
assets/grave.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

BIN
assets/player.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

8
rustfmt.toml 100644
View File

@ -0,0 +1,8 @@
unstable_features = true
reorder_imports = true
hard_tabs = true
control_brace_style = "ClosingNextLine"
imports_granularity = "Crate"
group_imports = "StdExternalCrate"
edition = "2021"
newline_style = "Unix"

View File

@ -0,0 +1,53 @@
use bevy::prelude::*;
use super::velocity::Velocity;
pub struct AnimationPlugin;
impl Plugin for AnimationPlugin {
fn build(&self, app: &mut App) {
app.add_systems(FixedUpdate, animation_tick);
}
}
#[derive(Bundle, Default)]
pub struct AnimationBundle {
pub timer: AnimationTimer,
pub atlas: TextureAtlas,
pub indices: AnimationIndices,
}
#[derive(Component, Default)]
pub struct AnimationIndices {
pub first_index: usize,
pub last_index: usize,
}
#[derive(Component, Default, Deref, DerefMut)]
pub struct AnimationTimer(pub Timer);
fn animation_tick(
time: Res<Time>,
mut query: Query<(
&mut AnimationTimer,
&mut TextureAtlas,
&AnimationIndices,
&Velocity,
)>,
) {
for (mut timer, mut atlas, anim_indicies, velocity) in &mut query {
if velocity.length() > 0.0 {
timer.tick(time.delta());
if timer.just_finished() {
atlas.index = if atlas.index == anim_indicies.last_index {
anim_indicies.first_index
}
else {
atlas.index + 1
}
}
}
}
}

View File

@ -0,0 +1,4 @@
pub mod animation;
pub mod mouse;
pub mod path_follow;
pub mod velocity;

View File

@ -0,0 +1,39 @@
use bevy::prelude::*;
pub struct MousePlugin;
impl Plugin for MousePlugin {
fn build(&self, app: &mut App) {
app.insert_resource(Mouse::default());
app.add_systems(PreUpdate, mouse_position);
}
}
#[derive(Resource, Default, Deref, DerefMut)]
pub struct Mouse(pub Vec2);
fn mouse_position(
mut mouse: ResMut<Mouse>,
window: Query<&Window>,
camera: Query<(&Camera, &GlobalTransform)>,
) {
let Ok(window) = window.get_single()
else {
return;
};
if let Some(position) = window.cursor_position() {
let Ok((camera, camera_transform)) = camera.get_single()
else {
return;
};
let window_size = Vec2::new(window.width(), window.height());
let ndc = ((position / window_size) * 2.0 - Vec2::ONE) * Vec2::new(1., -1.);
let world_pos = camera
.ndc_to_world(camera_transform, ndc.extend(-1.0))
.unwrap_or_default();
let world_pos: Vec2 = world_pos.truncate();
mouse.0 = world_pos;
}
}

View File

@ -0,0 +1,91 @@
use bevy::prelude::*;
pub struct PathFollow2DPlugin;
impl Plugin for PathFollow2DPlugin {
fn build(&self, app: &mut App) {}
}
#[derive(Component, Default)]
pub struct PathFollow2D {
pub progress: f32,
pub points: Vec<Vec2>,
pub looping: bool,
}
impl PathFollow2D {
pub fn length(&self) -> f32 {
let mut len = 0f32;
let no = self.points.len();
for i in 0..no {
if i < no - 1 {
len += &self.points[i].distance(self.points[i + 1]);
}
else if self.looping && no > 2 {
len += &self.points[i].distance(self.points[0]);
}
}
len
}
pub fn get_progress_ratio(&self) -> f32 {
self.progress / self.length()
}
pub fn set_progress(&mut self, progress: f32) {
self.progress = self.progress.clamp(0.0, self.length());
}
pub fn get_pos(&self, dist: f32) -> (Vec2, Vec2) {
let dist = dist.clamp(0.0, 1.0);
let mut start_d = 0.0;
let mut no = self.points.len();
if self.looping {
no += 1;
}
for i in 0..no {
let end_d = (i + 1) as f32 / (no - 1) as f32;
if dist < start_d || dist > end_d {
start_d = end_d;
continue;
}
let local_dist = (dist - start_d) / (end_d - start_d);
let i1 = i;
let mut i2 = 0;
if i < no - 1 {
i2 = i + 1;
}
if self.looping && i2 >= no - 1 {
i2 = 0;
}
let v1 = &self.points[i1];
let v2 = &self.points[i2];
let d = v1.distance(*v2);
let px = v1.x + (d * local_dist / d) * (v2.x - v1.x);
let py = v1.y + (d * local_dist / d) * (v2.y - v1.y);
let pos = Vec2::new(px, py);
let dir = *v2 - *v1;
let dir = dir.normalize();
return (pos, dir);
}
(Vec2::default(), Vec2::default())
}
}
#[derive(Bundle, Default)]
pub struct PathFollow2DBundle {
pub path: PathFollow2D,
pub transform: Transform,
}

View File

@ -0,0 +1,19 @@
use bevy::prelude::*;
pub struct VelocityPlugin;
impl Plugin for VelocityPlugin {
fn build(&self, app: &mut App) {
app.add_systems(FixedUpdate, apply_velocity);
}
}
#[derive(Component, Default, Deref, DerefMut)]
pub struct Velocity(pub Vec2);
fn apply_velocity(time: Res<Time>, mut query: Query<(&mut Transform, &Velocity)>) {
for (mut transform, velocity) in &mut query {
transform.translation.x += velocity.x * time.delta_seconds();
transform.translation.y += velocity.y * time.delta_seconds();
}
}

44
src/enemy.rs 100644
View File

@ -0,0 +1,44 @@
use bevy::{color::palettes::css::RED, prelude::*};
use crate::common::velocity::VelocityPlugin;
pub struct EnemyPlugin;
impl Plugin for EnemyPlugin {
fn build(&self, app: &mut App) {
// app.add_systems(Startup, setup);
app.add_systems(Update, debug_direction);
app.add_systems(FixedUpdate, outside_bounds);
}
}
#[derive(Component)]
pub struct Enemy;
fn debug_direction(query: Query<&Transform, With<Enemy>>, mut gizmos: Gizmos) {
for enemy in &query {
// let start = enemy.translation.truncate();
// let end = enemy.rotation.xyz().truncate() - start;
// gizmos.arrow_2d(start, end, RED);
}
}
fn outside_bounds(
enemies: Query<(Entity, &Transform), With<Enemy>>,
window: Query<&Window>,
mut commands: Commands,
) {
let window = window.single();
let screen_size: Vec3 = window.physical_size().as_vec2().extend(1.0);
for (entity, transform) in &enemies {
let pos = transform.translation.truncate();
if pos.x < -60.0
|| pos.y < -60.0
|| pos.x > screen_size.x + 60.0
|| pos.y > screen_size.y + 60.0
{
commands.entity(entity).despawn();
}
}
}

180
src/hud.rs 100644
View File

@ -0,0 +1,180 @@
use std::path::Path;
use bevy::{
asset::{io::AssetSourceId, AssetPath},
prelude::*,
};
use crate::{player::Player, score::Score, GameStartEvent, GameState, START_POSITION};
pub struct HUDPlugin;
impl Plugin for HUDPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Startup, setup);
app.add_systems(Update, start_button_logic);
}
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// let asset_path: AssetPath = "embedded://avoid_rs/assets/Xolonium-Regular.ttf".into();
let asset_path: AssetPath = "Xolonium-Regular.ttf".into();
let font: Handle<_> = asset_server.load(asset_path);
// let path = Path::new("avoid-rs").join("assets/Xolonium-Regular.ttf");
// let source = AssetSourceId::from("embedded");
// let asset_path = AssetPath::from_path(&path).with_source(source);
// let font: Handle<_> = asset_server.load(asset_path);
// score UI, top middle
commands
.spawn(NodeBundle {
style: Style {
width: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..Default::default()
},
..Default::default()
})
.with_children(|parent| {
parent.spawn((
TextBundle::from_section(
"0",
TextStyle {
font: font.clone(),
font_size: 64.0,
..Default::default()
},
)
.with_text_justify(JustifyText::Center)
.with_style(Style {
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..Default::default()
}),
ScoreText,
));
});
commands
.spawn(NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..Default::default()
},
..Default::default()
})
.with_children(|parent| {
// game info, middle of the screen
parent.spawn((
TextBundle::from_section(
"Dodge the creeps!",
TextStyle {
font: font.clone(),
font_size: 64.0,
..Default::default()
},
)
.with_text_justify(JustifyText::Center)
.with_style(Style {
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..Default::default()
}),
IntroMessageText,
StartMenuUI,
));
});
commands
.spawn(NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(90.0),
align_items: AlignItems::End,
justify_content: JustifyContent::Center,
..Default::default()
},
..Default::default()
})
.with_children(|parent| {
parent
.spawn((
ButtonBundle {
style: Style {
width: Val::Px(200.0),
height: Val::Px(100.0),
border: UiRect::all(Val::Px(5.0)),
// horizontally center child text
justify_content: JustifyContent::Center,
// vertically center child text
align_items: AlignItems::Center,
..default()
},
border_color: BorderColor(Color::BLACK),
border_radius: BorderRadius::all(Val::Px(10.0)),
background_color: Color::srgb(0.15, 0.15, 0.15).into(),
..default()
},
StartButton,
StartMenuUI,
))
.with_children(|parent| {
parent.spawn(TextBundle::from_section(
"Start",
TextStyle {
font: font.clone(),
font_size: 64.0,
color: Color::srgb(0.9, 0.9, 0.9),
},
));
});
});
}
#[derive(Component)]
pub struct ScoreText;
#[derive(Component)]
pub struct IntroMessageText;
#[derive(Component)]
pub struct StartButton;
#[derive(Component)]
pub struct StartMenuUI;
fn start_button_logic(
mut state: ResMut<NextState<GameState>>,
mut score: ResMut<Score>,
mut button: Query<(&Interaction, &mut BorderColor), With<StartButton>>,
mut player_query: Query<(&mut Transform, &mut Visibility), With<Player>>,
mut ui_elems_query: Query<&mut Visibility, (With<StartMenuUI>, Without<Player>)>,
mut score_text_query: Query<&mut Text, With<ScoreText>>,
) {
let (interaction, mut border_color) = button.single_mut();
match *interaction {
Interaction::Pressed => {
let mut score_text = score_text_query.single_mut();
score.0 = 0;
score_text.sections[0].value = format!("{}", score.0);
state.set(GameState::Playing);
let (mut player_transform, mut player_visibility) = player_query.single_mut();
for mut elem in &mut ui_elems_query {
*elem = Visibility::Hidden;
}
*player_visibility = Visibility::Visible;
player_transform.translation = START_POSITION;
}
Interaction::Hovered => border_color.0 = Color::WHITE,
Interaction::None => border_color.0 = Color::BLACK,
}
}

231
src/level.rs 100644
View File

@ -0,0 +1,231 @@
use std::f32::consts::PI;
use bevy::{
color::palettes::css::{LIME, RED},
math::bounding::{Aabb2d, BoundingCircle, BoundingVolume, IntersectsVolume},
prelude::*,
};
// use rand::Rng;
use crate::{
common::{
animation::{AnimationBundle, AnimationIndices, AnimationPlugin, AnimationTimer},
path_follow::{PathFollow2D, PathFollow2DBundle, PathFollow2DPlugin},
velocity::{Velocity, VelocityPlugin},
},
enemy::{Enemy, EnemyPlugin},
hud::{HUDPlugin, ScoreText, StartMenuUI},
player::{Player, PlayerPlugin},
score::{Score, ScorePlugin},
GameOverEvent, GameState,
};
pub struct LevelPlugin;
impl Plugin for LevelPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(ScorePlugin);
app.add_plugins(PlayerPlugin);
app.add_plugins(EnemyPlugin);
app.add_plugins(HUDPlugin);
app.add_plugins(VelocityPlugin);
app.add_plugins(AnimationPlugin);
app.add_plugins(PathFollow2DPlugin);
app.insert_resource(SpawnTimer(Timer::from_seconds(0.5, TimerMode::Repeating)));
app.add_systems(Startup, setup);
app.add_systems(Update, (debug_gizmos, debug_gizmos_config));
app.add_systems(
FixedUpdate,
(spawn_enemy, check_for_collisions, game_over).run_if(in_state(GameState::Playing)),
);
}
}
fn setup(mut commands: Commands) {
commands.spawn(PathFollow2DBundle {
path: PathFollow2D {
progress: 0.0,
points: vec![
Vec2::new(0.0, 720.0),
Vec2::new(480.0, 720.0),
Vec2::new(480.0, 0.0),
Vec2::new(0.0, 0.0),
],
looping: true,
..Default::default()
},
..Default::default()
});
}
#[derive(Resource, Deref, DerefMut)]
struct SpawnTimer(Timer);
#[derive(Bundle, Default)]
pub struct UnitBundle {
pub sprite: SpriteBundle,
pub velocity: Velocity,
pub animator: AnimationBundle,
}
#[derive(Component)]
pub struct Collider;
#[derive(Event, Default)]
pub struct CollisionEvent;
fn spawn_enemy(
mut commands: Commands,
asset_server: Res<AssetServer>,
time: Res<Time>,
mut timer: ResMut<SpawnTimer>,
path: Query<&PathFollow2D>,
mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
) {
timer.tick(time.delta());
if timer.finished() {
let path = path.single();
let rng = fastrand::f32();
let (spawn_pos, path_dir) = path.get_pos(rng);
let path_dir = path_dir.to_angle() - PI / 2.0;
let random_angle = fastrand::f32() * 2.0 - 1.0;
let random_angle = random_angle * (PI / 4.0);
let path_dir = path_dir + random_angle;
let direction = Vec2::from_angle(path_dir);
let velocity = direction.normalize() * fastrand::u32(150..350) as f32;
let direction = Quat::from_rotation_z(direction.to_angle());
let (idx, grid) = match fastrand::u32(0..3) {
0 => (1, UVec2::new(135, 96)),
1 => (2, UVec2::new(132, 96)),
2 => (3, UVec2::new(110, 190)),
_ => (1, UVec2::new(135, 96)),
};
let idx = format!("enemy{idx}.png");
let layout = TextureAtlasLayout::from_grid(grid, 2, 1, None, None);
let texture_atlas_layouts = texture_atlas_layouts.add(layout);
commands.spawn((
Enemy,
Collider,
UnitBundle {
sprite: SpriteBundle {
transform: Transform {
translation: spawn_pos.extend(1.0),
rotation: direction,
scale: Vec2::new(0.75, 0.75).extend(1.0),
},
texture: asset_server.load(idx),
..Default::default()
},
animator: AnimationBundle {
atlas: TextureAtlas {
layout: texture_atlas_layouts,
index: 0,
},
timer: AnimationTimer(Timer::from_seconds(0.3, TimerMode::Repeating)),
indices: AnimationIndices {
first_index: 0,
last_index: 1,
},
..Default::default()
},
velocity: Velocity(velocity),
},
));
}
}
fn check_for_collisions(
mut commands: Commands,
mut score: ResMut<Score>,
mut player_query: Query<(&mut Velocity, &Transform), With<Player>>,
collider_query: Query<(Entity, &Transform), (With<Collider>, With<Enemy>)>,
mut game_over_events: EventWriter<GameOverEvent>,
) {
let (mut player_velocity, player_transform) = player_query.single_mut();
for (collider_entity, collider_transform) in &collider_query {
let collision = player_collision(
BoundingCircle::new(player_transform.translation.truncate(), 20.0),
BoundingCircle::new(collider_transform.translation.truncate(), 30.0).aabb_2d(),
);
// game over
if collision {
game_over_events.send_default();
}
}
}
fn game_over(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut state: ResMut<NextState<GameState>>,
mut player_query: Query<&mut Visibility, With<Player>>,
enemies_query: Query<Entity, With<Enemy>>,
mut ui_elems_query: Query<&mut Visibility, (With<StartMenuUI>, Without<Player>)>,
mut game_over_events: EventReader<GameOverEvent>,
) {
if game_over_events.is_empty() {
return;
}
game_over_events.clear();
state.set(GameState::Menu);
let mut player_visibility = player_query.single_mut();
*player_visibility = Visibility::Hidden;
for mut elem in &mut ui_elems_query {
*elem = Visibility::Visible;
}
for enemy in &enemies_query {
commands.entity(enemy).despawn();
}
commands.spawn(AudioBundle {
source: asset_server.load("gameover.wav"),
// auto-despawn the entity when playback finishes
settings: PlaybackSettings::DESPAWN,
});
}
fn player_collision(player: BoundingCircle, bounding_box: Aabb2d) -> bool {
if !player.intersects(&bounding_box) {
return false;
}
true
}
fn debug_gizmos(mut gizmos: Gizmos, query: Query<&PathFollow2D>) {
for path in &query {
// let x = path.points.len();
// for i in 0..x {
// if i < x - 1 {
// gizmos.line_2d(path.points[i], path.points[i + 1], LIME);
// }
// else if path.looping && x > 2 {
// gizmos.line_2d(path.points[i], path.points[0], LIME);
// }
// }
// let p = path.get_pos(0.125);
// gizmos.circle_2d(p, 15.0, RED);
}
}
fn debug_gizmos_config(mut config_store: ResMut<GizmoConfigStore>) {
let (config, _) = config_store.config_mut::<DefaultGizmoConfigGroup>();
// config.line_width = 15.0;
}

111
src/main.rs 100644
View File

@ -0,0 +1,111 @@
use std::default;
use bevy::{
asset::{embedded_asset, embedded_path},
diagnostic::FrameTimeDiagnosticsPlugin,
prelude::*,
render::camera::Viewport,
window::{WindowMode, WindowResolution},
};
use common::mouse::{Mouse, MousePlugin};
use hud::HUDPlugin;
use level::LevelPlugin;
use player::PlayerPlugin;
use score::ScorePlugin;
mod common;
mod enemy;
mod hud;
mod level;
mod player;
mod score;
pub const SCREEN_WIDTH: u32 = 480;
pub const SCREEN_HEIGHT: u32 = 720;
pub const START_POSITION: Vec3 =
Vec3::new((SCREEN_WIDTH / 2) as f32, (SCREEN_HEIGHT / 2) as f32, 0.0);
#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, States)]
pub enum GameState {
#[default]
Menu,
Playing,
GameOver,
}
#[derive(Event, Default)]
struct GameOverEvent;
#[derive(Event, Default)]
struct GameStartEvent;
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2dBundle {
camera: Camera {
// viewport: Some(Viewport {
// physical_position: UVec2::new(0, 0),
// physical_size: UVec2::new(480, 720),
// ..Default::default()
// }),
..Default::default()
},
transform: Transform {
translation: Vec3 {
x: (SCREEN_WIDTH / 2) as f32,
y: (SCREEN_HEIGHT / 2) as f32,
z: 1.0,
},
..Default::default()
},
..Default::default()
});
}
fn debug_mouse(mouse: Res<Mouse>) {
// println!("x: {} y: {}", mouse.x, mouse.y);
}
fn main() {
App::new()
.add_plugins((
DefaultPlugins
.build()
.set(WindowPlugin {
primary_window: Some(Window {
title: "Avoid".to_string(),
name: Some("avoid.app".to_string()),
// resizable: false,
position: WindowPosition::Centered(MonitorSelection::Current),
resolution: WindowResolution::new(
SCREEN_WIDTH as f32,
SCREEN_HEIGHT as f32,
),
..Default::default()
}),
..Default::default()
})
.set(ImagePlugin::default_nearest()),
FrameTimeDiagnosticsPlugin,
// EmbeddedAssetPlugin,
LevelPlugin,
MousePlugin,
))
.init_state::<GameState>()
.add_event::<GameOverEvent>()
.add_event::<GameStartEvent>()
.add_systems(Startup, setup)
.add_systems(Update, debug_mouse)
.run();
}
struct EmbeddedAssetPlugin;
// impl Plugin for EmbeddedAssetPlugin {
// fn build(&self, app: &mut App) {
// let prefix = "avoid-rs/";
//
// // embedded_asset!(app, "../assets/player.png");
// embedded_asset!(app, "../assets/Xolonium-Regular.ttf");
// // embedded_asset!(app, "./assets/Xolonium-Regular.ttf");
// }
// }

97
src/player.rs 100644
View File

@ -0,0 +1,97 @@
use std::f32::consts::PI;
use bevy::{prelude::*, render::view::VisibilityPlugin};
use crate::{
common::{
animation::{AnimationBundle, AnimationIndices, AnimationPlugin, AnimationTimer},
velocity::Velocity,
},
level::{Collider, UnitBundle},
GameState, SCREEN_HEIGHT, SCREEN_WIDTH, START_POSITION,
};
pub struct PlayerPlugin;
impl Plugin for PlayerPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Startup, setup);
app.add_systems(
FixedUpdate,
(move_player).run_if(in_state(GameState::Playing)),
);
}
}
#[derive(Component)]
pub struct Player;
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
) {
let layout = TextureAtlasLayout::from_grid(UVec2::splat(135), 2, 1, None, None);
let texture_atlas_layouts = texture_atlas_layouts.add(layout);
commands.spawn((
Player,
Collider,
UnitBundle {
sprite: SpriteBundle {
transform: Transform {
translation: START_POSITION,
scale: Vec2::new(0.5, 0.5).extend(1.0),
..Default::default()
},
visibility: Visibility::Hidden,
texture: asset_server.load("player.png"),
..Default::default()
},
animator: AnimationBundle {
atlas: TextureAtlas {
layout: texture_atlas_layouts,
index: 0,
},
timer: AnimationTimer(Timer::from_seconds(0.3, TimerMode::Repeating)),
indices: AnimationIndices {
first_index: 0,
last_index: 1,
},
},
..Default::default()
},
));
}
fn move_player(
keys: Res<ButtonInput<KeyCode>>,
mut query: Query<(&mut Transform, &mut Velocity), With<Player>>,
window: Query<&Window>,
) {
let (mut transform, mut velocity) = query.single_mut();
velocity.0 = Vec2::ZERO;
if keys.pressed(KeyCode::ArrowRight) {
velocity.x += 1.0;
}
if keys.pressed(KeyCode::ArrowLeft) {
velocity.x -= 1.0;
}
if keys.pressed(KeyCode::ArrowUp) {
velocity.y += 1.0;
}
if keys.pressed(KeyCode::ArrowDown) {
velocity.y -= 1.0;
}
if velocity.length() > 0.0 {
velocity.0 = velocity.normalize() * 400.0;
let angle = velocity.to_angle() - PI / 2.0;
let angle = Quat::from_rotation_z(angle);
transform.rotation = angle;
}
let window = window.single();
let screen_size: Vec3 = window.physical_size().as_vec2().extend(1.0);
transform.translation = transform.translation.clamp(Vec3::ZERO, screen_size);
}

33
src/score.rs 100644
View File

@ -0,0 +1,33 @@
use bevy::prelude::*;
use crate::{hud::ScoreText, GameState};
pub struct ScorePlugin;
impl Plugin for ScorePlugin {
fn build(&self, app: &mut App) {
app.insert_resource(Score(0));
app.insert_resource(ScoreTimer(Timer::from_seconds(1.0, TimerMode::Repeating)));
app.add_systems(Update, score_tick.run_if(in_state(GameState::Playing)));
}
}
#[derive(Resource)]
pub struct Score(pub u32);
#[derive(Resource, Deref, DerefMut)]
struct ScoreTimer(Timer);
fn score_tick(
mut score: ResMut<Score>,
time: Res<Time>,
mut timer: ResMut<ScoreTimer>,
mut score_text: Query<&mut Text, With<ScoreText>>,
) {
let mut score_text = score_text.single_mut();
timer.tick(time.delta());
if timer.just_finished() {
score.0 += 1;
score_text.sections[0].value = format!("{}", score.0);
}
}