dynamic/
dynamic.rs

1#![expect(
2    unsafe_code,
3    reason = "Unsafe code is needed to work with dynamic components"
4)]
5
6//! This example show how you can create components dynamically, spawn entities with those components
7//! as well as query for entities with those components.
8
9use std::{alloc::Layout, collections::HashMap, io::Write, ptr::NonNull};
10
11use bevy::{
12    ecs::{
13        component::{
14            ComponentCloneBehavior, ComponentDescriptor, ComponentId, ComponentInfo, StorageType,
15        },
16        query::{ComponentAccessKind, QueryData},
17        world::FilteredEntityMut,
18    },
19    prelude::*,
20    ptr::{Aligned, OwningPtr},
21};
22
23const PROMPT: &str = "
24Commands:
25    comp, c   Create new components
26    spawn, s  Spawn entities
27    query, q  Query for entities
28Enter a command with no parameters for usage.";
29
30const COMPONENT_PROMPT: &str = "
31comp, c   Create new components
32    Enter a comma separated list of type names optionally followed by a size in u64s.
33    e.g. CompA 3, CompB, CompC 2";
34
35const ENTITY_PROMPT: &str = "
36spawn, s  Spawn entities
37    Enter a comma separated list of components optionally followed by values.
38    e.g. CompA 0 1 0, CompB, CompC 1";
39
40const QUERY_PROMPT: &str = "
41query, q  Query for entities
42    Enter a query to fetch and update entities
43    Components with read or write access will be displayed with their values
44    Components with write access will have their fields incremented by one
45
46    Accesses: 'A' with, '&A' read, '&mut A' write
47    Operators: '||' or, ',' and, '?' optional
48
49    e.g. &A || &B, &mut C, D, ?E";
50
51fn main() {
52    let mut world = World::new();
53    let mut lines = std::io::stdin().lines();
54    let mut component_names = HashMap::<String, ComponentId>::new();
55    let mut component_info = HashMap::<ComponentId, ComponentInfo>::new();
56
57    println!("{PROMPT}");
58    loop {
59        print!("\n> ");
60        let _ = std::io::stdout().flush();
61        let Some(Ok(line)) = lines.next() else {
62            return;
63        };
64
65        if line.is_empty() {
66            return;
67        };
68
69        let Some((first, rest)) = line.trim().split_once(|c: char| c.is_whitespace()) else {
70            match &line.chars().next() {
71                Some('c') => println!("{COMPONENT_PROMPT}"),
72                Some('s') => println!("{ENTITY_PROMPT}"),
73                Some('q') => println!("{QUERY_PROMPT}"),
74                _ => println!("{PROMPT}"),
75            }
76            continue;
77        };
78
79        match &first[0..1] {
80            "c" => {
81                rest.split(',').for_each(|component| {
82                    let mut component = component.split_whitespace();
83                    let Some(name) = component.next() else {
84                        return;
85                    };
86                    let size = match component.next().map(str::parse) {
87                        Some(Ok(size)) => size,
88                        _ => 0,
89                    };
90                    // Register our new component to the world with a layout specified by it's size
91                    // SAFETY: [u64] is Send + Sync
92                    let id = world.register_component_with_descriptor(unsafe {
93                        ComponentDescriptor::new_with_layout(
94                            name.to_string(),
95                            StorageType::Table,
96                            Layout::array::<u64>(size).unwrap(),
97                            None,
98                            true,
99                            ComponentCloneBehavior::Default,
100                        )
101                    });
102                    let Some(info) = world.components().get_info(id) else {
103                        return;
104                    };
105                    component_names.insert(name.to_string(), id);
106                    component_info.insert(id, info.clone());
107                    println!("Component {} created with id: {}", name, id.index());
108                });
109            }
110            "s" => {
111                let mut to_insert_ids = Vec::new();
112                let mut to_insert_data = Vec::new();
113                rest.split(',').for_each(|component| {
114                    let mut component = component.split_whitespace();
115                    let Some(name) = component.next() else {
116                        return;
117                    };
118
119                    // Get the id for the component with the given name
120                    let Some(&id) = component_names.get(name) else {
121                        println!("Component {name} does not exist");
122                        return;
123                    };
124
125                    // Calculate the length for the array based on the layout created for this component id
126                    let info = world.components().get_info(id).unwrap();
127                    let len = info.layout().size() / size_of::<u64>();
128                    let mut values: Vec<u64> = component
129                        .take(len)
130                        .filter_map(|value| value.parse::<u64>().ok())
131                        .collect();
132                    values.resize(len, 0);
133
134                    // Collect the id and array to be inserted onto our entity
135                    to_insert_ids.push(id);
136                    to_insert_data.push(values);
137                });
138
139                let mut entity = world.spawn_empty();
140
141                // Construct an `OwningPtr` for each component in `to_insert_data`
142                let to_insert_ptr = to_owning_ptrs(&mut to_insert_data);
143
144                // SAFETY:
145                // - Component ids have been taken from the same world
146                // - Each array is created to the layout specified in the world
147                unsafe {
148                    entity.insert_by_ids(&to_insert_ids, to_insert_ptr.into_iter());
149                }
150
151                println!("Entity spawned with id: {}", entity.id());
152            }
153            "q" => {
154                let mut builder = QueryBuilder::<FilteredEntityMut>::new(&mut world);
155                parse_query(rest, &mut builder, &component_names);
156                let mut query = builder.build();
157                query.iter_mut(&mut world).for_each(|filtered_entity| {
158                    let terms = filtered_entity
159                        .access()
160                        .try_iter_component_access()
161                        .unwrap()
162                        .map(|component_access| {
163                            let id = *component_access.index();
164                            let ptr = filtered_entity.get_by_id(id).unwrap();
165                            let info = component_info.get(&id).unwrap();
166                            let len = info.layout().size() / size_of::<u64>();
167
168                            // SAFETY:
169                            // - All components are created with layout [u64]
170                            // - len is calculated from the component descriptor
171                            let data = unsafe {
172                                std::slice::from_raw_parts_mut(
173                                    ptr.assert_unique().as_ptr().cast::<u64>(),
174                                    len,
175                                )
176                            };
177
178                            // If we have write access, increment each value once
179                            if matches!(component_access, ComponentAccessKind::Exclusive(_)) {
180                                data.iter_mut().for_each(|data| {
181                                    *data += 1;
182                                });
183                            }
184
185                            format!("{}: {:?}", info.name(), data[0..len].to_vec())
186                        })
187                        .collect::<Vec<_>>()
188                        .join(", ");
189
190                    println!("{}: {}", filtered_entity.id(), terms);
191                });
192            }
193            _ => continue,
194        }
195    }
196}
197
198// Constructs `OwningPtr` for each item in `components`
199// By sharing the lifetime of `components` with the resulting ptrs we ensure we don't drop the data before use
200fn to_owning_ptrs(components: &mut [Vec<u64>]) -> Vec<OwningPtr<'_, Aligned>> {
201    components
202        .iter_mut()
203        .map(|data| {
204            let ptr = data.as_mut_ptr();
205            // SAFETY:
206            // - Pointers are guaranteed to be non-null
207            // - Memory pointed to won't be dropped until `components` is dropped
208            unsafe {
209                let non_null = NonNull::new_unchecked(ptr.cast());
210                OwningPtr::new(non_null)
211            }
212        })
213        .collect()
214}
215
216fn parse_term<Q: QueryData>(
217    str: &str,
218    builder: &mut QueryBuilder<Q>,
219    components: &HashMap<String, ComponentId>,
220) {
221    let mut matched = false;
222    let str = str.trim();
223    match str.chars().next() {
224        // Optional term
225        Some('?') => {
226            builder.optional(|b| parse_term(&str[1..], b, components));
227            matched = true;
228        }
229        // Reference term
230        Some('&') => {
231            let mut parts = str.split_whitespace();
232            let first = parts.next().unwrap();
233            if first == "&mut" {
234                if let Some(str) = parts.next()
235                    && let Some(&id) = components.get(str)
236                {
237                    builder.mut_id(id);
238                    matched = true;
239                };
240            } else if let Some(&id) = components.get(&first[1..]) {
241                builder.ref_id(id);
242                matched = true;
243            }
244        }
245        // With term
246        Some(_) => {
247            if let Some(&id) = components.get(str) {
248                builder.with_id(id);
249                matched = true;
250            }
251        }
252        None => {}
253    };
254
255    if !matched {
256        println!("Unable to find component: {str}");
257    }
258}
259
260fn parse_query<Q: QueryData>(
261    str: &str,
262    builder: &mut QueryBuilder<Q>,
263    components: &HashMap<String, ComponentId>,
264) {
265    let str = str.split(',');
266    str.for_each(|term| {
267        let sub_terms: Vec<_> = term.split("||").collect();
268        if sub_terms.len() == 1 {
269            parse_term(sub_terms[0], builder, components);
270        } else {
271            builder.or(|b| {
272                sub_terms
273                    .iter()
274                    .for_each(|term| parse_term(term, b, components));
275            });
276        }
277    });
278}