Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ default = [
"bevy_picking",
"bevy_render",
"bevy_scene",
"bevy_scene2",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ValorZard I'm converting your top-level comment into a thread (please see the PR description):

(Not related to this comment at alll)
so this is probably out of scope from this PR, but how exactly will .bsn as a file format will work?
My original perception was that .bsn was basically JSX from React fused with the way Godot does scenes, and that seems to mostly be the case.
However, in the example you posted of the bsn! Macro having an observer callback, that’s just a regular rust function.
bsn! { Player on(|jump: On| { info!("Player jumped"); }) }
Will a .bsn file just have arbitrary rust code that runs during the game?

The idea is that .bsn will be the subset of bsn! that can be represented in a static file. In the immediate short term, that will not include things like the on function, as we cannot include arbitrary Rust code in asset files.

The primary goal of .bsn will be to represent static component values editable in the visual Bevy Editor. In the future we might be able to support more dynamic / script-like scenarios. But that is unlikely to happen in the short term.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drat. I thought I did the comment thing correctly.

but otherwise that makes sense. So .bsn and bsn! Aren’t fully equivalent. Interesting.

I guess Bevy could support something like Godot’s callables in the future maybe, and have some sort of id sorted in the .bsn file that could be converted to a function? Idk

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, function reflection + a registry should make stringly-typed callbacks defined in assets very feasible.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@3xau1o I'm converting your top level comment to a thread (please read the PR description)

can read mentions to Diffing for BSN

If the framework cannot accommodate a given approach in its current form (ex: coarse diffing)

Explore dynamic field-based lists for patches, which would BSN diffing better / more granular

Does it mean that BSN is using a slower v-dom like diffing reactivity approach in the spirit of react instead of a faster fine-grained reactivity system in the spirit of svelte5/solidjs/vue-vapor?

Is it possible to see the reasoning behind this when there is a tendency of moving towars fine-grained updated instead of brute diffing?

examples of recent libraries UI rewritten/moved from partial diffing to full fine grained reactivity include svelte 5 and vue vapor

a good reference is solidjs design

BSN does not currently support reactivity (please read the PR description). There have been many investigations into both fine grained and coarse reactive solutions (both diff-ing and signal-based) / we are well versed in the space at this point. The goal for this phase is to (if possible) build BSN in such a way that it can support both paradigms. Then we can iterate / have an ecosystem (potentially even cross-compatible) where ideas can compete. From there if a winner arises, we can bless it as the "go-to" / default solution.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can confirm there's multiple people in the community who I know are planning to experiment with different approaches. I'm looking forward to building a coarse-grained diffing system, and I'll bet @viridia will build a fine-grained solution.

I'm really pleased with this reactivity-agnostic approach, let's us avoid a big bikeshed and do more incremental exploratory work.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On one hand:

build BSN in such a way that it can support both paradigms ... where ideas can compete

On the other hand:

We want to define scenes as patches of Templates.

Can a diff-based system be the foundation of an efficient implementation of fine-grained reactivity? The suggestion seems to be that this is supposed to be a neutral foundation, but I'd argue that it is not. You can't build a (true) reactive system on top of a diff-based system without many compromises.

Copy link
Contributor

@NthTensor NthTensor Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you may be confusing two different things: Inheritance and reactivity. Patches are just for inheritance. Personally, I don't see how they relate to reactivity (or incrementalization) at all.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NthTensor Uhh... Gotcha. They're more like layers then, right? Maybe the confusing "patch" terminology could be avoided.

Copy link

@3xau1o 3xau1o Jul 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build BSN in such a way that it can support both paradigms ... where ideas can compete

this is only possible if BSN is only a DSL syntax like JSX which is used in both Reactjs and Solidjs
that will leave BSN as a tree description tool rather that a composable component system

there is also the need to realize that

  • fine grained reactivity is mostly compile time
  • diffing is mostly runtime

they're so different, mostly opposite, that's why Vue.js vapor was mostly a rewrite instead of an upgrade from Vue.js 3 vdom, Vue Vapor and Vue3 are not compatible, they only use the same vue syntax, same as React and Solidjs use JSX

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fine grained reactivity is mostly compile time

I think you might be mixing up unrelated things. The compilation you're speaking of in e.g. SolidJS is compiling away the framework into plain JS functions. This has no relevance to the Bevy / Rust case...

Copy link

@3xau1o 3xau1o Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think you are missing the point, which is that signals allow update effects to be known as a directed graph, that's why brute diffing like React does it's unrequired, instead fields are simply directly updated when deps change without recomposing the tree

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is only possible if BSN is only a DSL syntax like JSX

I view BSN as being a DSL like JSX. We're all familiar with how signals work, there's about five to ten different implementations for bevy already (and five to ten diffing incrementalism implementations). Both will be possible using BSN, and in fact people in the bevy discord have already demonstrated some prototypes of signal-like systems using this PR.

"bevy_image",
"bevy_mesh",
"bevy_camera",
Expand Down Expand Up @@ -178,6 +179,7 @@ default = [
"wayland",
"debug",
"zstd_rust",
"experimental_bevy_feathers",
]

# Recommended defaults for no_std applications
Expand Down Expand Up @@ -240,6 +242,9 @@ bevy_render = ["bevy_internal/bevy_render"]
# Provides scene functionality
bevy_scene = ["bevy_internal/bevy_scene"]

# Provides scene functionality
bevy_scene2 = ["bevy_internal/bevy_scene2", "bevy_asset"]

# Provides raytraced lighting (experimental)
bevy_solari = ["bevy_internal/bevy_solari"]

Expand Down Expand Up @@ -612,6 +617,7 @@ bevy_image = { path = "crates/bevy_image", version = "0.17.1", default-features
bevy_reflect = { path = "crates/bevy_reflect", version = "0.17.1", default-features = false }
bevy_render = { path = "crates/bevy_render", version = "0.17.1", default-features = false }
bevy_state = { path = "crates/bevy_state", version = "0.17.1", default-features = false }
bevy_scene2 = { path = "crates/bevy_scene2", version = "0.17.1", default-features = false }
# Needed to poll Task examples
futures-lite = "2.0.1"
futures-timer = { version = "3", features = ["wasm-bindgen", "gloo-timers"] }
Expand All @@ -622,6 +628,7 @@ event-listener = "5.3.0"
anyhow = "1"
accesskit = "0.21"
nonmax = "0.5"
variadics_please = "1"

[target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
ureq = { version = "3.0.8", features = ["json"] }
Expand Down Expand Up @@ -2824,6 +2831,14 @@ description = "Demonstrates loading from and saving scenes to files"
category = "Scene"
wasm = false

[[example]]
name = "bsn"
path = "examples/scene/bsn.rs"

[[example]]
name = "ui_scene"
path = "examples/scene/ui_scene.rs"

# Shaders
[[package.metadata.example_category]]
name = "Shaders"
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_a11y/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ impl ManageAccessibilityUpdates {
///
/// This behavior may or may not be intended, so please utilize
/// `AccessibilityNode`s with care.
#[derive(Component, Clone, Deref, DerefMut)]
#[derive(Component, Clone, Deref, DerefMut, Default)]
#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
pub struct AccessibilityNode(
/// A representation of this component's entity to `AccessKit`.
Expand Down
8 changes: 7 additions & 1 deletion crates/bevy_animation/src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,16 @@ pub struct AnimationGraph {
}

/// A [`Handle`] to the [`AnimationGraph`] to be used by the [`AnimationPlayer`](crate::AnimationPlayer) on the same entity.
#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)]
#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, PartialEq, Eq, From)]
#[reflect(Component, Default, Clone)]
pub struct AnimationGraphHandle(pub Handle<AnimationGraph>);

impl Default for AnimationGraphHandle {
fn default() -> Self {
Self(Handle::default())
}
}

impl From<AnimationGraphHandle> for AssetId<AnimationGraph> {
fn from(handle: AnimationGraphHandle) -> Self {
handle.id()
Expand Down
47 changes: 41 additions & 6 deletions crates/bevy_asset/src/handle.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
use crate::{
meta::MetaTransform, Asset, AssetId, AssetIndexAllocator, AssetPath, InternalAssetId,
UntypedAssetId,
meta::MetaTransform, Asset, AssetId, AssetIndexAllocator, AssetPath, AssetServer,
InternalAssetId, UntypedAssetId,
};
use alloc::sync::Arc;
use bevy_reflect::{std_traits::ReflectDefault, Reflect, TypePath};
use bevy_ecs::{
error::Result,
template::{GetTemplate, Template, TemplateContext},
};
use bevy_reflect::{Reflect, TypePath};
use core::{
any::TypeId,
hash::{Hash, Hasher},
Expand Down Expand Up @@ -130,7 +134,7 @@ impl core::fmt::Debug for StrongHandle {
///
/// [`Handle::Strong`], via [`StrongHandle`] also provides access to useful [`Asset`] metadata, such as the [`AssetPath`] (if it exists).
#[derive(Reflect)]
#[reflect(Default, Debug, Hash, PartialEq, Clone)]
#[reflect(Debug, Hash, PartialEq, Clone)]
pub enum Handle<A: Asset> {
/// A "strong" reference to a live (or loading) [`Asset`]. If a [`Handle`] is [`Handle::Strong`], the [`Asset`] will be kept
/// alive until the [`Handle`] is dropped. Strong handles also provide access to additional asset metadata.
Expand All @@ -150,6 +154,9 @@ impl<T: Asset> Clone for Handle<T> {
}

impl<A: Asset> Handle<A> {
pub fn default() -> Self {
Handle::Uuid(AssetId::<A>::DEFAULT_UUID, PhantomData)
}
/// Returns the [`AssetId`] of this [`Asset`].
#[inline]
pub fn id(&self) -> AssetId<A> {
Expand Down Expand Up @@ -189,9 +196,37 @@ impl<A: Asset> Handle<A> {
}
}

impl<A: Asset> Default for Handle<A> {
impl<T: Asset> GetTemplate for Handle<T> {
type Template = HandleTemplate<T>;
}

pub struct HandleTemplate<T> {
path: AssetPath<'static>,
marker: PhantomData<T>,
}

impl<T> Default for HandleTemplate<T> {
fn default() -> Self {
Handle::Uuid(AssetId::<A>::DEFAULT_UUID, PhantomData)
Self {
path: Default::default(),
marker: Default::default(),
}
}
}

impl<I: Into<AssetPath<'static>>, T> From<I> for HandleTemplate<T> {
fn from(value: I) -> Self {
Self {
path: value.into(),
marker: PhantomData,
}
}
}

impl<T: Asset> Template for HandleTemplate<T> {
type Output = Handle<T>;
fn build(&mut self, context: &mut TemplateContext) -> Result<Handle<T>> {
Ok(context.resource::<AssetServer>().load(&self.path))
}
}

Expand Down
30 changes: 10 additions & 20 deletions crates/bevy_asset/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,12 @@ impl<A: Asset> VisitAssetDependencies for Option<Handle<A>> {
}
}

impl VisitAssetDependencies for UntypedAssetId {
fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
visit(*self);
}
}

impl VisitAssetDependencies for UntypedHandle {
fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
visit(self.id());
Expand Down Expand Up @@ -503,34 +509,18 @@ impl<const N: usize> VisitAssetDependencies for [UntypedHandle; N] {
}
}

impl<A: Asset> VisitAssetDependencies for Vec<Handle<A>> {
fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
for dependency in self {
visit(dependency.id().untyped());
}
}
}

impl VisitAssetDependencies for Vec<UntypedHandle> {
fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
for dependency in self {
visit(dependency.id());
}
}
}

impl<A: Asset> VisitAssetDependencies for HashSet<Handle<A>> {
impl<V: VisitAssetDependencies> VisitAssetDependencies for Vec<V> {
fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
for dependency in self {
visit(dependency.id().untyped());
dependency.visit_dependencies(visit);
}
}
}

impl VisitAssetDependencies for HashSet<UntypedHandle> {
impl<V: VisitAssetDependencies> VisitAssetDependencies for HashSet<V> {
fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
for dependency in self {
visit(dependency.id());
dependency.visit_dependencies(visit);
}
}
}
Expand Down
14 changes: 14 additions & 0 deletions crates/bevy_asset/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,20 @@ impl AssetServer {
self.load_asset(LoadedAsset::new_with_dependencies(asset))
}

// TODO: this is a hack: this allows the asset to pretend to be from the path, but this will cause issues in practice
#[must_use = "not using the returned strong handle may result in the unexpected release of the asset"]
pub fn load_with_path<'a, A: Asset>(
&self,
path: impl Into<AssetPath<'a>>,
asset: A,
) -> Handle<A> {
let loaded_asset: LoadedAsset<A> = asset.into();
let erased_loaded_asset: ErasedLoadedAsset = loaded_asset.into();
let path: AssetPath = path.into();
self.load_asset_untyped(Some(path.into_owned()), erased_loaded_asset)
.typed_debug_checked()
}

pub(crate) fn load_asset<A: Asset>(&self, asset: impl Into<LoadedAsset<A>>) -> Handle<A> {
let loaded_asset: LoadedAsset<A> = asset.into();
let erased_loaded_asset: ErasedLoadedAsset = loaded_asset.into();
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_ecs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ log = { version = "0.4", default-features = false }
bumpalo = "3"
subsecond = { version = "0.7.0-rc.0", optional = true }
slotmap = { version = "1.0.7", default-features = false }
downcast-rs = { version = "2", default-features = false, features = ["std"] }

concurrent-queue = { version = "2.5.0", default-features = false }
[target.'cfg(not(all(target_has_atomic = "8", target_has_atomic = "16", target_has_atomic = "32", target_has_atomic = "64", target_has_atomic = "ptr")))'.dependencies]
Expand Down
14 changes: 14 additions & 0 deletions crates/bevy_ecs/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ mod event;
mod message;
mod query_data;
mod query_filter;
mod template;
mod variant_defaults;
mod world_query;

use crate::{
Expand Down Expand Up @@ -754,3 +756,15 @@ pub fn derive_from_world(input: TokenStream) -> TokenStream {
}
})
}

/// Derives GetTemplate.
#[proc_macro_derive(GetTemplate, attributes(template, default))]
pub fn derive_get_template(input: TokenStream) -> TokenStream {
template::derive_get_template(input)
}

/// Derives VariantDefaults.
#[proc_macro_derive(VariantDefaults)]
pub fn derive_variant_defaults(input: TokenStream) -> TokenStream {
variant_defaults::derive_variant_defaults(input)
}
Loading