├── .github
├── FUNDING.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── Cargo.toml
├── LICENSE.md
├── Makefile.toml
├── README.md
├── README.tpl
├── assets
└── models
│ └── town.glb
├── cliff.toml
├── derive
├── Cargo.toml
└── src
│ ├── action.rs
│ ├── lib.rs
│ └── scorer.rs
├── examples
├── concurrent.rs
├── custom_measure.rs
├── farming_sim.rs
├── one_off.rs
├── sequence.rs
└── thirst.rs
├── src
├── actions.rs
├── choices.rs
├── evaluators.rs
├── lib.rs
├── measures.rs
├── pickers.rs
├── scorers.rs
└── thinker.rs
└── tests
├── derive_action.rs
├── derive_scorer.rs
└── steps.rs
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [zkat]
4 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push, pull_request]
4 |
5 | env:
6 | RUSTFLAGS: -Dwarnings
7 |
8 | jobs:
9 | fmt_and_docs:
10 | name: Check fmt & build docs
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | - name: Install Rust
15 | uses: dtolnay/rust-toolchain@master
16 | with:
17 | toolchain: stable
18 | components: rustfmt
19 | - name: rustfmt
20 | run: cargo fmt --all -- --check
21 | - name: docs
22 | run: cargo doc --no-deps
23 |
24 | build_and_test:
25 | name: Build & Test
26 | runs-on: ${{ matrix.os }}
27 | strategy:
28 | matrix:
29 | rust: [stable]
30 | os: [ubuntu-latest, macOS-latest, windows-latest]
31 |
32 | steps:
33 | - uses: actions/checkout@v3
34 | - name: Install Rust
35 | uses: dtolnay/rust-toolchain@master
36 | with:
37 | toolchain: ${{ matrix.rust }}
38 | components: clippy
39 | - if: matrix.os == 'ubuntu-latest'
40 | run: sudo apt-get install g++ pkg-config libx11-dev libasound2-dev libudev-dev
41 | - name: Clippy
42 | run: cargo clippy --all -- -D warnings
43 | - name: Run tests
44 | run: cargo test --all --verbose
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | Cargo.lock
3 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # `big-brain` Release Changelog
2 |
3 |
4 | ## 0.22.0 (2024-11-30)
5 |
6 | ### Features
7 |
8 | * **picker:** add HighestToScore Picker (#105) ([ab0385d5](https://github.com/zkat/big-brain/commit/ab0385d543a764f8a06013dbb69ef2193e1ef8ab))
9 | * **bevy:** Upgrade to bevy 0.15 (#106) ([868ebdb4](https://github.com/zkat/big-brain/commit/868ebdb4fc3116af095dcd1ea0685fa6c517ddfe))
10 |
11 |
12 | ## 0.21.1 (2024-07-09)
13 |
14 | This is just a patch release because of a hiccup during the release process for 0.21.1, but is otherwise identical.
15 |
16 |
17 | ## 0.21.0 (2024-07-09)
18 |
19 | ### Features
20 |
21 | * **bevy:** Upgrade to bevy 0.14.0 (#101) ([3a952a24](https://github.com/zkat/big-brain/commit/3a952a244b01a09fa6157ae9349575935d159c91))
22 |
23 |
24 | ## 0.20.0 (2024-05-17)
25 |
26 | ### Features
27 |
28 | * **bevy:** Update for bevy 0.13 (#93) ([50415e55](https://github.com/zkat/big-brain/commit/50415e55f0a9937dc99cb8f0b0906e47cb390082))
29 | * **scorers:** Add support for generic types in ScorerBuilder (#96) ([98337faa](https://github.com/zkat/big-brain/commit/98337faacd5eb720cc820628fd1bfa07e6c8effa))
30 |
31 | ### Bug Fixes
32 |
33 | * **derive:** using prelude rather than ecs::xyz in derive (#99) ([128f55e0](https://github.com/zkat/big-brain/commit/128f55e008c933a3eae1df44de0455f56acfdff7))
34 |
35 |
36 | ## 0.19.0 (2024-01-19)
37 |
38 | ### Features
39 |
40 | * **set_unchecked:** Add `set_unchecked` method to `Score` (#78) ([e179cb53](https://github.com/zkat/big-brain/commit/e179cb53fa981a0cf08352590ce36799f7ee0235))
41 | * **bevy:** Migrated to Bevy 0.12 (#86) ([69c3a4a5](https://github.com/zkat/big-brain/commit/69c3a4a506fb5bcb94e26d87cccdb27cc180429a))
42 | * **bevy:** Update to bevy 0.12.1 (#87) ([6befa384](https://github.com/zkat/big-brain/commit/6befa3844aec54b31886ff52f3e9f596ea57e088))
43 | * **thinker:** make ThinkerBuilder be Clone ([2e493d6b](https://github.com/zkat/big-brain/commit/2e493d6bb7339bd278a597555b707aa379eebbcb))
44 | * **generics:** support generic actions (#88) ([7f5a3845](https://github.com/zkat/big-brain/commit/7f5a3845947e40f5a2c6460060eaef3ec5649804))
45 |
46 | ### Bug Fixes
47 |
48 | * **despawn:** don't try to despawn if an entity doesn't exist ([bdf51c32](https://github.com/zkat/big-brain/commit/bdf51c3206e9afa6b35c1f5a199c6465c3086217))
49 |
50 |
51 | ## 0.18.0 (2023-07-16)
52 |
53 | ### Features
54 |
55 | * **bevy:** Update to bevy 0.11 (#79) ([72bb861f](https://github.com/zkat/big-brain/commit/72bb861f9b17f7bc052ff33bf4e4a3ab2876775a))
56 |
57 |
58 | ## 0.17.0 (2023-03-06)
59 |
60 | ### Features
61 |
62 | * **bevy:** Update to bevy 0.10 (#72) ([66ece025](https://github.com/zkat/big-brain/commit/66ece025a0b296decf8a0fe4c301b78bd0971d77))
63 |
64 |
65 | ## 0.16.0 (2023-01-30)
66 |
67 | Probably the biggest change in this release is removal of the blanket
68 | `ActionBuilder` and `ScorerBuilder` implementations for `Clone` types. This is
69 | a fairly significant breaking change, but one that is fairly easy to resolve:
70 | simply use the new `#[derive(ActionBuilder)]` and `#[derive(ScorerBuilder)]`
71 | macros to derive the necessary implementations for your Action and Scorer
72 | Components and you should be good to go.
73 |
74 | ### Features
75 |
76 | * **derive:** Add derive macros for Action and Scorer (#65) ([359bccef](https://github.com/zkat/big-brain/commit/359bccef46f67d286c7f89cbe1b93b7b37b46588))
77 | * **BREAKING CHANGE**: This gets rid of the blanket implementation for Action/ScorerBuilder on Clone things, and instead requires that people use derive macros (or manually implement the traits), if they want to go the clone-to-instantiate route.
78 | * **concurrenty:** Add ConcurrentMode configuration to Concurrently Action (#68) ([f6d04feb](https://github.com/zkat/big-brain/commit/f6d04feb18927250304554467da2cb1c6583fc2d))
79 | * **reflection:** Implement Reflect trait for all relevant types (#69) ([31543c78](https://github.com/zkat/big-brain/commit/31543c78bd398e7781ced90701e73691b265a6ce))
80 |
81 | ### Bug Fixes
82 |
83 | * **typo:** fix scorer argument name (#63) ([5be71335](https://github.com/zkat/big-brain/commit/5be71335b3a4429ed05c1d3e0969dc521400df09))
84 | * **derive:** some tweaks to new derive macros ([4a622a90](https://github.com/zkat/big-brain/commit/4a622a90bf895d35ec255971b5ca7d1c5c95b120))
85 |
86 |
87 | ## 0.15.0 (2022-11-13)
88 |
89 | ### Features
90 |
91 | * **deps:** Update to Bevy 0.9 (#59) ([8ce5cd1b](https://github.com/zkat/big-brain/commit/8ce5cd1b57a2de0c10bcbc1c1686187d680134d9))
92 |
93 | ### Bug Fixes
94 |
95 | * **spans:** Make ActionSpan::new() private ([4486af1d](https://github.com/zkat/big-brain/commit/4486af1db83fc4fdf50acd2eacfa7d96e64501f3))
96 | * **BREAKING CHANGE**: This function was previously public, but it was never meant to be used.
97 |
98 |
99 | ## 0.14.0 (2022-09-25)
100 |
101 | This is a fairly beefy release. The two main changes are the addition of
102 | significant new observability features, which let you debug/trace both
103 | big-brain and your own Scorers and Actions more easily in an integrated
104 | manner. Additionally, a new advanced composite Scorer was added,
105 | MeasuredScorer, which can be used to create some interesting behaviors when
106 | you have want to factor in multiple scorers when adding cases to Thinker.
107 |
108 | Besides that, there's several bugfixes to long-standing bugs and a couple of
109 | other features.
110 |
111 | Enjoy!
112 |
113 | ### Features
114 |
115 | * **entities:** Rename ActionEnt and ScorerEnt to Action and Scorer ([43f37959](https://github.com/zkat/big-brain/commit/43f3795929a4a67806a3a50113e9573d7dc95895))
116 | * **BREAKING CHANGE**: These are externally-exported, so you'll have to rename them yourself, too
117 | * **tracing:** add tracing support for Thinker/Action/Scorer (#55) ([a32bc01d](https://github.com/zkat/big-brain/commit/a32bc01d4280bad060f998782f03cbf82ce481cf))
118 | * **BREAKING CHANGE**: In the process of doing this, `spawn_action` and `spawn_scorer` were moved out of `ActionBuilder` and `ScorerBuilder` traits, respectively. This will likely affect any user-side composite actions and scorers. Use `scorers::spawn_scorer` and `actions::spawn_action` instead.
119 | * **thinker:** Add support for scheduling one-off Actions on a Thinker (#57) ([382d2014](https://github.com/zkat/big-brain/commit/382d20148d654ac65027e4ec68d964eb37364cac))
120 | * **measures:** Implement MeasuredScorer and some initial measures (#54) ([c6a6c5c9](https://github.com/zkat/big-brain/commit/c6a6c5c9d685c14f13b3906097a7231a3d648718))
121 |
122 | ### Bug Fixes
123 |
124 | * **actions,scorers:** Transform/GlobalTransform are no longer needed for hierarchies. ([df10f034](https://github.com/zkat/big-brain/commit/df10f034595d2f8800e4c68473c6039e4754a1ec))
125 | * **tracing:** fix warnings and wrong cfg feature name ([5ff39632](https://github.com/zkat/big-brain/commit/5ff396326e236aa4b408503696cf46121183e520))
126 | * **thinker:** `otherwise` clause no longer overrides running action (#56) ([849ab346](https://github.com/zkat/big-brain/commit/849ab3460e17c47796d2f5948b002973867411a1))
127 | * **BREAKING CHANGE**: This patch changes the behavior for `otherwise` such that it won't override an existing action if it's still running, but it'll still execute as soon as that action finishes. I think this is really what people expect this to do, so let's give it a shot!
128 | * **tracing:** drop action span scope before spawning next action ([ee899e4a](https://github.com/zkat/big-brain/commit/ee899e4a841fbfaf3c1e3541171a7533cd339ec2))
129 |
130 |
131 | ## 0.13.0 (2022-09-21)
132 |
133 | ### Features
134 |
135 | * **scorers:** make ScorerEnt public (#50) ([9e6d7f63](https://github.com/zkat/big-brain/commit/9e6d7f63f52f66ad6e80914913aad37c00570bb3))
136 | * **scorers:** make FixedScore members pub (#49) ([f0ddb9e5](https://github.com/zkat/big-brain/commit/f0ddb9e51cf96956b73ca2d7fc25812def676fef))
137 | * **picker:** Implement a Highest Picker (#52) ([4b48f94d](https://github.com/zkat/big-brain/commit/4b48f94d7221eedcaa1a64a6d2f100022f79164b))
138 | * **scorers:** Add ProductOfScorers composite scorer (#51) ([e425e234](https://github.com/zkat/big-brain/commit/e425e2348a53cd675070b4f2a3e7076242012c8b))
139 | * **actions:** make ActionEnt public, too ([fc30e752](https://github.com/zkat/big-brain/commit/fc30e752c66aa887f62a899827e5a8c9910eeec3))
140 | * **prelude:** Add ScorerEnt and ActionEnt to the prelude ([08b0598b](https://github.com/zkat/big-brain/commit/08b0598b1cd1fa5cae68932012ff20d76d0acbf6))
141 |
142 |
143 | ## 0.12.0 (2022-08-01)
144 |
145 | ### Features
146 |
147 | * **bevy:** Upgrade to Bevy 0.8. (#43) ([0f0c4479](https://github.com/zkat/big-brain/commit/0f0c4479ab84cc60dae3a8fdaab1d560807f0e3f))
148 |
149 |
150 | ## 0.11.0 (2022-05-05)
151 |
152 | ### Features
153 |
154 | * **deps:** upgrade to Bevy 0.7. (#37) ([38688789](https://github.com/zkat/big-brain/commit/38688789d08547e1cbc0d2a373fc58af39dea360))
155 |
156 |
157 | ## 0.10.0 (2022-01-15)
158 |
159 | This is my first pass at a significant API improvement. I iterated on it for a
160 | while and this is what I settled on. I look forward to continuing to evolve
161 | this API as I get more feedback and experience with it! Please let me know
162 | what you think!
163 |
164 | ### Breaking Changes
165 |
166 | * **thinker:** stop cancelling actions when they go under Picker thresholds ([4c72b3d1](https://github.com/zkat/big-brain/commit/4c72b3d11eaa42af4b99ccf9ea729306e589ada8))
167 | * **stages:** Strongly typed stages for BigBrainPlugin ([65ca646e](https://github.com/zkat/big-brain/commit/65ca646e3b92b178025591878e2df6a08714880f))
168 | * **builders:** Blanket impls for ActionBuilder and ScorerBuilder when Clone ([8bed75b5](https://github.com/zkat/big-brain/commit/8bed75b54a43c72b53fbf9e2605b942cb2c53214))
169 | * **api:** rename attach and hide it from docs ([6c64df4f](https://github.com/zkat/big-brain/commit/6c64df4fc1211abe19919a3628476b930b6e9919))
170 | * **choice:** return &Choice instead of cloning ([a76dcbb6](https://github.com/zkat/big-brain/commit/a76dcbb67d4f6ae402f03d22e8d526408d8d875f))
171 |
172 | ### Features
173 |
174 | * **example:** update thirst example ([edecc4c9](https://github.com/zkat/big-brain/commit/edecc4c95f76bcd69c042372140486f744f4ccea))
175 |
176 | ### Bug Fixes
177 |
178 | * **hierarchy:** make sure all the hierarchy stuff is set up properly ([372c13e2](https://github.com/zkat/big-brain/commit/372c13e207523ec919c6490682f628f7d21cebea))
179 | * **tests:** update tests ([94e1b1f6](https://github.com/zkat/big-brain/commit/94e1b1f685e6ab0be9d90bae5dfbd648ba87f1de))
180 |
181 |
182 | ## 0.9.0 (2022-01-09)
183 |
184 | ### Features
185 |
186 | * **deps:** update for bevy 0.6 (#31) ([b97f9273](https://github.com/zkat/big-brain/commit/b97f9273c5f5eceb010d8fa2b23abb534fb2cee1))
187 | * **perf:** Use SparseSet for ActionState ([5fc08176](https://github.com/zkat/big-brain/commit/5fc081765c1ed8788a7a5d1e940efbc66dc8aa8f))
188 |
189 |
190 | ## 0.8.0 (2021-09-19)
191 |
192 | ### Bug Fixes
193 |
194 | * **systems:** Fix steps, add a test and explicit systems ordering (#27) ([f33315c9](https://github.com/zkat/big-brain/commit/f33315c9b7b769a94baab17e3a9df9f5ebe924d2)) (credit: [@payload](https://github.com/payload))
195 |
196 |
197 | ## 0.7.0 (2021-09-16)
198 |
199 | ### Bug Fixes
200 |
201 | * **deps:** Don't include Bevy default features when used as a dependency. (#25) ([61558137](https://github.com/zkat/big-brain/commit/615581370a165645795966ac7c878ff492630ba2))
202 |
203 | ### Features
204 |
205 | * **license:** change license to Apache-2.0 ([d7d17772](https://github.com/zkat/big-brain/commit/d7d177729476af8ec1463d8957a35092a336098a))
206 | * **BREAKING CHANGE**: This is a significant licensing change. Please review.
207 |
208 |
209 | ## 0.6.0 (2021-05-20)
210 |
211 | #### Features
212 |
213 | * **pickers:** Make choices mod public (#23) ([92034cd0](https://github.com/zkat/big-brain/commit/92034cd04e629723893cfcd7730ce597083da9e7))
214 | * **scorers:** Added EvaluatingScorer (#24) ([1a1d5b3d](https://github.com/zkat/big-brain/commit/1a1d5b3d17d96a51084418128f0bfebe0ad8c702))
215 |
216 | #### Bug Fixes
217 |
218 | * **actions:** Concurrently was not setting its state to Failure ([d4a689f6](https://github.com/zkat/big-brain/commit/d4a689f6c60f509a71fb3a9ae4ca49dad263acab))
219 |
220 |
221 |
222 | ## 0.5.0 (2021-04-27)
223 |
224 | Got a few goodies in this release, mainly focused around composite actions and
225 | scorers, which were apparently broken.
226 |
227 | Shout out again to [@piedoomy](https://github.com/piedoomy) for some of these
228 | contributions!
229 |
230 | #### Features
231 |
232 | * **actions:** Add new Concurrently composite action ([6c736374](https://github.com/zkat/big-brain/commit/6c736374b4afd60af592a357ad2403304d3638d1))
233 | * **evaluators:** added inversed linear evaluator helper (#19) ([f871d19e](https://github.com/zkat/big-brain/commit/f871d19e93b6764088d6db5db1947fcb37143868))
234 | * **scorers:** Added WinningScorer composite scorer (#20) ([748b30ae](https://github.com/zkat/big-brain/commit/748b30aedcb0711f4180a8e24b457f01f0b84f6a))
235 |
236 | #### Breaking Changes
237 |
238 | * **scorers:** Composite Scorers now all use `.push()` instead of a mixture of `.push()` and `.when()`. Please update any usages of composite scorers ([63bad1fd](https://github.com/zkat/big-brain/commit/63bad1fd2c82eadc88107003dd819f3cfa7530a2)
239 |
240 | #### Bug Fixes
241 |
242 | * **scorers:**
243 | * Scorer builders now properly return themselves ([63bad1fd](https://github.com/zkat/big-brain/commit/63bad1fd2c82eadc88107003dd819f3cfa7530a2)
244 | * Fixed error where wrong component for `SumOfScorers` was attached (#21) ([71fd05a6](https://github.com/zkat/big-brain/commit/71fd05a64912b2cc88c76439543ea00a00267303))
245 |
246 |
247 |
248 |
249 | ## 0.4.0 (2021-04-26)
250 |
251 | #### Breaking Changes
252 |
253 | * **score:** scores are now 0.0..=1.0, not 0.0..=100.0 ([71f5122e](https://github.com/zkat/big-brain/commit/71f5122e9f5aa5b5965ad67f53ae9850f487d167), breaks [#](https://github.com/zkat/big-brain/issues/))
254 |
255 | #### Features
256 |
257 | * **evaluators:** Make all evaluators Clone ([4d5a5121](https://github.com/zkat/big-brain/commit/4d5a512171bf6f850893424c5baad03b0e686c26))
258 |
259 |
260 |
261 | ## 0.3.5 (2021-04-24)
262 |
263 | Previously, if a `Picker` re-picked the same action, and that action had been
264 | set to `Success` or `Failure`, it would just keep running the action in that
265 | state until it was time to switch to a different one.
266 |
267 | With this version, that behavior is changed, and `Failure` and `Success`
268 | actions that are re-picked will be respawned entirely (not even reused).
269 |
270 | Cheers to [@piedoomy](https://github.com/piedoomy) on Discord for pointing out
271 | this weird behavior!
272 |
273 | #### Bug Fixes
274 |
275 | * **thinker:** launch a new action when the current action is in an end state ([80d23f2f](https://github.com/zkat/big-brain/commit/80d23f2f2337a863c9cc3afbf944b25e3911db8c))
276 |
277 |
278 |
279 | ## 0.3.4 (2021-04-23)
280 |
281 | Welp. Turns out Thinkers were completely broken? This should work better :)
282 |
283 | #### Bug Fixes
284 |
285 | * **prelude:** Export ThinkerBuilder from prelude ([06cc03e1](https://github.com/zkat/big-brain/commit/06cc03e1dd563c708bff276f7a194c8c81a00a5a))
286 | * **thinker:**
287 | * disposed of ActiveThinker and circular state-setting ([7f8ed12b](https://github.com/zkat/big-brain/commit/7f8ed12b112152c3f8d548d0a2208cefdb1581af))
288 | * Need to do proper ptr_eq comparison here ([037a7c0d](https://github.com/zkat/big-brain/commit/037a7c0d0da065ea4cb5642047302d6bda13c670))
289 |
290 |
291 |
292 | ## 0.3.3 (2021-04-18)
293 |
294 | This fixes an issue with more children being added to an Actor causing Thinkers to get clobbered in really annoying ways.
295 |
296 | #### Bug Fixes
297 |
298 | * **thinker:** stop using the child/parent system for toplevel thinkers ([db16e2f6](https://github.com/zkat/big-brain/commit/db16e2f6ee97777b4df12e4ae435bf27b8012c7c))
299 |
300 |
301 |
302 | ## 0.3.2 (2021-04-17)
303 |
304 | This is a quick bugfix. Shoutout to [@ndarilek](https://github.com/ndarilek)
305 | for finding this one and giving me a chance to debug it!
306 |
307 | tl;dr: Bevy's hierarchy system *requires* that all children have `Transform`
308 | and `GlobalTransform` also attached, otherwise it just... kills them.
309 |
310 | #### Bug Fixes
311 |
312 | * **scorer:** stop attaching duplicate scorers ([10a6d022](https://github.com/zkat/big-brain/commit/10a6d022ec682e33b98309318020c9068be4cea2))
313 | * **thinker:** add Transform and GlobalTransform ([ed3a7cb3](https://github.com/zkat/big-brain/commit/ed3a7cb3c03e27b76b374f75ac179f29c979e4cf))
314 |
315 |
316 | ## 0.3.1 (2021-04-14)
317 |
318 | Just a quick release because I broke docs.rs :)
319 |
320 | #### Bug Fixes
321 |
322 | * **build:** remove .cargo/config.toml to make docs.rs happy ([393d9807](https://github.com/zkat/big-brain/commit/393d9807576d21c7234667b1f9914f1886579bd0))
323 |
324 |
325 |
326 | ## 0.3.0 (2021-04-14)
327 |
328 | This is a major overhaul of the Bevy API. It removes `.ron` support
329 | altogether, in favor of plain old Rust builders.
330 |
331 | #### Breaking Changes
332 |
333 | * The `.ron` Thinker definition API is gone. Use the ThinkerBuilder API instead.
334 | * The `Action` and `Scorer` derive macros are gone, including all of `big_brain_derive`.
335 | * Measures and Weights are gone.
336 | * `big-brain` no longer exports everything from the toplevel. Use `big_brain::prelude::*` instead.
337 |
338 | #### Bug Fixes
339 |
340 | Probably.
341 |
342 | #### Features
343 |
344 | * New builder-based Thinker API!
345 | * Composite Scorers: `FixedScore`, `AllOrNothing`, and `SumOfScorers`.
346 | * Composite Action: `Steps`, for sequential Action execution.
347 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "big-brain"
3 | version = "0.23.0"
4 | authors = ["Kat Marchán "]
5 | edition = "2021"
6 | description = "Rusty Utility AI library"
7 | exclude = ["assets/"]
8 | license = "Apache-2.0"
9 | readme = "README.md"
10 | keywords = ["utility-ai", "bevy", "ai", "ecs"]
11 | categories = ["game-development"]
12 | repository = "https://github.com/zkat/big-brain"
13 | homepage = "https://github.com/zkat/big-brain"
14 |
15 | [workspace]
16 |
17 | [dependencies]
18 | bevy = { version = "0.16.0", default-features = false, features = ["bevy_log"] }
19 | big-brain-derive = { version = "=0.23.0", path = "./derive" }
20 |
21 | [dev-dependencies]
22 | bevy = { version = "0.16.0", default-features = true }
23 | rand = { version = "0.8.5", features = ["small_rng"] }
24 |
25 | [features]
26 | trace = []
27 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright 2020 big-brain Contributors
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/Makefile.toml:
--------------------------------------------------------------------------------
1 | [tasks.changelog]
2 | workspace=false
3 | install_crate="git-cliff"
4 | command = "git-cliff"
5 | args = ["--prepend", "CHANGELOG.md", "-u", "--tag", "${@}"]
6 |
7 | [tasks.release]
8 | workspace=false
9 | install_crate="cargo-release"
10 | command = "cargo"
11 | args = ["release", "--workspace", "${@}"]
12 |
13 | [tasks.readme]
14 | workspace=false
15 | install_crate="cargo-readme"
16 | command = "cargo"
17 | args = ["readme", "-o", "README.md"]
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # `big-brain``
2 |
3 | [](https://crates.io/crates/big-brain)
4 | [](https://docs.rs/big-brain)
5 | [](./LICENSE.md)
7 |
8 | `big-brain` is a [Utility
9 | AI](https://en.wikipedia.org/wiki/Utility_system) library for games, built
10 | for the [Bevy Game Engine](https://bevyengine.org/)
11 |
12 | It lets you define complex, intricate AI behaviors for your entities based
13 | on their perception of the world. Definitions are heavily data-driven,
14 | using plain Rust, and you only need to program Scorers (entities that look
15 | at your game world and come up with a Score), and Actions (entities that
16 | perform actual behaviors upon the world). No other code is needed for
17 | actual AI behavior.
18 |
19 | See [the documentation](https://docs.rs/big-brain) for more details.
20 |
21 | #### Features
22 |
23 | * Highly concurrent/parallelizable evaluation.
24 | * Integrates smoothly with Bevy.
25 | * Proven game AI model.
26 | * Highly composable and reusable.
27 | * State machine-style continuous actions/behaviors.
28 | * Action cancellation.
29 |
30 | #### Example
31 |
32 | As a developer, you write application-dependent code to define
33 | [`Scorers`](#scorers) and [`Actions`](#actions), and then put it all
34 | together like building blocks, using [`Thinkers`](#thinkers) that will
35 | define the actual behavior.
36 |
37 | ##### Scorers
38 |
39 | `Scorer`s are entities that look at the world and evaluate into `Score`
40 | values. You can think of them as the "eyes" of the AI system. They're a
41 | highly-parallel way of being able to look at the `World` and use it to
42 | make some decisions later.
43 |
44 | ```rust
45 | use bevy::prelude::*;
46 | use big_brain::prelude::*;
47 |
48 | #[derive(Debug, Clone, Component, ScorerBuilder)]
49 | pub struct Thirsty;
50 |
51 | pub fn thirsty_scorer_system(
52 | thirsts: Query<&Thirst>,
53 | mut query: Query<(&Actor, &mut Score), With>,
54 | ) {
55 | for (Actor(actor), mut score) in query.iter_mut() {
56 | if let Ok(thirst) = thirsts.get(*actor) {
57 | score.set(thirst.thirst);
58 | }
59 | }
60 | }
61 | ```
62 |
63 | ##### Actions
64 |
65 | `Action`s are the actual things your entities will _do_. They are
66 | connected to `ActionState`s that represent the current execution state of
67 | the state machine.
68 |
69 | ```rust
70 | use bevy::prelude::*;
71 | use big_brain::prelude::*;
72 |
73 | #[derive(Debug, Clone, Component, ActionBuilder)]
74 | pub struct Drink;
75 |
76 | fn drink_action_system(
77 | mut thirsts: Query<&mut Thirst>,
78 | mut query: Query<(&Actor, &mut ActionState), With>,
79 | ) {
80 | for (Actor(actor), mut state) in query.iter_mut() {
81 | if let Ok(mut thirst) = thirsts.get_mut(*actor) {
82 | match *state {
83 | ActionState::Requested => {
84 | thirst.thirst = 10.0;
85 | *state = ActionState::Success;
86 | }
87 | ActionState::Cancelled => {
88 | *state = ActionState::Failure;
89 | }
90 | _ => {}
91 | }
92 | }
93 | }
94 | }
95 | ```
96 |
97 | ##### Thinkers
98 |
99 | Finally, you can use it when define the `Thinker`, which you can attach as
100 | a regular Component:
101 |
102 | ```rust
103 | fn spawn_entity(cmd: &mut Commands) {
104 | cmd.spawn((
105 | Thirst(70.0, 2.0),
106 | Thinker::build()
107 | .picker(FirstToScore { threshold: 0.8 })
108 | .when(Thirsty, Drink),
109 | ));
110 | }
111 | ```
112 |
113 | ##### App
114 |
115 | Once all that's done, we just add our systems and off we go!
116 |
117 | ```rust
118 | fn main() {
119 | App::new()
120 | .add_plugins(DefaultPlugins)
121 | .add_plugins(BigBrainPlugin::new(PreUpdate))
122 | .add_systems(Startup, init_entities)
123 | .add_systems(Update, thirst_system)
124 | .add_systems(PreUpdate, drink_action_system.in_set(BigBrainSet::Actions))
125 | .add_systems(PreUpdate, thirsty_scorer_system.in_set(BigBrainSet::Scorers))
126 | .run();
127 | }
128 | ```
129 |
130 | #### bevy version and MSRV
131 |
132 | The current version of `big-brain` is compatible with `bevy` 0.16.0.
133 |
134 | The Minimum Supported Rust Version for `big-brain` should be considered to
135 | be the same as `bevy`'s, which as of the time of this writing was "the
136 | latest stable release".
137 |
138 | #### Reflection
139 |
140 | All relevant `big-brain` types implement the bevy `Reflect` trait, so you
141 | should be able to get some useful display info while using things like
142 | [`bevy_inspector_egui`](https://crates.io/crates/bevy_inspector_egui).
143 |
144 | This implementation should **not** be considered stable, and individual
145 | fields made visible may change at **any time** and not be considered
146 | towards semver. Please use this feature **only for debugging**.
147 |
148 | #### Contributing
149 |
150 | 1. Install the latest Rust toolchain (stable supported).
151 | 2. `cargo run --example thirst`
152 | 3. Happy hacking!
153 |
154 | #### License
155 |
156 | This project is licensed under [the Apache-2.0 License](LICENSE.md).
157 |
--------------------------------------------------------------------------------
/README.tpl:
--------------------------------------------------------------------------------
1 | # `{{crate}}``
2 |
3 | {{readme}}
4 |
--------------------------------------------------------------------------------
/assets/models/town.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zkat/big-brain/7d3911c15078db332b97a91886de02d8c9bdd9b4/assets/models/town.glb
--------------------------------------------------------------------------------
/cliff.toml:
--------------------------------------------------------------------------------
1 | # configuration file for git-cliff (0.1.0)
2 |
3 | [changelog]
4 | # changelog header
5 | header = """
6 | # `big-brain` Release Changelog
7 |
8 | """
9 |
10 | # template for the changelog body
11 | # https://tera.netlify.app/docs/#introduction
12 | body = """
13 | {% if version %}\
14 |
15 | ## {{ version | replace(from="v", to="") }} ({{ timestamp | date(format="%Y-%m-%d") }})
16 | {% else %}\
17 | ## Unreleased
18 | {% endif %}\
19 | {% for group, commits in commits | filter(attribute="scope") | group_by(attribute="group") %}
20 | ### {{ group | upper_first }}
21 | {% for commit in commits %}
22 | {% if commit.scope %}\
23 | * **{{ commit.scope }}:** {{ commit.message }} ([{{ commit.id | truncate(length=8, end="") }}](https://github.com/zkat/big-brain/commit/{{ commit.id }}))
24 | {%- if commit.breaking %}
25 | * **BREAKING CHANGE**: {{ commit.breaking_description }}
26 | {%- endif %}\
27 | {% endif %}\
28 | {% endfor %}
29 | {% endfor %}
30 | """
31 |
32 | # remove the leading and trailing whitespace from the template
33 | trim = false
34 |
35 | # changelog footer
36 | # footer = """
37 | #
38 | # """
39 |
40 | [git]
41 | # allow only conventional commits
42 | # https://www.conventionalcommits.org
43 | conventional_commits = true
44 | # regex for parsing and grouping commits
45 | commit_parsers = [
46 | { message = "^feat*", group = "Features"},
47 | { message = "^fix*", group = "Bug Fixes"},
48 | { message = "^doc*", group = "Documentation"},
49 | { message = "^perf*", group = "Performance"},
50 | { message = "^refactor*", group = "Refactor"},
51 | { message = "^style*", group = "Styling"},
52 | { message = "^test*", group = "Testing"},
53 | { message = "^chore\\(release\\): prepare for*", skip = true},
54 | { message = "^chore*", group = "Miscellaneous Tasks"},
55 | { body = ".*security", group = "Security"},
56 | ]
57 | # filter out the commits that are not matched by commit parsers
58 | filter_commits = true
59 | # glob pattern for matching git tags
60 | # tag_pattern = "v?[0-9]*"
61 | # regex for skipping tags
62 | # skip_tags = "v0.1.0-beta.1"
63 |
--------------------------------------------------------------------------------
/derive/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "big-brain-derive"
3 | version = "0.23.0"
4 | authors = ["Kat Marchán "]
5 | description = "Procedural macros to simplify implementation of Big Brain traits"
6 | documentation = "https://docs.rs/big-brain-derive"
7 | homepage = "https://github.com/zkat/big-brain"
8 | repository = "https://github.com/zkat/big-brain"
9 | keywords = ["utility-ai", "bevy", "ai", "ecs"]
10 | categories = ["game-development"]
11 | license = "Apache-2.0"
12 | edition = "2021"
13 |
14 | [lib]
15 | proc-macro = true
16 |
17 | [features]
18 |
19 | [dependencies]
20 | proc-macro2 = "1.0"
21 | syn = { version = "1.0", features = ["parsing"]}
22 | quote = "1.0"
23 |
--------------------------------------------------------------------------------
/derive/src/action.rs:
--------------------------------------------------------------------------------
1 | //! Derive ActionBuilder on a given struct
2 | use proc_macro2::TokenStream;
3 | use quote::quote;
4 | use syn::{parse_macro_input, DeriveInput, Ident, Lit, LitStr, Meta};
5 |
6 | /// Derive ActionBuilder on a struct that implements Component + Clone
7 | pub fn action_builder_impl(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
8 | let input = parse_macro_input!(input as DeriveInput);
9 |
10 | let label = get_label(&input);
11 |
12 | let component_name = input.ident;
13 | let generics = input.generics;
14 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
15 |
16 | let component_string = component_name.to_string();
17 | let build_method = build_method(&component_name, &ty_generics);
18 | let label_method = label_method(
19 | label.unwrap_or_else(|| LitStr::new(&component_string, component_name.span())),
20 | );
21 |
22 | let gen = quote! {
23 | impl #impl_generics ::big_brain::actions::ActionBuilder for #component_name #ty_generics #where_clause {
24 | #build_method
25 | #label_method
26 | }
27 | };
28 |
29 | proc_macro::TokenStream::from(gen)
30 | }
31 |
32 | fn get_label(input: &DeriveInput) -> Option {
33 | let mut label: Option = None;
34 | let attrs = &input.attrs;
35 | for option in attrs {
36 | let option = option.parse_meta().unwrap();
37 | if let Meta::NameValue(meta_name_value) = option {
38 | let path = meta_name_value.path;
39 | let lit = meta_name_value.lit;
40 | if let Some(ident) = path.get_ident() {
41 | if ident == "action_label" {
42 | if let Lit::Str(lit_str) = lit {
43 | label = Some(lit_str);
44 | } else {
45 | panic!("Must specify a string for the `action_label` attribute")
46 | }
47 | }
48 | }
49 | }
50 | }
51 | label
52 | }
53 |
54 | fn build_method(component_name: &Ident, ty_generics: &syn::TypeGenerics) -> TokenStream {
55 | let turbofish = ty_generics.as_turbofish();
56 |
57 | quote! {
58 | fn build(&self, cmd: &mut ::bevy::prelude::Commands, action: ::bevy::prelude::Entity, _actor: ::bevy::prelude::Entity) {
59 | cmd.entity(action).insert(#component_name #turbofish::clone(self));
60 | }
61 | }
62 | }
63 |
64 | fn label_method(label: LitStr) -> TokenStream {
65 | quote! {
66 | fn label(&self) -> ::std::option::Option<&str> {
67 | ::std::option::Option::Some(#label)
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/derive/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! Big Brain Derive
2 | //! Procedural macros to simplify the implementation of Big Brain traits
3 | mod action;
4 | mod scorer;
5 |
6 | use action::action_builder_impl;
7 | use scorer::scorer_builder_impl;
8 |
9 | /// Derives ActionBuilder for a struct that implements Component + Clone
10 | #[proc_macro_derive(ActionBuilder, attributes(action_label))]
11 | pub fn action_builder_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
12 | action_builder_impl(input)
13 | }
14 |
15 | /// Derives ScorerBuilder for a struct that implements Component + Clone
16 | #[proc_macro_derive(ScorerBuilder, attributes(scorer_label))]
17 | pub fn scorer_builder_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
18 | scorer_builder_impl(input)
19 | }
20 |
--------------------------------------------------------------------------------
/derive/src/scorer.rs:
--------------------------------------------------------------------------------
1 | //! Derive ScorerBuilder on a given struct
2 | use proc_macro2::TokenStream;
3 | use quote::quote;
4 | use syn::{parse_macro_input, DeriveInput, Ident, Lit, LitStr, Meta};
5 |
6 | /// Derive ScorerBuilder on a struct that implements Component + Clone
7 | pub fn scorer_builder_impl(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
8 | let input = parse_macro_input!(input as DeriveInput);
9 |
10 | let label = get_label(&input);
11 |
12 | let component_name = input.ident;
13 | let generics = input.generics;
14 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
15 |
16 | let component_string = component_name.to_string();
17 | let build_method = build_method(&component_name, &ty_generics);
18 | let label_method = label_method(
19 | label.unwrap_or_else(|| LitStr::new(&component_string, component_name.span())),
20 | );
21 |
22 | let gen = quote! {
23 | impl #impl_generics ::big_brain::scorers::ScorerBuilder for #component_name #ty_generics #where_clause {
24 | #build_method
25 | #label_method
26 | }
27 | };
28 |
29 | proc_macro::TokenStream::from(gen)
30 | }
31 |
32 | fn get_label(input: &DeriveInput) -> Option {
33 | let mut label: Option = None;
34 | let attrs = &input.attrs;
35 | for option in attrs {
36 | let option = option.parse_meta().unwrap();
37 | if let Meta::NameValue(meta_name_value) = option {
38 | let path = meta_name_value.path;
39 | let lit = meta_name_value.lit;
40 | if let Some(ident) = path.get_ident() {
41 | if ident == "scorer_label" {
42 | if let Lit::Str(lit_str) = lit {
43 | label = Some(lit_str);
44 | } else {
45 | panic!("Must specify a string for the `scorer_label` attribute")
46 | }
47 | }
48 | }
49 | }
50 | }
51 | label
52 | }
53 |
54 | fn build_method(component_name: &Ident, ty_generics: &syn::TypeGenerics) -> TokenStream {
55 | let turbofish = ty_generics.as_turbofish();
56 |
57 | quote! {
58 | fn build(&self, cmd: &mut ::bevy::prelude::Commands, scorer: ::bevy::prelude::Entity, _actor: ::bevy::prelude::Entity) {
59 | cmd.entity(scorer).insert(#component_name #turbofish::clone(self));
60 | }
61 | }
62 | }
63 |
64 | fn label_method(label: LitStr) -> TokenStream {
65 | quote! {
66 | fn label(&self) -> ::std::option::Option<&str> {
67 | ::std::option::Option::Some(#label)
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/examples/concurrent.rs:
--------------------------------------------------------------------------------
1 | //! This example describes how to create a composite action that executes multiple sub-actions
2 | //! concurrently.
3 | //!
4 | //! `Race` succeeds when any of the sub-actions succeed.
5 | //! `Join` succeeds if all the sub-actions succeed.
6 |
7 | use bevy::log::LogPlugin;
8 | use bevy::prelude::*;
9 | use big_brain::prelude::*;
10 | use rand::rngs::SmallRng;
11 | use rand::{Rng, SeedableRng};
12 |
13 | /// An action where the actor has to guess a given number
14 | #[derive(Clone, Component, Debug, ActionBuilder)]
15 | pub struct GuessNumber {
16 | // Number to guess (between 0 and 10 included)
17 | to_guess: u8,
18 | // Rng to perform guesses
19 | rng: SmallRng,
20 | }
21 |
22 | fn guess_number_action(
23 | // A query on all current MoveToWaterSource actions.
24 | mut action_query: Query<(&Actor, &mut ActionState, &mut GuessNumber, &ActionSpan)>,
25 | ) {
26 | // Loop through all actions, just like you'd loop over all entities in any other query.
27 | for (_actor, mut action_state, mut guess_number, span) in &mut action_query {
28 | let _guard = span.span().enter();
29 |
30 | // Different behavior depending on action state.
31 | match *action_state {
32 | // Action was just requested; it hasn't been seen before.
33 | ActionState::Requested => {
34 | debug!(
35 | "Let's try to guess the secret number: {:?}",
36 | guess_number.to_guess
37 | );
38 | // We don't really need any initialization code here, since the queries are cheap enough.
39 | *action_state = ActionState::Executing;
40 | }
41 | ActionState::Executing => {
42 | // Guess a number. If we guessed right, succeed; else keep trying.
43 | let guess: u8 = guess_number.rng.gen_range(0..=10);
44 | debug!("Guessed: {:?}", guess);
45 | if guess == guess_number.to_guess {
46 | debug!(
47 | "Guessed the secret number: {:?}! Action succeeded.",
48 | guess_number.to_guess
49 | );
50 | *action_state = ActionState::Success;
51 | }
52 | }
53 | ActionState::Cancelled => {
54 | // Always treat cancellations, or we might keep doing this forever!
55 | // You don't need to terminate immediately, by the way, this is only a flag that
56 | // the cancellation has been requested. If the actor is balancing on a tightrope,
57 | // for instance, you may let them walk off before ending the action.
58 | *action_state = ActionState::Failure;
59 | }
60 | _ => {}
61 | }
62 | }
63 | }
64 |
65 | // We will use a dummy scorer that always returns 1.0
66 | #[derive(Clone, Component, Debug, ScorerBuilder)]
67 | pub struct DummyScorer;
68 |
69 | pub fn dummy_scorer_system(mut query: Query<(&Actor, &mut Score), With>) {
70 | for (Actor(_actor), mut score) in &mut query {
71 | score.set(1.0);
72 | }
73 | }
74 |
75 | pub fn init_entities(mut cmd: Commands) {
76 | let number_to_guess: u8 = 5;
77 | // We use the Race struct to build a composite action that will try to guess
78 | // multiple numbers. If any of the guesses are right, the whole `Race` action succeeds.
79 | let race_guess_numbers = Concurrently::build()
80 | .mode(ConcurrentMode::Race)
81 | .label("RaceToGuessNumbers")
82 | // ...try to guess a first number
83 | .push(GuessNumber {
84 | to_guess: number_to_guess,
85 | rng: SmallRng::from_entropy(),
86 | })
87 | // ...try to guess a second number
88 | .push(GuessNumber {
89 | to_guess: number_to_guess,
90 | rng: SmallRng::from_entropy(),
91 | });
92 |
93 | // We use the Join struct to build a composite action that will try to guess
94 | // multiple numbers. If all of the guesses are right, the whole `Race` action succeeds.
95 | let join_guess_numbers = Concurrently::build()
96 | .mode(ConcurrentMode::Join) // This is the default mode, so we could have omitted it.
97 | .label("JoinToGuessNumbers")
98 | // ...try to guess a first number
99 | .push(GuessNumber {
100 | to_guess: number_to_guess,
101 | rng: SmallRng::from_entropy(),
102 | })
103 | // ...try to guess a second number
104 | .push(GuessNumber {
105 | to_guess: number_to_guess,
106 | rng: SmallRng::from_entropy(),
107 | });
108 |
109 | // We'll use `Steps` to execute a sequence of actions.
110 | // First, we'll guess the numbers with 'Race', and then we'll guess the numbers with 'Join'
111 | // See the `sequence.rs` example for more details.
112 | let guess_numbers = Steps::build()
113 | .label("RaceAndThenJoin")
114 | .step(race_guess_numbers)
115 | .step(join_guess_numbers);
116 |
117 | // Build the thinker
118 | let thinker = Thinker::build()
119 | .label("GuesserThinker")
120 | // always select the action with the highest score
121 | .picker(Highest)
122 | .when(DummyScorer, guess_numbers);
123 |
124 | cmd.spawn(thinker);
125 | }
126 |
127 | fn main() {
128 | // Once all that's done, we just add our systems and off we go!
129 | App::new()
130 | .add_plugins(MinimalPlugins)
131 | .add_plugins(LogPlugin {
132 | // Use `RUST_LOG=big_brain=trace,concurrent=trace cargo run --example concurrent --features=trace` to see extra tracing output.
133 | filter: "big_brain=warn,concurrent=debug".to_string(),
134 | ..default()
135 | })
136 | .add_plugins(BigBrainPlugin::new(PreUpdate))
137 | .add_systems(Startup, init_entities)
138 | .add_systems(
139 | PreUpdate,
140 | (
141 | guess_number_action.in_set(BigBrainSet::Actions),
142 | dummy_scorer_system.in_set(BigBrainSet::Scorers),
143 | ),
144 | )
145 | .run();
146 | }
147 |
--------------------------------------------------------------------------------
/examples/custom_measure.rs:
--------------------------------------------------------------------------------
1 | //! This example demonstrates how to build a custom measure and use that
2 | //! in a Thinker.
3 |
4 | use bevy::ecs::component::Mutable;
5 | use bevy::log::LogPlugin;
6 | use bevy::prelude::*;
7 | use big_brain::prelude::*;
8 | use big_brain::scorers::MeasuredScorer;
9 |
10 | // Lets define a custom measure. There are quite a few built-in ones in big-brain,
11 | // so we'll create a slightly useless Measure that sums together the weighted scores,
12 | // but weights get divided by the Scorer's index in the Vec.
13 | #[derive(Debug, Clone)]
14 | pub struct SumWithDecreasingWeightMeasure;
15 |
16 | impl Measure for SumWithDecreasingWeightMeasure {
17 | fn calculate(&self, scores: Vec<(&Score, f32)>) -> f32 {
18 | scores
19 | .iter()
20 | .enumerate()
21 | .fold(0f32, |acc, (idx, (score, weight))| {
22 | acc + score.get() * weight / (1.0 + idx as f32)
23 | })
24 | }
25 | }
26 |
27 | // We'll keep this example fairly simple, let's have Waffles and Pancakes, and
28 | // try to optimise our happiness based on keeping our waffle and pancake level high.
29 | // Its kind of like the thirst example but sweeter.
30 | #[derive(Component, Debug)]
31 | pub struct Pancakes(pub f32);
32 |
33 | #[derive(Component, Debug)]
34 | pub struct Waffles(pub f32);
35 |
36 | pub fn eat_dessert(time: Res