Increase upper limit of children! (#18865)

# Objective

Currently, `bevy_ecs`'s `children!` macro only supports spawning up to
twelve children at once. Ideally there would be no limit.

## Solution

`children!` is limited because `SpawnableList`, [the primary trait bound
here](https://docs.rs/bevy/0.16.0-rc.5/bevy/ecs/hierarchy/struct.Children.html#method.spawn),
uses the fake variadics pattern on tuples of up to twelve elements.
However, since a tuple itself implements `SpawnableList`, we can simply
nest tuples of entities when we run out of room.

This PR achieves this using `macro_rules` macros with a bit of brute
force, following [some discussion on
Discord](https://discord.com/channels/691052431525675048/692572690833473578/1362174415458013314).
If we create patterns for lists of up to eleven bundles, then use a
repetition pattern to handle the rest, we can "special-case" the
recursion into a nested tuple.

In principle, this would permit an arbitrary number of children, but
Rust's recursion limits will cut things short at around 1400 elements by
default. Of course, it's generally not a good idea to stick that many
bundles in a single invocation, but it might be worth mentioning in the
docs.

## Implementation notes

### Why are cases 0-11 expanded by hand?

We could make use of a tertiary macro:

```rs
macro_rules! recursive_spawn {
    // so that this...
    ($a:expr, $b:expr) => {
        (
            $crate::spawn::Spawn($a),
            $crate::spawn::Spawn($b),
        )
    };
    
    // becomes this...
    ($a:expr, $b:expr) => {
        $crate::spawn_tuple!($a, $b)
    };
}
```

But I already feel a little bad exporting `recursive_spawn`. I'd really
like to avoid exposing more internals, even if they are annotated with
`#[doc(hidden)]`. If I had to guess, I'd say it'll also make the
expansion a tiny bit slower.

### Do we really need to handle up to twelve elements in the macro?

The macro is a little long, but doing it this way maximizes the
"flatness" of the types to be spawned. This should improve the codegen a
bit and makes the macro output a little bit easier to look at.

## Future work

The `related!` macro is essentially the same as `children!`, so if this
direction is accepted, `related!` should receive the same treatment. I
imagine we'd want to extract out the `recursive_spawn` macro into its
own file since it can be used for both. If this should be tackled in
this PR, let me know!

## Testing

This change is fairly trivial, but I added a single test to verify that
it compiles and nothing goes wrong once recursion starts happening. It's
pretty easy to verify that the change works in practice -- just spawn
over twelve entities as children at once!
This commit is contained in:
Corvus 2025-05-05 18:58:30 -06:00 committed by GitHub
parent aadd3a3ec2
commit a312170749
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 180 additions and 2 deletions

View File

@ -511,7 +511,7 @@ pub fn validate_parent_has_component<C: Component>(
#[macro_export]
macro_rules! children {
[$($child:expr),*$(,)?] => {
$crate::hierarchy::Children::spawn(($($crate::spawn::Spawn($child)),*))
$crate::hierarchy::Children::spawn($crate::recursive_spawn!($($child),*))
};
}
@ -731,6 +731,39 @@ mod tests {
assert_eq!(world.entity(id).get::<Children>().unwrap().len(), 2,);
}
#[test]
fn spawn_many_children() {
let mut world = World::new();
// 12 children should result in a flat tuple
let id = world
.spawn(children![(), (), (), (), (), (), (), (), (), (), (), ()])
.id();
assert_eq!(world.entity(id).get::<Children>().unwrap().len(), 12,);
// 13 will start nesting, but should nonetheless produce a flat hierarchy
let id = world
.spawn(children![
(),
(),
(),
(),
(),
(),
(),
(),
(),
(),
(),
(),
(),
])
.id();
assert_eq!(world.entity(id).get::<Children>().unwrap().len(), 13,);
}
#[test]
fn replace_children() {
let mut world = World::new();

View File

@ -356,6 +356,151 @@ impl<T: RelationshipTarget> SpawnRelated for T {
#[macro_export]
macro_rules! related {
($relationship_target:ty [$($child:expr),*$(,)?]) => {
<$relationship_target>::spawn(($($crate::spawn::Spawn($child)),*))
<$relationship_target>::spawn($crate::recursive_spawn!($($child),*))
};
}
// A tail-recursive spawn utility.
//
// Since `SpawnableList` is only implemented for tuples
// up to twelve elements long, this macro will nest
// longer sequences recursively. By default, this recursion
// will top out at around 1400 elements, but it would be
// ill-advised to spawn that many entities with this method.
//
// For spawning large batches of entities at a time,
// consider `SpawnIter` or eagerly spawning with `Commands`.
#[macro_export]
#[doc(hidden)]
macro_rules! recursive_spawn {
// direct expansion
($a:expr) => {
$crate::spawn::Spawn($a)
};
($a:expr, $b:expr) => {
(
$crate::spawn::Spawn($a),
$crate::spawn::Spawn($b),
)
};
($a:expr, $b:expr, $c:expr) => {
(
$crate::spawn::Spawn($a),
$crate::spawn::Spawn($b),
$crate::spawn::Spawn($c),
)
};
($a:expr, $b:expr, $c:expr, $d:expr) => {
(
$crate::spawn::Spawn($a),
$crate::spawn::Spawn($b),
$crate::spawn::Spawn($c),
$crate::spawn::Spawn($d),
)
};
($a:expr, $b:expr, $c:expr, $d:expr, $e:expr) => {
(
$crate::spawn::Spawn($a),
$crate::spawn::Spawn($b),
$crate::spawn::Spawn($c),
$crate::spawn::Spawn($d),
$crate::spawn::Spawn($e),
)
};
($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr) => {
(
$crate::spawn::Spawn($a),
$crate::spawn::Spawn($b),
$crate::spawn::Spawn($c),
$crate::spawn::Spawn($d),
$crate::spawn::Spawn($e),
$crate::spawn::Spawn($f),
)
};
($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr) => {
(
$crate::spawn::Spawn($a),
$crate::spawn::Spawn($b),
$crate::spawn::Spawn($c),
$crate::spawn::Spawn($d),
$crate::spawn::Spawn($e),
$crate::spawn::Spawn($f),
$crate::spawn::Spawn($g),
)
};
($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr) => {
(
$crate::spawn::Spawn($a),
$crate::spawn::Spawn($b),
$crate::spawn::Spawn($c),
$crate::spawn::Spawn($d),
$crate::spawn::Spawn($e),
$crate::spawn::Spawn($f),
$crate::spawn::Spawn($g),
$crate::spawn::Spawn($h),
)
};
($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $i:expr) => {
(
$crate::spawn::Spawn($a),
$crate::spawn::Spawn($b),
$crate::spawn::Spawn($c),
$crate::spawn::Spawn($d),
$crate::spawn::Spawn($e),
$crate::spawn::Spawn($f),
$crate::spawn::Spawn($g),
$crate::spawn::Spawn($h),
$crate::spawn::Spawn($i),
)
};
($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $i:expr, $j:expr) => {
(
$crate::spawn::Spawn($a),
$crate::spawn::Spawn($b),
$crate::spawn::Spawn($c),
$crate::spawn::Spawn($d),
$crate::spawn::Spawn($e),
$crate::spawn::Spawn($f),
$crate::spawn::Spawn($g),
$crate::spawn::Spawn($h),
$crate::spawn::Spawn($i),
$crate::spawn::Spawn($j),
)
};
($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $i:expr, $j:expr, $k:expr) => {
(
$crate::spawn::Spawn($a),
$crate::spawn::Spawn($b),
$crate::spawn::Spawn($c),
$crate::spawn::Spawn($d),
$crate::spawn::Spawn($e),
$crate::spawn::Spawn($f),
$crate::spawn::Spawn($g),
$crate::spawn::Spawn($h),
$crate::spawn::Spawn($i),
$crate::spawn::Spawn($j),
$crate::spawn::Spawn($k),
)
};
// recursive expansion
(
$a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr,
$g:expr, $h:expr, $i:expr, $j:expr, $k:expr, $($rest:expr),*
) => {
(
$crate::spawn::Spawn($a),
$crate::spawn::Spawn($b),
$crate::spawn::Spawn($c),
$crate::spawn::Spawn($d),
$crate::spawn::Spawn($e),
$crate::spawn::Spawn($f),
$crate::spawn::Spawn($g),
$crate::spawn::Spawn($h),
$crate::spawn::Spawn($i),
$crate::spawn::Spawn($j),
$crate::spawn::Spawn($k),
$crate::recursive_spawn!($($rest),*)
)
};
}