├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── examples └── bouncing_balls.rs ├── rustfmt.toml └── src ├── index.rs ├── lib.rs ├── refresh_policy.rs ├── storage.rs └── unique_multimap.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Cargo 2 | /target 3 | Cargo.lock 4 | 5 | # IDEs 6 | /.idea 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Change Log 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [0.7.0] - 2025-04-24 9 | 10 | Bevy version updated to `0.16`. 11 | 12 | ### Added 13 | - Added a new `RefereshPolicy`: `WhenInserted`. Uses observers to update 14 | the index as components are replaced rather than all at once later. 15 | Intended to be used with immutable components. 16 | 17 | ### Changed 18 | - Internal equality checks have been replaced by `const` functions to 19 | allow for better optimizations. 20 | 21 | ## [0.6.0] - 2024-12-01 22 | 23 | Bevy version updated to `0.14`. 24 | 25 | ### Changed 26 | - Type signature of `IndexStorage::removal_observer` updated to match 27 | [bevy's change to the `Observer` type](https://github.com/bevyengine/bevy/pull/15151). 28 | 29 | ## [0.5.0] - 2024-07-04 30 | 31 | Bevy version updated to `0.14`. 32 | 33 | ### Added 34 | - Added derives for `Eq`, `PartialEq`, `Debug`, `Copy`, and `Clone` to 35 | `UniquenessError`. 36 | 37 | ### Changed 38 | - `HashmapStorage` now uses Observers instead of `RemovedComponents` to 39 | know when to clean up stale entries. 40 | - `IndexRefreshPolicy` has been changed from a trait to an enum because 41 | the switch to Observers should eliminate the need for complex 42 | refresh policy configurations. 43 | - The `RefereshPolicy` associated type of `IndexInfo` is now a constant 44 | called `REFRESH_POLICY`. 45 | - Calling `lookup` directly on a storage resource no longer refreshes the 46 | storage if refresh policy is `WhenUsed`. This was moved up to `Index`'s 47 | `refresh` method. 48 | 49 | ### Removed 50 | - Removed concrete implementations of the old `IndexRefreshPolicy` trait 51 | such as `ConservativeRefreshPolicy`. 52 | 53 | ## [0.4.1] - 2024-04-20 54 | 55 | ### Added 56 | - Added `reflect` crate feature with `Reflect` derives for storage types. 57 | 58 | ## [0.4.0] - 2024-02-17 59 | 60 | Bevy version updated to `0.13`. 61 | 62 | ### Changed 63 | - `Index::lookup` now returns an `impl Iterator` instead of a 64 | `HashSet` to avoid unnecessary allocations. 65 | - `Index::refresh` is now a no-op if it was previously called during this `Tick`. 66 | `Index::force_refresh` can now be used to refresh the index unconditionally. 67 | 68 | ### Added 69 | - Added `Index::force_refresh` (see above). 70 | - Added `Index::lookup_single` and `Index::single` (panicking version) for 71 | cases when a lookup is expected to return only one `Entity`. 72 | - Added the `RefereshPolicy` associated type to the `IndexInfo` trait to allow 73 | specifying storage refresh behavior. 74 | - Also added the `IndexRefreshPolicy` trait to support this, as well as 5 75 | concrete policies that can be used, such as `ConservativeRefreshPolicy`. 76 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_mod_index" 3 | version = "0.7.0" 4 | description = "Allows using indexes to efficiently query for components by their values in the game engine Bevy." 5 | keywords = [ "index", "indexes", "indices", "bevy", "bevyengine"] 6 | categories = [ "game-development", "data-structures" ] 7 | homepage = "https://github.com/chrisjuchem/bevy_mod_index" 8 | repository = "https://github.com/chrisjuchem/bevy_mod_index" 9 | readme = "README.md" 10 | license = "MIT" 11 | edition = "2021" 12 | 13 | [profile.dev.package."*"] 14 | opt-level = 3 15 | 16 | [dependencies.bevy] 17 | version = "0.16.0" 18 | default-features = false 19 | 20 | # Dependencies for examples 21 | [dev-dependencies] 22 | rand = "0.8.5" 23 | 24 | [dev-dependencies.bevy] 25 | version = "0.16.0" 26 | features = ["dynamic_linking"] 27 | 28 | [features] 29 | reflect = [] 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bevy_mod_index 2 | 3 | [![](https://img.shields.io/crates/v/bevy_mod_index)](https://crates.io/crates/bevy_mod_index) 4 | [![](https://docs.rs/bevy_mod_index/badge.svg)](https://docs.rs/bevy_mod_index/latest/bevy_mod_index) 5 | [![](https://img.shields.io/crates/d/bevy_mod_index)](https://crates.io/crates/bevy_mod_index) 6 | [![](https://img.shields.io/badge/Bevy%20version-v0.16.x-orange)](https://crates.io/crates/bevy/0.16.0) 7 | [![](https://img.shields.io/github/license/chrisjuchem/bevy_mod_index?color=blue)](https://github.com/chrisjuchem/bevy_mod_index/blob/main/LICENSE) 8 | [![](https://img.shields.io/github/stars/chrisjuchem/bevy_mod_index?color=green)](https://github.com/chrisjuchem/bevy_mod_index/stargazers) 9 | 10 | A Rust crate that allows efficient querying for components by their values in 11 | the game engine [Bevy]. 12 | 13 | ## Compatability 14 | | Bevy Version | `bevy_mod_index` Version | 15 | |--------------|--------------------------| 16 | | 0.16 | 0.7.x | 17 | | 0.15 | 0.6.x | 18 | | 0.14 | 0.5.x | 19 | | 0.13 | 0.4.x | 20 | | 0.12 | 0.3.0 | 21 | | 0.11 | 0.2.0 | 22 | | 0.10 | 0.1.0 | 23 | 24 | ### Bevy Release Candidates 25 | 26 | I do not publish release candidates for this crate corresponding to Bevy's, 27 | but I do try to ensure that the main branch is compatible with at least RC. When there 28 | are active Bevy RCs, the table below will include git commits you can use with 29 | each RC version. 30 | 31 | | Bevy Version | `bevy_mod_index` SHA | 32 | |--------------|------------------------------------------| 33 | 34 | ## Features 35 | | Feature name | Description | 36 | |--------------|------------------------------------------------| 37 | | `reflect` | Adds reflect derives to the storage resources. | 38 | 39 | ## Use Cases 40 | It is quite common to want to write code in a system that only operates on 41 | components that have a certain value, e.g.: 42 | ```rust 43 | fn move_living_players(mut players: Query<&mut Transform, &Player>) { 44 | for (mut transform, player) in &players { 45 | if player.is_alive() { 46 | move_player(transform); 47 | } 48 | } 49 | } 50 | ``` 51 | 52 | With an index, we can change the code to: 53 | ```rust 54 | fn move_living_players( 55 | mut transforms: Query<&mut Transform>, 56 | player_alive_idx: Index 57 | ) { 58 | for entity in &player_alive_idx.get(true) { 59 | transforms.get(entity).unwrap().move_player(transform); 60 | } 61 | } 62 | ``` 63 | 64 | There are a few cases where a change like this may be beneficial: 65 | - If `is_alive` is expensive to calculate, indexes can we can save work by 66 | caching the results and only recomputing when the data actually changes. 67 | - If the component data that the result is calculated from doesn't change 68 | often, we can use the cached values across frames. 69 | - If components tend to change only in the beginning of a frame, and the 70 | results are needed multiple times later on, we can use the cached values 71 | across different systems, (or even the same system if it had been 72 | calculated multiple times). 73 | - If we don't care too much about performance, indexes can provide a nicer 74 | API to work with. 75 | 76 | Indexes add a non-zero amount of overhead, though, so introducing them can 77 | make your systems slower. Make sure to profile your systems before and after 78 | introducing indexes if you care about performance. 79 | 80 | ## Getting Started 81 | First, import the prelude. 82 | ```rust 83 | use bevy_mod_index::prelude::*; 84 | ``` 85 | 86 | Next, implement the `IndexInfo` trait. If your component only needs one index, 87 | you can implement this trait directly on the component. If you need more than 88 | one, you can use a simple unit struct for each index beyond the first. You can 89 | also use unit structs to give more descriptive names, even if you only need one 90 | index. 91 | 92 | You must specify: 93 | - the type of component to be indexed, 94 | - the type of value that you want to be able to use for lookups, 95 | - a function for calculating that value for a component, 96 | - how to store the relationship between an entity and the value calculated from 97 | its appropriate component, and 98 | - when the index should refresh itself with the latest data. 99 | ```rust 100 | struct NearOrigin {} 101 | impl IndexInfo for NearOrigin { 102 | type Component = Transform; 103 | type Value = bool; 104 | type Storage = HashmapStorage; 105 | const REFRESH_POLICY: IndexRefreshPolicy = IndexRefreshPolicy::WhenRun; 106 | 107 | fn value(t: &Transform) -> bool { 108 | t.translation.length() < 5.0 109 | } 110 | } 111 | ``` 112 | 113 | Finally, include the `Index` system param in your systems and use it to query 114 | for entities! 115 | ```rust 116 | fn count_players_and_enemies_near_spawn( 117 | players: Query<(), With<(&Player, &Transform)>>, 118 | enemies: Query<(), With<(&Enemy, &Transform)>>, 119 | index: Index, 120 | ) { 121 | let (mut player_count, mut enemy_count) = (0, 0); 122 | 123 | let entities_near_spawn: HashSet = index.lookup(true); 124 | for entity in entities_near_spawn.into_iter() { 125 | if let Ok(()) = players.get(entity) { 126 | player_count += 1; 127 | } 128 | if let Ok(()) = enemies.get(entity) { 129 | enemy_count += 1; 130 | } 131 | } 132 | 133 | println!("There are {} players and {} enemies near spawn!", player_count, enemy_count) 134 | } 135 | ``` 136 | 137 | ## Storage Implementations 138 | `HashmapStorage` uses a `Resource` to cache a mapping between `Entity`s and the values computed 139 | from their components. It uses a custom `SystemParam` to fetch the data that it needs to update 140 | itself when needed. This is a good default choice, especially when the number of `Entity`s returned 141 | by a `lookup` is expected to be just a small percentage of those in the entire query. 142 | 143 | `NoStorage`, as the name implies, does not store any index data. Instead, it loops over all 144 | data each time it is queried, computing the `value` function for each component, exactly like 145 | the first `move_living_players` example above. This option allows you to use the index API 146 | without incurring as much overhead as `HashmapStorage` (though still more than directly looping 147 | over all components yourself). 148 | 149 | ## Refresh Policies 150 | Indexes using `HashmapStorage` must be periodically `refresh`ed for them to be able to accurately 151 | reflect the status of components as they are added, changed, and removed. Specifying an 152 | `IndexRefreshPolicy` configures the index to automatically refresh itself for you with one of 153 | several different timings. 154 | 155 | Indexes for immutable components should use `IndexRefreshPolicy::WhenInserted`, which uses observers 156 | to update the index and avoid checking if refreshes are necessary every frame. For mutable components, 157 | `IndexRefreshPolicy::WhenRun` is a good default if you're not sure which refresh policy to use, but 158 | other policies can be found [in the docs](https://docs.rs/bevy_mod_index/latest/bevy_mod_index/refresh_policy/enum.IndexRefreshPolicy.html). 159 | 160 | ## Reflection 161 | Reflection for the storage resources can be enabled by selecting the optional `reflect` crate 162 | feature. This is mainly useful for inspecting the underlying storage with `bevy-inspector-egui`. 163 | 164 | In order for the resources to appear in the inspector, you will need to manually register the 165 | storage for each index, e.g. `app.register_type::>();` Make sure that 166 | you also derive `Reflect` for your `IndexInfo` type and any associated components/values. 167 | 168 | Note: You should not rely on the internal structure of these resources, since they may change across 169 | releases. 170 | 171 | ## API Stability 172 | Consider the API to be extremely unstable as I experiment with what names and patterns feel 173 | most natural and expressive, and also work on supporting new features. 174 | 175 | ## Performance 176 | I have not put a lot of effort into optimizing the performance indexes yet. However, I have 177 | done some initial tests under to get a sense of approximately how much overhead they add. 178 | 179 | With 1 million entities, while none of the components change frame-to-frame, using the 180 | component itself as the index value, operation on ~300 entities takes: 181 | - 2-4x as long as a naive iteration when using `NoStorage`. 182 | - 3-5x as long as a naive iteration when using `HashmapStorage`. 183 | 184 | With the same setup, except that 5% of the entities are updated every frame, performance for 185 | `HashmapStorage` drops to 30-40x as long as naive iteration. 186 | 187 | I am currently in the process of adding more concrete benchmarks, and I do have some plans 188 | for changes that will affect performance. 189 | 190 | ## Get in contact 191 | If you have suggestions for improvements to the API, or ideas about improving performance, 192 | I'd love to hear them. File an issue, or even better, reach out in the `bevy_mod_index` 193 | `#crate-help` thread on Bevy's [discord]. 194 | 195 | ## Troubleshooting 196 | - `Query<(bevy_ecs::entity::Entity, &bevy_mod_index::index::test::Number, bevy_ecs::query::fetch::ChangeTrackers), ()> in system bevy_mod_index::index::test::adder_some::{{closure}} accesses component(s) bevy_mod_index::index::test::Number in a way that conflicts with a previous system parameter. Consider using ``Without`` to create disjoint Queries or merging conflicting Queries into a ``ParamSet``.` 197 | - Indexes use a read-only query of their components to update the index before it is used. 198 | If you have a query that mutably access these components in the same system as an `Index`, 199 | you can [combine them into a `ParamSet`][ParamSet]. 200 | 201 | ## Future work 202 | - Docs 203 | - Option to update the index when components change instead of when the index is used. 204 | - Naively, requires engine support for custom `DerefMut` hooks, but this would likely 205 | add overhead even when indexes aren't used. Other solutions may be possible. 206 | - Perhaps the `Component` derive will one day accept an attribute that enables/disables 207 | change detection by specifying `&mut T` or `Mut` as the reference type, and we could 208 | add a third option for `IndexedMut` that would automatically look up all indexes for 209 | the component in some resource and add the entity to a list to be re-indexed. 210 | - See https://github.com/bevyengine/bevy/pull/7499 for a draft implementation. 211 | - More storage options besides `HashMap`. 212 | - Sorted container to allow for querying "nearby" values. 213 | - 1D data should be simple enough, but would also like to support kd-trees for positions. 214 | - Indexes over more than one `Component`. 215 | - Indexes for subsets of a `Component` 216 | - Replacing Components with arbitrary queries may cover both of these cases. 217 | - Derive for simple cases of IndexInfo where the component itself is used as the value. 218 | 219 | [Bevy]: https://bevyengine.org/ 220 | [discord]: https://discord.gg/bevy 221 | [ParamSet]: https://docs.rs/bevy/latest/bevy/ecs/system/struct.ParamSet.html 222 | -------------------------------------------------------------------------------- /examples/bouncing_balls.rs: -------------------------------------------------------------------------------- 1 | use std::f32::consts::PI; 2 | 3 | use bevy::{ 4 | color::palettes, 5 | math::Vec3Swizzles, 6 | platform::collections::hash_map::HashMap, 7 | prelude::*, 8 | }; 9 | use bevy_mod_index::prelude::*; 10 | use rand::{rngs::ThreadRng, seq::IteratorRandom, thread_rng, Rng}; 11 | 12 | const N_BALLS: usize = 1000; 13 | const MAX_WIDTH: f32 = 640.; 14 | const MAX_HEIGHT: f32 = 360.; 15 | 16 | #[derive(Component)] 17 | struct Velocity(Vec2); 18 | #[derive(Component)] 19 | struct Size(f32); 20 | 21 | #[derive(Resource, Default)] 22 | struct Colors(Vec>); 23 | impl Colors { 24 | fn random(&self, rng: &mut ThreadRng) -> MeshMaterial2d { 25 | self.0.iter().choose(rng).unwrap().clone() 26 | } 27 | } 28 | 29 | struct RegionIndex; 30 | impl IndexInfo for RegionIndex { 31 | type Component = Transform; 32 | type Value = Region; 33 | type Storage = HashmapStorage; 34 | const REFRESH_POLICY: IndexRefreshPolicy = IndexRefreshPolicy::WhenRun; 35 | 36 | fn value(t: &Transform) -> Region { 37 | get_region(&t.translation.xy()) 38 | } 39 | } 40 | 41 | #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] 42 | enum Region { 43 | TopLeft, 44 | TopCenter, 45 | TopRight, 46 | CenterLeft, 47 | CenterCenter, 48 | CenterRight, 49 | BottomLeft, 50 | BottomCenter, 51 | BottomRight, 52 | } 53 | 54 | fn get_region(v: &Vec2) -> Region { 55 | match (v.x / MAX_WIDTH, v.y / MAX_HEIGHT) { 56 | (x, y) if x < -0.33 && y < -0.33 => Region::BottomLeft, 57 | (x, y) if x < -0.33 && y > 0.33 => Region::TopLeft, 58 | (x, _) if x < -0.33 => Region::CenterLeft, 59 | (x, y) if x > 0.33 && y < -0.33 => Region::BottomRight, 60 | (x, y) if x > 0.33 && y > 0.33 => Region::TopRight, 61 | (x, _) if x > 0.33 => Region::CenterRight, 62 | (_, y) if y < -0.33 => Region::BottomCenter, 63 | (_, y) if y > 0.33 => Region::TopCenter, 64 | (_, _) => Region::CenterCenter, 65 | } 66 | } 67 | 68 | fn setup( 69 | mut commands: Commands, 70 | mut meshes: ResMut>, 71 | mut materials: ResMut>, 72 | mut color_materials: ResMut, 73 | ) { 74 | commands.spawn(Camera2d); 75 | 76 | let colors: [Color; 20] = [ 77 | palettes::css::AQUAMARINE.into(), 78 | palettes::css::BISQUE.into(), 79 | palettes::css::BLUE.into(), 80 | palettes::css::CRIMSON.into(), 81 | palettes::css::DARK_GRAY.into(), 82 | palettes::css::DARK_GREEN.into(), 83 | palettes::css::FUCHSIA.into(), 84 | palettes::css::GOLD.into(), 85 | palettes::css::INDIGO.into(), 86 | palettes::css::PINK.into(), 87 | palettes::css::SALMON.into(), 88 | palettes::css::PURPLE.into(), 89 | palettes::css::OLIVE.into(), 90 | palettes::css::MAROON.into(), 91 | palettes::css::GREEN.into(), 92 | palettes::css::SILVER.into(), 93 | palettes::css::VIOLET.into(), 94 | palettes::css::WHITE.into(), 95 | palettes::css::YELLOW_GREEN.into(), 96 | palettes::css::ORANGE.into(), 97 | ]; 98 | for color in colors { 99 | color_materials 100 | .0 101 | .push(MeshMaterial2d(materials.add(ColorMaterial::from(color)))); 102 | } 103 | 104 | let size_range = 2..8; 105 | let mut mesh_map = HashMap::<_, Mesh2d>::default(); 106 | for x in size_range.clone() { 107 | mesh_map.insert(x, Mesh2d(meshes.add(Circle::new(x as f32)))); 108 | } 109 | 110 | let mut rng = thread_rng(); 111 | for z in 0..N_BALLS { 112 | let size = rng.gen_range(size_range.clone()); 113 | 114 | commands.spawn(( 115 | mesh_map.get(&size).unwrap().clone(), 116 | color_materials.random(&mut rng), 117 | Transform::from_xyz( 118 | (rng.gen::() - 0.5) * MAX_WIDTH, 119 | (rng.gen::() - 0.5) * MAX_HEIGHT, 120 | z as f32, 121 | ), 122 | Velocity(Vec2::from_angle(rng.gen::() * 2. * PI) * (rng.gen::() * 3. + 0.5)), 123 | Size(size as f32), 124 | )); 125 | } 126 | } 127 | 128 | fn move_balls(mut balls: Query<(&mut Transform, &Velocity)>) { 129 | for (mut t, v) in &mut balls { 130 | t.translation += Vec3::from((v.0, 0.)); 131 | } 132 | } 133 | 134 | fn bounce(mut balls: Query<(&Transform, &mut Velocity, &Size)>) { 135 | for (t, mut v, s) in &mut balls { 136 | if t.translation.x - s.0 < -MAX_WIDTH || t.translation.x + s.0 > MAX_WIDTH { 137 | v.0.x *= -1.; 138 | } 139 | if t.translation.y - s.0 < -MAX_HEIGHT || t.translation.y + s.0 > MAX_HEIGHT { 140 | v.0.y *= -1.; 141 | } 142 | } 143 | } 144 | 145 | fn update_colors( 146 | mut index: Index, 147 | colors: Res, 148 | click: Res>, 149 | window: Single<&Window>, 150 | mut commands: Commands, 151 | ) { 152 | if click.just_pressed(MouseButton::Left) { 153 | if let Some(mut pos) = window.cursor_position() { 154 | pos.x -= MAX_WIDTH; 155 | pos.y -= MAX_HEIGHT; 156 | // convert screen space to world space 157 | pos.y = -pos.y; 158 | let cursor_region = get_region(&pos); 159 | 160 | let mat = colors.random(&mut thread_rng()); 161 | for e in index.lookup(&cursor_region) { 162 | commands.entity(e).insert(mat.clone()); 163 | } 164 | } 165 | } 166 | } 167 | 168 | fn main() { 169 | App::new() 170 | .add_plugins(DefaultPlugins) 171 | .insert_resource(Colors::default()) 172 | .add_systems(Startup, setup) 173 | .add_systems(Update, (move_balls, bounce)) 174 | .add_systems(Update, update_colors) 175 | .run(); 176 | } 177 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_layout="HorizontalVertical" 2 | -------------------------------------------------------------------------------- /src/index.rs: -------------------------------------------------------------------------------- 1 | use crate::refresh_policy::{refresh_index_system, IndexRefreshPolicy}; 2 | use crate::storage::IndexStorage; 3 | use bevy::ecs::archetype::Archetype; 4 | use bevy::ecs::component::Tick; 5 | use bevy::ecs::system::{ 6 | ReadOnlySystemParam, 7 | RunSystemOnce, 8 | StaticSystemParam, 9 | SystemMeta, 10 | SystemParam, 11 | }; 12 | use bevy::ecs::world::unsafe_world_cell::UnsafeWorldCell; 13 | use bevy::prelude::*; 14 | use std::hash::Hash; 15 | 16 | /// Implement this trait on your own types to specify how an [`Index`] should behave. 17 | /// 18 | /// If there is a single canonical way to index a [`Component`], you can implement this 19 | /// for that component directly. Otherwise, it is recommended to implement this for a 20 | /// unit struct/enum. 21 | pub trait IndexInfo: Sized + 'static { 22 | /// The type of component to be indexed. 23 | type Component: Component; 24 | /// The type of value to be used when looking up components. 25 | type Value: Send + Sync + Hash + Eq + Clone; 26 | /// The type of storage to use for the index. 27 | type Storage: IndexStorage; 28 | /// Defines when the index should be automatically refreshed. 29 | const REFRESH_POLICY: IndexRefreshPolicy; 30 | 31 | /// The function used by [`Index::lookup`] to determine the value of a component. 32 | /// 33 | /// The values returned by this function are typically cached by the storage, so 34 | /// this should always return the same value given equal [`Component`]s. 35 | fn value(c: &Self::Component) -> Self::Value; 36 | } 37 | 38 | /// A [`SystemParam`] that allows you to lookup [`Component`]s that match a certain value. 39 | pub struct Index<'w, 's, I: IndexInfo + 'static> { 40 | storage: ResMut<'w, I::Storage>, 41 | refresh_data: 42 | StaticSystemParam<'w, 's, >::RefreshData<'static, 'static>>, 43 | } 44 | 45 | /// Error returned by [`Index::lookup_single`] if there is not exactly one Entity with the 46 | /// requested value. 47 | #[derive(Eq, PartialEq, Debug, Copy, Clone)] 48 | pub enum UniquenessError { 49 | /// There were no entities with the requested value. 50 | NoEntities, 51 | /// There was more than one entity with the requested value. 52 | MultipleEntities, 53 | } 54 | 55 | #[doc(hidden)] 56 | /// Thanks Jon https://youtu.be/CWiz_RtA1Hw?t=815 57 | pub trait Captures {} 58 | impl Captures for T {} 59 | 60 | // todo impl deref instead? need to move storage? 61 | impl<'w, 's, I: IndexInfo> Index<'w, 's, I> { 62 | /// Get all of the entities with relevant components that evaluate to the given value 63 | /// using [`I::value`][`IndexInfo::value`]. 64 | /// 65 | /// Refreshes the index if it has not yet been refreshed in this system and the index's 66 | /// [`REFRESH_POLICY`][`IndexInfo::REFRESH_POLICY`] is [`WhenUsed`][`IndexRefreshPolicy::WhenUsed`]. 67 | pub fn lookup<'i, 'self_>( 68 | &'self_ mut self, 69 | val: &'i I::Value, 70 | ) -> impl Iterator + Captures<(&'w (), &'s (), &'self_ (), &'i ())> { 71 | if I::REFRESH_POLICY.is_when_used() { 72 | self.refresh(); 73 | } 74 | self.storage.lookup(val, &mut self.refresh_data) 75 | } 76 | 77 | /// Get the single entity with relevant components that evaluate to the given value 78 | /// using [`I::value`][`IndexInfo::value`]. 79 | /// 80 | /// Refreshes the index if it has not yet been refreshed in this system and the index's 81 | /// [`REFRESH_POLICY`][`IndexInfo::REFRESH_POLICY`] is [`WhenUsed`][`IndexRefreshPolicy::WhenUsed`]. 82 | /// 83 | /// Returns an error if there is not exactly one `Entity` returned by the lookup. 84 | /// See [`Index::single`] for the panicking version. 85 | pub fn lookup_single(&mut self, val: &I::Value) -> Result { 86 | let mut it = self.lookup(val); 87 | match (it.next(), it.next()) { 88 | (None, _) => Err(UniquenessError::NoEntities), 89 | (Some(e), None) => Ok(e), 90 | (Some(_), Some(_)) => Err(UniquenessError::MultipleEntities), 91 | } 92 | } 93 | 94 | /// Get the single entity with relevant components that evaluate to the given value 95 | /// using [`I::value`][`IndexInfo::value`]. 96 | /// 97 | /// Refreshes the index if it has not yet been refreshed in this system and the index's 98 | /// [`REFRESH_POLICY`][`IndexInfo::REFRESH_POLICY`] is [`WhenUsed`][`IndexRefreshPolicy::WhenUsed`]. 99 | /// 100 | /// Panics if there is not exactly one `Entity` returned by the lookup. 101 | /// See [`Index::lookup_single`] for the version that returns a result instead. 102 | pub fn single(&mut self, val: &I::Value) -> Entity { 103 | match self.lookup_single(val) { 104 | Err(UniquenessError::NoEntities) => panic!("Expected 1 entity in index, found 0."), 105 | Ok(e) => e, 106 | Err(UniquenessError::MultipleEntities) => { 107 | panic!("Expected 1 entity in index, found multiple.") 108 | } 109 | } 110 | } 111 | 112 | /// Refresh the underlying [`IndexStorage`] for this index if it hasn't already been refreshed 113 | /// this [`Tick`]. 114 | /// 115 | /// Note: 1 [`Tick`] = 1 system, not 1 frame. 116 | /// 117 | /// This is called automatically at the time specified by the index's [`REFRESH_POLICY`][`IndexInfo::REFRESH_POLICY`]. 118 | pub fn refresh(&mut self) { 119 | self.storage.refresh(&mut self.refresh_data) 120 | } 121 | 122 | /// Unconditionally refresh the underlying [`IndexStorage`] for this index. 123 | /// 124 | /// This must be called before the index will reflect changes made earlier in the same system. 125 | pub fn force_refresh(&mut self) { 126 | self.storage.force_refresh(&mut self.refresh_data) 127 | } 128 | } 129 | 130 | #[doc(hidden)] 131 | pub struct IndexFetchState<'w, 's, I: IndexInfo + 'static> { 132 | storage_state: as SystemParam>::State, 133 | refresh_data_state: >::RefreshData<'static, 'static>, 137 | > as SystemParam>::State, 138 | } 139 | unsafe impl<'w, 's, I> SystemParam for Index<'w, 's, I> 140 | where 141 | I: IndexInfo + 'static, 142 | { 143 | type State = IndexFetchState<'static, 'static, I>; 144 | type Item<'_w, '_s> = Index<'_w, '_s, I>; 145 | fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { 146 | if !world.contains_resource::() { 147 | world.init_resource::(); 148 | if I::REFRESH_POLICY.is_each_frame() { 149 | world 150 | .resource_mut::() 151 | .get_mut(First) 152 | .expect("Can't find `First` schedule.") 153 | .add_systems(refresh_index_system::); 154 | } 155 | 156 | if let Some(obs) = I::Storage::insertion_observer() { 157 | world.spawn(obs); 158 | // Catch up on missed data 159 | world.run_system_once(refresh_index_system::).unwrap(); 160 | } 161 | 162 | if let Some(obs) = I::Storage::removal_observer() { 163 | world.spawn(obs); 164 | } 165 | } 166 | IndexFetchState { 167 | storage_state: as SystemParam>::init_state(world, system_meta), 168 | refresh_data_state: >::RefreshData<'static, 'static>, 172 | > as SystemParam>::init_state(world, system_meta), 173 | } 174 | } 175 | unsafe fn new_archetype( 176 | state: &mut Self::State, 177 | archetype: &Archetype, 178 | system_meta: &mut SystemMeta, 179 | ) { 180 | unsafe { 181 | as SystemParam>::new_archetype( 182 | &mut state.storage_state, 183 | archetype, 184 | system_meta, 185 | ); 186 | >::RefreshData<'static, 'static>, 190 | > as SystemParam>::new_archetype( 191 | &mut state.refresh_data_state, archetype, system_meta 192 | ); 193 | } 194 | } 195 | fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { 196 | as SystemParam>::apply( 197 | &mut state.storage_state, 198 | system_meta, 199 | world, 200 | ); 201 | >::RefreshData<'static, 'static>> as SystemParam>::apply( 202 | &mut state.refresh_data_state, 203 | system_meta, 204 | world, 205 | ); 206 | } 207 | unsafe fn get_param<'w2, 's2>( 208 | state: &'s2 mut Self::State, 209 | system_meta: &SystemMeta, 210 | world: UnsafeWorldCell<'w2>, 211 | change_tick: Tick, 212 | ) -> Self::Item<'w2, 's2> { 213 | let mut idx = Index { 214 | storage: unsafe { 215 | >::get_param( 216 | &mut state.storage_state, 217 | system_meta, 218 | world, 219 | change_tick, 220 | ) 221 | }, 222 | refresh_data: unsafe { 223 | >::RefreshData<'static, 'static>, 227 | > as SystemParam>::get_param( 228 | &mut state.refresh_data_state, 229 | system_meta, 230 | world, 231 | change_tick, 232 | ) 233 | }, 234 | }; 235 | if I::REFRESH_POLICY.is_when_run() { 236 | idx.refresh() 237 | } 238 | idx 239 | } 240 | } 241 | unsafe impl<'w, 's, I: IndexInfo + 'static> ReadOnlySystemParam for Index<'w, 's, I> 242 | where 243 | ResMut<'w, I::Storage>: ReadOnlySystemParam, 244 | StaticSystemParam<'w, 's, >::RefreshData<'static, 'static>>: 245 | ReadOnlySystemParam, 246 | { 247 | } 248 | 249 | #[cfg(test)] 250 | mod test { 251 | use crate::prelude::*; 252 | use bevy::prelude::*; 253 | 254 | #[derive(Component, Clone, Eq, Hash, PartialEq, Debug)] 255 | struct Number(usize); 256 | 257 | //todo: maybe make this a derive macro 258 | impl IndexInfo for Number { 259 | type Component = Self; 260 | type Value = Self; 261 | type Storage = HashmapStorage; 262 | const REFRESH_POLICY: IndexRefreshPolicy = IndexRefreshPolicy::WhenRun; 263 | 264 | fn value(c: &Self::Component) -> Self::Value { 265 | c.clone() 266 | } 267 | } 268 | 269 | fn add_some_numbers(mut commands: Commands) { 270 | commands.spawn(Number(10)); 271 | commands.spawn(Number(10)); 272 | commands.spawn(Number(20)); 273 | commands.spawn(Number(30)); 274 | } 275 | 276 | fn checker>(number: usize, amount: usize) -> impl Fn(Index) { 277 | move |mut idx: Index| { 278 | let num = &Number(number); 279 | let set = idx.lookup(num); 280 | let n = set.count(); 281 | assert_eq!( 282 | n, amount, 283 | "Index returned {} matches for {}, expectd {}.", 284 | n, number, amount, 285 | ); 286 | } 287 | } 288 | 289 | fn adder_all(n: usize) -> impl Fn(Query<&mut Number>) { 290 | move |mut nums: Query<&mut Number>| { 291 | for mut num in &mut nums { 292 | num.0 += n; 293 | } 294 | } 295 | } 296 | 297 | fn adder_some( 298 | n: usize, 299 | condition: usize, 300 | ) -> impl Fn(ParamSet<(Query<&mut Number>, Index)>) { 301 | move |mut nums_and_index: ParamSet<(Query<&mut Number>, Index)>| { 302 | let num = &Number(condition); 303 | for entity in nums_and_index 304 | .p1() 305 | .lookup(num) 306 | .collect::>() 307 | .into_iter() 308 | { 309 | let mut nums = nums_and_index.p0(); 310 | let mut nref: Mut = nums.get_mut(entity).unwrap(); 311 | nref.0 += n; 312 | } 313 | } 314 | } 315 | 316 | #[test] 317 | fn test_index_lookup() { 318 | App::new() 319 | .add_systems(Startup, add_some_numbers) 320 | .add_systems(Update, checker::(10, 2)) 321 | .add_systems(Update, checker::(20, 1)) 322 | .add_systems(Update, checker::(30, 1)) 323 | .add_systems(Update, checker::(40, 0)) 324 | .run(); 325 | } 326 | 327 | #[test] 328 | fn test_index_lookup_single() { 329 | App::new() 330 | .add_systems(Startup, add_some_numbers) 331 | .add_systems(Update, |mut idx: Index| { 332 | let num = Number(20); 333 | assert_eq!(vec![idx.single(&num)], idx.lookup(&num).collect::>()); 334 | }) 335 | .run(); 336 | } 337 | #[test] 338 | #[should_panic] 339 | fn test_index_lookup_single_but_zero() { 340 | App::new() 341 | .add_systems(Startup, add_some_numbers) 342 | .add_systems(Update, |mut idx: Index| { 343 | idx.single(&Number(55)); 344 | }) 345 | .run(); 346 | } 347 | #[test] 348 | #[should_panic] 349 | fn test_index_lookup_single_but_many() { 350 | App::new() 351 | .add_systems(Startup, add_some_numbers) 352 | .add_systems(Update, |mut idx: Index| { 353 | idx.single(&Number(10)); 354 | }) 355 | .run(); 356 | } 357 | 358 | #[test] 359 | fn test_changing_values() { 360 | App::new() 361 | .add_systems(Startup, add_some_numbers) 362 | .add_systems(PreUpdate, checker::(10, 2)) 363 | .add_systems(PreUpdate, checker::(20, 1)) 364 | .add_systems(PreUpdate, checker::(30, 1)) 365 | .add_systems(Update, adder_all(5)) 366 | .add_systems(PostUpdate, checker::(10, 0)) 367 | .add_systems(PostUpdate, checker::(20, 0)) 368 | .add_systems(PostUpdate, checker::(30, 0)) 369 | .add_systems(PostUpdate, checker::(15, 2)) 370 | .add_systems(PostUpdate, checker::(25, 1)) 371 | .add_systems(PostUpdate, checker::(35, 1)) 372 | .run(); 373 | } 374 | 375 | #[test] 376 | fn test_changing_with_index() { 377 | App::new() 378 | .add_systems(Startup, add_some_numbers) 379 | .add_systems(PreUpdate, checker::(10, 2)) 380 | .add_systems(PreUpdate, checker::(20, 1)) 381 | .add_systems(Update, adder_some(10, 10)) 382 | .add_systems(PostUpdate, checker::(10, 0)) 383 | .add_systems(PostUpdate, checker::(20, 3)) 384 | .run(); 385 | } 386 | 387 | #[test] 388 | fn test_same_system_detection() { 389 | let manual_refresh_system = 390 | |mut nums_and_index: ParamSet<(Query<&mut Number>, Index)>| { 391 | let mut idx = nums_and_index.p1(); 392 | let twenties = idx.lookup(&Number(20)).collect::>(); 393 | assert_eq!(twenties.len(), 1); 394 | 395 | for entity in twenties.into_iter() { 396 | nums_and_index.p0().get_mut(entity).unwrap().0 += 5; 397 | } 398 | idx = nums_and_index.p1(); // reborrow here so earlier p0 borrow succeeds 399 | 400 | // Hasn't refreshed yet 401 | assert_eq!(idx.lookup(&Number(20)).count(), 1); 402 | assert_eq!(idx.lookup(&Number(25)).count(), 0); 403 | 404 | // already refreshed once this frame, need to use force. 405 | idx.refresh(); 406 | assert_eq!(idx.lookup(&Number(20)).count(), 1); 407 | assert_eq!(idx.lookup(&Number(25)).count(), 0); 408 | 409 | idx.force_refresh(); 410 | assert_eq!(idx.lookup(&Number(20)).count(), 0); 411 | assert_eq!(idx.lookup(&Number(25)).count(), 1); 412 | }; 413 | 414 | App::new() 415 | .add_systems(Startup, add_some_numbers) 416 | .add_systems(Update, manual_refresh_system) 417 | .run(); 418 | } 419 | 420 | fn remover(n: usize) -> impl Fn(Index, Commands) { 421 | move |mut idx: Index, mut commands: Commands| { 422 | for entity in idx.lookup(&Number(n)) { 423 | commands.get_entity(entity).unwrap().remove::(); 424 | } 425 | } 426 | } 427 | 428 | fn despawner(n: usize) -> impl Fn(Index, Commands) { 429 | move |mut idx: Index, mut commands: Commands| { 430 | for entity in idx.lookup(&Number(n)) { 431 | commands.get_entity(entity).unwrap().despawn(); 432 | } 433 | } 434 | } 435 | 436 | fn next_frame(world: &mut World) { 437 | world.clear_trackers(); 438 | } 439 | 440 | #[test] 441 | fn test_removal_detection() { 442 | App::new() 443 | .add_systems(Startup, add_some_numbers) 444 | .add_systems(PreUpdate, checker::(20, 1)) 445 | .add_systems(PreUpdate, checker::(30, 1)) 446 | .add_systems(Update, remover(20)) 447 | .add_systems(PostUpdate, (next_frame, remover(30)).chain()) 448 | // Detect component removed this earlier this frame 449 | .add_systems(Last, checker::(30, 0)) 450 | // Detect component removed after we ran last stage 451 | .add_systems(Last, checker::(20, 0)) 452 | .run(); 453 | } 454 | 455 | #[test] 456 | fn test_despawn_detection() { 457 | App::new() 458 | .add_systems(Startup, add_some_numbers) 459 | .add_systems(PreUpdate, checker::(20, 1)) 460 | .add_systems(PreUpdate, checker::(30, 1)) 461 | .add_systems(Update, despawner(20)) 462 | .add_systems(PostUpdate, (next_frame, despawner(30)).chain()) 463 | // Detect component removed this earlier this frame 464 | .add_systems(Last, checker::(30, 0)) 465 | // Detect component removed after we ran last stage 466 | .add_systems(Last, checker::(20, 0)) 467 | .run(); 468 | } 469 | 470 | #[test] 471 | fn test_despawn_detection_2_frames() { 472 | let mut app = App::new(); 473 | app.add_systems(Startup, add_some_numbers) 474 | .add_systems(PostStartup, checker::(20, 1)) 475 | .add_systems(PostStartup, checker::(30, 1)); 476 | 477 | app.add_systems(Update, despawner(20)); 478 | app.update(); 479 | 480 | // Clear update schedule 481 | app.world_mut() 482 | .resource_mut::() 483 | .insert(Schedule::new(Update)); 484 | app.update(); 485 | 486 | app.add_systems(Update, despawner(30)) 487 | // Detect component removed this earlier this frame 488 | .add_systems(Last, checker::(30, 0)) 489 | // Detect component removed multiple frames ago 490 | .add_systems(Last, checker::(20, 0)); 491 | app.update(); 492 | } 493 | 494 | #[test] 495 | fn test_insertion_observer() { 496 | struct ObserverIndex; 497 | impl IndexInfo for ObserverIndex { 498 | type Component = Number; 499 | type Value = Number; 500 | type Storage = HashmapStorage; 501 | const REFRESH_POLICY: IndexRefreshPolicy = IndexRefreshPolicy::WhenInserted; 502 | fn value(c: &Self::Component) -> Self::Value { 503 | c.clone() 504 | } 505 | } 506 | 507 | fn replacer(diff: usize) -> impl Fn(Query<(Entity, &Number)>, Commands) { 508 | move |q: Query<(Entity, &Number)>, mut commands: Commands| { 509 | for (e, n) in q { 510 | commands.entity(e).insert(Number(n.0 + diff)); 511 | } 512 | } 513 | } 514 | 515 | let mut app = App::new(); 516 | app.add_systems(Startup, add_some_numbers) 517 | .add_systems(PostStartup, checker::(10, 2)) 518 | .add_systems(PostStartup, checker::(20, 1)) 519 | .add_systems(PostStartup, checker::(30, 1)); 520 | app.add_systems(First, remover(20)); 521 | app.add_systems(PreUpdate, checker::(20, 0)); 522 | app.add_systems(Update, replacer(5)); 523 | app.add_systems(PostUpdate, checker::(15, 2)); 524 | app.add_systems(PostUpdate, checker::(35, 1)); 525 | 526 | app.update(); 527 | } 528 | } 529 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A crate that allows using indexes to efficiently query for components 2 | //! by their values in the game engine Bevy. 3 | //! 4 | //! To use indexes, include the [`Index`][crate::index::Index] 5 | //! [`SystemParam`](bevy::ecs::system::SystemParam) as an argument to your systems. 6 | //! [`Index`][crate::index::Index] is generic over [`IndexInfo`][crate::index::IndexInfo], which is 7 | //! a trait that you must implement on your own types to define the behavior of the index. 8 | 9 | #![warn(missing_docs)] 10 | #![deny(unsafe_op_in_unsafe_fn)] 11 | #![allow(clippy::needless_lifetimes)] 12 | 13 | /// Main index logic. 14 | pub mod index; 15 | 16 | /// Various types of storage for maintaining indexes. 17 | pub mod storage; 18 | 19 | /// Policy definitions and utilities for automatically refreshing indexes. 20 | pub mod refresh_policy; 21 | 22 | mod unique_multimap; 23 | 24 | /// Commonly used types. 25 | pub mod prelude { 26 | pub use crate::index::{Index, IndexInfo}; 27 | pub use crate::refresh_policy::*; 28 | pub use crate::storage::{HashmapStorage, IndexStorage, NoStorage}; 29 | } 30 | -------------------------------------------------------------------------------- /src/refresh_policy.rs: -------------------------------------------------------------------------------- 1 | use crate::index::{Index, IndexInfo}; 2 | 3 | #[derive(Copy, Clone, Eq, PartialEq)] 4 | /// Defines when an [`Index`] should be automatically refreshed. 5 | /// 6 | /// Refreshing an [`Index`] is required for it to be able to accurately reflect the status of 7 | /// [`Component`][bevy::ecs::component::Component]s as they are added, changed, and removed. 8 | pub enum IndexRefreshPolicy { 9 | /// Refresh the index whenever a system with an [`Index`] argument is run. 10 | /// 11 | /// This is a good default for most use cases. 12 | WhenRun, 13 | /// Refresh the index the first time [`lookup`][crate::index::Index::lookup] is called in each system. 14 | /// 15 | /// Compared to `WhenRun`, this requires an extra check in each [`lookup`][crate::index::Index::lookup] 16 | /// to see if the index needs to be refreshed or not, but saves the overhead of an entire refresh 17 | /// when [`lookup`][crate::index::Index::lookup] is never called. 18 | WhenUsed, 19 | /// Refresh the index once during the [`First`][bevy::app::First] 20 | /// [`Schedule`][bevy::ecs::schedule::Schedule]. 21 | /// 22 | /// To refresh during a different schedule, you should use the [`Manual`][`IndexRefreshPolicy::Manual`] 23 | /// refresh policy and manually add the [`refresh_index_system`] to the desired schedule. 24 | EachFrame, 25 | /// Use [`Observers`][bevy::ecs::observer::Observer] to refresh the index on a per-entity basis 26 | /// as components are inserted and removed. 27 | /// 28 | /// This is best used with [`Immutable`][bevy::ecs::component::Immutable] components, as otherwise, 29 | /// component mutations will be missed unless you refresh the index manually. 30 | WhenInserted, 31 | /// Never refresh the [`Index`] automatically. 32 | /// 33 | /// You must call [`refresh`][crate::index::Index::refresh] manually if any components are 34 | /// changed or removed. 35 | Manual, 36 | } 37 | 38 | // These impls are needed because PartialEq is not const. 39 | impl IndexRefreshPolicy { 40 | pub(crate) const fn is_when_run(&self) -> bool { 41 | matches!(self, IndexRefreshPolicy::WhenRun) 42 | } 43 | pub(crate) const fn is_when_used(&self) -> bool { 44 | matches!(self, IndexRefreshPolicy::WhenUsed) 45 | } 46 | pub(crate) const fn is_each_frame(&self) -> bool { 47 | matches!(self, IndexRefreshPolicy::EachFrame) 48 | } 49 | pub(crate) const fn is_when_inserted(&self) -> bool { 50 | matches!(self, IndexRefreshPolicy::WhenInserted) 51 | } 52 | #[expect(dead_code)] 53 | pub(crate) const fn is_manual(&self) -> bool { 54 | matches!(self, IndexRefreshPolicy::Manual) 55 | } 56 | } 57 | 58 | /// A [`System`][bevy::ecs::system::System] that refreshes the index every frame. 59 | /// 60 | /// This system can be useful to ensure that all removed entities are reflected properly 61 | /// by the index. It is automatically added to the app for each index with its 62 | /// [`REFRESH_POLICY`][`IndexInfo::REFRESH_POLICY`] set to [`EachFrame`][`IndexRefreshPolicy::EachFrame`] 63 | pub fn refresh_index_system(mut idx: Index) { 64 | idx.refresh(); 65 | } 66 | -------------------------------------------------------------------------------- /src/storage.rs: -------------------------------------------------------------------------------- 1 | use crate::index::IndexInfo; 2 | use crate::unique_multimap::UniqueMultiMap; 3 | use bevy::ecs::component::Tick; 4 | use bevy::ecs::system::{StaticSystemParam, SystemChangeTick, SystemParam}; 5 | use bevy::prelude::*; 6 | use std::marker::PhantomData; 7 | 8 | #[cfg(feature = "reflect")] 9 | use bevy::reflect::Reflect; 10 | 11 | /// Defines the internal storage for an index, which is stored as a [`Resource`]. 12 | /// 13 | /// You should not need this for normal use beyond including the `Storage` type 14 | /// in your [`IndexInfo`] implementations, but you can use this to customize 15 | /// the storage of your index's data if necessary 16 | /// 17 | /// This crate provides the following storage implementations: 18 | /// 19 | /// [`HashmapStorage`], [`NoStorage`] 20 | pub trait IndexStorage: Resource + Default { 21 | /// [`SystemParam`] that is fetched alongside this storage [`Resource`] when 22 | /// an [`Index`][crate::index::Index] is included in a system. 23 | /// 24 | /// It is passed in when querying or updating the index. 25 | type RefreshData<'w, 's>: SystemParam; 26 | 27 | /// Get all of the entities with relevant components that evaluate to the given value 28 | /// using [`I::value`][`IndexInfo::value`]. 29 | fn lookup<'w, 's>( 30 | &mut self, 31 | val: &I::Value, 32 | data: &mut StaticSystemParam>, 33 | ) -> impl Iterator; 34 | 35 | /// Refresh this storage with the latest state from the world if it hasn't already been refreshed 36 | /// this [`Tick`]. 37 | /// 38 | /// Note: 1 [`Tick`] = 1 system, not 1 frame. 39 | fn refresh<'w, 's>(&mut self, data: &mut StaticSystemParam>); 40 | 41 | /// Unconditionally refresh this storage with the latest state from the world. 42 | fn force_refresh<'w, 's>(&mut self, data: &mut StaticSystemParam>); 43 | 44 | /// Observer to be run whenever a component tracked by this Index is inserted. 45 | /// 46 | /// No observer will be registered if this returns `None`. 47 | fn insertion_observer() -> Option; 48 | 49 | /// Observer to be run whenever a component tracked by this Index is removed. 50 | /// 51 | /// No observer will be registered if this returns `None`. 52 | fn removal_observer() -> Option; 53 | } 54 | 55 | // ================================================================== 56 | 57 | /// [`IndexStorage`] implementation that maintains a HashMap from values to [`Entity`]s whose 58 | /// components have that value. 59 | #[cfg_attr(feature = "reflect", derive(Reflect))] 60 | #[cfg_attr(feature = "reflect", reflect(Resource))] 61 | #[derive(Resource)] 62 | pub struct HashmapStorage { 63 | map: UniqueMultiMap, 64 | last_refresh_tick: Tick, 65 | removed_entities: Vec, 66 | } 67 | 68 | impl Default for HashmapStorage { 69 | fn default() -> Self { 70 | Self { 71 | map: Default::default(), 72 | last_refresh_tick: Tick::new(0), 73 | removed_entities: Vec::with_capacity(16), 74 | } 75 | } 76 | } 77 | 78 | impl IndexStorage for HashmapStorage { 79 | type RefreshData<'w, 's> = HashmapStorageRefreshData<'w, 's, I>; 80 | 81 | fn lookup<'w, 's>( 82 | &mut self, 83 | val: &I::Value, 84 | _data: &mut StaticSystemParam>, 85 | ) -> impl Iterator { 86 | self.map.get(val).copied() 87 | } 88 | 89 | fn refresh<'w, 's>(&mut self, data: &mut StaticSystemParam>) { 90 | if self.last_refresh_tick != data.ticks.this_run() { 91 | self.force_refresh(data); 92 | } 93 | } 94 | 95 | fn force_refresh<'w, 's>(&mut self, data: &mut StaticSystemParam>) { 96 | for entity in self.removed_entities.iter() { 97 | self.map.remove(entity); 98 | } 99 | self.removed_entities.clear(); 100 | for (entity, component) in &data.components { 101 | if component.last_changed().is_newer_than( 102 | // Subtract 1 so that changes from the system where the index was updated are seen. 103 | // The `is_newer_than` implementation assumes we don't care about those changes since 104 | // "this" system is the one that made the change, but for indexing, we do care. 105 | Tick::new(self.last_refresh_tick.get().wrapping_sub(1)), 106 | data.ticks.this_run(), 107 | ) { 108 | self.map.insert(&I::value(&component), entity); 109 | } 110 | } 111 | self.last_refresh_tick = data.ticks.this_run(); 112 | } 113 | 114 | fn insertion_observer() -> Option { 115 | if I::REFRESH_POLICY.is_when_inserted() { 116 | Some(Observer::new( 117 | |trigger: Trigger, 118 | mut storage: ResMut>, 119 | components: Query<&I::Component>| { 120 | let target = trigger.target(); 121 | let component = components 122 | .get(target) 123 | .expect("Component that was just inserted is missing!"); 124 | 125 | println!("INSERTION"); 126 | storage.map.insert(&I::value(component), target); 127 | }, 128 | )) 129 | } else { 130 | None 131 | } 132 | } 133 | 134 | fn removal_observer() -> Option { 135 | Some(Observer::new( 136 | |trigger: Trigger, mut storage: ResMut>| { 137 | if I::REFRESH_POLICY.is_when_inserted() { 138 | storage.map.remove(&trigger.target()); 139 | } else { 140 | storage.removed_entities.push(trigger.target()); 141 | } 142 | }, 143 | )) 144 | } 145 | } 146 | 147 | type ComponentsQuery<'w, 's, T> = 148 | Query<'w, 's, (Entity, Ref<'static, ::Component>)>; 149 | 150 | #[doc(hidden)] 151 | #[derive(SystemParam)] 152 | pub struct HashmapStorageRefreshData<'w, 's, I: IndexInfo> { 153 | components: ComponentsQuery<'w, 's, I>, 154 | ticks: SystemChangeTick, 155 | } 156 | 157 | //====================================================================== 158 | 159 | /// [`IndexStorage`] implementation that doesn't actually store anything. 160 | /// 161 | /// Whenever it is queried, it iterates over all components like you naively would if you weren't 162 | /// using an index. This allows you to use the `Index` interface without actually using any extra 163 | /// memory. 164 | /// 165 | /// This storage never needs to be refreshed, so the [`Manual`](IndexRefreshPolicy::Manual) refresh 166 | /// policy is usually the best choice for index definitions that use `NoStorage`. 167 | #[derive(Resource)] 168 | #[cfg_attr(feature = "reflect", derive(Reflect))] 169 | #[cfg_attr(feature = "reflect", reflect(Resource))] 170 | pub struct NoStorage { 171 | #[cfg_attr(feature = "reflect", reflect(ignore))] 172 | phantom: PhantomData I>, 173 | } 174 | impl Default for NoStorage { 175 | fn default() -> Self { 176 | Self { 177 | phantom: PhantomData, 178 | } 179 | } 180 | } 181 | 182 | impl IndexStorage for NoStorage { 183 | type RefreshData<'w, 's> = Query<'w, 's, (Entity, &'static I::Component)>; 184 | 185 | fn lookup<'w, 's>( 186 | &mut self, 187 | val: &I::Value, 188 | data: &mut StaticSystemParam>, 189 | ) -> impl Iterator { 190 | data.iter() 191 | .filter_map(|(e, c)| if I::value(c) == *val { Some(e) } else { None }) 192 | } 193 | 194 | fn refresh<'w, 's>(&mut self, _data: &mut StaticSystemParam>) {} 195 | 196 | fn force_refresh<'w, 's>(&mut self, _data: &mut StaticSystemParam>) {} 197 | 198 | fn insertion_observer() -> Option { 199 | None 200 | } 201 | 202 | fn removal_observer() -> Option { 203 | None 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/unique_multimap.rs: -------------------------------------------------------------------------------- 1 | use bevy::platform::collections::{ 2 | hash_map::HashMap, 3 | hash_set::{HashSet, Iter}, 4 | }; 5 | use std::hash::Hash; 6 | 7 | #[cfg(feature = "reflect")] 8 | use bevy::reflect::Reflect; 9 | 10 | /// Map where a key can have multiple values, but a value can only exist for one key at a time. 11 | /// Re-inserting a value is a no-op if it already exists under the same key, otherwise the value is 12 | /// removed from under its present key and added under the new key. 13 | #[cfg_attr(feature = "reflect", derive(Reflect))] 14 | pub struct UniqueMultiMap { 15 | map: HashMap>, 16 | rev_map: HashMap, 17 | } 18 | 19 | impl Default for UniqueMultiMap { 20 | fn default() -> Self { 21 | Self { 22 | map: Default::default(), 23 | rev_map: Default::default(), 24 | } 25 | } 26 | } 27 | 28 | impl UniqueMultiMap 29 | where 30 | K: Hash + Eq + Clone, 31 | V: Hash + Eq + Clone, 32 | { 33 | pub fn get(&self, k: &K) -> impl Iterator { 34 | MultiMapValueIter { 35 | inner: self.map.get(k).map(|hashset| hashset.iter()), 36 | } 37 | } 38 | 39 | /// Returns value's old key 40 | // Todo: don't rely on clone 41 | pub fn insert(&mut self, new_k: &K, v: V) -> Option { 42 | let maybe_old_k = self.rev_map.insert(v.clone(), new_k.clone()); 43 | 44 | if let Some(old_k) = &maybe_old_k { 45 | // insert value into same key: no-op 46 | if old_k == new_k { 47 | return maybe_old_k; 48 | } 49 | 50 | // remove old value; its key must exist according to rev_map 51 | self.purge_from_forward(old_k, &v, "insert"); 52 | } 53 | // insert new value 54 | self.map.get_mut_or_insert_default(new_k).insert(v); 55 | 56 | maybe_old_k 57 | } 58 | 59 | /// Returns value's old key 60 | pub fn remove(&mut self, v: &V) -> Option { 61 | let maybe_old_k = self.rev_map.remove(v); 62 | 63 | if let Some(old_k) = &maybe_old_k { 64 | self.purge_from_forward(old_k, v, "remove"); 65 | } 66 | 67 | maybe_old_k 68 | } 69 | 70 | // Removes v from k's set, removing the set completely if it would be empty 71 | // Panics if k is not in the forward map. 72 | fn purge_from_forward(&mut self, k: &K, v: &V, fn_name: &str) { 73 | let old_set = self.map.get_mut(k).unwrap_or_else(|| { 74 | panic!( 75 | "{}: Cached key from rev_map was not present in forward map!", 76 | fn_name 77 | ) 78 | }); 79 | match old_set.len() { 80 | 1 => { 81 | self.map.remove(k); 82 | } 83 | _ => { 84 | old_set.remove(v); 85 | } 86 | } 87 | } 88 | } 89 | 90 | trait HashMapExt { 91 | #[expect(dead_code)] 92 | fn get_or_insert_default(&mut self, k: &K) -> &V; 93 | fn get_mut_or_insert_default(&mut self, k: &K) -> &mut V; 94 | } 95 | 96 | impl HashMapExt for HashMap { 97 | fn get_or_insert_default(&mut self, k: &K) -> &V { 98 | if !self.contains_key(k) { 99 | self.insert(k.clone(), V::default()); 100 | } 101 | // We just inserted a value if one wasn't there, so unwrap is ok 102 | self.get(k).unwrap() 103 | } 104 | 105 | fn get_mut_or_insert_default(&mut self, k: &K) -> &mut V { 106 | if !self.contains_key(k) { 107 | self.insert(k.clone(), V::default()); 108 | } 109 | // We just inserted a value if one wasn't there, so unwrap is ok 110 | self.get_mut(k).unwrap() 111 | } 112 | } 113 | 114 | struct MultiMapValueIter<'a, V> { 115 | inner: Option>, 116 | } 117 | impl<'a, V> Iterator for MultiMapValueIter<'a, V> { 118 | type Item = &'a V; 119 | 120 | fn next(&mut self) -> Option { 121 | self.inner.as_mut().and_then(|iter| iter.next()) 122 | } 123 | } 124 | --------------------------------------------------------------------------------