Compare commits

...

3 Commits

Author SHA1 Message Date
Elijah Voigt ca3db16a48 Add common sense info to debug (fps, entity count) 3 months ago
Elijah Voigt 9f307281f4 Ui example with nav open/close 3 months ago
Elijah Voigt c349ce08d0 Starting on ui example 3 months ago

11
Cargo.lock generated

@ -2226,6 +2226,7 @@ dependencies = [
"bevy", "bevy",
"bevy_rapier3d", "bevy_rapier3d",
"chrono", "chrono",
"lipsum",
"serde", "serde",
"thiserror 2.0.12", "thiserror 2.0.12",
"walkdir", "walkdir",
@ -2780,6 +2781,16 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]]
name = "lipsum"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "636860251af8963cc40f6b4baadee105f02e21b28131d76eba8e40ce84ab8064"
dependencies = [
"rand",
"rand_chacha",
]
[[package]] [[package]]
name = "litrs" name = "litrs"
version = "0.4.1" version = "0.4.1"

@ -17,6 +17,9 @@ version = "0.30.0"
version = "0.16.1" version = "0.16.1"
features = ["wayland", "dynamic_linking"] features = ["wayland", "dynamic_linking"]
[dev-dependencies]
lipsum = "*"
[build-dependencies] [build-dependencies]
chrono = "*" chrono = "*"
walkdir = "*" walkdir = "*"

@ -50,10 +50,16 @@ fn write_monologues_file() {
walkdir::WalkDir::new("assets/trees") walkdir::WalkDir::new("assets/trees")
.into_iter() .into_iter()
.filter_map(|entry| entry.ok()) .filter_map(|entry| entry.ok())
.filter(|entry| { .filter(|entry| entry.path().extension() == Some(std::ffi::OsStr::new("mono")))
entry.path().extension() == Some(std::ffi::OsStr::new("mono"))
})
.for_each(|entry| { .for_each(|entry| {
let _ = writeln!(file, "{}", entry.path().to_string_lossy().strip_prefix("assets/").unwrap()); let _ = writeln!(
file,
"{}",
entry
.path()
.to_string_lossy()
.strip_prefix("assets/")
.unwrap()
);
}); });
} }

@ -0,0 +1,74 @@
//! This example illustrates scrolling in Bevy UI.
use games::*;
fn main() {
let mut app = App::new();
app.add_plugins(BaseGamePlugin::default())
.add_systems(Startup, (setup_list, setup_nav_tree));
app.run();
}
fn setup_list(mut commands: Commands) {
// Scrolling list
commands
.spawn((
Node {
flex_direction: FlexDirection::Column,
// If height is not set, we need both align_self: Stetch and overflow: scroll()
height: Val::Percent(50.0),
// align_self: AlignSelf::Stretch,
overflow: Overflow::scroll(),
..default()
},
BackgroundColor(RED.into()),
))
.with_children(|parent| {
// List items
(0..250).for_each(|i| {
parent.spawn(Text(format!("Item {i}")));
});
})
.observe(scroll);
}
fn setup_nav_tree(mut commands: Commands) {
// Nav Tree
commands
.spawn((
Node {
position_type: PositionType::Absolute,
right: Val::Px(0.0),
top: Val::Percent(50.0),
min_width: Val::Px(25.0),
min_height: Val::Px(25.0),
..default()
},
BackgroundColor(RED.into()),
))
.with_children(|parent| {
parent
.spawn((
Node {
position_type: PositionType::Absolute,
right: Val::Px(25.0),
min_width: Val::Px(25.0),
min_height: Val::Px(25.0),
..default()
},
BackgroundColor(ORANGE.into()),
))
.with_children(|parent| {
parent.spawn((
Node {
position_type: PositionType::Absolute,
right: Val::Px(25.0),
min_width: Val::Px(25.0),
min_height: Val::Px(25.0),
..default()
},
BackgroundColor(YELLOW.into()),
));
});
});
}

