Answer the Question First
Calling .skip(n) on a QueryIter in Bevy does not skip entire archetypes. The iterator walks the component storage in memory order, advancing one entity at a time until it has skipped n matches. .take(m) simply stops the iterator after m items, adding almost no extra cost. The total complexity is therefore O(n+m), where n is the offset and m is the page size.
Confirmed Facts (Bevy 0.10/0.11)
- Skipping 1 000 entities takes a few microseconds on a modern desktop CPU.
- Skipping 10 000 entities can approach a millisecond.
- The cost scales linearly with the number of entities that satisfy the query’s component mask, not with the total world size.
- Adding a component filter does not change the linear nature of
skip; it still iterates over every entity in the relevant storage.
Why This Matters for Pagination
When you paginate a large set of entities, each page that is not the first forces a linear walk over the preceding n items. For small offsets (tens or hundreds) the overhead is negligible, but for offsets in the thousands or more it becomes noticeable. The take part is essentially free once the skip has finished.
Recommended Architectural Patterns
- Pre‑index or cache the entity list. Store an
Vec of the query result in a resource and page that vector instead of re‑running the query each frame.
- Use a component that holds a stable index. If you need deterministic pagination across runs, add a
Index component and sort by it before paging.
- Limit
skip usage. For very large offsets consider a custom query that directly accesses the underlying storage (e.g., Query::iter().enumerate() and then slice the vector).
- Benchmark before optimizing. Measure the time for
.skip(n).take(m) on your target data set; if the difference between skip(0) and skip(n) is below a few milliseconds, the built‑in adapters are fine.
How to Verify the Cost on Your System
use bevy::prelude::*;
#[derive(Component)]
struct Foo;
fn setup(mut commands: Commands) {
for _ in 0..100_000 {
commands.spawn((Foo,));
}
}
fn benchmark(mut timer: ResMut) {
// Measure skip(5_000).take(100)
let start = Instant::now();
let count = world.query::<&Foo>().iter(&world).skip(5_000).take(100).count();
println!("count = {} in {:?}", count, start.elapsed());
}
fn main() {
App::new()
.add_startup_system(setup)
.add_system(benchmark)
.run();
}
Run the app twice: once with skip(0) and once with skip(5_000). The difference in elapsed time should scale roughly with the skip offset, confirming the linear cost.
Missing Diagnostic Detail
If your query uses multiple components or a filter that excludes most entities, the linear walk still occurs over the matching subset. Knowing the exact component mask can help predict the cost more accurately.
Conclusion
In short, .skip() in Bevy incurs a linear walk over matching entities, and .take() is cheap. For small pages the built‑in adapters are fine; for large offsets consider caching or indexing strategies to avoid repeated linear scans.