Starting on the most basic trees implementation

Also:
* Track files with git-lfs (png, xcf)
* Debugger tooltip for UI elements
* Make tooltip render above everything else
main
Elijah Voigt 4 months ago
parent c14f2484e6
commit 04777a4e33

@ -3,13 +3,21 @@ codegen-backend = true
[profile.dev] [profile.dev]
codegen-backend = "cranelift" codegen-backend = "cranelift"
opt-level = 1
[profile.dev.package."*"] [profile.dev.package."*"]
codegen-backend = "llvm" codegen-backend = "llvm"
opt-level = 3
[profile.release] [profile.release]
lto = true codegen-units = 1
opt-level = 'z' opt-level = "z"
lto = "thin"
[profile.wasm-release]
inherits = "release"
opt-level = "s"
strip = "debuginfo"
# for Linux # for Linux
[target.x86_64-unknown-linux-gnu] [target.x86_64-unknown-linux-gnu]

@ -2,6 +2,10 @@
This is a mono-repo I am working on containing many mini-games I am working on. This is a mono-repo I am working on containing many mini-games I am working on.
## Games
* [tetris](src/bin/tetris/README.md)
## Building a game ## Building a game
You can build games in this project with `cargo` provided by Rust: You can build games in this project with `cargo` provided by Rust:

@ -0,0 +1,2 @@
*.png filter=lfs diff=lfs merge=lfs -text
*.xcf filter=lfs diff=lfs merge=lfs -text

BIN
assets/placeholder/tree.png (Stored with Git LFS)

Binary file not shown.

BIN
assets/placeholder/tree.xcf (Stored with Git LFS)

Binary file not shown.

@ -27,7 +27,8 @@ fn main() {
control_ball, control_ball,
process_events, process_events,
sync_resource_to_ui::<InsideArea>.run_if(resource_changed::<InsideArea>), sync_resource_to_ui::<InsideArea>.run_if(resource_changed::<InsideArea>),
).run_if(in_state(GameState::Main)), )
.run_if(in_state(LoadingState::Idle)),
) )
.run(); .run();
} }
@ -204,8 +205,7 @@ fn process_events(
(*flags == CollisionEventFlags::SENSOR).then_some(WithinBounds::Exit(*a, *b)) (*flags == CollisionEventFlags::SENSOR).then_some(WithinBounds::Exit(*a, *b))
} }
}) })
.for_each(|within_bounds| { .for_each(|within_bounds| match within_bounds {
match within_bounds {
WithinBounds::Enter(a, b) => { WithinBounds::Enter(a, b) => {
if let Ok(x) = areas.get(a) { if let Ok(x) = areas.get(a) {
inside.0 = Some(x.clone()); inside.0 = Some(x.clone());
@ -218,6 +218,5 @@ fn process_events(
inside.0 = None; inside.0 = None;
} }
} }
}
}) })
} }

