-
Notifications
You must be signed in to change notification settings - Fork 351
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ECS chapter for new book #182
Changes from 111 commits
252325d
8a3cf22
8f903e0
4f0bc41
ac89c61
cee206b
c69c59b
c8ede28
1e32b8f
b15e909
1a145a8
38c54c3
8574c9e
4c7e9fa
f91b0a2
6a060a5
713a57f
b054d15
b39bdd6
815beb4
1152b21
d6cc1fe
38821f5
607f71a
98e46f4
a812e4f
e3e927a
e158ab0
cb4cecb
b24a3a6
aaf3e23
5cb00f0
c9d7fe2
d0653de
cddfea3
5dad8ad
3537668
8a1e053
8289828
0b664f5
04a2d33
f4dff89
8878e81
01402b5
3058e79
58d1b7c
4ed2ca5
d5636c8
8352bd2
29eaf59
16356d5
ed89be5
8f27ecd
20a2fb6
80df83f
11260e7
49eed9c
0420c95
b05c7f5
3ae04bf
cd44547
ebf131e
31dd304
e99da61
b0b671b
34f3198
6f5dff0
e776c74
9847aba
f734bc5
528a91f
b35d0b8
8e7794c
b0a7a0b
d82ee9b
9896278
1c28e56
62b998a
07f4528
50dbe1c
bef720e
a4ef5f8
a0b0363
ab2410c
b6b0de4
0e9677d
0b38fba
138fa48
70fbcb1
c2f9070
b2f30c1
a3a00ad
3e370de
190c6a2
ec50cff
f2799f5
fc9a7ad
d8ebee3
4f169ab
ee2bfc3
4bd12cc
ce83f85
d9d038e
f45037d
0bcb450
af47f49
34ef339
1c2e1d8
83a7939
8a8731a
18cbae9
5c6f450
86fa2ba
c62f9cb
32ea535
0328a1e
7690cc7
fdeab37
a03852c
137be61
dc31a3a
8442122
06d9aea
6f7a0c4
8e6f32f
d658883
5579722
6712313
df9864e
6cb86c9
9b0efb8
e3f41c3
c26a280
e4147f7
d562f22
f39156b
581ee6b
575af57
3206cae
1bca9d8
1e9b8a6
ea4434c
55beec6
1013543
9089603
69e840b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
# This is a basic workflow to help you get started with Actions | ||
|
||
name: CI | ||
|
||
# Controls when the action will run. Triggers the workflow on pushes to master | ||
# or any pull request or pull request | ||
on: | ||
push: | ||
branches: [master] | ||
pull_request: | ||
|
||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel | ||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
if: github.ref != 'refs/heads/master' # Only on PRs, see also build_and_deploy below | ||
steps: | ||
- uses: actions/checkout@v2 | ||
|
||
- name: "Build website" | ||
uses: shalzz/zola-deploy-action@master | ||
env: | ||
PAGES_BRANCH: gh-pages | ||
BUILD_DIR: . | ||
BUILD_ONLY: true | ||
TOKEN: fake-secret | ||
|
||
build_and_deploy: | ||
runs-on: ubuntu-latest | ||
if: github.ref == 'refs/heads/master' # Only on a push to the master branch (like when PR's are merged) | ||
steps: | ||
- uses: actions/checkout@master | ||
|
||
- name: "Build and deploy website" | ||
uses: shalzz/zola-deploy-action@master | ||
env: | ||
PAGES_BRANCH: gh-pages | ||
BUILD_DIR: . | ||
TOKEN: ${{ secrets.CART_PAT }} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,3 @@ | ||
public | ||
**/.DS_Store | ||
.idea/ | ||
content/assets |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,4 +6,148 @@ template = "book-section.html" | |
page_template = "book-section.html" | ||
+++ | ||
|
||
TODO: high-level overview of how the ECS works | ||
Bevy is fundamentally powered by its ECS (Entity Component System): almost all data is stored as components on entities, and all logic is executed by its systems. | ||
|
||
As we [mentioned in the last chapter](../welcome/app/_index.md), all of our data is stored in a {{rust_type(type="struct" crate="bevy_ecs" name="World")}} on our {{rust_type(type="struct" crate="bevy" name="App")}}). | ||
We can think of our **entity-component data storage** as a giant in-memory database: | ||
|
||
* each row is an **entity**, representing an object (perhaps a player, tile, or UI button) in our game | ||
* each column is a type of **component**, storing data of a particular type (perhaps the sprite, team or life of a player entity) in an [efficient way](https://github.com/bevyengine/bevy/pull/1525) that keeps data of the same type tightly packed together | ||
* each cell is a component of a particular entity, which has a concrete value we can look up and change | ||
* we access data from this database using **queries**, which fetch entities with the specified components | ||
* the primary key of this database is the {{rust_type(type="struct" crate="bevy_ecs" name="Entity")}} identifier, which can be used to look up specific entities using {{rust_type(type="struct" crate="bevy_ecs" name="Query" method = "get")}} | ||
|
||
Of course, this database is [very ragged](https://www.transdatasolutions.com/what-is-ragged-data/): not all entities will have every component! | ||
We can use this fact to specialize behavior between entities: systems only perform work on entities with the correct combination of components. | ||
You don't want to apply gravity to entities without a position in your world, and you're only interested in using the UI layout algorithm to control the layout of UI entities! | ||
|
||
When we want to go beyond this tabular data storage, we can use **resources**: global singletons which store data in monolithic blobs. | ||
You might use resources to interface with other libraries, store unique bits of state like the game's score, or store secondary data structures like indexes to augment your use of entity-component data. | ||
|
||
In order to manipulate and act on this data, we must use systems. | ||
**Systems** are Rust functions that request specific data, such as resources and entities, from the {{rust_type(type="struct" crate="bevy_ecs" name="World")}}. They define a query in their parameters (arguments) that selects data with a particular combination of components. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I personally think that these "core concept" introductions would much more approachable if they followed a similar pattern to the current Bevy Book ECS intro: The bullet points help visually break up the concepts. The simple examples isolated by concept help ground the concepts in reality (without burying them in a large blob of code). I'm now realizing that I made the mistake of using Transform in the Query when I literally just introduced the Position component. And leaking the internals of Entity isn't particularly helpful as an introduction (especially when those leaked internals are incorrect!). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like the bullet points a lot. I'm going to steal from this. |
||
All of the rules and behaviours of our game are governed by systems. | ||
|
||
Once the systems are added to our app the **runner** takes in this information and automatically runs our systems: typically once during each pass of the **game loop** according to the rules defined in their **schedule**. | ||
Bevy's default execution strategy runs systems in parallel by default, without the need for any manual setup. | ||
Because the **function signature** of each of our systems fully define the data it can access, we can ensure that only one system can change a piece of data at once (although any number can read from a piece of data at the same time). | ||
Systems within the same **stage** are allowed to run in parallel with each other (as long as their data access does not conflict), and are assigned to a free thread as soon as one is free. | ||
|
||
When we need to access data in complex, cross-cutting ways that are not cleanly modelled by our systems' function signatures, we can defer the work until we have exclusive access to the entire world's data: executing **commands** generated in earlier systems at the end of each stage or performing complex logic (like saving the entire game) in our own **exclusive systems**. | ||
You will first encounter this when spawning and despawning entities: we have no way of knowing precisely which other components our entities might have, and so we are forced to wait until we can ensure that we can safely write to *all* component data at once. | ||
|
||
## ECS by example | ||
|
||
Before we dive into the details of each of these features, let's take a quick look at a simple game that you can run and play. | ||
Unsurprisingly, the different parts of the ECS tend to be closely linked: components are not very useful without a way to spawn entities and systems that run our logic are very dull if we can't discuss the data they can access. | ||
The details of each part are more easily grasped if you have a basic sense of the whole. | ||
|
||
```rust | ||
use bevy::app::AppExit; | ||
use bevy::log::LogPlugin; | ||
use bevy::prelude::*; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is ... a lot to take in all at once. I understand what you're going for (providing real context to ground concepts in reality before introducing them individually), but this might be peoples' first practical introduction to:
I think its ok to assume some level of familiarity with Rust (as the goal here isn't to teach people rust), so to an extent (1) (2), and (5) are hard to avoid. But there will be a percentage of people who will ignore that advice, be new to both Bevy and Rust, find this code block as their first exposure to Bevy ECS and immediately think "wow thats a lot to take in". They might even close their tab and move on to something else. I certainly don't want first time users' first experience with Bevy ECS to be: // This is a fast but insecure HashMap (dictionary, for those coming from Python)
// implementation that Bevy re-exports for internal and external use
use bevy::utils::HashMap; I personally like the narrative of feeding small, very scoped / bite-sized code blocks first. And each time having the user think "yeah ok that makes sense and doesnt scare me". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree, this example is way too complex for a new user. |
||
|
||
// This component defines our entity's life total. | ||
#[derive(Component)] | ||
struct Life(f32); | ||
|
||
// This component is used to mark if our entity is currently airborne. | ||
#[derive(Component)] | ||
struct Falling { | ||
// The higher the initial height of falling, the higher the damage. | ||
height: f32, | ||
alice-i-cecile marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
fn main() { | ||
App::new() | ||
// These plugins create the basic framework | ||
.add_plugins(MinimalPlugins) | ||
// This allows us to report player health using `info!` | ||
.add_plugin(LogPlugin) | ||
// Because we've added this system as a startup system, | ||
// it runs exactly once before any ordinary system | ||
.add_startup_system(spawn_player_system) | ||
// Ordinary systems run once per frame (or pass of the game loop). | ||
.add_system(gravity_system.label("gravity")) | ||
// We need to make sure we report fall damage after gravity | ||
// Otherwise it won't have been calculated yet | ||
.add_system(fall_damage_system.after("gravity")) | ||
.run(); | ||
} | ||
|
||
// This system spawns the player at a fairly high elevation. | ||
fn spawn_player_system(mut commands: Commands) { | ||
const INITIAL_HEIGHT: f32 = 15.0; | ||
|
||
// Entities must be spawned in a delayed fashion with commands. | ||
commands | ||
.spawn() | ||
// We can add components to entities that we are spawning with the .insert() | ||
.insert(Life(20.0)) | ||
// Transform is the standard position component in Bevy, | ||
// controlling the translation, rotation and scale of entities | ||
.insert(Transform::from_translation(Vec3::new( | ||
0.0, | ||
INITIAL_HEIGHT, | ||
0.0, | ||
))) | ||
.insert(Falling { | ||
height: INITIAL_HEIGHT, | ||
}); | ||
|
||
// This expression creates a second entity, with a slightly different set of components | ||
commands | ||
.spawn() | ||
// We can customize the starting values of our components | ||
// by changing the data stored in the structs we pass in | ||
.insert(Life(30.0)) | ||
// This player begins on the ground | ||
// So we're not inserting the Falling component | ||
.insert(Transform::from_translation(Vec3::new(0.0, 0.0, 0.0))); | ||
} | ||
|
||
// This system pulls down the entity towards the ground (at y = 0), at a constant velocity, | ||
// only while it's falling. | ||
// The With<Falling> filter ensures that only entities with the `Falling` component are affected | ||
fn gravity_system(mut query: Query<&mut Transform, With<Falling>>) { | ||
const FALL_RATE: f32 = 1.0; | ||
|
||
// Performing the same operation on each entity returned by the query | ||
// using a loop is a very common pattern | ||
for mut transform in query.iter_mut() { | ||
transform.translation.y = (transform.translation.y - FALL_RATE).max(0.0); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there could be a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A name might not be necessary if we want to keep it simple, but adding a simple log would help when running this example. |
||
} | ||
} | ||
|
||
// This system deals damage to falling entities based on the height from which it fell | ||
fn fall_damage_system( | ||
mut commands: Commands, | ||
// By adding `Entity` to our query, we can extract | ||
// the unique identifier of the entity we're iterating over | ||
mut query: Query<(Entity, &mut Life, &Falling, &mut Transform)>, | ||
mut exit_events: EventWriter<AppExit>, | ||
) { | ||
// Each of the components in our query must be present | ||
// on an entity for it to be returned in our query. | ||
// This system will loop over the first entity spawned, but not the second. | ||
for (entity, mut life, falling, mut transform) in query.iter_mut() { | ||
// Our entity has touched the ground | ||
if transform.translation.y <= 0.0 { | ||
transform.translation.y = 0.0; | ||
// We're using the `Entity` information from our query | ||
// to ensure we're removing the `Falling` component from the correct entity | ||
commands.entity(entity).remove::<Falling>(); | ||
|
||
// Falling from small heights shouldn't hurt players at all | ||
let damage = (falling.height - 3.0).max(0.0); | ||
// .0 accesses the first field of our Life(f32) tuple struct | ||
life.0 = (life.0 - damage).max(0.0); | ||
info!("Damage: {}", damage); | ||
// End the game as soon as the first entity has collided with the ground | ||
exit_events.send(AppExit); | ||
} | ||
} | ||
} | ||
``` | ||
|
||
If you'd like to see more tiny but cohesive examples like this, check out our [game examples](https://github.com/bevyengine/bevy/tree/latest/examples/game) on the Bevy GitHub repository. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a fan of starting with a database analogy. It assumes the user understands databases (what is a row, column, cell, primary key, "ragged databases", tabular data storage, in-memory databases vs distributed databases, etc). Even I didn't know what "ragged" meant in a database context and I've clocked a ton of hours in the database world. Encouraging people to learn a niche term, how it relates to databases, then how that database concept relates to Bevy ECS feels pretty roundabout.
I think the analogy is helpful as either an aside that we link to, or as a one liner (to help people with the context to appreciate it). But I don't like that it is central to the initial framing. And it should probably be accompanied by a list of ways Bevy ECS isn't like a database (ex: sparse sets, the archetype abstraction, change detection, generational indices, etc).
I think it would be clearer (and more universally palatable) to introduce Bevy ECS concepts directly first, rather than via analogy. What role does each piece fill, how might you use each piece in practice, etc.