Replies: 67 comments 180 replies
-
Quite exciting! Overall, I like the ideas that are present here, but would like to highlight some I think are important. First, I want to emphasize the question around whether we want data bindings: The existing examples show promise as a way of deriving an inactive scene composition from existing game state, and keeping it up to date with said game state, but there's nothing about UI feeding back into the game state - not even a button. I'd like to see an example of how a button works under this framework, where it seems scenes are our "reusable widget abstraction", and ideally some more complex examples featuring where two-way data binding shines, such as a textbox or a slider, if anything so we know what tradeoffs we're making. Second, I have some concerns about scene files sometimes being too transparent an abstraction - e.g. consider someone making a "Fancy Div widget" composed of a bunch of divs. I feel it should be possible to expose certain values as "These are here to be overriden" without requiring upstream users know the exact location of these values in the underlying widget. So, perhaps some form of function-ness to widgets. (Though worth noting, this is the point I'm least confident in, due to a lack of experience) Third, another concern is how the current examples lack anything which could be considered "UI state". It seems the entire UI is derived directly from game state. For various things like animated widgets, we will most likely require a way for UI to have and manage some state of its own. This state could (and in my opinion should) be stored in the ECS at the end of the day, but its still a conversation that needs to be had, and I'm hoping i can start it. To wrap things up, most of the points I raised here are stuff I ran into back when I was implementing my last UI prototype https://github.com/TheRawMeatball/ui4, after multiple failed prototypes, and which took a strongly data-binding based approach to things. It is old code, based on an ancient version of bevy, and ugly as sin, but it was feature-complete enough to implement some simple animations and a TodoMVC example, so perhaps it's still worth a look. |
Beta Was this translation helpful? Give feedback.
-
In the scene format, I think we should consider giving props to scenes themselves as top-level values that can be referenced at arbitrary places within the scene. This makes scenes more "function-like" giving them inputs, and potentially simplifies the data flow (you could avoid implicit cascading. Cascading seems tricky because for instance we want to support selectively cascading a pair of different Scenes with top-level pluggable props also start to look like something that can be modelled and composed using a visual node-graph editor, which may be a usecase we want to support in the future. Additionally, consider syntax for doing arithmetic in scene definitions, similar to css |
Beta Was this translation helpful? Give feedback.
-
One way to add interactivity could be to use a functional hypermedia approach. Having systems/functions that return scenes, and then having components that connect to them and describe some sort of swapping strategy. This is inspired by the HTMX library. I imagine this working something like this: // Could query to get the element to be swapped, the element that triggered the swap, etc.
fn example(trigger: Query<(&SwapTrigger, &Example)>, score: Res<Score>) -> Bsn {
let score = score.0 * trigger.single().1.score_multiplier.get();
bsn! {
Div [
Label { val: {format!("Score: {score}")} }
]
}
}
fn time(t: Res<Time>) -> Bsn {
let time = t.elapsed_seconds;
bsn! { Label { val: {format!("Time: {time}")} }
}
fn setup() -> Bsn {
bsn! {
(Div Example { score_multiplier: 2. } SwapOnClick { func: "example" target: SwapTarget::Children }) // Swaps children when clicked
(Label SwapOnUpdate { func: "time" target: SwapTarget::Self }) // Swap every frame
(Div { width: 20 } SwapOnHover { func: "foo" target: SwapTarget::Name("/A/B") }) // Swaps neighboring element when hovered
#A(Div) [ #B(Div) ]
}
} This example would require the more advanced entity paths described to be able to reference neighboring/non-children entities dynamically. |
Beta Was this translation helpful? Give feedback.
-
Just a few quick comments (I'll likely look into things a bit more later, but wanted to jump in with initial thoughts now):
|
Beta Was this translation helpful? Give feedback.
-
Prior art for an ECS based scene format/DSL that supports hierarchies/reactivity/property overriding/variables: |
Beta Was this translation helpful? Give feedback.
-
I feel like it would be useful to have a list of high-level goals for Bevy UI, as it can help frame many of these discussions. An example of a goal I'm unsure of is how general purpose we want Bevy UI to be - should it be focused on video game UIs, or should it be a contender for the de facto Rust GUI framework? I'd also love to know the relative importance of these various goals. For example, to what degree do we prioritize ergonomics over performance, or vice versa? To help facilitate this, I've gathered a list of goals and non-goals from various other UI frameworks. These aren't in any particular order, and I'm sure I'm missing many, but I feel it's a good start.
It may also be worthwhile doing a survey of which frameworks make which tradeoffs. egui nails the simple and concise goals, at the tradeoff of efficiency and flexibility (due to using immediate mode). Is there any possible way to retain that simplicity? I've personally found it incredible how much progress I can make using egui. |
Beta Was this translation helpful? Give feedback.
-
You can solve this by putting the parser in a separate crate (e.g. bevy_bsn_parser). Then bevy_bsn_macro and bevy_bsn can both depend on it. If you avoid circular dependencies you could put the parser in bevy_bsn and import that, but it might include other stuff and increase your proc macro compile time ;) |
Beta Was this translation helpful? Give feedback.
-
Syntax
I appreciate the terse expressiveness of these examples. It might be a little too terse; for example, I'd like a separate syntax for 'override' and 'child'. Maybe something like the familiar 'spread' operator: fn setup() -> Bsn {
// easier to spot 'override' at a glance
bsn! {
Div [
Div {
width: 100
..@("player.bsn")
}
@("player.bsn")
]
}
} In the "Nested Entity Overrides" example, this code looks like it spawns children under foo.bsn. // bar.bsn
Div [
// What is this?
// A div, whose children I am overriding?
// Or another node, which I am providing children to?
@"foo.bsn" [
@A(Div { width: 20 } )
]
] In React, some 'override-like' configuration is often achieved by using props which accept React elements, just like // `title` and `content` are props which take `Bsn`, the same type as `children`
bsn! {
Header {
title: Span { width: 100 } [ "My Title" ]
content: Div { width: 200 }
}
} This also gives a natural way of 'referring' to other entities/elements, by just exposing them as part of the API. Generally I think what is or is not "overrideable" should be a first-class part of how our data is defined, and the cleanest way to do this would be through the type system. TerminologyAt risk of premature bikeshedding, I think some early terminology might make this stuff easier to talk about. Specifically I think we should have terms for:
PhilosophyTo me, what makes React successful is that it basically models UI as a pure-ish function of app state : UI. If our approach is basically reactive around a return type, that should keep us on the right track. Ideally the 'scene functions' which return Bsn would be akin to This also means we should have a robust model around side effects and impure operations. Something like React hooks would be worth investigating if we're committed to a 'pure-ish' approach. |
Beta Was this translation helpful? Give feedback.
-
Something that worries me that I don't see addressed here is that this looks like you're going to have to pass some context around like If I have multiple separate panels that deal with completely independent things (like for instance in an editor) shoving everything through the same context gets difficult to work with quickly. |
Beta Was this translation helpful? Give feedback.
-
I wonder if all I recognize that in the current concept, If the amount of codegen is really a concern, I thought about two things that could be done:
|
Beta Was this translation helpful? Give feedback.
-
This is interesting! I'd like to share some of my own experience with these subjects, since there's a lot of overlap. At work, I've co-created an asset format with similarities to BSN. It's called bauble, and it's open source, but it's mostly undocumented. I've also co-created a Rust frontend for Unreal's Slate UI, called // Bauble files are checked for correctness, parsed, and serialized to binary by our asset system at
// build time. These are type imports from `mgui`. You can import any type that implements
// `FromBauble`, or any asset.
use mgui_backend::{
script::ExplicitScript,
ui::Ui,
widget::{Border, Button, Column, Row, Text}
};
ui = Ui {
// The $ references an asset from the scope. In this case, it's a Rust script (which may instead
// be written inline).
init: $init,
// Attributes are usually used for optional parameters
content: #[padding = 5, width = 2] Border(Row([
// In bauble specifically, you can replace certain attributes and field with scripts. The
// `ExplicitScript($color)` is a workaround. Normally, it'd just be `$color`.
#[color = ExplicitScript($color)] Button {
content: #[color = #FFFFFF] Text("Toggle Me"),
press: $select,
},
])),
}
// `copy` means the value is copied wherever referenced. Without `copy`, the full path of the
// referenced asset is given to `FromBauble` instead.
//
// The `#{}` syntax is for raw values, which are basically strings. The multiple `{}`s are to escape
// the inner `{}`s, similar to Rust's `r##""##` syntax.
//
// These Rust scripts are compiled to wasm at build time
copy init = #{{
struct State {
selected: u8,
}
ctx.insert(State {
selected: 0,
});
}}
// This is a generator, so it returns a value. Generators may generate bauble markup for UI. We use
// a `ui!` macro that's basically the `format!` macro, but it may have inline expressions.
copy color = #{{
match ctx.local::<State>().selected {
true => "888888",
false => "CCCCCC",
}
.to_string()
}}
// This is an event, so it accepts a value, which is `()` in this case. It knows this because it's
// used in a button.
copy toggle = #{
let selected = &mut ctx.local_mut::<State>().selected;
*selected = !*selected;
ctx.temp_hardcoded_function_to_react_to_the_selection_reactivity_is_hard_lol(*selected);
} Here's #[derive(Clone, Debug, Deserialize, FromBauble, Serialize)]
#[bauble(
bounds = (
C: for<'a> FromBauble<'a>,
Co: for<'a> FromBauble<'a> + ColorTrait,
P: for<'a> FromBauble<'a>
),
path = mgui_backend::widget,
)]
pub struct Button<C, Co, P> {
/// Child widget
pub content: C,
// Alignment of the child widget
#[bauble(attribute, default)]
pub h_align: Alignment,
#[bauble(attribute, default)]
pub v_align: Alignment,
#[bauble(attribute, default = Co::WHITE)]
pub color: Co,
/// Event to fire when the button is pressed
pub press: P,
}
// And in some other module somewhere
// `Generator`'s first generic is the type specified in bauble. The second is the type that a script
// should return.
pub type Button =
mgui::widget::generic::Button<Box<MarkupWidget>, Generator<Color, String>, Event<()>>; Our requirements are very different, but I hope it's useful to see another similar sort of solution. Anyway, about BSN and BSNI like that you avoided XML syntax. We used attributes in bauble to avoid having to mix properties with child widgets, and it looks like you're going with something similar. I think I like that you have them appended to the name instead of prepended. It's great to use the same syntax in the asset files as in code. Unfortunately, though, asset files tend to be more limited. In BSN's current design, it doesn't seem like you could, for example, make buttons with Rust closures, so I think people will prefer to use code when possible. We have the opposite problem, though, where buttons must use a wasm script defined in the asset system. If you could use wasm for the asset system and ECS stuff for runtime, you might be able to get best of both worlds. But wasi (wasm but not on the web) is such a mess to work with, so it might not work for you. UI without some sort of scripting tends to be very constraining, so I hope you find a good solution. We have type checking on our bauble assets, and it's very nice. I'm not sure how you could do that in Bevy/BSN, though, since assets are loaded at runtime after startup. I enjoy #[derive(Clone, Debug, FromBauble)]
#[bauble(flatten)]
pub enum Generator<L, S>
where
L: Debug + for<'a> FromBauble<'a>,
{
Literal(L),
Script {
src: ScriptSrc,
// This is like `default`, but you may not specify it in bauble
#[bauble(as_default)]
_s: PhantomData<S>,
// We don't actually have this field on this type, but I've added it to show how attributes
// work with flatten.
#[bauble(attribute)]
my_attribute: String,
},
/// If `L` is a `Raw`, it will take precedence over `Script`, so use this if you need a script
ExplicitScript(ExplicitScript),
} Finally, I like the name "BSN", because I'm reading it as "bosun" in my head, which is kind of cute.
|
Beta Was this translation helpful? Give feedback.
-
StatementThe reason why I created Bevy-Lunex is because I was overwhelmed by all the web-tech-inspired frameworks everywhere. Beginner friendlyOne of my main concerns is that Bevy UI should not become overwhelming to the beginner user. Let's paint a scenario in which you are a beginner indie dev who picked up Bevy recently and just finished his game jam prototype. He wants to quickly bash together a main menu and call it a day. The current proposal uses semi-advanced concepts that are not immediately understandable to a beginner. Just by looking at the code, I don't understand how data/logic relation is modelled. I will have to read a lot about it, which is a little discouraging (as the person). Personally, I think that if Bevy wants to become mainstream, it should be attractive to game jammers. Office suiteI also like the idea that we are preparing the UI to be used for more classic stuff than directly tailored for games. Bevy already has a low power consumption mode. That can make it a very good choice for regular desktop applications, not just games. I am very excited about this. Data relationI am also curious about how we can manage data across the divs. Rusts borrow checker is a little stubborn when it comes to UI and we need to dance around it. It doesn't let us store pointers to other data types (obviously), so I wonder what other pattern will we use access data A in div Foo from div Bar. Like buttons for example. When we click on a div button we expect it to trigger something that will change other divs. How do we define which div precisely should change? I solved this by using a tree-like structure hierarchy and by using paths. I'm curious about other takes on this problem. LogicAlso, just to voice a concern, we have an amazing ECS ecosystem, but web frameworks are not ECS based, so I hope we won't gravitate too much to "non-ECS" concepts. I think I stroke a nice balance with Bevy-Lunex in this regard, where you can use Bevy Systems to query for button entities and add any logic desired. An example can be to enlarge the button image by 1.2 if the cursor hovers over it. For any button that I want to do, I just add the component to it with no changes required. I would like to see this get adopted here in some form. ClosingI will leave the more technical stuff to be debated by more qualified people than me. I don't have that much experience in that regard. I just rode with everything that seemed to work :D |
Beta Was this translation helpful? Give feedback.
-
Something I want to be a part of this discussion is relation based UI. I'm still working on some changes for the next release of aery that enables this but this is just some dogfood code. The main thing to notice here is that it's an extension of how current // This would be part of a UI lib.
// Can be made completely generic and a write once use everywhere thing with individual widget logic delegated to systems callbacks with one shot systems.
// Otherwise currently requires a marker component + system pairing.
#[derive(Component)]
pub struct ScrollSpan {
pub lower_bound: f32,
pub upper_bound: f32,
pub view: f32,
pub pos: f32,
}
#[derive(Component)]
pub struct VerticalScroll;
#[rustfmt::skip]
impl VerticalScroll {
pub fn update(
focus: Res<Focus>,
mut wheel_evr: EventReader<MouseWheel>,
mut spans: Query<&mut ScrollSpan, With<Self>>,
widgets: Query<((), Relations<Widget>)>,
scroll_areas: Query<(), With<ScrollArea>>,
) {
let Some(scroll) = wheel_evr.iter().last() else { return };
widgets
.ops()
.join::<Widget>(&mut spans)
.join::<Widget>(&scroll_areas)
.traverse_targets::<Widget>([focus.entity])
.for_each(|_, _, (mut span, _)| {
span.pos = (-scroll.y * (span.view / 4.).max(1.))
.pipe(|shift| shift + span.pos)
.clamp(span.lower_bound, span.upper_bound);
ControlFlow::Exit
});
}
pub fn draw(
spans: Query<(&Rect, &ScrollSpan, &Paint, &MainColor, Option<&AltColor>), With<Self>>,
mut painter: ShapePainter,
) {
for (rect, span, paint, main, alt) in spans.iter() {
let pill = (span.upper_bound - span.lower_bound)
.pipe(|denom| (span.view / denom, span.pos / denom, rect.sizes()))
.pipe(|(t_view, t_pos, sizes)| ((sizes.y - sizes.y * t_view), t_pos))
.pipe(|(view, t_pos)| (*rect).tap_mut(|rect| {
rect.north_east.y -= view * t_pos;
rect.south_west.y += view * (1. - t_pos);
rect.z += 0.5;
}));
for (rect, color) in alt
.map(|color| (pill, **color))
.into_iter()
.chain(std::iter::once((*rect, **main)))
{
painter.color = color;
painter.corner_radii = Vec4::splat(paint.roundness);
painter.set_translation(rect.center().extend(rect.z));
painter.rect(rect.sizes());
painter.reset();
}
}
}
} // The spawn API can be turned into a UI DSL with some cleanup work
// This is tile based so rect layout is handled by a tiling algorithm but other styling is done via regular components.
commands
.spawn((Tiling::H, Rect::default(), MainColor(theme.mantle)))
.scope::<Widget>(|mut rect| rect // Add children of type `Widget` in this scope
.add(ScrollArea)
.add((
VerticalScroll,
ScrollSpan { upper_bound: 256., view: 16., ..default() },
MainColor(theme.surface0),
AltColor(theme.overlay0),
Paint::default(),
Rect::default(),
))
.add((
HorizontalSCroll,
ScrollSpan { upper_bound: 512., view: 32., ..default() }
))
.add(ScrollSpan { upper_bound: 512., view: 32., ..default() })
}); I've modeled a scroll area with this that can represent arbitrary tensor data. You can scroll in as many dimensions as you like by adding more and more children. For my use case I currently only need input for vertical and horizontal scrolling, scroll bar drawing for vertical scrolling & 1-2 more dimensions for some complex widgets. This becomes even more powerful with fragmenting relations cause they're can be more efficient for queries involving entities that are not immediately local. For UI specifically there's also many benefits to using relations like:
|
Beta Was this translation helpful? Give feedback.
-
One thing missing from the discussion so far is a mention of the new-ish declarative UI frameworks for mobile, Kotlin Compose and SwiftUI. I'm not sure they match the goals set out above as they are for a different use case, but a lot of thought has gone into their design and implementation. Both use UI code written in the native language, come with data binding primitives, have IDE support with previews, and have inline styling. Sadly I've also not used either much, so I can't go into detail of what we could learn from them. I'd appreciate it if someone could jump in here and fill in the gaps :) |
Beta Was this translation helpful? Give feedback.
-
Some additional thoughts:
|
Beta Was this translation helpful? Give feedback.
-
I find one of the hardest things to get right in any UI framework is reactivity: data binding, event handling, composability, and managing both local and global state. Additionally, if Bevy is going to offer a first-class UI solution, prioritizing developer ergonomics and code readability is a must. This comment is an exploration of the design space for incorporating reactivity into @cart's initial scene proposal within the Bevy game engine, focusing on readability and ergonomics. I won't be implementing anything—just exploring the design space. Important The intention here is to explore the design space, not to rigorously specify technical details. This means:
Note I chose to use the word Scenario 1: A simple counter widgetProject development time and bug count grow quadratically with the number of lines of code in a project. If bevy UI is going to be competitive with other UI frameworks, we are going to need to be similar in code size, readability, and ergonomics to other frameworks. With this in mind, I'm going to be using Svelte as both inspiration and a baseline to evaluate our designs, as I find it tends to set the bar in terms of developer experience. The svelte counter widget is just 14 lines of simple, self explanatory code. I'm hoping we can get the bevy counter widget to share these properties Svelte Counter ExampleTry it on the Svelte REPL <script>
export let count = 0;
function increment() {
count += 1;
}
function decrement() {
count -= 1;
}
</script>
<button on:click={decrement}>-</button>
{count}
<button on:click={increment}>+</button> Bringing Data Binding to the Bevy Scene Format// counter.bsn
Div [
Button {text="-" on:click={decrement}}
Label {bind:text={count}}
Button {text="+" on:click={increment}}
] I've made a few changes to @cart's proposed Bevy scene format here. Notably, I'm employing The scene file is responsible for describing the node layout of the scene, data bindings, and event bindings. In the future we'll likely want to introduce simple logic in the form of loops, if, else if, and else blocks. Thoughts / future improvements:
Binding Rust Logic to the Counter Widget#[derive(Default, Component, Scene, Reflect)]
#[scene(path="counter.bsn")]
struct CounterWidget {
pub count: isize,
}
#[reflect]
impl CounterWidget {
fn increment(&mut self, time: Res<Time<Real>>) {
self.count += 1;
// No real reason for this, other than to demonstrate that event handlers may wish to use system parameters
println!("Increment clicked at time: {}", time.elapsed())
}
fn decrement(&mut self) {
self.count -= 1;
}
}
// Setting up the app
app.register_scene::<CounterWidget>();
// Creating an instance of the widget is as simple as spawning an entity with a NodeBundle and a CounterWidget.
app.spawn((
NodeBundle {...},
CounterWidget::default()
)) I'm hoping that we can create an API as ergonomic as this, with If reflection doesn't work, I'm fairly confident we can use a dedicated set of attribute macros to generate the boilerplate. I do worry that this is a bit too magical. It's not immediately apparent that That said, I love how readable and maintainable this is. It's every bit as short as the svelte implementation, and it's understandable at a glance. I've tried finding less magical equivalents to this using normal systems, and it typically requires much more code to wrap your head around and feels error prone. Under the CoversThe above example is cool and all, but what exactly is it doing? This is a lower level implementation that doesn't use reflect or macros. I've tried to keep the API as ergonomic as possible, but still feel it's less than ideal. Our "magical" reflect-based or macro-based wrapper will be built on top of this. First, we know that we'll want a component to represent our widget's local state. #[derive(Default, Component)]
struct CounterWidget {
count: isize,
}
impl CounterWidget {
fn increment(&mut self, time: Res<Time<Real>>) {
self.count += 1;
// No real reason for this, other than to demonstrate that event handlers may wish to use system parameters
println!("Increment clicked at time: {}", time.elapsed())
}
fn decrement(&mut self) {
self.count -= 1;
}
} The widget is backed by a scene, so we'll implement a Scene trait on it. impl Scene for CounterWidget {
fn path() -> &'static str {
"counter.bsn"
}
fn initialize(scene: &mut SceneInit) {
scene.add_listener("increment", counter_increment);
scene.add_listener("decrement", counter_decrement);
scene.add_system(update_counter_on_count_change);
}
}
fn counter_increment(
mut event: ListenerMut<ClickEvent>,
counters: Query<&mut Counter>,
time: Res<Time<Real>>,
) {
counters.get_mut(event.entity).increment(time);
}
fn counter_decrement(mut event: ListenerMut<ClickEvent>, counters: Query<&mut Counter>) {
counters.get_mut(event.entity).decrement();
}
fn update_counter_on_count_change(
mut counters: Query<(&CounterWidget, &mut SceneRef), Changed<CounterWidget>>,
) {
for (counter, mut scene_ref) in &mut counters {
// TODO: This will update the binding if any field on CounterWidget changed. Look into memoization
scene_ref.update_binding("count", counter.count);
}
} Warning This Scene API is very much a work in progress, and I'd love suggestions for how to improve things. I'm generally a fan of the event listeners, but don't like how the data binding is being done right now. Implementing
All that's left now is to register the // Setting up the app
app.register_scene::<CounterWidget>();
// Creating an instance of the widget is as simple as spawning an entity with a NodeBundle and a CounterWidget.
app.spawn((
NodeBundle {...},
CounterWidget::default()
)) Scenario 2: Coming SoonI'm going to wait for feedback before diving into another scenario, but I want to continue down this road if everyone likes where it's going. I have plans for scene event dispatchers, nested widgets, two-way data binding, slotted content, and bsn logic (loops, match, and if/else). Please let me know if you have suggestions or if there's anything else I should look into. Thank you! |
Beta Was this translation helpful? Give feedback.
-
Over the last month I've been doing a lot of prototyping on UI solutions. One thing that has stymied me again and again is that there's a fundamental conflict between React-style reactivity and ECS. For example, suppose you want to have a React-style "hook" that returns a reference to a resource. Let's use Dioxus-style syntax for this example:
This seems simple, but it won't work in Bevy. The reason is because in order for "Cx" to be able to return a mutable reference to a resource, it has to have a mutable borrow on the ECS world. Which in this case produces a borrow conflict. Thus, your UI component can access at most a single mutable resource - which is fine for simple components, but obviously runs in to scaling problems for anything complex. This would be trivial to solve if, for example, you could pass around a reference to a resource in an Note that interior mutability of You can also try to do something like dependency injection, like one-shot systems:
Here I'm assuming that
However, even if you manage to solve these problems, there's another problem: event handlers:
The problem here is that the event handler (and I mean this generically, whether you are using bevy_mod_picking or some other system) lives longer than the function that creates the UI. (Unless we're talking pure immediate mode, which has other problems). Thus, you can't pass around You can of course design an event handler system in which the handler accesses the resource independently (not using a closure), but now you lose the reactive tracking relationship between the render function and the handler. Why do I say that there's a "fundamental conflict"? You have to think about what life was like before React. One of the key advancements of React and others like is is the idea that UI components are self-contained and modular. A UI component isn't just about rendering, it's also about state and interaction, and all those things are contained within the component and can be instantiated many times. Unlike, say, Qt or Java Swing, a React component doesn't need to define a bunch of data types and functions at the global level. Any state that it needs can be shared via closures with its event handlers, it all comes together as a package, you don't have to create/register a separate state object every time you want to re-use a widget. ECS is just the opposite: the ECS philosophy is that everything gets shattered into individual fragments (systems, components, resources) all of which exist at a global level. Complex widgets are no longer self-contained packages. Everything needs to be visible to the scheduler so that it can work its parallelism magic. Frameworks like Dioxus or Iced don't have these problems because their states are portable: you can pass around state from one widget to another, up and down the hierarchy, without having to pass some mutable borrow of a World. |
Beta Was this translation helpful? Give feedback.
-
For a while now I've been considering if adding for-each systems may be a big ergonomics boost, specifically for event handling and What are for-each systems?Today, a typical bevy system may look like this: fn apply_velocity(
mut query: Query<(&mut Transform, &Velocity), Without<Disabled>>,
time: Res<Time>,
) {
for (mut transform, velocity) in &mut query {
transform.translation.x += velocity.x * time.delta_seconds();
transform.translation.y += velocity.y * time.delta_seconds();
}
}
// Register the system
app.add_systems(FixedUpdate, apply_velocity) A for-each system automatically applies the for loop for you: fn apply_velocity(transform: &mut Transform, velocity: &Velocity, time: Res<Time>) {
transform.translation.x += velocity.x * time.delta_seconds();
transform.translation.y += velocity.y * time.delta_seconds();
}
// Register the system
app.add_systems(FixedUpdate, apply_velocity.for_each::<Without<Disabled>()) Additionally, there should be some way to run a for-each system on a single entity: world.run_system(apply_velocity.on_entity(my_entity)) How does this improve event handling ergonomics?Currently, in event handlers, we have to write a query to modify or read components of the entity handling the event. This feels unintuitive and unergonomic, as we already know there is exactly one entity handling the event, meaning we need to do Example: bevy_eventlistener goblin exampleConsider the minimal example of the excellent bevy_eventlistener library. It spawns a "Goblin" entity with 3 pieces of armor. commands
.spawn((
Name::new("Goblin"),
HitPoints(50),
On::<Attack>::run(take_damage),
))
.with_children(|parent| {
parent.spawn((
Name::new("Helmet"),
Armor(5),
On::<Attack>::run(block_attack),
));
parent.spawn((
Name::new("Socks"),
Armor(10),
On::<Attack>::run(block_attack),
));
parent.spawn((
Name::new("Shirt"),
Armor(15),
On::<Attack>::run(block_attack),
));
}); Each piece of armor can receive an "Attack" event. The armor reduces the damage of the attack, potentially preventing the event from propagating up to the Goblin. /// A callback placed on [`Armor`], checking if it absorbed all the [`Attack`] damage.
fn block_attack(mut attack: ListenerMut<Attack>, armor: Query<(&Armor, &Name)>) {
let (armor, armor_name) = armor.get(attack.target).unwrap();
// Handle the attack
}
/// A callback on the armor wearer, triggered when a piece of armor is not able to block an attack,
/// or the wearer is attacked directly.
fn take_damage(
attack: Listener<Attack>,
mut hp: Query<(&mut HitPoints, &Name)>,
mut commands: Commands,
mut app_exit: EventWriter<bevy::app::AppExit>,
) {
let (mut hp, name) = hp.get_mut(attack.listener()).unwrap();
// Take damage, subtracting health from hp
} Notice that in both event handlers, we need to do fn block_attack(armor: &Armor, name: &Name, mut attack: ListenerMut<Attack>) {
// Handle the attack
}
fn take_damage(
hp: &mut HitPoints,
name: &Name,
attack: Listener<Attack>,
mut commands: Commands,
mut app_exit: EventWriter<bevy::app::AppExit>,
) {
// Take damage, subtracting health from hp
} Example 2: A
|
Beta Was this translation helpful? Give feedback.
-
Hi! Interesting discussion, but I'm confused by the idea of multiple inheritance of scenes. Wouldn't this complicate the scene analysis? In this approach: object's property can be A) cascaded from one of the upper objects in the scene tree or B) inherited from one of the parent scenes, and all those sources can also inherit recursively. It's important to minimize possible count of sources, so initial value assignment to the property could be easily found (instead of overriding it in the child object - it often becomes a mess on practice). There are reasons why modern programming languages refuse multiple inheritance. Worth mentioning that style and structure separation (i.e., CSS vs. clean HTML) is very good practice. |
Beta Was this translation helpful? Give feedback.
-
Does anyone interested in implementing Ribir to Bevy? They have their own WGPU backend and there are ready-made widgets. I think it might fit Bevy well. |
Beta Was this translation helpful? Give feedback.
-
I'm making a multiplayer game that has both a dedicated server and client application. The client has the full Bevy rendering pipeline and such, but the dedicated client doesn't - the feature is disabled. I want to be able to use the same scene file for both applications. If I had to maintain two, it'd be a massive pain, since it'd be a larger workload and introduces potential issues where each app has different components in the same scene. I'll give this example in some pseudo-YAML definition. components:
- type: Handle<Mesh>
path: "../mdls/hello.obj"
- type: Transform On the client, both the mesh handle and transform components would be loaded. But on the server, since it doesn't even know what a mesh is, I want it to simply ignore the |
Beta Was this translation helpful? Give feedback.
-
Hey everyone 👋 I have proposal in my crate bevy-compose for reactivity using the react/Xilem pattern. This uses a "medium-grain" reactive approach that has a coarse reactive system like react, but finer control with Counter app example: fn ui(count: Res<Count>) -> impl Compose {
flex((
format!("High five count: {}", count.0),
flex("Up high!").on_click(|mut count: ResMut<Count>| count.0 += 1),
flex("Down low!").on_click(|mut count: ResMut<Count>| count.0 -= 1),
))
} More advanced counter example, using fn ui() -> impl Compose {
lazy(|count: Res<Count>| {
memo(
count.0,
flex((
format!("High five count: {}", count.0),
flex("Up high!")
.on_click(|mut count: ResMut<Count>| count.0 += 1)
.on_hover(|| {
dbg!("Hover!");
}),
flex("Down low!").on_click(|mut count: ResMut<Count>| count.0 -= 1),
if count.0 == 2 {
Some("The number 2!")
} else {
None
},
)),
)
})
} |
Beta Was this translation helpful? Give feedback.
-
I am not in the loop with Bevy project state and see a lotta great evolution going here. I just wanted to mention recently watching a jaw-dropping presentation by Rik Arends of Makepad who create this whole UI framework in Rust where UI is in a Figma-like format you can adapt at runtime. And they also built this great editor for doing so.. using Makepad: https://makepad.dev |
Beta Was this translation helpful? Give feedback.
-
Since I last posted here, I've been doing a ton of Bevy UI-related work (R&D mostly), written a lot of code, and learned many things. My earlier comments in this thread seem naive to me now :) If could get readers in this thread to take away just one idea, it would be the need to solve problems in the right order: don't build the penthouse before you've laid the foundation. More specifically, I would want to move the focus away from both widgets and template syntax:
The really hard problem to solve is "declarative incremental updating", a term which I will need to unpack:
The key reason why Unfortunately, as I have found, an incremental updating framework can't be just bolted on to ECS, at least not in a straightforward way. "Widgets" can't be "systems" because the runtime scheduling is completely different. Widgets also can't be systems because the same widget appears in many places, whereas re-registering a system doesn't cause it to get run multiple times. An updating solution may or may not be "reactive": immediate-mode systems like egui are both declarative and unify construction and updating, but they are not reactive in the sense of React, Solid or MobX. In the last six months I've built two different reactive UI frameworks for bevy: quill and bevy_reactor. The former was a coarse-grained approach modeled after React and Dioxus, and latter a fine-grained approach modeled after Solid. I've convinced myself that I can build a UI framework that's good enough to build an editor; however, I also recognize that my approach has a number of weaknesses that I can't solve by myself. One weakness is that these frameworks operate in exclusive mode. I know we've discussed this before, and while the general feedback I've gotten is that it's OK for a UI widget to require world access, I'm not sold on this. My reason has nothing to do with performance, but simply because coding anything complicated with exclusive world access means you have a lot more problems with the Rust borrow checker. It means that relatively simple algorithms turn into puzzles. The other problem is that even though my framework is built on top of ECS, the style of coding required to use it is not very ECS-like. That is, after all the effort we've spent educating people how to rethink their game architecture in terms of entities and components, then we get to UI and say "Oh, and you'll have to learn an entirely new approach when it comes to doing UI." Part of the tension here is due to a mismatch between what UI needs, and what ECS provides. For example:
The question I keep asking myself is, is it possible to build a widget that looks more like a system? One which supports dependency injection, non-exclusive access, and so on? And every time I start to think about this I run into a wall. In any case, what I have now is a framework that is powerful, in the sense that a small number of lines of code lets you do a lot. In that sense it's productive and ergonomic. However, it's also occasionally tricky and surprising, sometimes you have to jump through odd hoops to solve what ought to be a straightforward problem. (I should mention as an aside that while I have doubts about parts of my framework, there are other parts that I am completely happy with, such as style builders. More generally, it's mainly the small, inner reactive kernel that troubles me; most of what I have built on top of it I am fine with.) |
Beta Was this translation helpful? Give feedback.
-
With regards to referencing nested entity paths I think it would be nice to enforce that every entity has unique names or maybe an optional id property that if specified is used to reference that particular entity. Also the syntax does need improvement(I know this is still experimental) but I think an xml like syntax will be more approachable. |
Beta Was this translation helpful? Give feedback.
-
I came here to see the current and planned state of Bevy templating and updating. I quickly realized that this issue is TL, so I DR. Can someone in the know please recap? If it's too tentative at this point, then maybe just a solution candidate outline would be nice. |
Beta Was this translation helpful? Give feedback.
-
@cart I think that what I would like to see from you next is more detailed requirements around reactivity. Here are some options (mostly things that I have either prototyped or have been discussed) that can be used as a starting point. The first group of options is "kinds of reactions":
The second set of options relates to "kinds of data sources we can depend on"
A third set of requirements are more general, not necessarily related to reactivity:
The child widgets thing is trickier than it sounds. Take for example, a dialog box: the entity hierarchy only exists when the dialog box is open, or when it's in the middle of it's opening or closing transition animation. So the child entities of the dialog box shouldn't exist unless the dialog logic says that they should. But you want to be able to build a generic dialog box template that takes the contents as a parameter. This is easy if the contents parameter is a generating closure, hard if the child entities are created in advance. The easy answer is "we want all the things", but some of these things rely on particular design choices, and may be easier or harder if those design choices change. At the very least, there should be a sense of which are the most important. |
Beta Was this translation helpful? Give feedback.
-
Hey 👋 just wanted to share my design here: https://github.com/matthunz/bevy-compose My thinking is if we go the route of course-grained reactivity we can keep the systems running in parallel. let zombie = Template::new(
// Spawning a Zombie will spawn the following components:
Zombie,
(
// This only runs once.
|| Health(100),
// This runs every time a `Health` component is updated,
// and it's guraranteed to run before other systems using the `Damage` component.
|entity: In<Entity>, health_query: Query<&Health>, ...| {
let health = health_query.get(*entity).unwrap();
Damage(**health * 2)
},
),
); The systems can then be scheduled like normal systems: App::new().add_plugins(TemplatePlugin::default().add(zombie)) And the bundles spawned with: fn setup(mut commands: Commands) {
commands.spawn(Zombie);
} The systems are then automatically scheduled to run writers after readers (systems that return a component are guaranteed to run before systems that read that component). Systems are also reactive, and only run if items in their params have changed (this is somewhat inefficient now, but can eventually use I think this approach has serious benefits, one of which being we don't need to block the world. The downside of course is fine-grained reactivity always has better potential, but I've really come to like this pattern. |
Beta Was this translation helpful? Give feedback.
-
PanGui looks great and is worth learning from. HTML and CSS technology stacks should be thrown into the garbage dump of history. |
Beta Was this translation helpful? Give feedback.
-
I hope the new bevy UI would get inspiration from Kotlin Android Compose UI framework. it's one of the best UI systems I've experienced so far. |
Beta Was this translation helpful? Give feedback.
-
Introduction
My highest priority right now is to prepare Bevy's UI system for the Bevy Editor (and a secondary priority is improving the Bevy Scene system). For the past week I have been deep diving on the prior art in this space, experimenting, and consolidating my thoughts on the direction we should be going in.
I have looked at 3rd party Bevy UI frameworks like kayak_ui (author: @StarArawn), belly (author: @jkb0o), cuicui (author: @nicopap), and bevy_lunex (author: @IDEDARY). I'm combining that with my thoughts and opinions I've developed via professional experience with webdev (largely React and Angular).
I've also looked into bevy_proto (author: @MrGVSV): a surprisingly feature complete bevy scene system.
The TLDR is that I think we should take the best ideas from the frameworks above (especially kayak_ui, bevy_proto, and belly), add in a few new ideas and unify them into a single cohesive whole. I've started building a prototype to prove this out (which I have linked to below). My goals for this discussion are:
Here are some general thoughts and goals:
bsn!
macro.Style::color
property on an entity, it should override that specific property in any scenes inherited by that entity, but keep the other Style properties defined in the inherited scenes. This isn't something unique to Styles. All scene properties should be override-able / cascade-able. If inheriting from multiple scenes, properties should "cascade" across scenes in the order the scenes are referencedPbrBundle
andSpriteBundle
(although we can and probably should discuss adopting shorter names for schematics).apply
function that performs the "cascading" behavior.bsn
macro can register the source Rust file with the asset system to watch for changes. We can then hot-reload any changes that don't add new rust expressions to the bsn macro. This is inspired by themakepad
approach.kayak_ui
implementation, but there are improvements to be made (such as improving how we reload scenes when inputs change and making prop change detection more automatic and user-friendly).Note that I am currently focused on core Scene API design and dataflow. Once we resolve these things, we can focus on higher level concerns like "how will we support rounded corners", "should we use flexbox or morphorm", "what widgets will we build", "what will our Input widget look like", etc.
The Prototype
I have built a (very incomplete, super experimental, barely functional) implementation of many of these ideas, just to prove them out. Note that everything is open for discussion at this point (broad goals, implementations, syntax, etc). This is the result of a week of work and experimentation. It is not the platform we need to build on.
Note that this work is heavily inspired by bevy_proto and kayak_ui, with a little bit of belly and to a lesser extent the other libraries mentioned above. My goal is to take the best ideas from the ecosystem and mash them together with my own for a "best of all worlds" result. Note that I didn't take any code from any of these libraries, just ideas.
Also note that my prototype builds on Bevy Asset V2. This makes use of "direct asset loading" to enable producing final "combined" scenes from nested scenes. And the improved dependency tracking makes the code-driven BSN implementation much simpler.
A New Bevy Scene / UI Format
Here is what my proposed Bevy Scene (BSN) format looks like in its current form. Note that I have a working prototype implementation for everything I am covering here.
Here is a single entity with a single schematic:
Note that the Div schematic I'm using in these examples is not a final api, just a way to illustrate with familiar terminology. Style would likely have its own field on the Div schematic rather than having fields like
Style::width
splatted onto Div.Fields are entirely optional:
You can even skip everything!
Div
You can nest child entities like this:
You can define fields and nest like this:
Most entities will only need a single schematic. But you can add more than one using a tuple (just like we do in Bevy Rust code for Components):
You can assign names to entities using this syntax (very TBD). It will produce a Name component for the entity:
You can add the contents of a scene to an entity like this:
This is how you would add a scene as a child of another entity:
You can override schematic values in scenes like this:
Assuming the
player1.bsn
scene contains the#Player1
entity defined a few code blocks up, this would result in a final scene that is:I was pretty sure that the BSN format does not fit well into Serde data model, so I built a custom parser that produces an AST that gets converted into the final in-memory Scene asset (using TypeRegistry). This means that we currently need FromBsn implementations for every type in the scene. I'm not sure this is the right move yet (I just took the path I knew would work). Serde might still be an option, either in part (ex: use FromBsn when it exists for a type and fall back to serde when it doesn't) or in full (make a serde-compatible BSN deserializer). We could also rely on Reflect for everything but "value deserialization" (at the cost of some overhead).
Using BSN in Rust Code
Scene asset files can be spawned like this:
You can also use the
bsn!
macro to define your scenes inside Rust code:Rust Analyzer's autocomplete, import missing symbol, syntax highlighting, and go-to-definition all work because
bsn!
plays nicely with RA:You can also pass data in from systems and define expressions inline:
I've created a system adapter to spawn the returned BSN (not sure this will be the final api, but its convenient for testing!)
Note that the
bsn!
macro does not generate an Asset file (which would introduce more dynamic-ness and overhead than necessary). It directly creates efficient Prop instances inline, which are then turned into insertable Bundles and Components using the Schematic impl for the type and inserted into a given World. This makes it roughly equivalent to spawning Entities/Bundles/Components directly using World. The goal here is to make it an (optional) replacement for the currentcommand.spawn()
pattern people are using today in Bevy.Defining Schematics (and Props)
The
Div
schematic used in the examples above is defined like this:This generates
DivProps
:Prop
is a simple Option-like type that looks like this:In fact, we might be able to just use Option in the final impl!
Deriving Schematic for
Div
also producesimpl FromBsn for DivProps
code, which enables converting from the deserialized BSN AST to DivProps.If you add the
#[schematic]
attribute to a field, it will treat the Prop as a schematic instead of "just" a prop value:Handle
implementsSchematic
and usesAssetPath
as the Props impl:Note that schematics don't need to be components or bundles. They can "just" be fields.
Reactive Systems (Not Actually Reactive Yet ...)
I have not implemented reactivity yet (and this will be a challenge!). However I have wired up a pattern that I believe could be made reactive:
The idea here is that we can use the ECS to change-detect whenever the input
props
change (because they are stored on a Component, which has automatic change detection). We could also probably add a newIsChanged
trait forSystemParam
that we implement for change detectable parameters likeRes
, which would enable us to re-compute the scene whenscore
changes.Not sure this is the best path forward yet. But I think it is a reasonable start.
Missing Features / Next Steps
Please note that this is just a prototype. In its current form it is not usable for anything. Too many missing features!
Nested Entity Overrides
It should be possible to override values in child entities
Proper Hot Reloading of Children
Hot reloading currently works, but it will create duplicate children on each reload. We need to add logic to write on top of entities whenever possible, preserve moved entities (if they have the same name), and ensure we never create duplicates. Non-trivial! I suspect we can use this logic for reactivity as well.
Resolve "relative entity paths" in Schematics
It should be possible for an EntityPath Schematic to exist that allows entities to reference other entities in the hierarchy:
We also need to decide what restrictions we will place on this. Allowing arbitrary access to the hierarchy means we need to spawn all entities in a scene before we insert components. If we only allow accessing children, we can directly spawn "bottom up". Will we allow referencing entities "outside" of a scene?
For Loops in BSN Macro
We need to be able to generate children from iterators over inputs
Reactivity
As mentioned in the "reactive but not actually" example, this is a big chunk of work that needs to be done. We can learn from kayak_ui here, as well as more complete / battle tested reactive systems such as React, Solid.js, etc.
Versioned Imports and Migrations
Currently "asset bsn files" use the "short name" as defined in the TypeRegistry. This, on its own, is not a safe approach to referencing Schematics. Every schematic must have a version number associated with it so we can properly migrate it across Bevy versions. I think we'll want something like "versioned imports" to encode this. It should not be possible to access a type in a scene file without specifying the version (or at least, we should warn about this on load). For simplicity, we likely want a "bevy scene prelude" that pulls in all built in bevy schematics:
We'll also want some way of resolving conflicts (ex: import aliases and/or fully qualified schematic names).
Deriving Schematic for Enums and Tuple Structs
Improved Errors
The BSN parser does not currently provide line information / "error location context" in the source asset. This is a must have.
Nested Code-Driven
Bsn
Scene Dependency TrackingCurrently when nesting a code-driven Bsn inside of another code-driven Bsn, I don't properly wait for scene dependencies in the child to load before trying to spawn the parent.
ToBsn
We'll need to be able to serialize Schematics to BSN (my prototype can only read the format).
Other Missing Features
bsn!
macro.bsn!
macro yetOpen Questions
Props
generated in the Schematic derive (and the Prop-cascading system generally) and think "@cart why didn't you use Reflect for this?". I'm reasonably certain we could do this if we really wanted to (ex: something like Reflect::apply + DynamicStruct with Options wrapping values). I chose not to follow this path because that would incur some pretty serious overhead (hashing, string comparisons, dynamic dispatch, etc), making the system an unsuitable replacement for code-driven scene spawning (and making our asset-driven scene spawning less efficient too). I'm open to increasing our usage of Reflect (ex: maybe for removing the need for FromBsn derives everywhere), but not if it comes at the cost of good performance. I'm reasonably certain that Props are the right tool for this job. Reflect will still be useful for things like entity inspectors, auto-generating widgets for scene/schematic editors, migrations, diffing rust types for dynamic lib reloading etc.bsn!
macro and the scene format? Should we? It would ensure both behave exactly the same.Res<T: Schematic>
schematic and anAsset<T: Schematic>
schematic), but we might also want special syntax for it.Call to Action
I've made this post as early as possible because I think our first priority should be synchronizing vision and consolidating efforts. Please read through this (especially if you are one of the "3rd party bevy plugin developers" listed in this post) develop your opinions on where we should be headed, and respond here.
Beta Was this translation helpful? Give feedback.
All reactions