diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 04aa607..5cb049a 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -30,6 +30,9 @@ jobs:
- name: Cargo clippy
run: cargo clippy --workspace --all-targets --all-features -- -Dwarnings --no-deps
+ - name: Cargo clippy (examples)
+ run: cargo clippy --examples -- -Dwarnings --no-deps
+
- name: Cargo fmt
run: cargo fmt --all -- --check
diff --git a/.rustfmt.toml b/.rustfmt.toml
index 430a04f..2a9b31e 100644
--- a/.rustfmt.toml
+++ b/.rustfmt.toml
@@ -10,8 +10,8 @@ normalize_comments = true
normalize_doc_attributes = true
overflow_delimited_expr = true
reorder_impl_items = true
-single_line_if_else_max_width = 80 # 50
-single_line_let_else_max_width = 80 # 50
+single_line_if_else_max_width = 80 # 50
+single_line_let_else_max_width = 80 # 50
unstable_features = true
use_field_init_shorthand = true
use_try_shorthand = true
diff --git a/Cargo.lock b/Cargo.lock
index 668c68e..7fb8900 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -90,7 +90,7 @@ dependencies = [
"getrandom",
"once_cell",
"version_check",
- "zerocopy",
+ "zerocopy 0.7.35",
]
[[package]]
@@ -867,8 +867,10 @@ dependencies = [
"bevy_app",
"bevy_ecs",
"bevy_utils",
+ "tracing-error",
"tracing-log",
"tracing-subscriber",
+ "tracing-tracy",
"tracing-wasm",
]
@@ -1088,6 +1090,7 @@ dependencies = [
"naga",
"naga_oil",
"nonmax",
+ "profiling",
"ruzstd",
"send_wrapper",
"serde",
@@ -2203,11 +2206,26 @@ dependencies = [
"log",
"macro_rules_attribute",
"macros",
+ "rand",
"serde",
"toml",
"web-sys",
]
+[[package]]
+name = "generator"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "186014d53bc231d0090ef8d6f03e0920c54d85a5ed22f4f2f74315ec56cf83fb"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "libc",
+ "log",
+ "rustversion",
+ "windows 0.54.0",
+]
+
[[package]]
name = "gethostname"
version = "0.4.3"
@@ -2848,6 +2866,19 @@ version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
+[[package]]
+name = "loom"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
+dependencies = [
+ "cfg-if",
+ "generator",
+ "scoped-tls",
+ "tracing",
+ "tracing-subscriber",
+]
+
[[package]]
name = "mach2"
version = "0.4.2"
@@ -3587,6 +3618,15 @@ dependencies = [
"unicode-xid",
]
+[[package]]
+name = "ppv-lite86"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dee4364d9f3b902ef14fab8a1ddffb783a1cb6b4bba3bfc1fa3922732c7de97f"
+dependencies = [
+ "zerocopy 0.6.6",
+]
+
[[package]]
name = "presser"
version = "0.3.1"
@@ -3622,6 +3662,20 @@ name = "profiling"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58"
+dependencies = [
+ "profiling-procmacros",
+ "tracing",
+]
+
+[[package]]
+name = "profiling-procmacros"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd"
+dependencies = [
+ "quote",
+ "syn 2.0.72",
+]
[[package]]
name = "quick-xml"
@@ -3653,6 +3707,18 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
"rand_core",
]
@@ -3661,6 +3727,9 @@ name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
[[package]]
name = "range-alloc"
@@ -3790,6 +3859,12 @@ dependencies = [
"windows-sys 0.52.0",
]
+[[package]]
+name = "rustversion"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
+
[[package]]
name = "ruzstd"
version = "0.7.0"
@@ -4222,6 +4297,16 @@ dependencies = [
"valuable",
]
+[[package]]
+name = "tracing-error"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e"
+dependencies = [
+ "tracing",
+ "tracing-subscriber",
+]
+
[[package]]
name = "tracing-log"
version = "0.2.0"
@@ -4251,6 +4336,17 @@ dependencies = [
"tracing-log",
]
+[[package]]
+name = "tracing-tracy"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9be7f8874d6438e4263f9874c84eded5095bda795d9c7da6ea0192e1750d3ffe"
+dependencies = [
+ "tracing-core",
+ "tracing-subscriber",
+ "tracy-client",
+]
+
[[package]]
name = "tracing-wasm"
version = "0.2.1"
@@ -4262,6 +4358,26 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "tracy-client"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63de1e1d4115534008d8fd5788b39324d6f58fc707849090533828619351d855"
+dependencies = [
+ "loom",
+ "once_cell",
+ "tracy-client-sys",
+]
+
+[[package]]
+name = "tracy-client-sys"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98b98232a2447ce0a58f9a0bfb5f5e39647b5c597c994b63945fcccd1306fafb"
+dependencies = [
+ "cc",
+]
+
[[package]]
name = "ttf-parser"
version = "0.24.0"
@@ -5232,13 +5348,34 @@ version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193"
+[[package]]
+name = "zerocopy"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6"
+dependencies = [
+ "byteorder",
+ "zerocopy-derive 0.6.6",
+]
+
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
- "zerocopy-derive",
+ "zerocopy-derive 0.7.35",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.72",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index a169c0a..cb46bb9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -21,10 +21,11 @@ dev = [
release = ["common", "embedded"]
common = []
# Individual features
-embedded = ["include_dir"]
-pixel_perfect = ["deferred"]
deferred = []
+embedded = ["include_dir"]
inspector = ["bevy-inspector-egui"]
+pixel_perfect = ["deferred"]
+trace = ["release", "bevy/trace_tracy"]
[dependencies]
# Bevy and plugins
@@ -39,15 +40,16 @@ bevy-inspector-egui = { version = "0.25", optional = true }
macros = { path = "macros" }
# Other dependencies
+anyhow = { version = "1.0" }
include_dir = { version = "0.7", optional = true }
log = { version = "*", features = [
"max_level_debug",
"release_max_level_warn",
] }
macro_rules_attribute = { version = "0.2" }
+rand = { version = "0.8" }
serde = { version = "1.0", features = ["derive"] }
toml = { version = "0.8" }
-anyhow = { version = "1.0" }
[target.'cfg(target_arch = "wasm32")'.dependencies]
web-sys = { version = "0.3", features = ["Storage"] }
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..699bef2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,99 @@
+# hello bevy ðĶ
+
+an opinionated [bevy](https://github.com/bevyengine/bevy) template for my projects.
+
+
+
+
+
+
+
+### features ðŋ
+
+- uses bevy 0.14
+- minimal and curated plugin support
+- fully featured accesible menu with keyboard, mouse and gamepad navigation
+- ci that checks errors and lint
+- creates binaries for web, linux, mac and windows when releasing a tag
+- deploy to itch automatically
+- has a nix flake development shell for easy building
+
+### how to use it âĻ
+
+- use this template in a new project (on github, a green button on the top right)
+
+### runing locally ðš
+
+this project is configured to use dynamic linking for debug builds and fast recompiling by default.
+in order to have the fastest compile, you may install [mold](https://github.com/rui314/mold) and use rust nightly (`rustup default nightly`).
+if you don't want some of these features, go to [.cargo/config](.cargo/config) and follow the instructions, or remove it to disable optimizations all together.
+
+to run a debug build use cargo:
+
+```sh
+cargo run
+```
+
+you can also play around with some of the included examples with `cargo run --example `. and if you want to get started quickly, copy any example to `src/main.rs`!
+
+if you have nix installed, running `nix develop` you get a shell with all the dependencies already installed.
+
+### release ðŧ
+
+in order to create a release build with binaries for all platforms you have two options: either you trigger it manually on the actions page or you add a tag like '[anything]0.1' with the version you want.
+
+```sh
+git tag -a "0.1" -m "test release"
+git push --tags
+```
+
+if you want to also deploy this build to itch, go to the repository settings > secrets > actions and add:
+
+```
+ITCH_API_KEY = [your api key]
+```
+
+to run a release build locally:
+
+```sh
+cargo run --release --no-default-features --features release
+```
+
+### profiling ð
+
+bevy has built in support for the [tracy](https://github.com/wolfpld/tracy) profiler. you can profile your game easily:
+
+```sh
+tracy-capture -o capture.tracy &
+cargo run --release --no-default-features --features trace
+```
+
+and then view the result with:
+
+```sh
+tracy capture.tracy
+```
+
+### other projects ð
+
+this is heavily based on [TheBevyFlock/bevy_quickstart](https://github.com/TheBevyFlock/bevy_quickstart) and [NiklasEi/bevy_game_template](https://github.com/NiklasEi/bevy_game_template). please use these more general templates that are robust and have community support. hello bevy is hardly tested and very tailored to my preferences.
+
+### plugins ðŠī
+
+this template intends to use as little external dependencies as possible to facilitate version updates and avoid bloat. that said, there are a few awesome community plugins that make everything as easy as possible.
+
+- [leafwing-input-manager](https://github.com/Leafwing-Studios/leafwing-input-manager): an awesome way of handling input from multiple sources and create simple bindings
+- [bevy_mod_picking](https://github.com/aevyrie/bevy_mod_picking): used to select things on the screen. only the ui picker is enabled by default, used for mouse navigation
+- [bevy-inspector-egui](https://github.com/jakobhellermann/bevy-inspector-egui): optional and only enabled when using the `inspector` feature. it provides a very useful world inspector
+
+### license ð
+
+this project is dual licensed under MIT and Apache 2.0, do what you want with it!
+
+the files under assets may come from other sources and have different licenses:
+
+- `icons/bevy.png` and `icons/pixelbevy.png` from [cart](https://github.com/bevyengine/bevy_github_ci_template/issues/45#issue-2022210264), **not** open
+- `sounds/boing.ogg`, sound effect from [bigsoundbank.com](https://bigsoundbank.com/high-pitched-tom-1-s2329.html), [CC0](https://creativecommons.org/publicdomain/zero/1.0/)
+- `music/rain.ogg`, sound effect from [bigsoundbank.com](https://bigsoundbank.com/summer-rain-on-terrace-s1019.html), [CC0](https://creativecommons.org/publicdomain/zero/1.0/)
+- `fonts/pixel.ttf`, public pixel font from [ggbot](https://ggbot.itch.io/public-pixel-font), [CC0](https://creativecommons.org/publicdomain/zero/1.0/)
+- `fonts/sans.tff`, outfit font from [google](https://fonts.google.com/specimen/Outfit), [OFL](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL)
diff --git a/examples/dvd.rs b/examples/dvd.rs
new file mode 100644
index 0000000..c31316a
--- /dev/null
+++ b/examples/dvd.rs
@@ -0,0 +1,169 @@
+use game::prelude::*;
+use rand::{distributions::Standard, prelude::*};
+
+fn main() {
+ App::new().add_plugins((GamePlugin, plugin)).run();
+}
+
+fn plugin(app: &mut App) {
+ app.add_event::()
+ .add_systems(OnEnter(GameState::Play), init.run_if(run_once()))
+ .add_systems(
+ Update,
+ (
+ update_velocity.in_set(PlaySet::Update),
+ on_collision
+ .in_set(PlaySet::ReadEvents)
+ .run_if(on_event::()),
+ ),
+ );
+}
+
+// Components
+// ---
+
+/// Applies a velocity to an entity every frame.
+#[derive(Component)]
+struct Velocity(Vec2);
+
+/// Marker for the score counter entity.
+#[derive(Component)]
+struct Counter(u32);
+
+// Events
+// ---
+
+/// Event that is triggered when an entity collides with the screen border.
+#[derive(Event)]
+struct CollisionEvent;
+
+// Systems
+// ---
+
+/// Spawn the initial objects.
+fn init(
+ mut cmd: Commands,
+ options: Res,
+ meta_assets: Res>,
+ font_assets: Res>,
+) {
+ // Moving balls
+ for velocity in [
+ Vec2::new(300., 250.),
+ Vec2::new(-150., 400.),
+ Vec2::new(200., -350.),
+ ] {
+ cmd.spawn((
+ SpriteBundle {
+ texture: meta_assets.get(&MetaAssetKey::BevyLogo).clone_weak(),
+ sprite: Sprite {
+ custom_size: Some(Vec2::splat(96.)),
+ ..default()
+ },
+ transform: Transform::from_translation(Vec3::new(0., 0., 1.)),
+ ..default()
+ },
+ Velocity(velocity),
+ ));
+ }
+
+ // Counter text
+ cmd.spawn((
+ Text2dBundle {
+ text: Text::from_section("0", TextStyle {
+ font: font_assets.get(&FontAssetKey::Main).clone_weak(),
+ font_size: 192.,
+ color: options.palette.light,
+ }),
+ ..default()
+ },
+ Counter(0),
+ ));
+}
+
+/// Update the position of the objects with the `Velocity` component and check
+/// for collisions with the window border
+fn update_velocity(
+ time: Res,
+ window: Query<&Window, With>,
+ mut objects: Query<(&mut Velocity, &mut Transform, &mut Sprite)>,
+ mut collision_writer: EventWriter,
+) {
+ let mut rng = rand::thread_rng();
+ let window = single!(window);
+ let win_bound = Rect::from_center_size(Vec2::ZERO, window.size());
+
+ for (mut vel, mut trans, mut sprite) in objects.iter_mut() {
+ // Update position based on velocity
+ let t = &mut trans.translation;
+ *t += vel.0.extend(0.) * time.delta_seconds();
+
+ // Calculate the sprite bound
+ let obj_bound = Rect::from_center_size(
+ trans.translation.truncate(),
+ sprite.custom_size.expect("Sprite needs a custom size"),
+ );
+
+ // Calculate the interection with the level borders and check if there was a
+ // collision
+ let intersection = win_bound.intersect(obj_bound).size();
+ let mut collision = false;
+ if intersection.x < obj_bound.width() {
+ vel.0.x *= -1.;
+ trans.translation.x += (obj_bound.width() - intersection.x) * vel.0.x.signum();
+ collision = true;
+ }
+ if intersection.y < obj_bound.height() {
+ vel.0.y *= -1.;
+ trans.translation.y += (obj_bound.height() - intersection.y) * vel.0.y.signum();
+ collision = true;
+ }
+
+ // If there is a collision, create a new random color and send a collision event
+ if collision {
+ sprite.color = *rng.gen::();
+ collision_writer.send(CollisionEvent);
+ }
+ }
+}
+
+/// When there is a collision, increase the counder and play a bounce sound.
+fn on_collision(
+ mut cmd: Commands,
+ mut counter: Query<(&mut Text, &mut Counter)>,
+ sound_assets: Res>,
+ mut collision_reader: EventReader,
+) {
+ let (mut text, mut counter) = single_mut!(counter);
+
+ for CollisionEvent in collision_reader.read() {
+ counter.0 += 1;
+ text.sections[0].value = counter.0.to_string();
+
+ cmd.spawn(AudioBundle {
+ source: sound_assets.get(&SoundAssetKey::Boing).clone_weak(),
+ settings: PlaybackSettings::DESPAWN,
+ });
+ }
+}
+
+// Helpers
+// ---
+
+/// Wrapper for color to be able to derive traits for it.
+struct ColorWrapper(Color);
+
+/// Generate a random color from a hue.
+impl Distribution for Standard {
+ fn sample(&self, rng: &mut R) -> ColorWrapper {
+ ColorWrapper(Color::hsl(rng.gen::() * 360., 0.8, 0.8))
+ }
+}
+
+impl std::ops::Deref for ColorWrapper {
+ type Target = Color;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
diff --git a/examples/jump.rs b/examples/jump.rs
new file mode 100644
index 0000000..24c1b43
--- /dev/null
+++ b/examples/jump.rs
@@ -0,0 +1,314 @@
+#![allow(clippy::type_complexity)]
+
+use bevy::math::bounding::*;
+use game::prelude::*;
+use rand::prelude::*;
+
+const PLAYER_SIZE: Vec2 = Vec2::splat(72.);
+const PLATFORM_SIZE: Vec2 = Vec2::new(125., 25.);
+const SPACE_BETWEEN_PLATFORMS: u32 = 150;
+const GRAVITY: f32 = -8000.;
+const JUMP_VEL: f32 = 1800.;
+const MOVE_VEL: f32 = 800.;
+const BOUNCE_CUTOFF: f32 = 150.;
+const BOUNCE_FACTOR: f32 = 0.3;
+const MOVE_CUTOFF: f32 = 100.;
+const MOVE_FACTOR: f32 = 0.75;
+const JUMP_BUFFER: f32 = 0.1;
+
+fn main() {
+ App::new().add_plugins((GamePlugin, plugin)).run();
+}
+
+fn plugin(app: &mut App) {
+ app.add_systems(OnEnter(GameState::Play), init.run_if(on_setup()))
+ .add_systems(
+ Update,
+ (
+ update_player.in_set(PlaySet::Update),
+ (update_counter, update_camera).in_set(PlaySet::Animation),
+ (check_collision, spawn_platforms, check_game_over).after(update_player),
+ )
+ .run_if(in_state(GameState::Play)),
+ )
+ .add_systems(OnEnter(GameState::End), reset);
+}
+
+// Resources
+// ---
+
+/// This could go in `SaveData`, but since this is an example we make it
+/// separate.
+#[derive(Resource, Default)]
+struct ExampleData {
+ last_platform: u32,
+}
+
+// Components
+// ---
+
+#[derive(Component, Default)]
+struct Player {
+ velocity: Vec2,
+ max_height: f32,
+ can_jump: bool,
+ jump_buffer: Option,
+}
+
+#[derive(Component)]
+struct Platform;
+
+#[derive(Component, Default)]
+struct Counter(u32);
+
+#[derive(Component)]
+struct CameraFollow;
+
+// Systems
+// ---
+
+/// Spawn the initial objects.
+fn init(
+ mut cmd: Commands,
+ window: Query<&Window, With>,
+ mut camera: Query<(Entity, &mut Transform), With>,
+ options: Res,
+ meta_assets: Res>,
+ font_assets: Res>,
+) {
+ let size = single!(window).size();
+
+ // Player
+ cmd.spawn((
+ SpriteBundle {
+ texture: meta_assets.get(&MetaAssetKey::BevyLogo).clone_weak(),
+ transform: Transform::from_translation(Vec3::new(0., 0., 10.)),
+ sprite: Sprite {
+ custom_size: Some(PLAYER_SIZE),
+ ..default()
+ },
+ ..default()
+ },
+ Player::default(),
+ ));
+
+ // Floor
+ cmd.spawn((
+ SpriteBundle {
+ sprite: Sprite {
+ color: options.palette.dark,
+ custom_size: Some(Vec2::new(size.x, PLATFORM_SIZE.y * 2.)),
+ ..default()
+ },
+ transform: Transform::from_xyz(0., -size.y / 2. + PLATFORM_SIZE.y, 5.),
+ ..default()
+ },
+ Platform,
+ ));
+
+ // Counter
+ cmd.spawn((
+ Text2dBundle {
+ text: Text::from_section("0", TextStyle {
+ font: font_assets.get(&FontAssetKey::Main).clone_weak(),
+ font_size: 150.,
+ color: options.palette.light,
+ }),
+ ..default()
+ },
+ Counter::default(),
+ CameraFollow,
+ ));
+
+ // Adds a camera follow to the initial camera
+ let (entity, mut trans) = single_mut!(camera);
+ cmd.entity(entity).insert(CameraFollow);
+ trans.translation.y = 0.;
+
+ // Keeps track of the last platform generated
+ cmd.insert_resource(ExampleData::default())
+}
+
+fn update_player(
+ time: Res,
+ input: Query<&ActionState>,
+ window: Query<&Window, With>,
+ mut player: Query<(&mut Player, &mut Transform)>,
+) {
+ let (mut player, mut trans) = single_mut!(player);
+ let input = single!(input);
+ let size = single!(window).size();
+
+ // Gravity
+ player.velocity.y += GRAVITY * time.delta_seconds();
+
+ // Jump
+ let mut jump_input = input.just_pressed(&Action::Act);
+ if jump_input && !player.can_jump {
+ // Create a small time buffer for jump inputs
+ player.jump_buffer = Some(Timer::from_seconds(JUMP_BUFFER, TimerMode::Once));
+ }
+ if let Some(buffer) = player.jump_buffer.as_mut() {
+ if !buffer.tick(time.delta()).finished() {
+ jump_input = true;
+ }
+ };
+ if jump_input && player.can_jump {
+ player.velocity.y = JUMP_VEL;
+ player.can_jump = false;
+ }
+
+ // Move
+ let dir = input.clamped_axis_pair(&Action::Move).x;
+ if dir.abs() > 0.2 {
+ player.velocity.x = dir * MOVE_VEL;
+ } else if player.velocity.x.abs() > MOVE_CUTOFF {
+ player.velocity.x *= MOVE_FACTOR;
+ } else {
+ player.velocity.x = 0.;
+ }
+
+ // Update position based on velocity and loop around
+ trans.translation += player.velocity.extend(0.) * time.delta_seconds();
+ trans.translation.x = (trans.translation.x + size.x / 2.).rem_euclid(size.x) - size.x / 2.;
+
+ // Update max height
+ player.max_height = player.max_height.max(trans.translation.y);
+}
+
+/// Checks collision between the player and the platforms
+fn check_collision(
+ mut player: Query<(&mut Player, &mut Transform)>,
+ platforms: Query<(&Sprite, &Transform), (With, Without)>,
+) {
+ let (mut player, mut trans) = single_mut!(player);
+
+ // Only check for collisions if the player is going down (allows to pass
+ // platforms from below)
+ if player.velocity.y <= 0. {
+ let player_bounds = Aabb2d::new(trans.translation.truncate(), PLAYER_SIZE / 2.);
+
+ for (sprite, platform) in platforms.iter() {
+ let platform_bounds = Aabb2d::new(
+ platform.translation.truncate(),
+ sprite.custom_size.unwrap_or(PLATFORM_SIZE) / 2.,
+ );
+
+ // Check the collision between the player and the platform bounds
+ // Only register a collision if it happens with the player above
+ let collision = {
+ if player_bounds.intersects(&platform_bounds) {
+ let closest = platform_bounds.closest_point(player_bounds.center());
+ let offset = player_bounds.center() - closest;
+ offset.y > 0.
+ } else {
+ false
+ }
+ };
+
+ if collision {
+ // Relocate the player perfectly on top of the platform
+ trans.translation.y =
+ platform.translation.y + platform_bounds.half_size().y + PLAYER_SIZE.y / 2.;
+ // If the player is going quick enough, bounce
+ player.velocity.y = if player.velocity.y.abs() > BOUNCE_CUTOFF {
+ player.velocity.y.abs() * BOUNCE_FACTOR
+ } else {
+ 0.
+ };
+ player.can_jump = true;
+ }
+ }
+ }
+}
+
+/// Creates platforms as the player goes up
+fn spawn_platforms(
+ mut cmd: Commands,
+ player: Query<&Player>,
+ window: Query<&Window, With>,
+ mut data: ResMut,
+ options: Res,
+) {
+ let mut rng = rand::thread_rng();
+ let player = single!(player);
+ let size = single!(window).size();
+
+ while data.last_platform * SPACE_BETWEEN_PLATFORMS < (player.max_height + size.y) as u32 {
+ data.last_platform += 1;
+ let x = (rng.gen::() - 0.5) * size.x;
+
+ cmd.spawn((
+ SpriteBundle {
+ sprite: Sprite {
+ color: options.palette.dark.lighter(rng.gen::() * 0.2 - 0.05),
+ custom_size: Some(PLATFORM_SIZE),
+ ..default()
+ },
+ transform: Transform::from_xyz(
+ x.round(),
+ data.last_platform as f32 * SPACE_BETWEEN_PLATFORMS as f32 - size.y / 2.,
+ 5.,
+ ),
+ ..default()
+ },
+ Platform,
+ ));
+ }
+}
+
+/// Makes the camera follow the player.
+fn update_camera(player: Query<&Player>, mut camera: Query<&mut Transform, With>) {
+ let player = single!(player);
+ let target = player.max_height;
+
+ for mut trans in camera.iter_mut() {
+ trans.translation.y = trans.translation.y.lerp(target, 0.3);
+ }
+}
+
+/// Updates the score counter
+fn update_counter(mut counter: Query<(&mut Counter, &mut Text)>, player: Query<&Player>) {
+ let (mut counter, mut text) = single_mut!(counter);
+ let player = single!(player);
+
+ counter.0 = (player.max_height as u32 / SPACE_BETWEEN_PLATFORMS).saturating_sub(1);
+ if let Some(section) = text.sections.first_mut() {
+ section.value = counter.0.to_string();
+ }
+}
+
+/// Checks if the player fell off the screen and transitions to the end state.
+fn check_game_over(
+ mut state: ResMut>,
+ player: Query<&Transform, With>,
+ camera: Query<&Transform, With>,
+ window: Query<&Window, With>,
+) {
+ let player = single!(player);
+ let camera = single!(camera);
+ let size = single!(window).size();
+
+ if player.translation.y < camera.translation.y - size.y / 2. {
+ state.set(GameState::End);
+ }
+}
+
+/// When entering the end state, reset the setup of the game so that init can
+/// run again and go back to the play state.
+/// We could do this automatically with the new `StateScoped`, which is very
+/// useful, but that takes away some flexibility. For example, going to the menu
+/// would delete the entities. A mixture of both could be possible and
+/// benefitial for larger games.
+fn reset(
+ mut cmd: Commands,
+ mut state: ResMut>,
+ entities: Query, With, With)>>,
+) {
+ for entity in &entities {
+ cmd.entity(entity).despawn();
+ }
+
+ cmd.reset_setup();
+ state.set(GameState::Play);
+}
diff --git a/src/base.rs b/src/base.rs
index 0a2cca5..398e214 100644
--- a/src/base.rs
+++ b/src/base.rs
@@ -16,7 +16,7 @@ pub mod prelude {
pub use super::{
data::{GameOptions, Persistent, SaveData},
later::LaterCommandExt,
- sets::PlaySet,
+ sets::{on_setup, PlaySet, SetupCommandExt},
states::GameState,
};
}
diff --git a/src/base/data.rs b/src/base/data.rs
index ad2013d..464dc77 100644
--- a/src/base/data.rs
+++ b/src/base/data.rs
@@ -1,7 +1,6 @@
//! Defines persistent data structures.
//! For a more complete solution, look at
-use bevy::window::{PrimaryWindow, WindowResized};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use crate::prelude::*;
diff --git a/src/base/sets.rs b/src/base/sets.rs
index 7fad948..f83a7f3 100644
--- a/src/base/sets.rs
+++ b/src/base/sets.rs
@@ -11,13 +11,14 @@ pub(super) fn plugin(app: &mut App) {
Update,
(
PlaySet::Timers,
- PlaySet::Input,
PlaySet::Update,
PlaySet::ReadEvents,
PlaySet::Animation,
)
- .chain(),
- );
+ .chain()
+ .run_if(in_state(GameState::Play)),
+ )
+ .insert_resource(GameSetup);
}
/// Main grouping of systems inside the `GameState::Play` state.
@@ -26,8 +27,6 @@ pub(super) fn plugin(app: &mut App) {
pub enum PlaySet {
/// Tick timers and other `Time` based systems.
Timers,
- /// Systems that handle input.
- Input,
/// General gameplay systems.
#[default]
Update,
@@ -36,3 +35,56 @@ pub enum PlaySet {
/// Animations and other systems that happen after everything is calculated.
Animation,
}
+
+/// Resource used to reset the state of `on_setup`.
+#[derive(Resource)]
+pub struct GameSetup;
+
+/// This function is very similar to `run_once`, but it allows to reset the
+/// state. Its purpose is to use it with systems that need to run only once for
+/// setup purposes, but that can be triggered again in the future, for example,
+/// when starting a new game.
+///
+/// It is an alternative to `StateScoped` that provides greater flexibility for
+/// entities, it is recommended to use both depending on the needs of each
+/// specific entities. For example, entities that should persist when pausing
+/// the game should use `on_setup`, but entities that don't store any state and
+/// can be recreated could use `StateScoped` since it simplifies the logic.
+///
+/// # Examples
+///
+/// ```
+/// use game::prelude::*;
+///
+/// fn plugin(app: &mut App) {
+/// app.add_systems(OnEnter(GameState::Play), init.run_if(on_setup()))
+/// .add_systems(OnEnter(GameState::End), reset);
+/// }
+///
+/// fn init() {
+/// info!("hi! uwu");
+/// }
+///
+/// fn reset(mut cmd: Commands, mut state: ResMut>) {
+/// cmd.reset_setup();
+/// state.set(GameState::Play);
+/// }
+/// ```
+pub fn on_setup() -> impl FnMut(Option>) -> bool + Clone {
+ resource_added::
+}
+
+/// Convenience function that allows to call `cmd.reset_game(...)`.
+pub trait SetupCommandExt {
+ /// Readds the `GameSetup` resource to trigger again all of the systems
+ /// conditioned by `on_setup`.
+ fn reset_setup(&mut self) -> &mut Self;
+}
+
+impl SetupCommandExt for Commands<'_, '_> {
+ fn reset_setup(&mut self) -> &mut Self {
+ self.remove_resource::();
+ self.insert_resource(GameSetup);
+ self
+ }
+}
diff --git a/src/components/camera/deferred.rs b/src/components/camera/deferred.rs
index 0e94245..0341711 100644
--- a/src/components/camera/deferred.rs
+++ b/src/components/camera/deferred.rs
@@ -1,18 +1,15 @@
//! Deferred rendering for the camera.
-use bevy::{
- render::{
- camera::RenderTarget,
- render_resource::{
- Extent3d,
- TextureDescriptor,
- TextureDimension,
- TextureFormat,
- TextureUsages,
- },
- view::RenderLayers,
+use bevy::render::{
+ camera::RenderTarget,
+ render_resource::{
+ Extent3d,
+ TextureDescriptor,
+ TextureDimension,
+ TextureFormat,
+ TextureUsages,
},
- window::WindowResized,
+ view::RenderLayers,
};
use crate::prelude::*;
@@ -45,10 +42,7 @@ fn init_canvas(
mut cmd: Commands,
mut camera: Query<(Entity, &mut Camera), With>,
mut images: ResMut>,
- #[cfg(not(feature = "pixel_perfect"))] window: Query<
- &Window,
- With,
- >,
+ #[cfg(not(feature = "pixel_perfect"))] window: Query<&Window, With>,
) {
// Calculate the size of the canvas
#[cfg(not(feature = "pixel_perfect"))]
diff --git a/src/lib.rs b/src/lib.rs
index 8456da8..417f28c 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,7 +2,6 @@
//! It uses plugins and submodules to structure the code.
// TODO: Code examples
-// TODO: Readme
#![feature(path_add_extension)]
#![allow(clippy::too_many_arguments)]
diff --git a/src/prelude.rs b/src/prelude.rs
index 61563ce..1802ad5 100644
--- a/src/prelude.rs
+++ b/src/prelude.rs
@@ -2,7 +2,12 @@
//! Includes modules from this crate and some redeclarations from dependencies.
pub use anyhow::{Context, Result};
-pub use bevy::{color::palettes::css, prelude::*, utils::HashMap};
+pub use bevy::{
+ color::palettes::css,
+ prelude::*,
+ utils::HashMap,
+ window::{PrimaryWindow, WindowResized},
+};
pub use macros::*;
pub use crate::{
diff --git a/src/ui.rs b/src/ui.rs
index 4022435..9dff292 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -12,7 +12,7 @@ pub(super) fn plugin(app: &mut App) {
/// The prelude of this module
pub mod prelude {
- pub use bevy_mod_picking::prelude::Listener;
+ pub use bevy_mod_picking::prelude::*;
pub use super::{
menu::MenuState,
diff --git a/src/ui/navigation.rs b/src/ui/navigation.rs
index 9744629..6238269 100644
--- a/src/ui/navigation.rs
+++ b/src/ui/navigation.rs
@@ -1,7 +1,6 @@
//! Ui navigation system that allows for mouse, keyboard and gamepad input.
use bevy::ecs::component::{ComponentHooks, StorageType};
-use bevy_mod_picking::prelude::*;
use crate::prelude::*;