@ -10,18 +10,10 @@ impl Plugin for BaseGamePlugin {
.add_plugins(MeshPickingPlugin) .add_plugins(MeshPickingPlugin)
.add_plugins(RapierPhysicsPlugin::<NoUserData>::default()) .add_plugins(RapierPhysicsPlugin::<NoUserData>::default())
.add_plugins(LoadingPlugin) .add_plugins(LoadingPlugin)
.add_systems(Startup, setup_camera) .add_systems(Startup, setup_camera);
.init_state::<GameState>();
} }
} }
#[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)]
pub enum GameState {
#[default]
Loading,
Main,
}
/// System to toggle the visibility of entities based on their state /// System to toggle the visibility of entities based on their state
pub fn toggle_state_visibility<S: States + Component>( pub fn toggle_state_visibility<S: States + Component>(
mut q: Query<(Entity, &mut Visibility, &S)>, mut q: Query<(Entity, &mut Visibility, &S)>,

@ -1,7 +1,5 @@
use games::*; use games::*;
fn main() { fn main() {
App::new() App::new().add_plugins(BaseGamePlugin).run();
.add_plugins(BaseGamePlugin)
.run();
} }

@ -0,0 +1,5 @@
use games::*;
fn main() {
App::new().add_plugins(BaseGamePlugin).run();
}

@ -0,0 +1,162 @@
#![allow(clippy::type_complexity)]
use games::*;
fn main() {
App::new()
.add_plugins(BaseGamePlugin)
.insert_resource(ClearColor(WHITE.into()))
.add_systems(
Startup,
(init_trees, init_ui, position_camera.after(setup_camera)),
)
.add_systems(
Update,
dialog_engine.run_if(input_just_pressed(KeyCode::KeyN)),
)
.run();
}
#[derive(Component)]
struct Tree;
fn init_trees(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
server: Res<AssetServer>,
) {
let tree_card_mesh = meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(1.0)));
let tree_image = server.load("placeholder/tree.png");
// Spawn placeholder tree (red)
{
let tree_material_red = materials.add(StandardMaterial {
base_color_texture: Some(tree_image.clone()),
base_color: RED.into(),
alpha_mode: AlphaMode::Blend,
..default()
});
let tree_transform_red =
Transform::from_xyz(-15.0, 0.0, 15.0).with_scale(Vec3::splat(10.0));
commands.spawn((
Tree,
Mesh3d(tree_card_mesh.clone()),
MeshMaterial3d(tree_material_red),
tree_transform_red,
Name::new("Red"),
));
}
// Spawn placeholder tree (green)
{
let tree_material_green = materials.add(StandardMaterial {
base_color_texture: Some(tree_image.clone()),
base_color: GREEN.into(),
alpha_mode: AlphaMode::Blend,
..default()
});
let tree_transform_green =
Transform::from_xyz(15.0, 0.0, 15.0).with_scale(Vec3::splat(10.0));
commands.spawn((
Tree,
Mesh3d(tree_card_mesh.clone()),
MeshMaterial3d(tree_material_green),
tree_transform_green,
Name::new("Green"),
));
}
// Spawn placeholder tree (blue)
{
let tree_material_blue = materials.add(StandardMaterial {
base_color_texture: Some(tree_image.clone()),
base_color: BLUE.into(),
alpha_mode: AlphaMode::Blend,
..default()
});
let tree_transform_blue =
Transform::from_xyz(0.0, 0.0, -15.0).with_scale(Vec3::splat(10.0));
commands.spawn((
Tree,
Mesh3d(tree_card_mesh.clone()),
MeshMaterial3d(tree_material_blue),
tree_transform_blue,
Name::new("Blue"),
));
}
}
#[derive(Component)]
struct DialogBox;
fn init_ui(mut commands: Commands) {
commands.spawn((
DialogBox,
BackgroundColor(BLACK.with_alpha(0.9).into()),
Node {
align_self: AlignSelf::End,
justify_self: JustifySelf::Center,
width: Val::Percent(98.0),
max_height: Val::Percent(25.0),
align_items: AlignItems::Center,
margin: UiRect::all(Val::Percent(1.0)),
padding: UiRect::all(Val::Percent(1.0)),
flex_direction: FlexDirection::Column,
..default()
},
));
}
fn position_camera(mut query: Query<&mut Transform, (With<Camera>, With<Camera3d>)>) {
use std::f32::consts::PI;
query.iter_mut().for_each(|mut t| {
*t = Transform::from_xyz(0.0, 100.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y);
t.rotate_y(-PI * 0.5)
})
}
#[derive(Component)]
struct DialogOption;
#[derive(Component)]
struct DialogLine;
fn dialog_engine(
mut commands: Commands,
dialog_box: Single<Entity, With<DialogBox>>,
mut idx: Local<usize>,
) {
let dialog: Vec<Vec<&str>> = vec![
vec!["A", "B", "C"],
vec!["E", "F", "G"],
vec!["H", "I"],
vec!["J", "K"],
vec!["L"],
];
info!("Show options: {:?}", dialog.get(*idx));
commands.entity(*dialog_box).with_children(|parent| {
if let Some(options) = dialog.get(*idx) {
options.iter().for_each(|option| {
parent
.spawn((Text::new(*option), Button, DialogOption))
.observe(choose_dialog);
});
}
});
*idx += 1;
}
fn choose_dialog(trigger: Trigger<Pointer<Click>>, mut commands: Commands) {
info!("Choosing dialog {:?}", trigger.target());
commands
.entity(trigger.target())
.remove::<Button>()
.remove::<DialogOption>()
.insert(DialogLine);
}

