Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into cargo-generate
Browse files Browse the repository at this point in the history
  • Loading branch information
janhohenheim committed Jul 18, 2024
2 parents 94f2bb8 + c6df5af commit ff87826
Show file tree
Hide file tree
Showing 13 changed files with 197 additions and 114 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ _Brought to you by the Bevy Jam working group._

# Bevy Quickstart

This template is a great way to get started on a new [Bevy](https://bevyengine.org/) gameespecially for a game jam!
This template is a great way to get started on a new [Bevy](https://bevyengine.org/) gameespecially for a game jam!
Start with a [basic project structure](#write-your-game) and [CI / CD](#release-your-game) that can deploy to [itch.io](https://itch.io).
You can [try this template in your web browser!](https://the-bevy-flock.itch.io/bevy-quickstart)

Don't want to read through the whole README? [@ChristopherBiscardi](https://github.com/ChristopherBiscardi) made a video on how to use the template from start to finish:

[<img src="./docs/thumbnail.png" width=40% height=40% alt="A video tutorial for bevy_quickstart"/>](https://www.youtube.com/watch?v=ESBRyXClaYc)

## Prerequisites

We assume that you know how to use Bevy already and have seen the [official Quick Start Guide](https://bevyengine.org/learn/quick-start/introduction/).
Expand Down Expand Up @@ -80,6 +84,12 @@ After you've fiddled with it, rename it to `.cargo/config.toml` to enable it.
This template uses [GitHub workflows](https://docs.github.com/en/actions/using-workflows) to run tests and build releases.
See [Workflows](./docs/workflows.md) for more information.

## Known Issues

There are some known issues in Bevy that require some arcane workarounds.
To keep this template simple, we have opted not to include those workarounds.
You can read about them in the [Known Issues](./docs/known-issues.md) document.

## License

The source code in this repository is licensed under any of the following at your option:
Expand Down
38 changes: 24 additions & 14 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,37 +95,47 @@ By returning `EntityCommands`, you can easily chain multiple widgets together an

### Pattern

Preload your assets by encapsulating them in a struct:
Define your assets in an enum so each variant maps to a `Handle`:

```rust
#[derive(PartialEq, Eq, Hash, Reflect)]
pub enum SomeAsset {
pub enum SpriteKey {
Player,
Enemy,
Powerup,
}

#[derive(Resource, Reflect, Deref, DerefMut)]
pub struct SomeAssets(HashMap<SomeAsset, Handle<Something>>);

impl SomeAssets {
pub fn new(asset_server: &AssetServer) -> Self {
// load them from disk via the asset server
}
impl AssetKey for SpriteKey {
type Asset = Image;
}

pub fn all_loaded(&self, assets: &Assets<Something>) -> bool {
self.0.iter().all(|(_, handle)| assets.contains(handle))
impl FromWorld for HandleMap<SpriteKey> {
fn from_world(world: &mut World) -> Self {
let asset_server = world.resource::<AssetServer>();
[
(SpriteKey::Player, asset_server.load("player.png")),
(SpriteKey::Enemy, asset_server.load("enemy.png")),
(SpriteKey::Powerup, asset_server.load("powerup.png")),
]
.into()
}
}
```

Then add them to the [loading screen](../src/screen/loading.rs) functions `enter_loading` and `check_all_loaded`.
Then set up preloading in a plugin:

```rust
app.register_type::<HandleMap<SpriteKey>>();
app.init_resource::<HandleMap<SpriteKey>>();
```

### Reasoning

This pattern is inspired by [bevy_asset_loader](https://github.com/NiklasEi/bevy_asset_loader).
In general, by preloading your assets, you can avoid hitches during gameplay.
By using an enum to represent your assets, you don't leak details like file paths into your game code and can easily change the asset that is loaded at a single point.
By preloading your assets, you can avoid hitches during gameplay.

Using an enum to represent your assets encapsulates their file path from the rest of the game code,
and gives you access to static tooling like renaming in an IDE, and compile errors for an invalid name.

## Spawning

Expand Down
27 changes: 27 additions & 0 deletions docs/known-issues.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Known Issues

## My audio is stuttering on web

There are a number of issues with audio on web, so this is not an exhaustive list. The short version is that you can try the following:

- If you use materials, make sure to force render pipelines to [load at the start of the game](https://github.com/rparrett/bevy_pipelines_ready/blob/main/src/lib.rs).
- Keep the FPS high.
- Advise your users to play on Chromium-based browsers.
- Apply the suggestions from the blog post [Workaround for the Choppy Music in Bevy Web Builds](https://necrashter.github.io/bevy-choppy-music-workaround).

## My game window is flashing white for a split second when I start the game on native

The game window is created before the GPU is ready to render everything.
This means that it will start with a white screen for a little bit.
The workaround is to [spawn the Window hidden](https://github.com/bevyengine/bevy/blob/release-0.14.0/examples/window/window_settings.rs#L29-L32)
and then [make it visible after a few frames](https://github.com/bevyengine/bevy/blob/release-0.14.0/examples/window/window_settings.rs#L56-L64).

## My character or camera is not moving smoothly

Choppy movement is often caused by movement updates being tied to the frame rate.
See the [physics_in_fixed_timestep](https://github.com/bevyengine/bevy/blob/main/examples/movement/physics_in_fixed_timestep.rs) example
for how to fix this.

A camera not moving smoothly is pretty much always caused by the camera position being tied too tightly to the character's position.
To give the camera some inertia, use the [`smooth_nudge`](https://github.com/bevyengine/bevy/blob/main/examples/movement/smooth_follow.rs#L127-L142)
to interpolate the camera position towards its target position.
Binary file added docs/thumbnail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
142 changes: 84 additions & 58 deletions src/game/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,44 @@ use bevy::{
utils::HashMap,
};

pub(super) fn plugin(app: &mut App) {
app.register_type::<HandleMap<ImageKey>>();
app.init_resource::<HandleMap<ImageKey>>();

app.register_type::<HandleMap<SfxKey>>();
app.init_resource::<HandleMap<SfxKey>>();

app.register_type::<HandleMap<SoundtrackKey>>();
app.init_resource::<HandleMap<SoundtrackKey>>();
}

#[derive(PartialEq, Eq, Hash, Reflect)]
pub enum ImageAsset {
pub enum ImageKey {
Ducky,
}

#[derive(Resource, Reflect, Deref, DerefMut)]
pub struct ImageAssets(HashMap<ImageAsset, Handle<Image>>);

impl ImageAssets {
pub fn new(asset_server: &AssetServer) -> Self {
let mut assets = HashMap::new();
impl AssetKey for ImageKey {
type Asset = Image;
}

assets.insert(
ImageAsset::Ducky,
impl FromWorld for HandleMap<ImageKey> {
fn from_world(world: &mut World) -> Self {
let asset_server = world.resource::<AssetServer>();
[(
ImageKey::Ducky,
asset_server.load_with_settings(
"images/ducky.png",
|settings: &mut ImageLoaderSettings| {
settings.sampler = ImageSampler::nearest();
},
),
);

Self(assets)
}

pub fn all_loaded(&self, assets: &Assets<Image>) -> bool {
self.0.iter().all(|(_, handle)| assets.contains(handle))
)]
.into()
}
}

#[derive(PartialEq, Eq, Hash, Reflect)]
pub enum SfxAsset {
pub enum SfxKey {
ButtonHover,
ButtonPress,
Step1,
Expand All @@ -44,58 +50,78 @@ pub enum SfxAsset {
Step4,
}

#[derive(Resource, Reflect, Deref, DerefMut)]
pub struct SfxAssets(HashMap<SfxAsset, Handle<AudioSource>>);

impl SfxAssets {
pub fn new(asset_server: &AssetServer) -> Self {
let mut assets = HashMap::new();

assets.insert(
SfxAsset::ButtonHover,
asset_server.load("audio/sfx/button_hover.ogg"),
);
assets.insert(
SfxAsset::ButtonPress,
asset_server.load("audio/sfx/button_press.ogg"),
);
assets.insert(SfxAsset::Step1, asset_server.load("audio/sfx/step1.ogg"));
assets.insert(SfxAsset::Step2, asset_server.load("audio/sfx/step2.ogg"));
assets.insert(SfxAsset::Step3, asset_server.load("audio/sfx/step3.ogg"));
assets.insert(SfxAsset::Step4, asset_server.load("audio/sfx/step4.ogg"));

Self(assets)
}
impl AssetKey for SfxKey {
type Asset = AudioSource;
}

pub fn all_loaded(&self, assets: &Assets<AudioSource>) -> bool {
self.0.iter().all(|(_, handle)| assets.contains(handle))
impl FromWorld for HandleMap<SfxKey> {
fn from_world(world: &mut World) -> Self {
let asset_server = world.resource::<AssetServer>();
[
(
SfxKey::ButtonHover,
asset_server.load("audio/sfx/button_hover.ogg"),
),
(
SfxKey::ButtonPress,
asset_server.load("audio/sfx/button_press.ogg"),
),
(SfxKey::Step1, asset_server.load("audio/sfx/step1.ogg")),
(SfxKey::Step2, asset_server.load("audio/sfx/step2.ogg")),
(SfxKey::Step3, asset_server.load("audio/sfx/step3.ogg")),
(SfxKey::Step4, asset_server.load("audio/sfx/step4.ogg")),
]
.into()
}
}

#[derive(PartialEq, Eq, Hash, Reflect)]
pub enum SoundtrackAsset {
pub enum SoundtrackKey {
Credits,
Gameplay,
}

impl AssetKey for SoundtrackKey {
type Asset = AudioSource;
}

impl FromWorld for HandleMap<SoundtrackKey> {
fn from_world(world: &mut World) -> Self {
let asset_server = world.resource::<AssetServer>();
[
(
SoundtrackKey::Credits,
asset_server.load("audio/soundtracks/Monkeys Spinning Monkeys.ogg"),
),
(
SoundtrackKey::Gameplay,
asset_server.load("audio/soundtracks/Fluffing A Duck.ogg"),
),
]
.into()
}
}

pub trait AssetKey: Sized {
type Asset: Asset;
}

#[derive(Resource, Reflect, Deref, DerefMut)]
pub struct SoundtrackAssets(HashMap<SoundtrackAsset, Handle<AudioSource>>);

impl SoundtrackAssets {
pub fn new(asset_server: &AssetServer) -> Self {
let mut assets = HashMap::new();
assets.insert(
SoundtrackAsset::Credits,
asset_server.load("audio/soundtracks/Monkeys Spinning Monkeys.ogg"),
);
assets.insert(
SoundtrackAsset::Gameplay,
asset_server.load("audio/soundtracks/Fluffing A Duck.ogg"),
);
Self(assets)
#[reflect(Resource)]
pub struct HandleMap<K: AssetKey>(HashMap<K, Handle<K::Asset>>);

impl<K: AssetKey, T> From<T> for HandleMap<K>
where
T: Into<HashMap<K, Handle<K::Asset>>>,
{
fn from(value: T) -> Self {
Self(value.into())
}
}

pub fn all_loaded(&self, assets: &Assets<AudioSource>) -> bool {
self.0.iter().all(|(_, handle)| assets.contains(handle))
impl<K: AssetKey> HandleMap<K> {
pub fn all_loaded(&self, asset_server: &AssetServer) -> bool {
self.values()
.all(|x| asset_server.is_loaded_with_dependencies(x))
}
}
26 changes: 15 additions & 11 deletions src/game/audio/sfx.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
use bevy::{audio::PlaybackMode, prelude::*};
use rand::prelude::SliceRandom;
use rand::seq::SliceRandom;

use crate::game::assets::{SfxAsset, SfxAssets};
use crate::game::assets::{HandleMap, SfxKey};

pub(super) fn play_sfx(trigger: Trigger<Sfx>, mut commands: Commands, sfxs: Res<SfxAssets>) {
pub(super) fn play_sfx(
trigger: Trigger<Sfx>,
mut commands: Commands,
sfx_handles: Res<HandleMap<SfxKey>>,
) {
let event = trigger.event();
let source = match event {
Sfx::ButtonHover => &sfxs[&SfxAsset::ButtonHover],
Sfx::ButtonPress => &sfxs[&SfxAsset::ButtonPress],
Sfx::Step => random_step(&sfxs),
Sfx::ButtonHover => &sfx_handles[&SfxKey::ButtonHover],
Sfx::ButtonPress => &sfx_handles[&SfxKey::ButtonPress],
Sfx::Step => random_step(&sfx_handles),
}
.clone_weak();
let settings = PlaybackSettings {
Expand All @@ -26,12 +30,12 @@ pub enum Sfx {
Step,
}

fn random_step(sfxs: &SfxAssets) -> &Handle<AudioSource> {
fn random_step(sfx_handles: &HandleMap<SfxKey>) -> &Handle<AudioSource> {
[
&sfxs[&SfxAsset::Step1],
&sfxs[&SfxAsset::Step2],
&sfxs[&SfxAsset::Step3],
&sfxs[&SfxAsset::Step4],
&sfx_handles[&SfxKey::Step1],
&sfx_handles[&SfxKey::Step2],
&sfx_handles[&SfxKey::Step3],
&sfx_handles[&SfxKey::Step4],
]
.choose(&mut rand::thread_rng())
.unwrap()
Expand Down
8 changes: 4 additions & 4 deletions src/game/audio/soundtrack.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use bevy::{audio::PlaybackMode, prelude::*};

use crate::game::assets::{SoundtrackAsset, SoundtrackAssets};
use crate::game::assets::{HandleMap, SoundtrackKey};

pub(super) fn play_soundtrack(
trigger: Trigger<Soundtrack>,
mut commands: Commands,
soundtracks: Res<SoundtrackAssets>,
soundtrack_handles: Res<HandleMap<SoundtrackKey>>,
query: Query<Entity, With<SoundtrackMarker>>,
) {
let event = trigger.event();
Expand All @@ -17,8 +17,8 @@ pub(super) fn play_soundtrack(
Soundtrack::Disable => {
return;
}
Soundtrack::Credits => &soundtracks[&SoundtrackAsset::Credits],
Soundtrack::Gameplay => &soundtracks[&SoundtrackAsset::Gameplay],
Soundtrack::Credits => &soundtrack_handles[&SoundtrackKey::Credits],
Soundtrack::Gameplay => &soundtrack_handles[&SoundtrackKey::Gameplay],
}
.clone_weak();

Expand Down
1 change: 1 addition & 0 deletions src/game/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub(super) fn plugin(app: &mut App) {
app.add_plugins((
animation::plugin,
audio::plugin,
assets::plugin,
movement::plugin,
spawn::plugin,
));
Expand Down
Loading

0 comments on commit ff87826

Please sign in to comment.