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]
codegen-backend = "cranelift"
opt-level = 1
[profile.dev.package."*"]
codegen-backend = "llvm"
opt-level = 3
[profile.release]
lto = true
opt-level = 'z'
codegen-units = 1
opt-level = "z"
lto = "thin"
[profile.wasm-release]
inherits = "release"
opt-level = "s"
strip = "debuginfo"
# for Linux
[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.
## Games
* [tetris](src/bin/tetris/README.md)
## Building a game
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,
process_events,
sync_resource_to_ui::<InsideArea>.run_if(resource_changed::<InsideArea>),
).run_if(in_state(GameState::Main)),
)
.run_if(in_state(LoadingState::Idle)),
)
.run();
}
@ -204,8 +205,7 @@ fn process_events(
(*flags == CollisionEventFlags::SENSOR).then_some(WithinBounds::Exit(*a, *b))
}
})
.for_each(|within_bounds| {
match within_bounds {
.for_each(|within_bounds| match within_bounds {
WithinBounds::Enter(a, b) => {
if let Ok(x) = areas.get(a) {
inside.0 = Some(x.clone());
@ -218,6 +218,5 @@ fn process_events(
inside.0 = None;
}
}
}
})
}

@ -10,18 +10,10 @@ impl Plugin for BaseGamePlugin {
.add_plugins(MeshPickingPlugin)
.add_plugins(RapierPhysicsPlugin::<NoUserData>::default())
.add_plugins(LoadingPlugin)
.add_systems(Startup, setup_camera)
.init_state::<GameState>();
.add_systems(Startup, setup_camera);
}
}
#[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
pub fn toggle_state_visibility<S: States + Component>(
mut q: Query<(Entity, &mut Visibility, &S)>,

@ -1,7 +1,5 @@
use games::*;
fn main() {
App::new()
.add_plugins(BaseGamePlugin)
.run();
App::new().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::*;
/// Debugging systems, resources, events, etc.
@ -8,7 +13,14 @@ impl Plugin for DebuggingPlugin {
app.init_state::<DebuggingState>()
.init_resource::<ToolTip>()
.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(OnEnter(DebuggingState::On), enable_wireframe)
.add_systems(OnExit(DebuggingState::On), disable_wireframe)
.add_systems(
Update,
(
@ -20,9 +32,11 @@ impl Plugin for DebuggingPlugin {
toggle_debug_state.run_if(on_keyboard_press(KeyCode::F12)),
(
hover_mesh.run_if(on_event::<Pointer<Over>>),
hover_ui.run_if(on_event::<Pointer<Over>>),
tooltip_follow.run_if(any_component_changed::<Window>),
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()),
SyncResource::<ToolTip>::default(),
BackgroundColor(BLACK.with_alpha(0.9).into()),
GlobalZIndex(i32::MAX),
Node {
position_type: PositionType::Absolute,
margin: UiRect { left: Val::Px(20.0), ..default() },
margin: UiRect {
left: Val::Px(20.0),
..default()
},
align_content: AlignContent::Center,
justify_content: JustifyContent::Center,
..default()
@ -121,14 +139,16 @@ fn hover_mesh(
mut tooltip: ResMut<ToolTip>,
meshes: Query<(&Transform, Option<&Name>), With<Mesh3d>>,
) {
out_events.read().filter_map(|Pointer { target, .. }| {
meshes.contains(*target).then_some(*target)
}).for_each(|_| {
out_events
.read()
.filter_map(|Pointer { target, .. }| meshes.contains(*target).then_some(*target))
.for_each(|_| {
tooltip.0 = "".into();
});
over_events.read().filter_map(|Pointer { target, .. }| {
meshes.contains(*target).then_some(*target)
}).for_each(|e| {
over_events
.read()
.filter_map(|Pointer { target, .. }| meshes.contains(*target).then_some(*target))
.for_each(|e| {
if let Ok((t, n)) = meshes.get(e) {
let pos = (t.translation.x, t.translation.y, t.translation.z);
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::{
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},
prelude::*,
};

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

Loading…
Cancel
Save