@ -1,3 +1,8 @@
use bevy::{
color::palettes::css::MAGENTA,
pbr::wireframe::{WireframeConfig, WireframePlugin},
};
use super::*; use super::*;
/// Debugging systems, resources, events, etc. /// Debugging systems, resources, events, etc.
@ -8,7 +13,14 @@ impl Plugin for DebuggingPlugin {
app.init_state::<DebuggingState>() app.init_state::<DebuggingState>()
.init_resource::<ToolTip>() .init_resource::<ToolTip>()
.add_plugins(RapierDebugRenderPlugin::default().disabled()) .add_plugins(RapierDebugRenderPlugin::default().disabled())
.add_plugins(WireframePlugin::default())
.insert_resource(WireframeConfig {
global: false,
default_color: MAGENTA.into(),
})
.add_systems(Startup, init_debug_ui) .add_systems(Startup, init_debug_ui)
.add_systems(OnEnter(DebuggingState::On), enable_wireframe)
.add_systems(OnExit(DebuggingState::On), disable_wireframe)
.add_systems( .add_systems(
Update, Update,
( (
@ -20,9 +32,11 @@ impl Plugin for DebuggingPlugin {
toggle_debug_state.run_if(on_keyboard_press(KeyCode::F12)), toggle_debug_state.run_if(on_keyboard_press(KeyCode::F12)),
( (
hover_mesh.run_if(on_event::<Pointer<Over>>), hover_mesh.run_if(on_event::<Pointer<Over>>),
hover_ui.run_if(on_event::<Pointer<Over>>),
tooltip_follow.run_if(any_component_changed::<Window>), tooltip_follow.run_if(any_component_changed::<Window>),
sync_resource_to_ui::<ToolTip>.run_if(resource_changed::<ToolTip>), sync_resource_to_ui::<ToolTip>.run_if(resource_changed::<ToolTip>),
).run_if(in_state(DebuggingState::On)) )
.run_if(in_state(DebuggingState::On)),
), ),
); );
} }
@ -59,9 +73,13 @@ fn init_debug_ui(mut commands: Commands) {
TextColor(WHITE.into()), TextColor(WHITE.into()),
SyncResource::<ToolTip>::default(), SyncResource::<ToolTip>::default(),
BackgroundColor(BLACK.with_alpha(0.9).into()), BackgroundColor(BLACK.with_alpha(0.9).into()),
GlobalZIndex(i32::MAX),
Node { Node {
position_type: PositionType::Absolute, position_type: PositionType::Absolute,
margin: UiRect { left: Val::Px(20.0), ..default() }, margin: UiRect {
left: Val::Px(20.0),
..default()
},
align_content: AlignContent::Center, align_content: AlignContent::Center,
justify_content: JustifyContent::Center, justify_content: JustifyContent::Center,
..default() ..default()
@ -121,14 +139,16 @@ fn hover_mesh(
mut tooltip: ResMut<ToolTip>, mut tooltip: ResMut<ToolTip>,
meshes: Query<(&Transform, Option<&Name>), With<Mesh3d>>, meshes: Query<(&Transform, Option<&Name>), With<Mesh3d>>,
) { ) {
out_events.read().filter_map(|Pointer { target, .. }| { out_events
meshes.contains(*target).then_some(*target) .read()
}).for_each(|_| { .filter_map(|Pointer { target, .. }| meshes.contains(*target).then_some(*target))
.for_each(|_| {
tooltip.0 = "".into(); tooltip.0 = "".into();
}); });
over_events.read().filter_map(|Pointer { target, .. }| { over_events
meshes.contains(*target).then_some(*target) .read()
}).for_each(|e| { .filter_map(|Pointer { target, .. }| meshes.contains(*target).then_some(*target))
.for_each(|e| {
if let Ok((t, n)) = meshes.get(e) { if let Ok((t, n)) = meshes.get(e) {
let pos = (t.translation.x, t.translation.y, t.translation.z); let pos = (t.translation.x, t.translation.y, t.translation.z);
let name = match n { let name = match n {
@ -141,3 +161,39 @@ fn hover_mesh(
} }
}); });
} }
fn hover_ui(
mut over_events: EventReader<Pointer<Over>>,
mut out_events: EventReader<Pointer<Out>>,
mut tooltip: ResMut<ToolTip>,
nodes: Query<Option<&Name>, With<Node>>,
) {
out_events
.read()
.filter_map(|Pointer { target, .. }| nodes.contains(*target).then_some(*target))
.for_each(|_| {
tooltip.0 = "".into();
});
over_events
.read()
.filter_map(|Pointer { target, .. }| nodes.contains(*target).then_some(*target))
.for_each(|e| {
if let Ok(n) = nodes.get(e) {
let name = match n {
Some(x) => x,
None => "???",
};
tooltip.0 = format!("ID: {e}\nName: {name}");
} else {
warn!("Failed to query data");
}
});
}
fn enable_wireframe(mut wireframe_config: ResMut<WireframeConfig>) {
wireframe_config.global = true;
}
fn disable_wireframe(mut wireframe_config: ResMut<WireframeConfig>) {
wireframe_config.global = false;
}

@ -11,7 +11,8 @@ pub use std::fmt::Display;
pub use bevy::{ pub use bevy::{
asset::LoadState, asset::LoadState,
color::palettes::css::{BLACK, RED, WHITE}, color::palettes::css::{BLACK, BLUE, GREEN, RED, WHITE},
input::common_conditions::{input_just_pressed, input_pressed},
input::{ButtonState, keyboard::KeyboardInput, mouse::MouseMotion}, input::{ButtonState, keyboard::KeyboardInput, mouse::MouseMotion},
prelude::*, prelude::*,
}; };

@ -1,4 +1,3 @@
use super::*; use super::*;
/// Systems run during loading state /// Systems run during loading state
@ -7,10 +6,11 @@ pub struct LoadingPlugin;
impl Plugin for LoadingPlugin { impl Plugin for LoadingPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
// PERF: We check all asset classes every frame, should be event based // PERF: We check all asset classes every frame, should be event based
app app.init_resource::<TrackLoadingProgress>()
.init_resource::<TrackLoadingProgress>() .init_state::<LoadingState>()
.add_systems(PreUpdate, reset_progress) .add_systems(PreUpdate, reset_progress)
.add_systems(Update, .add_systems(
Update,
( (
track_loading::<AnimationClip>, track_loading::<AnimationClip>,
track_loading::<AudioSource>, track_loading::<AudioSource>,
@ -18,20 +18,26 @@ impl Plugin for LoadingPlugin {
track_loading::<Gltf>, track_loading::<Gltf>,
track_loading::<Image>, track_loading::<Image>,
track_loading::<Shader>, track_loading::<Shader>,
).run_if(in_state(GameState::Loading)), )
.run_if(in_state(LoadingState::Active)),
) )
.add_systems(PostUpdate, check_progress); .add_systems(PostUpdate, check_progress);
} }
} }
#[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)]
pub enum LoadingState {
#[default]
Active,
Idle,
}
/// Resource for tracking asset loading progress /// Resource for tracking asset loading progress
#[derive(Resource, Default)] #[derive(Resource, Default)]
struct TrackLoadingProgress(Vec<bool>); struct TrackLoadingProgress(Vec<bool>);
/// At the start of the update clear progress /// At the start of the update clear progress
fn reset_progress( fn reset_progress(mut progress: ResMut<TrackLoadingProgress>) {
mut progress: ResMut<TrackLoadingProgress>,
) {
progress.0.clear() progress.0.clear()
} }
@ -44,9 +50,7 @@ fn track_loading<A: Asset>(
let all_loaded = assets let all_loaded = assets
.ids() .ids()
.map(|id| server.load_state(id)) .map(|id| server.load_state(id))
.all(|load_state| { .all(|load_state| !load_state.is_loading() && !load_state.is_failed());
!load_state.is_loading() && !load_state.is_failed()
});
progress.0.push(all_loaded); progress.0.push(all_loaded);
} }
@ -54,9 +58,9 @@ fn track_loading<A: Asset>(
/// At the end of the frame check if all asset classes are loaded /// At the end of the frame check if all asset classes are loaded
fn check_progress( fn check_progress(
progress: Res<TrackLoadingProgress>, progress: Res<TrackLoadingProgress>,
mut next_state: ResMut<NextState<GameState>>, mut next_state: ResMut<NextState<LoadingState>>,
) { ) {
if progress.0.iter().all(|x| *x) { if progress.0.iter().all(|x| *x) {
next_state.set(GameState::Main); next_state.set(LoadingState::Idle);
} }
} }

Loading…
Cancel
Save