Skip to content

Commit 70958b3

Browse files
authored
feat: ✨ Parallelizable Script Systems with Res and Query parameters & Schedule debugging utilities (#361)
# Summary Major step towards #244 - Adds a `bevy_system_reflection` crate to organize all the things that would be nice to upstream to bevy eventually and aren't really core logic - Moves `ReflectSchedule` etc into that crate - Adds a few more things to the reflected sets, schedules and systems - Adds `DynamicSystemParam` enum which we use to store config for script systems - Adds `ScriptSystem` which can be converted into a system using the `IntoSystem` trait via `ScriptSystemBuilder` - The custom script system allows us to use default_system_sets predictably for script systems, which previously all lived under the same `SystemTypeSet` which didn't let us do ordering between them - Adds utilities for script writers for printing dot graphs of system schedules - Splits `WorldGuard::new` into `new_exclusive` and `new_non_exclusive` # Example this allows you to parameterize systems like: ```lua world.add_system( post_update_schedule, system_builder("my_parameterised_system", script_id) :resource(ResourceTypeA) :query(world.query():component(ComponentA):component(ComponentB)) :resource(ResourceTypeB) ) ``` and then have the callback run with those parameters passed in: ```lua function my_parameterised_system(resourceA,query,resourceB) print("my_parameterised_system") runs[#runs + 1] = "my_non_exclusive_system" assert(resourceA ~= nil, "Expected to get resource but got nil") assert(query ~= nil, "Expected to get query but got nil") assert(resourceB ~= nil, "Expected to get resource but got nil") assert(#resourceA.bytes == 6, "Expected 6 bytes, got: " .. #resourceA.bytes) assert(resourceB.string == "Initial Value", "Expected 'Initial Value', got: " .. resourceB.string) assert(#query == 2, "Expected 3 results, got: " .. #query) for i,result in pairs(query) do components = result:components() assert(#components == 2, "Expected 2 components, got " .. #components) local componentA = components[1] local componentB = components[2] assert(componentA._1 == "Default", "Expected 'Default', got: " .. componentA._1) assert(componentB._1 == "Default", "Expected 'Default', got: " .. componentA._1) end end ``` The system will setup a specially sandboxed world guard for the system which only allows access to those set components and resources, this combined with the archetype component access setup in the new system type, allows us to parallelize against any other bevy system where there are no overlaps in access!. One can also create exclusive systems via `:exclusive` allowing a normal uninhibiting world guard Tests - [x] ordering between script systems - [x] exclusive & non exclusive systems - [x] check these run in parallel as expected - [x] test scripts can only access resources and queries as expected
1 parent 4747697 commit 70958b3

39 files changed

+2887
-468
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ members = [
9393
"crates/ladfile",
9494
"crates/lad_backends/mdbook_lad_preprocessor",
9595
"crates/ladfile_builder",
96+
"crates/bevy_system_reflection",
9697
]
9798
resolver = "2"
9899
exclude = ["crates/bevy_api_gen", "crates/macro_tests"]
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
// perfect application of AI
2+
// helps proompting LLMs to do good for this project
3+
4+
# Convert lua to rhai script
5+
Convert the current test to a rhai test. below are examples and instructions on the conversions necessary:
6+
7+
## Dynamic function calls
8+
Functions which are not native to rhai MUST be explicitly called using the below syntax:
9+
10+
```rhai
11+
type.function.call(arguments...)
12+
```
13+
14+
native rhai functions MUST NOT be converted using the syntax above.
15+
16+
Below is a list of some of the native functions their argument names and some constants available in rhai:
17+
18+
```csv
19+
function,arguments
20+
is_odd,
21+
is_even,
22+
min,"a, b"
23+
max,"a, b"
24+
to_float,
25+
to_decimal,
26+
abs,value
27+
sign,value
28+
is_zero,
29+
sin,angle
30+
cos,angle
31+
tan,angle
32+
sinh,angle
33+
cosh,angle
34+
tanh,angle
35+
hypot,"x, y"
36+
asin,value
37+
acos,value
38+
atan,value
39+
atan,"x, y"
40+
asinh,value
41+
acosh,value
42+
atanh,value
43+
sqrt,value
44+
exp,value
45+
ln,value
46+
log,value
47+
floor,
48+
ceiling,
49+
round,
50+
round,decimal_points
51+
int,
52+
fraction,
53+
round_up,decimal_points
54+
round_down,decimal_points
55+
round_half_up,decimal_points
56+
round_half_down,decimal_points
57+
to_int,
58+
to_decimal,
59+
to_float,
60+
to_degrees,
61+
to_radians,
62+
is_nan,
63+
is_finite,
64+
is_infinite,
65+
parse_int,"string, [radix]"
66+
parse_float,string
67+
parse_decimal,string
68+
to_binary,value
69+
to_octal,value
70+
to_hex,value
71+
PI,
72+
E,
73+
```
74+
75+
## Operators
76+
Operators are different in lua and rhai, below is a list of operators supported:
77+
78+
```
79+
Operators Assignment operators Supported types
80+
(see standard types)
81+
+, +=
82+
83+
INT
84+
FLOAT (if not no_float)
85+
Decimal (requires decimal)
86+
char
87+
string
88+
89+
-, *, /, %, **, -=, *=, /=, %=, **=
90+
91+
INT
92+
FLOAT (if not no_float)
93+
Decimal (requires decimal)
94+
95+
<<, >> <<=, >>=
96+
97+
INT
98+
99+
&, |, ^ &=, |=, ^=
100+
101+
INT (bit-wise)
102+
bool (non-short-circuiting)
103+
104+
&&, ||
105+
106+
bool (short-circuits)
107+
108+
==, !=
109+
110+
INT
111+
FLOAT (if not no_float)
112+
Decimal (requires decimal)
113+
bool
114+
char
115+
string
116+
BLOB
117+
numeric range
118+
()
119+
120+
>, >=, <, <=
121+
122+
INT
123+
FLOAT (if not no_float)
124+
Decimal (requires decimal)
125+
char
126+
string
127+
()
128+
```
129+
130+
## Function syntax
131+
Functions in rhai look like this:
132+
133+
```rhai
134+
fn function_name(arg1, arg2) {
135+
return value;
136+
}
137+
```
138+
139+
## Semicolons
140+
Every statement must end in a semicolon
141+
142+
143+
Below is a new section on Rhai strings that you can add to the prompt:
144+
145+
## Rhai Strings
146+
147+
Rhai supports different string types such as raw strings (enclosed by matching `#` and double-quotes), multi-line literal strings (enclosed by backticks to preserve exact formatting), and strings with interpolation (using `${…}` inside multi-line literals). These variants allow you to easily include complex content like newlines, quotes, and even embedded expressions while keeping the original formatting. Here are three examples:
148+
149+
````rhai
150+
// Raw string example:
151+
let raw_str = #"Hello, raw string! \n No escape sequences here."#;
152+
````
153+
154+
````rhai
155+
// Multi-line literal string example:
156+
let multi_line = `
157+
This is a multi-line literal string,
158+
which preserves whitespaces, newlines, and "quotes" exactly.
159+
`;
160+
````
161+
162+
````rhai
163+
// String interpolation example:
164+
let value = 42;
165+
let interpolated = `The answer is ${value}, which is computed dynamically.`;
166+
````
167+
168+
## Null Checks
169+
null checks can be performed by checking `type_of(value) == "()"`
170+
171+
## Examples
172+
Below is an example lua test and its equivalent rhai script:
173+
174+
### Lua
175+
```lua
176+
local entity_a = world.spawn()
177+
local entity_b = world.spawn()
178+
local entity_c = world.spawn()
179+
local entity_d = world._get_entity_with_test_component("CompWithFromWorldAndComponentData")
180+
181+
local component_with = world.get_type_by_name("CompWithFromWorldAndComponentData")
182+
local component_without = world.get_type_by_name("CompWithDefaultAndComponentData")
183+
184+
world.add_default_component(entity_a, component_with)
185+
world.add_default_component(entity_b, component_with)
186+
world.add_default_component(entity_c, component_with)
187+
188+
world.add_default_component(entity_b, component_without)
189+
190+
local found_entities = {}
191+
for i,result in pairs(world.query():component(component_with):without(component_without):build()) do
192+
table.insert(found_entities, result:entity())
193+
end
194+
195+
assert(#found_entities == 3, "Expected 3 entities, got " .. #found_entities)
196+
197+
expected_entities = {
198+
entity_c,
199+
entity_d,
200+
entity_a,
201+
}
202+
203+
for i, entity in ipairs(found_entities) do
204+
assert(entity:index() == expected_entities[i]:index(), "Expected entity " .. expected_entities[i]:index() .. " but got " .. entity:index())
205+
end
206+
```
207+
208+
### Rhai
209+
```rhai
210+
let entity_a = world.spawn_.call();
211+
let entity_b = world.spawn_.call();
212+
let entity_c = world.spawn_.call();
213+
let entity_d = world._get_entity_with_test_component.call("CompWithFromWorldAndComponentData");
214+
215+
let component_with = world.get_type_by_name.call("CompWithFromWorldAndComponentData");
216+
let component_without = world.get_type_by_name.call("CompWithDefaultAndComponentData");
217+
218+
world.add_default_component.call(entity_a, component_with);
219+
world.add_default_component.call(entity_b, component_with);
220+
world.add_default_component.call(entity_c, component_with);
221+
222+
world.add_default_component.call(entity_b, component_without);
223+
224+
let found_entities = [];
225+
for (result, i) in world.query.call().component.call(component_with).without.call(component_without).build.call() {
226+
found_entities.push(result.entity.call());
227+
}
228+
229+
assert(found_entities.len == 3, "Expected 3 entities, got " + found_entities.len);
230+
231+
let expected_entities = [
232+
entity_d,
233+
entity_a,
234+
entity_c,
235+
];
236+
237+
for (entity, i) in found_entities {
238+
assert(entity.index.call() == expected_entities[i].index.call(), "Expected entity " + expected_entities[i].index.call() + " but got " + entity.index.call());
239+
}
240+
```
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
2+
function on_test()
3+
local post_update_schedule = world.get_schedule_by_name("PostUpdate")
4+
5+
local test_system = post_update_schedule:get_system_by_name("on_test_post_update")
6+
7+
local system_a = world.add_system(
8+
post_update_schedule,
9+
system_builder("custom_system_a", script_id)
10+
:after(test_system)
11+
)
12+
13+
local system_b = world.add_system(
14+
post_update_schedule,
15+
system_builder("custom_system_b", script_id)
16+
:after(test_system)
17+
)
18+
19+
-- generate a schedule graph and verify it's what we expect
20+
local dot_graph = post_update_schedule:render_dot()
21+
22+
local expected_dot_graph = [[
23+
digraph {
24+
node_0 [label="bevy_mod_scripting_core::bindings::allocator::garbage_collector"];
25+
node_1 [label="on_test_post_update"];
26+
node_2 [label="script_integration_test_harness::dummy_before_post_update_system"];
27+
node_3 [label="script_integration_test_harness::dummy_post_update_system"];
28+
node_4 [label="custom_system_a"];
29+
node_5 [label="custom_system_b"];
30+
node_6 [label="SystemSet GarbageCollection"];
31+
node_7 [label="SystemSet ScriptSystem(custom_system_a)"];
32+
node_8 [label="SystemSet ScriptSystem(custom_system_b)"];
33+
node_0 -> node_6 [color=red, label="child of", arrowhead=diamond];
34+
node_4 -> node_7 [color=red, label="child of", arrowhead=diamond];
35+
node_5 -> node_8 [color=red, label="child of", arrowhead=diamond];
36+
node_1 -> node_4 [color=blue, label="runs before", arrowhead=normal];
37+
node_1 -> node_5 [color=blue, label="runs before", arrowhead=normal];
38+
node_2 -> node_3 [color=blue, label="runs before", arrowhead=normal];
39+
}
40+
]]
41+
42+
assert_str_eq(dot_graph, expected_dot_graph, "Expected the schedule graph to match the expected graph")
43+
end
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
fn on_test() {
2+
let post_update_schedule = world.get_schedule_by_name.call("PostUpdate");
3+
4+
let test_system = post_update_schedule.get_system_by_name.call("on_test_post_update");
5+
6+
let system_a = world.add_system.call(
7+
post_update_schedule,
8+
system_builder.call("custom_system_a", script_id)
9+
.after.call(test_system)
10+
);
11+
12+
let system_b = world.add_system.call(
13+
post_update_schedule,
14+
system_builder.call("custom_system_b", script_id)
15+
.after.call(test_system)
16+
);
17+
18+
// generate a schedule graph and verify it's what we expect
19+
let dot_graph = post_update_schedule.render_dot.call();
20+
21+
let expected_dot_graph = `
22+
digraph {
23+
node_0 [label="bevy_mod_scripting_core::bindings::allocator::garbage_collector"];
24+
node_1 [label="on_test_post_update"];
25+
node_2 [label="script_integration_test_harness::dummy_before_post_update_system"];
26+
node_3 [label="script_integration_test_harness::dummy_post_update_system"];
27+
node_4 [label="custom_system_a"];
28+
node_5 [label="custom_system_b"];
29+
node_6 [label="SystemSet GarbageCollection"];
30+
node_7 [label="SystemSet ScriptSystem(custom_system_a)"];
31+
node_8 [label="SystemSet ScriptSystem(custom_system_b)"];
32+
node_0 -> node_6 [color=red, label="child of", arrowhead=diamond];
33+
node_4 -> node_7 [color=red, label="child of", arrowhead=diamond];
34+
node_5 -> node_8 [color=red, label="child of", arrowhead=diamond];
35+
node_1 -> node_4 [color=blue, label="runs before", arrowhead=normal];
36+
node_1 -> node_5 [color=blue, label="runs before", arrowhead=normal];
37+
node_2 -> node_3 [color=blue, label="runs before", arrowhead=normal];
38+
}`;
39+
40+
assert_str_eq.call(dot_graph, expected_dot_graph, "Expected the schedule graph to match the expected graph");
41+
}

assets/tests/add_system/adds_system_in_correct_order.lua

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ function on_test()
2020
system_builder("custom_system_before", script_id)
2121
:before(test_system)
2222
)
23+
24+
local script_system_between = world.add_system(
25+
post_update_schedule,
26+
system_builder("custom_system_between", script_id)
27+
:after(test_system)
28+
:before(system_after)
29+
)
2330
end
2431

2532

@@ -39,10 +46,16 @@ function custom_system_after()
3946
runs[#runs + 1] = "custom_system_after"
4047
end
4148

49+
function custom_system_between()
50+
print("custom_system_between")
51+
runs[#runs + 1] = "custom_system_between"
52+
end
53+
4254
-- runs in the `Last` bevy schedule
4355
function on_test_last()
44-
assert(#runs == 3, "Expected 3 runs, got: " .. #runs)
56+
assert(#runs == 4, "Expected 4 runs, got: " .. #runs)
4557
assert(runs[1] == "custom_system_before", "Expected custom_system_before to run first, got: " .. runs[1])
4658
assert(runs[2] == "on_test_post_update", "Expected on_test_post_update to run second, got: " .. runs[2])
47-
assert(runs[3] == "custom_system_after", "Expected custom_system_after to run third, got: " .. runs[3])
59+
assert(runs[3] == "custom_system_between", "Expected custom_system_between to run third, got: " .. runs[3])
60+
assert(runs[4] == "custom_system_after", "Expected custom_system_after to run second, got: " .. runs[4])
4861
end

0 commit comments

Comments
 (0)