@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1750506804, "lastModified": 1752480373,
"narHash": "sha256-VLFNc4egNjovYVxDGyBYTrvVCgDYgENp5bVi9fPTDYc=", "narHash": "sha256-JHQbm+OcGp32wAsXTE/FLYGNpb+4GLi5oTvCxwSoBOA=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "4206c4cb56751df534751b058295ea61357bbbaa", "rev": "62e0f05ede1da0d54515d4ea8ce9c733f12d9f08",
"type": "github" "type": "github"
}, },
"original": { "original": {

@ -1,4 +1,4 @@
[toolchain] [toolchain]
channel = "nightly-2025-06-16" channel = "nightly-2025-07-16"
components = [ "rustfmt", "rustc-dev", "cargo", "clippy", "rust-analyzer", "rustc-codegen-cranelift" ] components = [ "rustfmt", "rustc-dev", "cargo", "clippy", "rust-analyzer", "rustc-codegen-cranelift" ]
targets = [ "x86_64-unknown-linux-gnu", "wasm32-unknown-unknown" ] targets = [ "x86_64-unknown-linux-gnu", "wasm32-unknown-unknown" ]

@ -27,6 +27,7 @@ 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_plugins(BaseUiPlugin)
.add_systems(Startup, setup_camera); .add_systems(Startup, setup_camera);
} }
} }

