Skip to content
Snippets Groups Projects
Verified Commit 1ef1041c authored by Louis's avatar Louis :fire:
Browse files

Clone Template

parent dfbdb1a0
No related branches found
No related tags found
No related merge requests found
Pipeline #231 passed with stages
in 23 minutes and 4 seconds
Showing
with 819 additions and 0 deletions
use std::marker::PhantomData;
use bevy::asset::LoadState;
use bevy::ecs::system::SystemParam;
use bevy::prelude::*;
use bevy::reflect::TypeUuid;
use micro_musicbox::prelude::AudioSource;
use crate::assets::{AssetHandles, FixedAssetNameMapping, SpriteSheetConfig};
#[derive(SystemParam)]
pub struct AssetTypeLoader<'w, 's> {
pub handles: ResMut<'w, AssetHandles>,
pub asset_server: Res<'w, AssetServer>,
pub atlas: ResMut<'w, Assets<TextureAtlas>>,
#[system_param(ignore)]
marker: PhantomData<&'s usize>,
}
macro_rules! load_basic_type {
($name: tt, $type: ty => $key: ident) => {
pub fn $name(&mut self, assets: &[FixedAssetNameMapping]) -> Vec<Handle<$type>> {
self.load_list(assets, |loader, path, key| {
let handle: Handle<$type> = loader.asset_server.load(&path);
loader.handles.$key.insert(key, handle.clone());
handle
})
}
};
}
macro_rules! load_state {
($container: expr => $key: ident) => {
$container
.asset_server
.get_group_load_state($container.handles.$key.values().map(|f| f.id))
};
}
impl<'w, 's> AssetTypeLoader<'w, 's> {
fn load_list<
T: Sync + Send + TypeUuid + 'static,
Loader: Fn(&mut AssetTypeLoader, String, String) -> Handle<T>,
>(
&mut self,
files: &[FixedAssetNameMapping],
load: Loader,
) -> Vec<Handle<T>> {
files
.iter()
.map(|(path, key)| load(self, path.to_string(), key.to_string()))
.collect()
}
load_basic_type!(load_images, Image => images);
load_basic_type!(load_audio, AudioSource => sounds);
load_basic_type!(load_font, Font => fonts);
pub fn load_spritesheet(
&mut self,
config: &SpriteSheetConfig,
assets: &[FixedAssetNameMapping],
) -> Vec<Handle<Image>> {
self.load_list(assets, |loader, path, key| {
let handle: Handle<Image> = loader.asset_server.load(&path);
loader
.handles
.images
.insert(key.to_string(), handle.clone());
let atlas = TextureAtlas::from_grid(
handle.clone(),
Vec2::new(config.tile_width as f32, config.tile_height as f32),
config.columns,
config.rows,
None,
None,
);
let atlas_handle = loader.atlas.add(atlas);
loader.handles.atlas.insert(key, atlas_handle);
handle
})
}
pub fn get_all_load_state(&self) -> Vec<LoadState> {
let image_state = self
.asset_server
.get_group_load_state(self.handles.images.values().map(|f| f.id()));
let atlas_state = self
.asset_server
.get_group_load_state(self.handles.images.values().map(|f| f.id()));
vec![image_state, atlas_state]
}
}
mod loader;
mod resources;
mod startup;
use bevy::app::{App, Plugin};
use iyes_loopless::condition::ConditionSet;
use iyes_loopless::prelude::AppLooplessStateExt;
pub use loader::AssetTypeLoader;
pub use resources::{AssetHandles, AssetNameMapping, FixedAssetNameMapping, SpriteSheetConfig};
use crate::system::flow::AppState;
pub struct AssetsPlugin;
impl Plugin for AssetsPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<AssetHandles>()
.add_enter_system(AppState::Preload, startup::start_preload_resources)
.add_enter_system(AppState::Preload, startup::start_load_resources)
.add_system_set(
ConditionSet::new()
.run_in_state(AppState::Setup)
.with_system(startup::check_load_resources)
.into(),
);
}
}
use bevy::prelude::*;
use bevy::utils::HashMap;
use micro_musicbox::prelude::AudioSource;
use micro_musicbox::utilities::{SuppliesAudio, TrackType};
#[derive(Copy, Clone, Debug)]
pub struct SpriteSheetConfig {
pub tile_width: usize,
pub tile_height: usize,
pub columns: usize,
pub rows: usize,
}
impl SpriteSheetConfig {
pub fn squares(tile_wh: usize, columns: usize, rows: usize) -> Self {
Self {
tile_width: tile_wh,
tile_height: tile_wh,
columns,
rows,
}
}
pub fn rectangles(tile_width: usize, tile_height: usize, columns: usize, rows: usize) -> Self {
Self {
tile_width,
tile_height,
columns,
rows,
}
}
}
#[derive(Default, Resource)]
pub struct AssetHandles {
pub images: HashMap<String, Handle<Image>>,
pub atlas: HashMap<String, Handle<TextureAtlas>>,
pub sounds: HashMap<String, Handle<AudioSource>>,
pub fonts: HashMap<String, Handle<Font>>,
}
macro_rules! fetch_wrapper {
($name: tt, $type: ty => $key: ident) => {
pub fn $name<T: ToString>(&self, name: T) -> Handle<$type> {
let key = name.to_string();
match self.$key.get(&key) {
Some(handle) => handle.clone_weak(),
None => {
let keys = self.$key.keys();
panic!(
"\n\nTried to fetch {} asset with a missing key: {}.\nPossible keys: {}\n\n",
stringify!($name),
name.to_string(),
keys.map(|k| format!("'{}'", k))
.collect::<Vec<String>>()
.join(", ")
)
}
}
}
};
}
impl AssetHandles {
fetch_wrapper!(image, Image => images);
fetch_wrapper!(atlas, TextureAtlas => atlas);
fetch_wrapper!(sound, AudioSource => sounds);
fetch_wrapper!(font, Font => fonts);
}
impl SuppliesAudio for AssetHandles {
fn resolve_track_name<T: ToString>(&self, name: T) -> TrackType<String> {
if self.sounds.contains_key(&name.to_string()) {
TrackType::Single(name.to_string())
} else {
TrackType::Missing
}
}
fn get_audio_track<T: ToString>(&self, name: T) -> Option<Handle<AudioSource>> {
self.sounds.get(&name.to_string()).map(Handle::clone_weak)
}
}
pub type AssetNameMapping = (String, String);
pub type FixedAssetNameMapping = (&'static str, &'static str);
use bevy::asset::LoadState;
use bevy::prelude::*;
use iyes_loopless::prelude::NextState;
use crate::assets::AssetTypeLoader;
use crate::system::flow::AppState;
pub fn start_preload_resources(mut commands: Commands) {
// TODO: Add preload commands here
commands.insert_resource(NextState(AppState::Setup))
}
pub fn start_load_resources(mut loader: AssetTypeLoader) {
loader.load_images(&[("splash.png", "splash")]);
loader.load_audio(&[("splash_sting.mp3", "splash_sting")]);
}
pub fn check_load_resources(mut commands: Commands, loader: AssetTypeLoader) {
let load_states = loader.get_all_load_state();
if load_states.iter().all(|state| *state == LoadState::Loaded) {
log::info!("Assets loaded successfully");
commands.insert_resource(NextState(AppState::Splash))
}
}
pub mod assets;
pub mod multiplayer;
pub mod splash_screen;
pub mod system;
use bevy::prelude::*;
use game_core::assets::AssetHandles;
use game_core::system::flow::AppState;
use game_core::system::resources::InitAppPlugins;
use iyes_loopless::prelude::AppLooplessStateExt;
use micro_musicbox::CombinedAudioPlugins;
// use remote_events::RemoteEventPlugin;
fn main() {
App::new()
.add_loopless_state(AppState::Preload)
.add_plugins(InitAppPlugins)
.add_plugin(game_core::assets::AssetsPlugin)
.add_plugins(CombinedAudioPlugins::<AssetHandles>::new())
.add_plugin(game_core::splash_screen::SplashScreenPlugin)
.add_plugin(game_core::system::camera::CameraManagementPlugin)
// .add_plugin(RemoteEventPlugin::<
// game_core::multiplayer::OutgoingEvent,
// game_core::multiplayer::IncomingEvent,
// >::new())
.run();
}
// use remote_events::events::{FromSocketMessage, ToSocketMessage};
// use serde::{Deserialize, Serialize};
//
// #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
// #[serde(tag = "type")]
// pub enum IncomingEvent {
// #[default]
// #[serde(rename = "void")]
// Void,
// }
//
// impl FromSocketMessage for IncomingEvent {
// fn from_text(value: String) -> Self {
// serde_json::from_str(value.as_str()).unwrap_or_default()
// }
//
// fn from_binary(value: Vec<u8>) -> Self {
// serde_json::from_slice(value.as_slice()).unwrap_or_default()
// }
// }
//
// #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
// #[serde(tag = "type")]
// pub enum OutgoingEvent {
// #[default]
// #[serde(rename = "void")]
// Void,
// }
//
// impl ToSocketMessage for OutgoingEvent {
// fn to_text(&self) -> String {
// serde_json::to_string(&self).unwrap_or(String::from("{}"))
// }
// }
use bevy::prelude::*;
#[derive(Clone)]
pub enum SplashAnimationType {
FadeColour { from: Color, to: Color },
Wait,
}
#[derive(Component, Clone)]
pub struct SplashAnimation {
pub duration: f32,
pub anim: SplashAnimationType,
pub then: Option<Box<SplashAnimation>>,
}
#[derive(Component, Clone)]
pub struct SplashAnimationTimer(pub f32);
#[derive(Bundle)]
pub struct SplashAnimationBundle {
animation: SplashAnimation,
timer: SplashAnimationTimer,
}
impl SplashAnimationBundle {
pub fn from_animation(animation: SplashAnimation) -> Self {
Self {
animation,
timer: SplashAnimationTimer(0.0),
}
}
}
impl SplashAnimation {
pub fn fade(
from: Color,
to: Color,
duration: f32,
then: Option<Box<SplashAnimation>>,
) -> SplashAnimation {
SplashAnimation {
anim: SplashAnimationType::FadeColour { from, to },
duration,
then,
}
}
pub fn wait(duration: f32, then: Option<Box<SplashAnimation>>) -> SplashAnimation {
SplashAnimation {
anim: SplashAnimationType::Wait,
duration,
then,
}
}
}
use bevy::prelude::*;
use iyes_loopless::prelude::{AppLooplessStateExt, ConditionSet};
use crate::system::flow::AppState;
mod components;
mod systems;
pub struct SplashScreenPlugin;
impl Plugin for SplashScreenPlugin {
fn build(&self, app: &mut App) {
app.add_enter_system(AppState::Splash, systems::setup_splash_screen)
.add_system_set(
ConditionSet::new()
.run_in_state(AppState::Splash)
.with_system(systems::tick_splash_system)
.into(),
)
.add_exit_system(AppState::Splash, systems::remove_splash_entities);
}
}
use bevy::prelude::*;
use bevy::render::texture::ImageSampler;
use iyes_loopless::state::NextState;
use micro_musicbox::music_box::MusicBox;
use crate::assets::AssetHandles;
use crate::splash_screen::components::{
SplashAnimation, SplashAnimationBundle, SplashAnimationTimer, SplashAnimationType,
};
use crate::system::flow::AppState;
use crate::system::load_config::virtual_size;
use crate::system::utilities::f32_min;
const C_TRANS: Color = Color::rgba(1.0, 1.0, 1.0, 0.0);
const C_WHITE: Color = Color::rgba(1.0, 1.0, 1.0, 1.0);
fn interpolate_colours(from: Color, to: Color, percent: f32) -> Color {
let diff_r = to.r() - from.r();
let diff_g = to.g() - from.g();
let diff_b = to.b() - from.b();
let diff_a = to.a() - from.a();
Color::rgba(
from.r() + diff_r * percent,
from.g() + diff_g * percent,
from.b() + diff_b * percent,
from.a() + diff_a * percent,
)
}
pub fn setup_splash_screen(
mut commands: Commands,
mut image_assets: ResMut<Assets<Image>>,
handles: Res<AssetHandles>,
mut music_box: MusicBox<AssetHandles>,
) {
let (window_width, window_height) = virtual_size();
let handle = match handles.images.get("splash") {
Some(handle) => handle,
None => {
log::error!("No splash image was found; Skipping");
commands.insert_resource(NextState(AppState::Menu));
return;
}
};
let image_data = image_assets
.get_mut(handle)
.expect("An image was stored without the handle being persisted");
commands.insert_resource(ClearColor(Color::hex("001122").unwrap()));
let scale_factor = match window_width > window_height {
true => window_height / image_data.texture_descriptor.size.height as f32,
false => window_width / image_data.texture_descriptor.size.width as f32,
};
image_data.sampler_descriptor = ImageSampler::linear();
music_box.play_sfx("splash_sting");
commands.spawn((
SpriteBundle {
texture: handle.clone_weak(),
sprite: Sprite {
color: C_TRANS,
..Default::default()
},
transform: Transform {
scale: [scale_factor, scale_factor, 1.0].into(),
..Default::default()
},
..Default::default()
},
SplashAnimationBundle::from_animation(SplashAnimation::wait(
1.0,
Some(Box::new(SplashAnimation::fade(
C_TRANS,
C_WHITE,
1.0,
Some(Box::new(SplashAnimation::wait(
1.0,
Some(Box::new(SplashAnimation::fade(
C_WHITE,
C_TRANS,
0.75,
Some(Box::new(SplashAnimation::wait(0.75, None))),
))),
))),
))),
)),
));
}
pub fn tick_splash_system(
mut commands: Commands,
time: Res<Time>,
mut query: Query<(&mut Sprite, &mut SplashAnimation, &mut SplashAnimationTimer)>,
) {
let tick = time.delta_seconds();
for (mut sprite, mut anims, mut timer) in query.iter_mut() {
timer.0 += tick;
match anims.anim {
SplashAnimationType::FadeColour { from, to } => {
let percent = f32_min(1.0, timer.0 / anims.duration);
sprite.color = interpolate_colours(from, to, percent);
}
SplashAnimationType::Wait => {}
}
if timer.0 >= anims.duration {
timer.0 = 0.0;
match &anims.then {
Some(anim) => {
*anims = *anim.clone();
}
None => {
commands.insert_resource(NextState(AppState::Menu));
}
}
}
}
}
pub fn remove_splash_entities(mut commands: Commands, query: Query<Entity, With<SplashAnimation>>) {
for entity in query.iter() {
commands.entity(entity).despawn_recursive();
}
}
use bevy::app::App;
use bevy::math::{Vec2, Vec3Swizzles};
use bevy::prelude::{
Camera2dBundle, Commands, Component, CoreStage, Entity, OrthographicProjection, Plugin, Query,
Transform, With,
};
use bevy::render::camera::ScalingMode;
use iyes_loopless::prelude::AppLooplessStateExt;
use crate::system::flow::AppState;
use crate::system::load_config::virtual_size;
/// A flag component to indicate which entity should be followed by the camera
#[derive(Component)]
pub struct ChaseCam;
/// A flag component to indicate a camera that should be used for rendering world entities and sprites
#[derive(Component)]
pub struct GameCamera;
/// System that creates a default orthographic camera, with correct tags for querying
pub fn spawn_orthographic_camera(mut commands: Commands) {
let (target_width, target_height) = virtual_size();
commands.spawn((
Camera2dBundle {
projection: OrthographicProjection {
left: -(target_width / 2.0),
right: (target_width / 2.0),
top: (target_height / 2.0),
bottom: -(target_height / 2.0),
scaling_mode: ScalingMode::Auto {
min_width: target_width,
min_height: target_height,
},
..Default::default()
},
..Default::default()
},
GameCamera,
));
}
/// System that takes the average location of all chase camera entities, and updates the location
/// of all world cameras to track the average location.
///
/// e.g. If a player entity is chased, and a mouse tracking entity is chased, the world cameras will
/// by updated to the midpoint between the player and the mouse
pub fn sync_chase_camera_location(
mut commands: Commands,
chased_query: Query<&Transform, With<ChaseCam>>,
camera_query: Query<(Entity, &Transform), With<GameCamera>>,
) {
let mut average_location = Vec2::new(0.0, 0.0);
let mut count = 0;
for location in chased_query.iter() {
average_location += location.translation.xy();
count += 1;
}
if count > 0 {
average_location /= count as f32;
}
for (entity, location) in camera_query.iter() {
commands.entity(entity).insert(Transform {
translation: average_location.extend(location.translation.z),
..*location
});
}
}
/// A marker struct for spawning and managing cameras. Cameras will be created on startup, and
/// will constantly have their positions synced
pub struct CameraManagementPlugin;
impl Plugin for CameraManagementPlugin {
fn build(&self, app: &mut App) {
app.add_enter_system(AppState::Preload, spawn_orthographic_camera)
.add_system_to_stage(CoreStage::PreUpdate, sync_chase_camera_location);
}
}
use bevy::prelude::StageLabel;
/// An enum representing the current set of systems that should be running.
/// Changing states should be done by inserting an `iyes_loopless` `NextState`
/// resource with the value of one of these app states
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Debug, Hash, StageLabel)]
pub enum AppState {
/// During the preload state, embedded resources will be registered with Bevy.
/// Embedded resources will be attached to the asset system with the path
/// `internal:{resource-name}`
Preload,
/// During the setup state, external resources will be registered by reading from
/// the filesystem or the network. Progress can be displayed using the embedded
/// resources loaded in the previous state
Setup,
/// The splash state exists solely to render attributions & logos before starting
/// the game
Splash,
/// An initial landing page that will present players with options
Menu,
/// The in game state runs all of the actual gameplay logic. Most of the runtime
/// will be spent here.
InGame,
}
#[cfg(not(target_arch = "wasm32"))]
mod setup {
pub fn get_asset_path_string() -> String {
std::env::current_dir()
.unwrap()
.join("assets")
.to_str()
.unwrap()
.to_string()
}
pub fn initial_size() -> (f32, f32) {
(1280.0, 720.0)
}
pub fn virtual_size() -> (f32, f32) {
(1280.0, 720.0)
}
}
#[cfg(target_arch = "wasm32")]
mod setup {
pub fn get_asset_path_string() -> String {
String::from("assets")
}
pub fn virtual_size() -> (f32, f32) {
(1280.0, 720.0)
}
#[cfg(feature = "no_aspect")]
pub fn initial_size() -> (f32, f32) {
static default_width: f32 = 1280.0;
static default_height: f32 = 720.0;
web_sys::window()
.and_then(|window: web_sys::Window| {
let w = window
.inner_width()
.ok()
.and_then(|val| val.as_f64().map(|v| v as f32))
.unwrap_or(default_width);
let h = window
.inner_height()
.ok()
.and_then(|val| val.as_f64().map(|v| v as f32))
.unwrap_or(default_height);
Some((w, h))
})
.unwrap_or((default_width, default_height))
}
#[cfg(not(feature = "no_aspect"))]
pub fn initial_size() -> (f32, f32) {
static default_width: f32 = 1280.0;
static default_height: f32 = 720.0;
static ratio: f32 = 1280.0 / 720.0;
web_sys::window()
.and_then(|window: web_sys::Window| {
let w = window
.inner_width()
.ok()
.and_then(|val| val.as_f64().map(|v| v as f32))
.unwrap_or(default_width);
let h = window
.inner_height()
.ok()
.and_then(|val| val.as_f64().map(|v| v as f32))
.unwrap_or(default_height);
Some((w, h / ratio))
})
.unwrap_or((default_width, default_height))
}
}
pub use setup::*;
pub mod camera;
pub mod flow;
pub mod load_config;
pub mod resources;
pub mod utilities;
pub mod window;
use bevy::app::PluginGroupBuilder;
use bevy::log::{Level, LogPlugin};
use bevy::prelude::*;
use bevy::window::{PresentMode, WindowMode};
use crate::system::load_config::{get_asset_path_string, initial_size};
pub struct DefaultResourcesPlugin;
impl Plugin for DefaultResourcesPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(Msaa { samples: 1 })
.insert_resource(ClearColor(Color::hex("040720").unwrap()));
}
}
pub fn configure_default_plugins() -> PluginGroupBuilder {
let (width, height) = initial_size();
DefaultPlugins
.set(WindowPlugin {
window: WindowDescriptor {
width,
height,
resizable: true,
mode: WindowMode::Windowed,
title: String::from("Bevy 2D Template"),
present_mode: PresentMode::AutoNoVsync,
..Default::default()
},
..Default::default()
})
.set(AssetPlugin {
asset_folder: get_asset_path_string(),
watch_for_changes: true,
})
.set(ImagePlugin::default_nearest())
.set(LogPlugin {
filter: String::from(
"info,symphonia_core=warn,symphonia_bundle_mp3=warn,wgpu_core=warn,wgpu_hal=warn",
),
level: Level::DEBUG,
})
}
pub struct InitAppPlugins;
impl PluginGroup for InitAppPlugins {
fn build(self) -> PluginGroupBuilder {
configure_default_plugins().add(DefaultResourcesPlugin)
}
}
#[inline]
pub fn f32_max(a: f32, b: f32) -> f32 {
if a > b {
a
} else {
b
}
}
#[inline]
pub fn f32_max_mag(a: f32, b: f32) -> f32 {
if a.abs() > b.abs() {
a.abs()
} else {
b.abs()
}
}
#[inline]
pub fn f32_min(a: f32, b: f32) -> f32 {
if a < b {
a
} else {
b
}
}
#[inline]
pub fn f32_min_mag(a: f32, b: f32) -> f32 {
if a.abs() < b.abs() {
a.abs()
} else {
b.abs()
}
}
use bevy::ecs::system::SystemParam;
use bevy::prelude::*;
use crate::system::camera::GameCamera;
/// A struct that provides several convenience methods for getting mouse and
/// window related information
#[derive(SystemParam)]
pub struct WindowManager<'w, 's> {
mouse: Res<'w, Input<MouseButton>>,
windows: Res<'w, Windows>,
cam_query: ParamSet<'w, 's, (Query<'w, 's, &'static Transform, With<GameCamera>>,)>,
}
impl<'w, 's> WindowManager<'w, 's> {
/// Conditionally run a function with the primary window. The function will not
/// run if the primary window does not exist - typically this is the desired behaviour.
///
/// ## Arguments
/// - `func`: an `FnOnce` callback that is given a [`bevy::prelude::Window`]
pub fn with_primary_window<Func: FnOnce(&Window)>(&self, func: Func) {
match self.windows.get_primary() {
Some(window) => func(window),
None => {}
}
}
pub fn get_primary_window(&self) -> Option<&Window> {
self.windows.get_primary()
}
pub fn get_mouse_press(&mut self) -> Option<Vec2> {
if self.mouse.just_pressed(MouseButton::Left) {
if let Some(window) = self.windows.get_primary() {
if let Some(position) = window.cursor_position() {
let window_size = Vec2::new(window.width() as f32, window.height() as f32);
let adjusted_position = position - window_size / 2.0;
if let Ok(camera_transform) = self.cam_query.p0().get_single() {
let world_position = camera_transform.compute_matrix()
* adjusted_position.extend(0.0).extend(1.0);
return Some(Vec2::new(world_position.x, world_position.y));
}
}
}
}
None
}
}
[toolchain]
channel = "nightly"
\ No newline at end of file
hard_tabs = true
group_imports = "StdExternalCrate"
use_field_init_shorthand = true
use_try_shorthand = true
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment