Replies: 25 comments 70 replies
-
Really nice to see progress here, I am really excited for this.
|
Beta Was this translation helpful? Give feedback.
-
For those who'd like to help out (or follow along), ongoing work will be done in concert with the Next Generation Scene working group on Discord. |
Beta Was this translation helpful? Give feedback.
-
I really like this proposal! I had some concerns when I started reading it, but by the end I just wished I could use it as is today. I'll still list the thoughts I had for discussion: Documentation of bundles/required componentsOne of the nice things about Bundles being structs, despite the verbosity, is that they are somewhat self-documenting. If you wanted to add a SpriteBundle, you could see what that entailed in the docs and via autocomplete, making it easy to see what components you might want to modify. I thought this was a pretty elegant way around how opaque ECS functionality can often be. It definitely also has it's issues, like not being able to distinguish between components that will be used to configure the functionality (e.g. Material) and components that will be used to store state (e.g. GlobalTransform), or even both depending on where it's used (e.g. Transform). In an ideal world this would all be meticulously manually documented, but in practice it of course rarely is, both in Bevy itself and external projects. The case of editors is also worth considering there: If you want to add a Sprite, you probably want the Transform to show up in the Editor, but having the GlobalTransform there would cause confusion. If you save a scene to disk, you probably don't want to save whatever GlobalTransform it had at the time of saving either. I don't have an answer there (perhaps some way to mark a required component as computed in a way that works via the inheritance system? Generate doc comments?) and it's not really much worse than before with this design anyway. Marker ComponentsUIs (and scenes) often have a lot of singleton items. Many UI and scene systems have ways of assigning unique labels to those items, like Godot scene unique names and HTML element IDs. In bevy, this is currently done with marker components, which have to be defined in Rust, or Interaction with dynamic components/scriptingWith imports in bsn, it raises the question of how to deal with scenarios where components are defined or added later, possibly via scripts which might themselves be part of scenes. This is probably too far out to start worrying about, but it feels easy to paint oneself into a corner there. All in all, I think this proposal would be a huge step in the ergonomics of bevy and I'm definitely in favor of it even as-is! |
Beta Was this translation helpful? Give feedback.
-
I really like this proposal! Here are just some initial thoughts reading through it for the first time: Blanket
|
Beta Was this translation helpful? Give feedback.
-
From a single pass, there are two things I really want to stress work this:
|
Beta Was this translation helpful? Give feedback.
-
Promising work, there are unambiguous wins here for sure (require components at minimum is great). The scene redesign you propose moves in a similar direction to what I ended up with in Questions
StylesMy experience is style inheritance is confusing and clumsy. A better approach is to parameterize. In Reactive BSN
|
Beta Was this translation helpful? Give feedback.
-
Reading over this proposal I'm generally in favour of everything presented, and largely how it's presented too. I do have some concerns around the What I'm concerned about is creating a very rigid type effectively at the core of the ECS, whose internals are fully public, making breaking changes quite likely (and potentially painful!). As an example, this It will require some type-shenanigans to setup, but I believe we want to be generic over the impl<CX: Provides<Entity> + Provides<World>> Construct<CX> for Foo {
type Props = Bar;
fn construct(context: CX, props: Self::Props) -> Result<Self, ConstructError> {
let entity = context.take::<Entity>();
let world = context.take::<World>();
// ...
}
} Within Bevy, there is a similar system built up with the // Before...
impl FromWorld for Foo { ... }
// After...
impl<CX: Provides<World>> Construct<CX> for Foo { ... } From an order of implementation perspective, I think the |
Beta Was this translation helpful? Give feedback.
-
Initial very ill-defined thoughts:
|
Beta Was this translation helpful? Give feedback.
-
I am very excited about this proposal and the impression that an MVP of this is not far, as I do agree that it only will get proper feedback once all have their hands on it and can give it the reality-test, there is only so much we can anticipate based on theory, at least I (being a non core bevy dev, and more of a user) can't. That being said I am missing a section about My other big concern is how we want to pull of the asset versioning in practice? My assumption is that we can leave everything out of a bsn-descriuption that we assume |
Beta Was this translation helpful? Give feedback.
-
Thanks for your work, really like this proposal. Question about EntityPatch children with inheritance. With the following patch.
And the other_patch tree is something like this
What will the final tree? It will be
And nesting
But what if the patch child does not exist? Runtime panic or warning or something? And what if I want to inheritance without any children leaving the tree only |
Beta Was this translation helpful? Give feedback.
-
The main thing I'm concerned about is what is See my crate |
Beta Was this translation helpful? Give feedback.
-
Thank you for the extensive write up Cart, this looks amazing. As @extrawurst says, for me as a bevy user, its too early to judge anything, it will have to be played with to truly understand what the consequences are. I'm mostly just super grateful for your continued effort on improving ergonomics! 🙏 |
Beta Was this translation helpful? Give feedback.
-
Overall, I'm excited for the direction this is taking. I have some concerns but I think we definitely need something "construct, patch, and bsn shaped". My main concerns are around entity identifiers, partial patching, and being able to remove children/components via patches. Partial PatchesAre patches meant to be a complete description of the entity hierarchy? Or do you intend to support patches that only affect some of the entities? Patch RemovalIt looks like patches are purely for adding/modifying, what are your thoughts on allowing patches to remove entities/components? Is that an anti-pattern? Entity IdentifiersAfter some discussion in discord it seems very useful to have a way to uniquely identify entities. Here are some use-cases:
|
Beta Was this translation helpful? Give feedback.
-
A bit of a bikeshed, but is there a reason why the syntax to describe attributes and hierarchy is separated like this: bsn! {
Node { width: px(200.), height: px(100.) } [
Img { image: @"logo.png" }
]
} Instead of like: bsn! {
Node {
width: px(200.),
height: px(100.),
Img { image: @"logo.png" }
}
} Which is more readable IMO. Dioxus's rsx! {
div {
class: "foo",
div { class: "bar" }
}
} I initially assumed that this is done to make RA happy, but it looks like that Dioxus managed to solve the problem here: DioxusLabs/dioxus#2421. So were there any other reasons behind this syntax decision? |
Beta Was this translation helpful? Give feedback.
-
Let's assume for a moment that BSN would integrate some kind of reactive framework. This requires a set of reactive primitives which would exist below the level of BSN, and which can be developed in advance of BSN, just as required components have been. There are a lot of different ways this can be designed, but the way I have done it is to define the most fundamental reactive elements as two ECS components which I call
Everything else - mutables, signals, memos, callbacks, views, templates and so on - are built on top of these two primitives. Both Quill and bevy_reactor have these primitives, despite the fact that their approach to reactivity is very different. There is considerable leeway in the design - nothing prevents you from splitting Similarly, there is a lot of design choice in how the tracking scope gets populated. Currently I use an explicitly passed So the very first step, if I wanted to incorporate this into BSN, would be to get convergence on the specific design of these two primitives, plus the ECS system used to drive them. |
Beta Was this translation helpful? Give feedback.
-
I read the Construct section of the proposal. It looks great, and reminds me of a few smaller things I've wanted before. I just want to note a couple IMO desirable properties that I didn't see explicitly mentioned for entity-spawning functions. fn red_player(entity: impl EntityBuilder) -> impl EntityBuilder {
entity
.build(player)
.add(Red)
} There should be an easy path to convert a "pure" entity-spawning function that doesn't accept input data to one that does (allowing e.g. It should also be possible to spawn related entities such as children within the entity-spawning function, and probably to access |
Beta Was this translation helpful? Give feedback.
-
Realised I accidentally put my top level comment in an unrelated thread. Copied it here for visibility: I would like to advocate for two animation related features when it comes to reactive bsn. The first feature is programmable transitions. I think it's an important function of crafting highly dynamic ui and animations ergonomically. In principle how is I'd envision that it'd work would be that entity removal and addition logic would be handled by the parent container instead of automatically sending commands to immediately remove/add the child entities from the tree. The container for example could add a component to the relevant children that would use a system to animate in or out the child elements. This is better than the alternatives seen in frameworks like react where both old and new subtrees have to be kept untidily around in the markup in order to animate transitions. The second feature would likely be easier to add later but think would be a huge boon for continuous animations (think health bars, sliders, or graphs). Here the functionality would extend the proposed React macro to allow one to specify an interpolator that would apply to the reactive component. This could for example be a spring interpolator or a tween. This functionality is directly inspired by SwiftUIs implicit animations which have seen a lot of success in practise. |
Beta Was this translation helpful? Give feedback.
-
Hello somewhat new around here, new to rust and to bevy :) So how would one define the "Bevy Data Model". ? "Bevy users will learn exactly one data model for everything, making it easier to "learn Bevy". Cognitive load will be reduced across the board. Context switching between UI and "the rest of Bevy" will not be required." |
Beta Was this translation helpful? Give feedback.
-
On Scene Saving/Loading Yes we are a bit new to rust and bevy but have a long experience at building shared scenes, and our test app we are building is a sort of "shared whiteboard", really a general scene that in a sense, IS a multi-player game ( a multi participant/player collaboration tool) It is mostly a UI with a renderer to make it all visible. In the past, over the years, we have set up these scenes as a "scene root" and basically blank, default camera, etc, until ones loads a scene (ie: connects to a running scene on a server). Our entities are the parts of the scene that are loaded or saved and are basically bags of identified properties. When one opens a scene from a server, the server starts sending a set of "commands" to the empty space, spawn entity, set position, set ( xxx named property) etc. The messages can contain a collapsed set of these commands ( as a list to be potentially executed in order on the receiving entity ), so a new entity can be defined in one large message and be complete enough before making it "renderable", when a new entity is created it is created with an ID passed in from the server and all subsequent update commands are sent/routed to those IDs. The scene root is also an Entity. Entities can refer to other resources and other entities. Ie might spawn a load/get of a resource it refers to, after it is spawned and starts constructing itself. I say updates because the same commands are used later to send updates from the server to the client's shared scene when it is "running" (ie: any entity in the scene mirror on the server ) is altered. Has any thought gone into what you guys are thinking to implement such a thing ? Some of the older work we have done for complex UIs can be seen here: https://didi.co/docs/BradPaley_June2021.pdf An image of our fledgeling shared whiteboard app written in rust+bevy |
Beta Was this translation helpful? Give feedback.
-
This is awesome! I'm on board with nearly everything presented. A few questions/comments/concerns:
Keeping in the spirit of the whole "patches are applied one by one," I think it would make more sense apply all patches left to right, top to bottom, while encouraging a convention of putting inheritance at the top:
or
These both read "first apply the button patch, then the thing patch, then the individual component patches."
In the example above if the user does not specify an
This would play into the editor nicely. A programmer could implement a bunch of different Actions, the editor could display them as options for the required OnClick component, and the non-technical designer could wire the UI up without ever seeing a line of code. I don't see how a designer would ever be able to create the BSN outlined in the callback examples solely via the editor GUI without significant understanding of bevy code. |
Beta Was this translation helpful? Give feedback.
-
@cart @SanderMertens I've been experimenting with the ghost nodes PR #15341 and it's working beautifully. I'm in the process of refactoring bevy_reactor to use the new feature, and so far I have ported With ghost nodes, there's no longer a need to keep around two separate trees (one for the "DOM" and a different tree for the reactive component hierarchy). Instead, the special control-flow nodes can be interleaved within the "DOM" as ghost nodes, as was discussed previously in this topic. Take for example, the
The test condition is re-evaluated when needed. In a non-reactive design, it would be executed every frame; in a reactive system, it's only executed when one or more of it's dependencies change. If the result of the test changes from previously, then the children of the ghost node are despawned (using In addition to avoiding the need for two hierarchies, there are other advantages:
This last point deserves some explanation: the ghost node's reactive logic is entirely self-contained, and does not require anything special in the way of parents or children - it's just components and systems driving the control-flow logic. Because of this, nothing prevents you from having children that use a different reactive framework - so it's theoretically possible to have an entity hierarchy where different parts of the tree are generated by BSN, bevy_reactor, sickle_ui, bevy_cobweb and so on. Obviously, these different frameworks are going to require different support mechanisms, so it might not be that simple, but it's not impossible. This has resulted in the removal of a lot of code from bevy_reactor, but I suspect that even more can be removed, which is something I am working on. One puzzle I am ruminating over is that I'd like to make the |
Beta Was this translation helpful? Give feedback.
-
The original post does not indicate what BSN stands for, and I found it difficult to see what the macro name The term BSN seems to be used here as a matter of course, but I think |
Beta Was this translation helpful? Give feedback.
-
How much progress in bsn do we have? |
Beta Was this translation helpful? Give feedback.
-
I've taken a swing at implementing an extremely minimal bsn-like declarative template macro (found here: Leafwing-Studios/i-cant-believe-its-not-bsn#10), focusing on Posting here in case anyone wants to try it out while they wait for cart to finish the full system. Having written this, I'm starting to see why BSN has taken as long as it has. There's tremendous complexity in the static asset loading (which this implementation sidesteps entirely by just moving around rust codeblocks). |
Beta Was this translation helpful? Give feedback.
-
Preamble
My vision for Bevy UI is largely a vision for better Bevy Scenes and an improved Bevy data model. The general angle is:
Embrace the "Bevy data model" for both Scenes and UI. Fill in functionality and UX gaps where necessary.
This document serves as an introduction to my plan, a description of how it should be implemented, and an outline of what I have already built.
It also serves as a kickoff for the Next Generation Scene/UI System Working Group. This is still just a proposal. I want everyone interested to read, discuss, and iterate on it with me. That being said, I'm confident enough with it (and far enough into the implementation of it), that I am biased toward moving forward quickly (and iterating), especially on the fundamental pieces, rather than dragging on for months.
Unifying Scenes and UI yields significant fruit:
From my research, I feel strongly that at their core, the requirements for UI are not fundamentally different from the requirements for scenes. The fact that historically unified UI is uncommon (or poorly implemented) in the industry is because building UI frameworks is hard and building scene systems is hard. Very few organizations / individuals have ever embarked on doing both at the same time. The few orgs I'm aware of that have done both have either (1) built a Scene system first and then built a UI system on top of it (Godot, the "older" Unity UI system) or (2) built a UI system first and then tried building a scene system in it (look into React.js gamedev if you're interested).
I say "at their core" because I fully acknowledge that UI and Scenes do have requirements for some features that may not perfectly overlap (ex: styles and accessibility should probably be optimized for UI use cases over scene use cases and might justify context-specific systems). At least in the short term, things like "reactive scenes" outside of a UI scope will probably be a more niche concept than "reactive UI", which is currently commonplace. That being said, sharing a common core allows us to explore where (and how) these intersections should be shared. For example: styling 3D scenes in the same way you style UI is an interesting concept worth exploring. I suspect that reactive scenes in the context of "2D and 3D game scenes" will grow in popularity (given the capabilities I've seen in Flecs)
Bevy is uniquely positioned to build the first popular, high quality, delightful to use unified general purpose Scene / UI system that seamlessly crosses the boundary between code-driven scenes and asset-driven scenes. This has me very excited, and I think I can find others to share in my excitement.
This is a followup to my older Bevy UI and Scene Evolution kickoff discussion.
Here are the pillars of my strategy:
World
access, the current entity, input data, etc), in a way that encapsulates that behavior.bsn!
macro and accompanying asset format that enable ergonomically composing Bevy Scenes. This builds on the previous pillars.Note that many of these pillars are standalone features that can be reviewed and merged independently. The rest are built in layers. Most of them can (and should) be merged on their own individual merits, even outside of the context of the scene system. I'm listing the pillars in "proposed merge order". Most of these pillars are already implemented.
Required Components
Bevy's current Bundles as the "unit of construction" hamstring the UI user experience and have been a pain point in the Bevy ecosystem generally when composing scenes:
ButtonBundle { style: Style::default(), ..default() }
makes no mention of theButton
component symbol, which is what makes the Entity a "button"!SomeBundle { component_name: ComponentName::new() }
..default()
when spawning them in Rust code, due to the additional layer of nestingRequired Components (which I have fully implemented) solve this by allowing you to define which components a given component needs, and how to construct those components when they aren't explicitly provided.
This is what a
ButtonBundle
looks like with Bundles (the current approach):And this is what it looks like with Required Components:
With Required Components, we mention only the most relevant components. Every component required by
Node
(ex:Style
,FocusPolicy
, etc) is automatically brought in!Efficiency
Style
andFocusPolicy
are inserted manually, they will not be initialized and inserted as part of the required components system. Efficient!IDE Integration
The
#[require(SomeComponent)]
macro has been written in such a way that Rust Analyzer can provide type-inspection-on-hover andF12
/ go-to-definition for required components.Custom Constructors
The
require
syntax expects aDefault
constructor by default, but it can be overridden with a custom constructor:Multiple Inheritance
You may have noticed by now that this behaves a bit like "multiple inheritance". One of the problems that this presents is that it is possible to have duplicate requires for a given type at different levels of the inheritance tree:
This is allowed (and encouraged), although this doesn't appear to occur much in practice. First: only one version of
X
is initialized and inserted forZ
. In the case above, I think we can all probably agree that it makes the most sense to use thex2
constructor forX
, becauseY
'sx1
constructor exists "beneath"Z
in the inheritance hierarchy;Z
's constructor is "more specific".The algorithm is simple and predictable:
#[require()]
, recursively visit the require list of each of the components in the list (this is a depth Depth First Search). When a constructor is found, it will only be used if one has not already been found.From a user perspective, just think about this as the following:
Foo
directly on a spawned componentBar
will result in that constructor being used (and overriding existing constructors lower in the inheritance tree). This is the classic "inheritance override" behavior people expect.Required Components does generally result in a model where component values are decoupled from each other at construction time. Notably, some existing Bundle patterns use bundle constructors to initialize multiple components with shared state. I think (in general) moving away from this is necessary:
Construct
Some components require
World
state to be constructed, which prevents them from being initialized using parameterless constructors likeDefault
. Some examples:EntityPath
that queries the hierarchy using theName
component)Critically, by abstracting over these "world requirements" for a given type, we relieve the conceptual and ergonomic pressure to manually retrieve these requirements from World and pass in their parameters to construct the final type during initialization. Bevy users are tired of needing to pull in
AssetServer
,Assets<StandardMaterial>
,Assets<Mesh>
, etc to construct and orchestrate everything manually. And in the context of scenes, doing this manually is not justifiable. This is an important missing piece of the puzzle, both in the Bevy Scene space and in manual/direct entity initialization.Therefore, Bevy is missing an abstraction that defines a "constructor" for a given type that:
AssetPath
for aHandle
).AssetServer
for aHandle
)Construct
is a trait that does exactly that:Where
ConstructContext
is (currently) just:This is what the implementation for asset
Handle<T>
looks like:Note that Construct is fallible, allowing users to recover from errors (when using it directly), and Scene systems to report errors when they occur.
Deriving Construct
Construct
can be manually implemented. For some types, such asHandle<T>
, this makes sense because it requires very specific inputs and internal logic. However for types that use those types, Construct can (and should) be derived:An astute reader might notice a potential problem: we know
Handle<Config>
required aConstruct
impl. Does that meanTeam
andString
also needConstruct
impls? What if we can't implementConstruct
for these types (ex:String
) due to Rust's orphan rule?Fortunately, this problem is easily solved: we just implement
Construct
for every type that implementsDefault
andClone
(note that Props requireClone
because we need new prop instances for each spawned instance of a scene ... we can discuss this, but I currently think this constraint + data pattern is the right call).Notice that Construct doesn't really "construct" anything for
Default
types. The constructed type is the prop type, so it just passes it through. Put another way, aDefault
type is both the Props and the Constructed type. It is a no-op.That means
Team
(from the example above) can just look like this:And we can still "construct"
Team
(or use it as a field on a derivedConstruct
type).Usage
Construct
can be used manually like this:Additionally, context can call construct directly. Note the removal of the manual
.into()
and the removal of theConstruct::construct
stutter:Or alternatively:
Construct Required Components
Required Components support Construct:
When
construct
is specified, the component will be queued for construction and inserted at the next command flush (this uses the same underlying mechanism as component hooks / observers).Builders
A builder syntax is also possible (but not yet implemented ... API to be determined). Here is one idea:
Compare that to the "manual way":
Alternatively (or in addition to) the builder pattern, we could probably also implement a Bundle-style api like this:
That being said, I think
bsn!
will be preferred over all of these styles. I don't think we need to prioritize them in the short term (or until it becomes clear that there is demand for them).Encapsulation
Notably,
Construct
-driven initialization, in addition to being more ergonomic, especially as it scales, is also fully encapsulated ... no dependencies required. This allows us to build APIs like:We can now spawn these player entities "anywhere" from any system without needing to know whatever complex initialization dependencies / logic they require.
We now have the makings of something resembling a scene system, but we haven't really even built one yet! Also take note of how Construct and Required Components feed into each other. Imagine how much worse this would look with bundles everywhere!
Construct Derive Codegen
The
Construct
derive assumes that every field also implementsConstruct
.For this input:
It produces code that looks like this:
The
ConstructProp
enum allows the type to be constructed either with theConstruct::Prop
type or an actual instance of the value:This flexibility is critical for entities constructed in code, as it allows for passing manually constructed "actual values" into scenes, alongside their "props".
An astute reader may notice that in theory, we could remove the need to manually specify
#[construct]
, given that all fields do actually implementConstruct
.There are two good reasons:
ConstructProp
(and not just the ones that need props), that blocks our ability to do the blanketConstruct
impl forDefault
types, as the fields on those types won't haveConstructProp
wrappers.Construct::construct
, we only branch on fields that actually need props. In practice, the Rust compiler does optimize these branches out when they aren't needed, but only at higher optimization levels. By removing the enum entirely for these fields, we ensure it is optimal at all optimization levels.Scenes and Patches
In order to support scenarios like inheritance and styles, sets of changes must be able to apply on top of each other. In other words, we need scenes to be defined as "patches on data", not the data itself.
The new Patch system gives the Bevy scene system these capabilities.
Using the Patch Trait
First, lets look at the new
Patch
trait:This allows a type to use its context to modify the value of a
Construct
type'sProps
. In practice,Patch
is implemented for functions that modifyConstruct::Props
, while also retaining the context of the originalConstruct
type. Aka, theConstructPatch
type:All types that implement
Construct
have apatch
helper function, which allows developers to ergonomically define a patch for a type:Importantly,
blue_patch
still encodes the originalConstruct
context (aka, that it is for thePlayer
type). This means that you can use theconstruct_from_patch
method onConstructContext
to construct the "actual type" from aPatch
that patches the "props type":Patch
is also implemented for tuples of patches:EntityPatch
When
Patch
is combined with the newEntityPatch
type, things start to get very interesting:This defines a "patch tree" for an entity. This is essentially what amounts to an in-code scene system, which requires no special DSL. You can define whatever DSL you want (this proposal will outline BSN in a bit, but anything that translates to
EntityPatch
will work). You can also just define theEntityPatch
type directly.EntityPatch
encodes inheritance information for each entity in the tree. Notably inheritance is statically encoded. This means for anEntityPatch
with no inheritance, we can cheaply spawn it immediately without needing to do the comparatively expensive inheritance resolution logic (which requires hash maps, potentially waiting on the asset system to finish loading assets, etc):This will recursively construct and insert all components in the patch tree. Importantly, this approach means we can use entity patches to cheaply and immediately spawn patches (aka scene definitions), making them a suitable replacement for manually spawning entities in Bevy using the traditional
commands.spawn()
. This may not feel particularly compelling, given the ergonomics of EntityPatch. However, with the introduction of a DSL like BSN, this becomes desirable!DynamicScene and DynamicPatch
Patch
andEntityPatch
provide nice static trees of patches. But to resolve inheritance, and to support scenes loaded from asset files, we need to support more dynamic scenarios.DynamicPatch
andDynamicScene
are the equivalents for these scenarios:Notably
ConstructPatch
implements bothDynamicPatch
andPatch
. ThereforeEntityPatch
is fully compatible with the dynamic system. Scenes loaded from files are responsible for implementingDynamicPatch
. This means patches can be combined across contexts (a BSN asset file, BSN defined inbsn!
macro, a directly definedEntityPatch
, a JSON asset file, ajson!
scene macro, etc).Scene Trait
The
Scene
trait serves as a destination trait forEntityPatch
. In general, it exists because the generics for EntityPatch are hard to specify. Additionally, it is Box-able. It allows you to write code like this:Additionally, for DSLs like BSN, it allows you to write code like this:
BSN
BSN is a new syntax / file format / scene system that makes it easy to define nested hierarchies of entities and their components. It builds on the existing pillars listed above.BSN exists to:
Construct
manually (ex: inherited field overrides, asset system integration)Overview
BSN can be thought of as a "simplified Rusty syntax that supports hierarchy, scene inheritance, and overrides". It ties directly to existing Bevy and Rust concepts.
The
bsn!
macro returns anEntityPatch
tree. This makes it interoperable with thePatch
/DynamicPatch
/DynamicScene
system (defined in the previous section). It also means that you can embed the results ofbsn!
inside of anotherbsn!
!Likewise, it relies on
Construct
and Required Components to improve its capabilities and usability. If you haven't yet, read those sections above to understand how those concepts relate.Disclaimer: these examples use essentially a 1:1 port of the existing Bevy UI types. I have a strong desire to rework the boundaries and terminology of these components to better fit the new scene system.
Entities and Components
You can define a new top-level entity in BSN by specifying a Component:
Node
You can add more than one Component to the entity like this:
Notice that this uses tuple syntax, just like it would if you were inserting a group of components in normal rust:
As an aside, note that you can use
bsn!
as a direct replacement for that if you want!Fields
You can define fields on a component like this:
Notice that unlike raw Rust, there is no need to specify
..default()
. Nice!Components for an entity can / should be broken up into multiple lines when there isn't enough room:
Values vs Props
Now lets consider the following Component:
When specifying fields in BSN, by default they are assumed to be "actual values" (either literals or identifiers available in the current scope). For example:
So far, we haven't really taken advantage of the power of
Construct
(see the `Construct section above if you haven't already). You can use the following syntax to specify a construct prop instead of a normal field:This expects an
AssetPath
(or something that converts into one, such as a string) becauseHandle
'sConstruct::Props
type isAssetPath
. When this BSN is spawned, it will use thatAssetPath
to retrieve a strong handle to thelogo.png
image asset. Fantastic!This also works for other types of fields (such as named fields). You can mix and match "prop fields" and "value fields":
Expressions
The
bsn!
macro supports expressions, which are just snippets of Rust code:Functions
You can call functions in scope like this:
This is a great way to cut down on boilerplate and abstract out things like unit/type conversions. This is what it looks like without the helpers:
Nice and explicit, but much less ergonomic!
Children
BSN entities can define children like this:
Inheritance
BSN entities can inherit from other BSNs. This will spawn the inherited scene with the changes specified for the current entity written on top:
Note that this "direct in-code inheritance" pattern hasn't yet been proven out. I think it is possible, but we may need to do this instead:
In that case, the results of the
button()
function would be assigned to the"button_scene.bsn"
path, and normal asset scene resolution would be used.Values are always "patched on top":
Transform's default translation is
{ x: 0.0, y: 0.0, z: 0.0 }
. The final value in the example above is{ x: 1.0, y: 2.0, z: 0.0 }
because the values are "patched" on top of the original (in this case default)Transform
value.Multiple inheritance is supported. In all cases, inheritance is applied from right to left, where left is applied "on top" of the result of the right hand side.
The Case Against Bundles
All of the BSN examples so far look the way they do because they have avoided Bundles. Lets compare "BSN + Bundles" against "BSN + Required Components":
Look at all of that extra nesting and extra characters! Not to mention the other issues mentioned in the Required Components section. Existing UI frameworks set a high bar for ergonomics and Bundles will not clear that bar. And non-UI Bevy developers also deserve high legibility and ergonomics!
bsn!
Code ExpansionBSN relies on the
Construct
trait at its core to initialize types. Lets explore the Rust expansion of this simple case:This expands to:
Now lets take a look at setting fields:
This expands to:
Notice the
into()
which allows us to pass in any value that converts into the desired type.Now lets consider a hierarchical example:
This expands to:
And finally, lets look at how
Construct::Props
syntax expands:The expansion:
BSN Assets
BSN is also an "asset format", meaning it can be composed in files and loaded by the Bevy Asset system:
bsn!
macros in code can inherit from BSN Assets:BSN Asset Imports
Imagine we have the following scene:
This has a problem! Unlike
bsn!
in code, which uses the current Rust import context to decide how to resolve theSprite
type (ex:use bevy:prelude::*
), the scene asset doesn't have that context!We could specify the full type path manually:
However this gets repetitive if you have multiple sprites:
This is where BSN Asset Imports come into play:
Notice that this looks exactly like Rust import syntax!
However this still has a problem!
Import Versioning
Unlike Rust code, which is compiled against a specific crate versions, BSN asset files have no direct tie to the version of a crate defining a type. This can easily cause compatibility issues if a type changes across versions. For example, pretend that in an older Bevy version,
Sprite::image
was calledSprite::texture
:If our app is running Bevy 0.14 and we see
Sprite::texture
in a BSN asset file, we need to know whether or not it was authored with the intent to be used in the current Bevy version, or a previous Bevy version (so we can properly migrate it).Therefore, BSN Assets require version numbers in their imports:
If we load this in Bevy 0.13, it will work as expected. If we load it in Bevy 0.14, it will attempt to migrate the scene from 0.13 to 0.14.
This same approach is used for non-Bevy types:
For rapid development workflows, we will build editor tooling that does not require incrementing an app's version number for every change, and instead detects schema changes as they occur and runs the migrations when relevant (asking for user input whenever a change could be destructive).
Preludes
Importing every type manually would make BSN hard to compose manually, hard to read, and noisier in version control as things change:
Therefore, much like Rust, BSN supports (and encourages) the use of wildcard imports (and the prelude pattern):
BSN Event Handlers
UI is event-driven by nature. As such BSN needs to support handlers natively. This has not been implemented, but I think we have a few paths forward (and we can implement more than one if we want, although we should try to have "one way" to do this sort of thing):
Path 1: Special Observer Syntax
We could extend
EntityPatch
with the ability to define observers:This would then allow us to add "special observer syntax" to BSN. For example, something like this:
Note that this cannot be phrased as a component on the
MyButton
entity because each Observer needs to be its own entity.I do believe we need something like this to make observers usable within the context of BSN. Sadly directly spawning Observers using BSN (as independent entities) isn't ergonomically viable (although it is possible!).
Path 2: Component Driven Event Handlers
Event handlers are explicitly defined on components. This is a very similar idea to Observers (and would build on the same underlying infrastructure), but the Component-driven approach plays naturally with the current scene system without the need for additions:
Path 3: Construct Observer
We could implement
Construct
forObserver
, allowing us to define entity targets:Obviously not ideal ergonomically, as it would require defining an
EntityPath
manually, which also requires naming the target entity.However we could also default
target
to../
, which would allow for this:Or this, if we're willing to rework Observer to be a tuple with the function as the first field:
Styles
First, to avoid confusion, I will pretend that Bevy's current
Style
component has been broken up into smaller pieces and no longer exists. Given thatNode
is essentially the "core UI element type", I think there is a strong argument to move most existingStyle
fields toNode
:With that out of the way, lets discuss BSN styles. Lets consider the following hierarchy of entities in BSN:
First, lets notice that we could simply "style" this by using the BSN inheritance feature:
However this would only round the top level
Node
. To apply to the others, we must manually apply the style to everything:"Inheritance as styles" is surprisingly effective (and notably "free" given that we already have inheritance for other reasons). However it does have significant downsides:
Therefore, I strongly suspect that we will want a style system that supports these scenarios effectively. However I believe this type of style system:
That being said, I strongly believe that a BSN style system should look like BSN. I think any style system that (1) decouples Styles from strongly typed Components (2) does not look like BSN or (3) does not have the flexibility of styling arbitrary Components, should be off the table.
I believe that the proposed
Patch
/DynamicPatch
system, in combination withDynamicScene
andScene
provide the baseline tools to build some new DynamicStyle system on top.This is not a prescriptive (or fully thought out) design, but I think styles can/should look something like this:
A "scope" (as mentioned in the comments above) could be:
Applying a style to a specific entity in BSN would likely need its own syntax/keyword too (although we might be able to use relations or work out a "component merging" scheme to use components).
We might also want to consider "untyped" styles:
However in practice I'm not yet convinced "untyped styles" are desirable. Most of these "generic" cases are resolved by having "standard" components (ex: anyone wanting to set width this way should probably just use
Node { width: px(100.) }
, and if they are setting it on an entity with a component containing a width field that is not Node, thenwidth
means a completely different thing ... applying the style across contexts would be risky and/or nonsensical).BSN Sets
So far in this proposal, BSN is assumed to be a single hierarchy:
However there are cases where defining multiple scenes in a given declaration makes sense. For example, defining a "child" scene and spawning it multiple times at the root. This concept is handled by BSN Sets. Note that they have not been implemented yet, the design is still a bit rough, and I don't think they should be implemented as part of the MVP:
In addition to facilitating code reuse, it also provides a tool for flattening out deeply nested scenes:
Could become:
It also provides a tool for modules / code organization:
This approach also opens the doors to other relevant scenarios, such as Inline Assets:
Also Styles (as mentioned in a previous section):
In general, I think we should discuss how we might go about expressing these types of patterns.
Reactive BSN
Reactive BSN is still largely an open question, however aspects of this scenario have already been proven out in the context of BSN. Specifically, in an older version of this proposal, I built "simple" top-level reactivity for component fields. Here is what defining a "reactive component" looked like in that proposal:
Some things to note:
Counter
is made reactive simply byrequire
-ingReact
with thecounter
constructor. This works becauserequire
allowsinto()
conversion, andReact
implementsFrom<Bsn<T>>
. The "reactive system" queries for entities with aReact
component, sorequire
-ing it makes the entity visible to the reactive system. Neat!Counter
is mutated, it re-runs the "reactive" parts of the BSN. This is done by topologically sorting the hierarchy and manually walking down it to check if any reactive components have changedI think the strategy for Reactive BSN should use fine grained observer-style reactivity. I think this is the only viable strategy for a general purpose reactive system in the context of Bevy:
Anyone familiar with reactive systems will probably remark that top-level reactivity is the "easy part". Things get much more interesting when branching / looping / adding removing children gets involved. Examples like this are easy to write, but hard to implement:
That being said, I strongly believe that reactive Bevy UI can look and behave similar to this.
There are also open questions about how to expose ECS data to reactive BSN in ways it can react. I'm partial to something similar to system syntax. For example, we could make it possible to react to changes to resources like this:
This is a "hard problem" (and possibly not viable in practice), but we could also consider reacting to Queries like this:
Talin (@viridia on github and DreamerTalin on Discord) has been investigating the "reactive Bevy" space for a awhile now. They have built:
bevy_reactor
: a general purpose fine-grained reactive framework for Bevy ECSobsidian_ui
: a reactive UI framework on top of bevy_reactorquill
: a general purpose coarse-grained reactive framework for Bevy ECSI think we should give these a serious review and consider how they would fit into the BSN picture. At the very least, we should view them as a "proven path" that we should learn from.
Adapting Bevy UI to the New APIs
Style
toNode
. Ex:width
,height
,min_width
,min_height
,left
,right
, etc. Move properties that don't make sense on Node to other new components where relevant.UiImage
toImg
Ultimately it could end up looking something like this:
MVP Remaining Work
My goal is to get a baseline feature-set for BSN released soon so we can review, discuss, iterate, and (ideally) merge in the near future. BSN unlocks significantly improved workflows. The sooner we get it in peoples' hands the better ... even if it is missing features.
I would like to land the following in the BSN MVP:
Construct
trait and derivebsn!
macro (without BSN Set features)bsn
macro, such as calling functions)And here is list of my biggest remaining TODO items:
bsn!
macro to work with the new Patch / Construct systemBorderRadius::all()
andpx(10.0)
)UiImage(handle)
(currently only "expression style"UiImage({handle})
is supported)Construct::visit_dependencies(&self, visitor: impl DependencyVisitor)
function.And some stretch goals for the MVP:
Post MVP Work
bsn!
macro auto-formatter (command line tool + editor extensions)Where is the code?
As mentioned above, a solid chunk of this proposal is implemented. But it is currently in a weird "middle ground" state between the old proposal impl and new proposal impl. It'll need about a week or two of work to get it into a state that isn't confusing for the reader.
I'm going on a week long trip starting today, so I should have some code ready for consumption in 2-3 weeks. Until then, use this time to review and discuss this proposal.
Some Additional Open Questions
Unify Required Components and BSN Inheritance?
Can we (and should we) unify Required Components and BSN Inheritance? There is conceptual overlap, and BSN's "layered patching" approach works better for some initialization scenarios (ex: granular inherited field "overrides" instead of fully overwriting on init). The biggest concerns here are UX and performance (Component Requires can be maximally efficient and maximally ergonomic because their scope is smaller than BSN).
Should we tie Scene Inheritance to Components?
"Reactive scenes" are naturally driven by and tied to a specific component. The question then becomes: should we embrace this for non-reactive scenes too?
For example:
Put another way: "should we support / encourage developers to tie their scenes to specific components, or we should require that they reference those scenes directly".
If we embrace this pattern, we might want to ensure that "scene tied" components like
Player
can only be spawned via the scene system to ensure that everything is spawned "at once".Should we use
<>
instead of()
for entities in BSN?()
is nice because it matches the raw Rust Bevy component bundle syntax. It also plays nicer withsyn
, which doesn't have easy support for parsing<>
groups.<>
is nice because it reads more like an "element" for those familiar with HTML.Should
()
or<>
always be required? (in the interest of consistency / alignment)Currently for single-component entities, they are allowed to omit the
()
:However to ensure everything is aligned and consistent, we could force
()
everywhere:Or force
<>
if we roll with that syntax:Beta Was this translation helpful? Give feedback.
All reactions