@ -56,6 +56,8 @@ fn main() {
.add_observer(remove_tree_monologue) .add_observer(remove_tree_monologue)
.add_observer(hide_monologue_preview) .add_observer(hide_monologue_preview)
.add_observer(populate_tree) .add_observer(populate_tree)
.add_observer(show_monologue_list)
.add_observer(hide_monologue_list)
.run(); .run();
} }
@ -119,13 +121,14 @@ fn init_debug_ui(mut commands: Commands) {
commands commands
.spawn(( .spawn((
Node { Node {
height: Val::Percent(90.0), max_height: Val::Percent(90.0),
align_self: AlignSelf::Center, align_self: AlignSelf::Center,
justify_self: JustifySelf::Start, justify_self: JustifySelf::Start,
..default() ..default()
}, },
MonologuesContainer, MonologuesContainer,
GlobalZIndex(i32::MAX - 1), GlobalZIndex(i32::MAX - 1),
BackgroundColor(PINK.into()),
DebuggingState::On, DebuggingState::On,
)) ))
.with_children(|parent| { .with_children(|parent| {
@ -134,6 +137,7 @@ fn init_debug_ui(mut commands: Commands) {
height: Val::Percent(100.0), height: Val::Percent(100.0),
flex_direction: FlexDirection::Column, flex_direction: FlexDirection::Column,
padding: UiRect::all(Val::Px(10.0)), padding: UiRect::all(Val::Px(10.0)),
overflow: Overflow::scroll_y(),
..default() ..default()
}, },
BackgroundColor(PINK.with_alpha(0.9).into()), BackgroundColor(PINK.with_alpha(0.9).into()),
@ -144,6 +148,7 @@ fn init_debug_ui(mut commands: Commands) {
height: Val::Percent(100.0), height: Val::Percent(100.0),
flex_direction: FlexDirection::Column, flex_direction: FlexDirection::Column,
padding: UiRect::all(Val::Px(10.0)), padding: UiRect::all(Val::Px(10.0)),
overflow: Overflow::scroll_y(),
..default() ..default()
}, },
BackgroundColor(ORANGE.with_alpha(0.9).into()), BackgroundColor(ORANGE.with_alpha(0.9).into()),
@ -528,13 +533,11 @@ fn delete_tree(trigger: Trigger<Pointer<Click>>, mut commands: Commands) {
} }
/// Load all monologues so they are in the asset store and trigger on-load events /// Load all monologues so they are in the asset store and trigger on-load events
fn load_monologues( fn load_monologues(server: ResMut<AssetServer>, mut loaded_assets: Local<Vec<Handle<Monologue>>>) {
server: ResMut<AssetServer>, *loaded_assets = include_str!("../../../assets/trees/MONOLOGUES")
mut loaded_assets: Local<Vec<Handle<Monologue>>>, .split("\n")
) { .map(|path| server.load(path))
*loaded_assets = include_str!("../../../assets/trees/MONOLOGUES").split("\n").map(|path| { .collect();
server.load(path)
}).collect();
} }
fn spawn_debug_buttons( fn spawn_debug_buttons(
@ -625,6 +628,32 @@ fn preview_monologue(
} }
} }
fn show_monologue_list(
trigger: Trigger<Pointer<Over>>,
container: Query<Entity, With<MonologuesContainer>>,
children: Query<&Children>,
mut visibility: Query<&mut Visibility>,
) {
if let Ok(root) = container.get(trigger.target()) {
children.iter_descendants(root).for_each(|e| {
*visibility.get_mut(e).unwrap() = Visibility::Inherited;
});
}
}
fn hide_monologue_list(
trigger: Trigger<Pointer<Out>>,
container: Query<Entity, With<MonologuesContainer>>,
children: Query<&Children>,
mut visibility: Query<&mut Visibility>,
) {
if let Ok(root) = container.get(trigger.target()) {
children.iter_descendants(root).for_each(|e| {
*visibility.get_mut(e).unwrap() = Visibility::Hidden;
});
}
}
fn spawn_monologue_tree( fn spawn_monologue_tree(
trigger: Trigger<Pointer<Click>>, trigger: Trigger<Pointer<Click>>,
tree_monologues: Query<&TreeMonologue, With<Button>>, tree_monologues: Query<&TreeMonologue, With<Button>>,
@ -700,6 +729,7 @@ fn hide_monologue_preview(
mut commands: Commands, mut commands: Commands,
) { ) {
if !query.contains(trigger.target()) { if !query.contains(trigger.target()) {
info!("Processing over: {:?}", trigger.target());
commands.entity(*preview).despawn_related::<Children>(); commands.entity(*preview).despawn_related::<Children>();
} }
} }
@ -709,17 +739,18 @@ fn drag_tree(
state: Res<State<DebuggingState>>, state: Res<State<DebuggingState>>,
mut query: Query<&mut Transform, With<Tree>>, mut query: Query<&mut Transform, With<Tree>>,
camera: Single<(&Camera, &GlobalTransform), With<Camera>>, camera: Single<(&Camera, &GlobalTransform), With<Camera>>,
window: Single<&Window> window: Single<&Window>,
) { ) {
if *state.get() == DebuggingState::On { if *state.get() == DebuggingState::On {
if let Ok(mut t) = query.get_mut(trigger.target()) { if let Ok(mut t) = query.get_mut(trigger.target()) {
let world_position = window let world_position = window
.cursor_position() .cursor_position()
.and_then(|cursor| { .and_then(|cursor| camera.0.viewport_to_world(camera.1, cursor).ok())
camera.0.viewport_to_world(camera.1, cursor).ok() .map(|ray| {
}).map(|ray| {
// Compute ray's distance to entity // Compute ray's distance to entity
let distance = ray.intersect_plane(t.translation, InfinitePlane3d::new(t.up())).unwrap(); let distance = ray
.intersect_plane(t.translation, InfinitePlane3d::new(t.up()))
.unwrap();
ray.get_point(distance) ray.get_point(distance)
}); });
t.translation = world_position.unwrap(); t.translation = world_position.unwrap();

@ -42,9 +42,7 @@ impl Display for MonologueLine {
impl From<String> for MonologueLine { impl From<String> for MonologueLine {
fn from(value: String) -> Self { fn from(value: String) -> Self {
MonologueLine { MonologueLine { value }
value
}
} }
} }
@ -57,7 +55,7 @@ impl Into<String> for MonologueLine {
impl From<&str> for MonologueLine { impl From<&str> for MonologueLine {
fn from(value: &str) -> Self { fn from(value: &str) -> Self {
MonologueLine { MonologueLine {
value: value.into() value: value.into(),
} }
} }
} }
@ -102,7 +100,7 @@ impl AssetLoader for MonologueLoader {
// Skip any empty lines or comments // Skip any empty lines or comments
} else if line.starts_with("#") || line.is_empty() { } else if line.starts_with("#") || line.is_empty() {
// Skip comments and blank lines // Skip comments and blank lines
// everything else we read as a monologue line // everything else we read as a monologue line
} else { } else {
monologue.batches.last_mut().unwrap().add_line(line.into()); monologue.batches.last_mut().unwrap().add_line(line.into());
} }

@ -1,3 +1,5 @@
use std::collections::VecDeque;
use super::*; use super::*;
/// Debugging systems, resources, events, etc. /// Debugging systems, resources, events, etc.
@ -7,6 +9,8 @@ impl Plugin for DebuggingPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.init_state::<DebuggingState>() app.init_state::<DebuggingState>()
.init_resource::<ToolTip>() .init_resource::<ToolTip>()
.init_resource::<Fps>()
.init_resource::<EntityCount>()
.add_plugins(RapierDebugRenderPlugin::default().disabled()) .add_plugins(RapierDebugRenderPlugin::default().disabled())
// Added by Rapier // Added by Rapier
// .add_plugins(AabbGizmoPlugin) // .add_plugins(AabbGizmoPlugin)
@ -24,6 +28,10 @@ impl Plugin for DebuggingPlugin {
.run_if(on_event::<Pointer<Over>>.or(on_event::<Pointer<Out>>)), .run_if(on_event::<Pointer<Over>>.or(on_event::<Pointer<Out>>)),
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>),
track_fps,
sync_resource_to_ui::<Fps>.run_if(resource_changed::<Fps>),
track_entity_count,
sync_resource_to_ui::<EntityCount>.run_if(resource_changed::<EntityCount>),
) )
.run_if(in_state(DebuggingState::On)), .run_if(in_state(DebuggingState::On)),
), ),
@ -89,6 +97,37 @@ fn init_debug_ui(mut commands: Commands) {
}, },
)); ));
// Version string for troubleshooting
commands.spawn((
DebuggingState::On,
Name::new("FPS"),
Text::new("FPS: ##.#"),
TextColor(WHITE.into()),
BackgroundColor(BLACK.into()),
SyncResource::<Fps>::default(),
Node {
width: Val::Auto,
align_self: AlignSelf::Start,
justify_self: JustifySelf::End,
..default()
},
));
commands.spawn((
DebuggingState::On,
Name::new("Entity Count"),
Text::new("Entities: ###"),
TextColor(WHITE.into()),
BackgroundColor(BLACK.into()),
SyncResource::<EntityCount>::default(),
Node {
width: Val::Auto,
align_self: AlignSelf::Start,
justify_self: JustifySelf::Center,
..default()
},
));
// Tooltip // Tooltip
commands.spawn(( commands.spawn((
DebuggingState::On, DebuggingState::On,
@ -260,3 +299,48 @@ fn toggle_aabb_gizmo(
let (_, aabb_group) = config_store.config_mut::<AabbGizmoConfigGroup>(); let (_, aabb_group) = config_store.config_mut::<AabbGizmoConfigGroup>();
aabb_group.draw_all = *state.get() == DebuggingState::On; aabb_group.draw_all = *state.get() == DebuggingState::On;
} }
#[derive(Resource, Default, Debug)]
struct Fps(f32);
impl Display for Fps {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
writeln!(f, "FPS: {:0.1}", self.0)
}
}
fn track_fps(
time: Res<Time>,
mut fps: ResMut<Fps>,
mut history: Local<VecDeque<f32>>,
) {
// Get the time to render the last frame
let d = time.delta_secs();
// Add the latest delta to the list
history.push_back(d);
// Ensure the vecdeque doesn't get too long
if history.len() > 64 {
history.pop_front();
}
// Set FPS to 1/averageDeltaTime
fps.0 = 1.0 / (history.iter().fold(0.0, |acc, e| acc + e) / history.len() as f32);
}
#[derive(Resource, Default, Debug)]
struct EntityCount(usize);
impl Display for EntityCount {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
writeln!(f, "Entities: {}", self.0)
}
}
fn track_entity_count(
query: Query<Entity>,
mut count: ResMut<EntityCount>,
) {
count.0 = query.iter().len();
}

@ -1,5 +1,13 @@
use super::*; use super::*;
pub(crate) struct BaseUiPlugin;
impl Plugin for BaseUiPlugin {
fn build(&self, app: &mut App) {
// TODO
}
}
/// Marker component for handling Resource -> Ui Sync /// Marker component for handling Resource -> Ui Sync
#[derive(Component, Default, Debug)] #[derive(Component, Default, Debug)]
pub struct SyncResource<R: Resource + Default + Display>(R); pub struct SyncResource<R: Resource + Default + Display>(R);
@ -15,3 +23,30 @@ pub fn sync_resource_to_ui<R: Resource + Default + Display>(
t.0 = format!("{}", *r); t.0 = format!("{}", *r);
}); });
} }
/// Updates the scroll position of scrollable nodes in response to mouse input
pub fn scroll(trigger: Trigger<Pointer<Scroll>>, mut scrollers: Query<&mut ScrollPosition>) {
let Pointer {
event: Scroll { unit, x, y, .. },
..
} = trigger.event();
let (dx, dy) = match unit {
MouseScrollUnit::Line => (x * 16.0, y * 16.0),
MouseScrollUnit::Pixel => (x * 1., y * 1.),
};
if let Ok(mut pos) = scrollers.get_mut(trigger.target()) {
pos.offset_x -= dx;
pos.offset_y -= dy;
}
}
#[derive(Component, Debug)]
#[relationship(relationship_target = NavParent)]
pub(crate) struct NavChild(Entity);
#[derive(Component, Debug)]
#[relationship_target(relationship = NavChild)]
pub(crate) struct NavParent(Vec<Entity>);

Loading…
Cancel
Save