diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index cc90457bb9..0000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: CI - -on: - pull_request: - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - - name: "Build Bevy Assets" - run: cd generate-assets && ./generate_assets.sh - - - name: "Build website" - uses: shalzz/zola-deploy-action@master - env: - PAGES_BRANCH: gh-pages - BUILD_DIR: . - BUILD_ONLY: true - TOKEN: fake-secret \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index b330b9929b..0000000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Deploy - -on: - push: - branches: [master] - schedule: - - cron: '0 0 * * *' - workflow_dispatch: - -jobs: - build_and_deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - - name: "Build Bevy Assets" - run: cd generate-assets && ./generate_assets.sh - - - name: "Build and deploy website" - if: github.repository_owner == 'bevyengine' - uses: shalzz/zola-deploy-action@master - env: - PAGES_BRANCH: gh-pages - BUILD_DIR: . - TOKEN: ${{ secrets.CART_PAT }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000000..78b8144f2d --- /dev/null +++ b/.github/workflows/main.yml @@ -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 }} diff --git a/.gitignore b/.gitignore index 71da54c70d..7b24706ebb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ public **/.DS_Store .idea/ -content/assets + +generate-assets/target/ +generate-errors/target/ diff --git a/README.md b/README.md index db052ccc12..5b12a19c90 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,14 @@ The source files for https://bevyengine.org. This includes official Bevy news an ## Zola -The Bevy website is built using the Zola static site engine. In our experience, it is fast, flexible, and straightforward to use. +The Bevy website is built using the Zola static site engine. In our experience, it is fast, flexible, and straightforward to use. [Check it out here](https://www.getzola.org/)! To check out any local changes you've made: -1. [Download Zola](https://www.getzola.org/). -2. Clone the Bevy Website git repo and enter that directory: - 1. `git clone https://github.com/bevyengine/bevy-website.git` - 2. `cd bevy-website` +1. Download Zola. +2. Clone the Bevy Website git repo and move to that directory: + 1. `git clone https://github.com/bevyengine/bevy-website.git` + 2. `cd bevy-website` 3. Start the Zola server with `zola serve`. A local server should start and you should be able to access a local version of the website from there. + diff --git a/config.toml b/config.toml index 38e9358b76..5a7faf47e1 100644 --- a/config.toml +++ b/config.toml @@ -22,7 +22,8 @@ taxonomies = [ # Whether to do syntax highlighting # Theme can be customised by setting the `highlight_theme` variable to a theme supported by Zola highlight_code = true -highlight_theme = "css" +highlight_theme = "base16-ocean-dark" + [extra] # Put all your custom variables here diff --git a/content/learn/book/ecs/_index.md b/content/learn/book/ecs/_index.md index d1f1081755..a15ff3d39a 100644 --- a/content/learn/book/ecs/_index.md +++ b/content/learn/book/ecs/_index.md @@ -4,6 +4,345 @@ weight = 2 sort_by = "weight" template = "book-section.html" page_template = "book-section.html" +insert_anchor_links = "right" +++ -TODO: high-level overview of how the ECS works +Bevy is fundamentally powered by its ECS (a central [paradigm](https://ajmmertens.medium.com/ecs-from-tool-to-paradigm-350587cdf216) for organizing and operating on data which stands Entity-Component-System): almost all data is stored as components which belong to entities, and all logic is executed by its systems. + +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 component data from 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, each in their own monolithic blob. +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. +All of the rules and behaviours of our game are governed by systems. + +Once the systems are added to our app, the **runner** takes 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 thread to perform work 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 but fun whack-a-mole 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::prelude::*; + +// 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; + +// We want to randomize our moles, +// so we're also using the `rand` crate. +// Be sure to add `rand = "0.8"` to your Cargo.toml +use rand::distributions::{Distribution, Uniform}; +use rand::seq::IteratorRandom; + +// The main function defines all the code that our program will run +// in an imperative fashion +fn main() { + // The App stores entities, their components and all resources in the World + // and systems in its Schedule + App::new() + // Sets up the graphics and input that we need for this example + .add_plugins(DefaultPlugins) + // We only want to run this once, at the start of the app, so it's a startup system + .add_startup_system(spawn_camera_system) + // This resource is explicitly initialized with a new value + .insert_resource(Score(0)) + // These systems all run once per frame, + // in parallel if possible + .add_system(report_score_system) + // This resource is initialized using its FromWorld trait impl + .init_resource::() + // This resource is initialized using its Default trait impl + .init_resource::() + .add_system(spawn_mole_system) + .add_system(despawn_on_timer_system) + .add_system(whack_system) + .run(); +} + +/// Resource that stores the current score +struct Score(isize); + +/// Creates a standard 2D camera +fn spawn_camera_system(mut commands: Commands) { + // This spawns a new entity for our camera + // with the set of components defined by the OrthographicCameraBundle + commands.spawn_bundle(OrthographicCameraBundle::new_2d()); +} + +/// Reports the value of the Score resource for the player +fn report_score_system(score: Res) { + // We only want to report the new score when it has changed + if score.is_changed() { + println!("Score: {}", score.0); + } +} + +const MOLE_SIZE: f32 = 50.0; + +/// Marker component for our moles +#[derive(Component)] +struct Mole; + +/// Component that stores how long this entity has left to live +#[derive(Component)] +struct DespawnTimer(Timer); + +impl Default for DespawnTimer { + fn default() -> Self { + const MOLE_LIFESPAN: f32 = 5.0; + DespawnTimer(Timer::from_seconds(MOLE_LIFESPAN, false)) + } +} + +/// Component that determines how many points each mole is worth +// Enums can be added as components! +// We need the additional traits to allow us to use this type as a key in our MoleColors hashmap +#[derive(Component, PartialEq, Eq, Hash, Clone, Copy)] +enum MoleType { + Avoid, + Click, + Prioritize, +} + +impl MoleType { + /// The number of points each type of mole is worth when clicked + fn points(&self) -> isize { + match *self { + MoleType::Avoid => -10, + MoleType::Click => 1, + MoleType::Prioritize => 5, + } + } + + /// The color each type of mole will have when spawned + fn color(&self) -> Color { + match *self { + MoleType::Avoid => Color::RED, + MoleType::Click => Color::BLUE, + MoleType::Prioritize => Color::VIOLET, + } + } + + /// Lists the types of moles for easy iteration + fn types() -> impl Iterator { + [MoleType::Avoid, MoleType::Click, MoleType::Prioritize] + .iter() + // This allows us to return MoleType as our item, + // rather than just a reference to the objects in this array (&MoleType) + .copied() + } +} + +/// A resource to store the colors for our moles in an efficient way +struct MoleColors { + map: HashMap>, +} + +// This trait allows us to initialize a resource in more complex ways +impl FromWorld for MoleColors { + fn from_world(world: &mut World) -> Self { + // Here, we're grabbing the Assets resource directly from the world + // so we can record the materials that we create in it + let mut material_assets = world.get_resource_mut::>().unwrap(); + let mut mole_colors = MoleColors { + map: HashMap::default(), + }; + + // The types() method on MoleTypes we made above returns an iterator, + // so we can loop over it directly + for mole_type in MoleType::types() { + // Each mole type has its own color. + // We need to create a ColorMaterial from our simple RGBA color + let color_material: ColorMaterial = mole_type.color().into(); + // Then, we add that new color material to our assets collection, + // storing the original handle created for it + let material_handle = material_assets.add(color_material); + // Assets are expensive and batching is critical for performance, + // so we're storing reference-counted pointers to a single common asset + // called "handles", then cloning references as needed + mole_colors.map.insert(mole_type, material_handle); + } + + // The last expression evaluated is implicitly (and idiomatically) returned in Rust + mole_colors + } +} + +impl MoleColors { + fn get_handle(&self, mole_type: MoleType) -> Handle { + // We need to clone a new handle for each new mole we create. + // We can skip reference-counting using `clone_weak` rather than `clone`, + // since we know that the colors stored in our resource will never be unloaded + self.map + .get(&mole_type) + .expect("Missing color handle") + .clone_weak() + } +} + +/// Resource that tracks when the next mole should be spawned +struct SpawnTimer(Timer); + +impl Default for SpawnTimer { + fn default() -> Self { + const SPAWN_PERIOD: f32 = 0.5; + + // The second value `true` sets the `repeating` field of the timer, + // causing the timer to reset when it ticks down to 0 + SpawnTimer(Timer::from_seconds(SPAWN_PERIOD, true)) + } +} + +/// Spawns a new random mole whenever the SpawnTimer has elapsed +fn spawn_mole_system( + mut commands: Commands, + mut spawn_timer: ResMut, + windows: Res, + time: Res