diff --git a/.cargo/config_fast_builds.toml b/.cargo/config_fast_builds.toml index 4ee56c480c157..2d82ca39a89c0 100644 --- a/.cargo/config_fast_builds.toml +++ b/.cargo/config_fast_builds.toml @@ -30,7 +30,7 @@ rustflags = [ [target.x86_64-pc-windows-msvc] linker = "rust-lld.exe" # Use LLD Linker rustflags = [ - "-Zshare-generics=n", + "-Zshare-generics=n", # (Nightly) "-Zthreads=0", # (Nightly) Use improved multithreading with the recommended amount of threads. ] diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 61f1d0c7ee24e..c4f6e2bd0f487 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1 @@ -# These are supported funding model platforms - -custom: https://bevyengine.org/community/donate/ +custom: https://bevyengine.org/donate/ diff --git a/.github/contributing/engine_style_guide.md b/.github/contributing/engine_style_guide.md index 5ccc1c59a2586..ff656373fdb4d 100644 --- a/.github/contributing/engine_style_guide.md +++ b/.github/contributing/engine_style_guide.md @@ -14,6 +14,7 @@ For more advice on contributing to the engine, see the [relevant section](../../ 4. Use \`variable_name\` code blocks in comments to signify that you're referring to specific types and variables. 5. Start comments with capital letters. End them with a period if they are sentence-like. 3. Use comments to organize long and complex stretches of code that can't sensibly be refactored into separate functions. +4. When using [Bevy error codes](https://bevyengine.org/learn/errors/) include a link to the relevant error on the Bevy website in the returned error message `... See: https://bevyengine.org/learn/errors/#b0003`. ## Rust API guidelines diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37222a932513d..d1d0909cac009 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -189,7 +189,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Check for typos - uses: crate-ci/typos@v1.18.2 + uses: crate-ci/typos@v1.19.0 - name: Typos info if: failure() run: | diff --git a/.github/workflows/validation-jobs.yml b/.github/workflows/validation-jobs.yml index 7e7d2029ac440..932ff4041f4fb 100644 --- a/.github/workflows/validation-jobs.yml +++ b/.github/workflows/validation-jobs.yml @@ -286,3 +286,31 @@ jobs: run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev - name: Run cargo udeps run: cargo udeps + + check-example-showcase-patches-still-work: + if: ${{ github.event_name == 'merge_group' }} + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-check-showcase-patches-${{ hashFiles('**/Cargo.toml') }} + - uses: dtolnay/rust-toolchain@stable + - name: Installs cargo-udeps + run: cargo install --force cargo-udeps + - name: Install alsa and udev + run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev + - name: Apply patches + run: | + for patch in tools/example-showcase/*.patch; do + git apply --ignore-whitespace $patch + done + - name: Build with patches + run: cargo build diff --git a/Cargo.toml b/Cargo.toml index 03021605904ef..9c43da5a15bd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -155,6 +155,9 @@ bevy_winit = ["bevy_internal/bevy_winit"] # Adds support for rendering gizmos bevy_gizmos = ["bevy_internal/bevy_gizmos", "bevy_color"] +# Provides a collection of developer tools +bevy_dev_tools = ["bevy_internal/bevy_dev_tools"] + # Tracing support, saving a file in Chrome Tracing format trace_chrome = ["trace", "bevy_internal/trace_chrome"] @@ -318,6 +321,9 @@ embedded_watcher = ["bevy_internal/embedded_watcher"] # Enable stepping-based debugging of Bevy systems bevy_debug_stepping = ["bevy_internal/bevy_debug_stepping"] +# Enable support for the ios_simulator by downgrading some rendering capabilities +ios_simulator = ["bevy_internal/ios_simulator"] + [dependencies] bevy_dylib = { path = "crates/bevy_dylib", version = "0.14.0-dev", default-features = false, optional = true } bevy_internal = { path = "crates/bevy_internal", version = "0.14.0-dev", default-features = false } @@ -332,6 +338,7 @@ bytemuck = "1.7" futures-lite = "2.0.1" crossbeam-channel = "0.5.0" argh = "0.1.12" +thiserror = "1.0" [[example]] name = "hello_world" @@ -973,6 +980,17 @@ description = "Plays an animation from a skinned glTF" category = "Animation" wasm = true +[[example]] +name = "animation_graph" +path = "examples/animation/animation_graph.rs" +doc-scrape-examples = true + +[package.metadata.example.animation_graph] +name = "Animation Graph" +description = "Blends multiple animations together with a graph" +category = "Animation" +wasm = true + [[example]] name = "morph_targets" path = "examples/animation/morph_targets.rs" @@ -1105,6 +1123,16 @@ description = "Illustrate how to add custom log layers" category = "Application" wasm = false +[[example]] +name = "log_layers_ecs" +path = "examples/app/log_layers_ecs.rs" + +[package.metadata.example.log_layers_ecs] +name = "Advanced log layers" +description = "Illustrate how to transfer data between log layers and Bevy's ECS" +category = "Application" +wasm = false + [[example]] name = "plugin" path = "examples/app/plugin.rs" @@ -1227,6 +1255,17 @@ description = "Embed an asset in the application binary and load it" category = "Assets" wasm = true +[[example]] +name = "extra_asset_source" +path = "examples/asset/extra_source.rs" +doc-scrape-examples = true + +[package.metadata.example.extra_asset_source] +name = "Extra asset source" +description = "Load an asset from a non-standard asset source" +category = "Assets" +wasm = true + [[example]] name = "hot_asset_reloading" path = "examples/asset/hot_asset_reloading.rs" @@ -1678,6 +1717,17 @@ description = "Displays each contributor as a bouncy bevy-ball!" category = "Games" wasm = true +[[example]] +name = "desk_toy" +path = "examples/games/desk_toy.rs" +doc-scrape-examples = true + +[package.metadata.example.desk_toy] +name = "Desk Toy" +description = "Bevy logo as a desk toy using transparent windows! Now with Googly Eyes!" +category = "Games" +wasm = false + [[example]] name = "game_menu" path = "examples/games/game_menu.rs" @@ -2234,6 +2284,17 @@ description = "Illustrates how to (constantly) rotate an object around an axis" category = "Transforms" wasm = true +[[example]] +name = "align" +path = "examples/transforms/align.rs" +doc-scrape-examples = true + +[package.metadata.example.align] +name = "Alignment" +description = "A demonstration of Transform's axis-alignment feature" +category = "Transforms" +wasm = true + [[example]] name = "scale" path = "examples/transforms/scale.rs" @@ -2500,6 +2561,17 @@ description = "Illustrates how to use 9 Slicing in UI" category = "UI (User Interface)" wasm = true +[[example]] +name = "ui_texture_atlas_slice" +path = "examples/ui/ui_texture_atlas_slice.rs" +doc-scrape-examples = true + +[package.metadata.example.ui_texture_atlas_slice] +name = "UI Texture Atlas Slice" +description = "Illustrates how to use 9 Slicing for TextureAtlases in UI" +category = "UI (User Interface)" +wasm = true + [[example]] name = "viewport_debug" path = "examples/ui/viewport_debug.rs" @@ -2680,6 +2752,40 @@ description = "A scene showcasing 3D gizmos" category = "Gizmos" wasm = true +[[example]] +name = "axes" +path = "examples/gizmos/axes.rs" +doc-scrape-examples = true + +[package.metadata.example.axes] +name = "Axes" +description = "Demonstrates the function of axes gizmos" +category = "Gizmos" +wasm = true + +[[example]] +name = "light_gizmos" +path = "examples/gizmos/light_gizmos.rs" +doc-scrape-examples = true + +[package.metadata.example.light_gizmos] +name = "Light Gizmos" +description = "A scene showcasing light gizmos" +category = "Gizmos" +wasm = true + +[[example]] +name = "fps_overlay" +path = "examples/dev_tools/fps_overlay.rs" +doc-scrape-examples = true +required-features = ["bevy_dev_tools"] + +[package.metadata.example.fps_overlay] +name = "FPS overlay" +description = "Demonstrates FPS overlay" +category = "Dev tools" +wasm = true + [profile.wasm-release] inherits = "release" opt-level = "z" @@ -2693,3 +2799,4 @@ panic = "abort" [package.metadata.docs.rs] cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] +all-features = true diff --git a/README.md b/README.md index 713784409901d..a8018aa0daad2 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ This [list][cargo_features] outlines the different cargo features supported by B Bevy is the result of the hard work of many people. A huge thanks to all Bevy contributors, the many open source projects that have come before us, the [Rust gamedev ecosystem](https://arewegameyet.rs/), and the many libraries we build on. -A huge thanks to Bevy's [generous sponsors](https://bevyengine.org). Bevy will always be free and open source, but it isn't free to make. Please consider [sponsoring our work](https://bevyengine.org/community/donate/) if you like what we're building. +A huge thanks to Bevy's [generous sponsors](https://bevyengine.org). Bevy will always be free and open source, but it isn't free to make. Please consider [sponsoring our work](https://bevyengine.org/donate/) if you like what we're building. This project is tested with BrowserStack. diff --git a/assets/animation_graphs/Fox.animgraph.ron b/assets/animation_graphs/Fox.animgraph.ron new file mode 100644 index 0000000000000..a1b21f1254172 --- /dev/null +++ b/assets/animation_graphs/Fox.animgraph.ron @@ -0,0 +1,35 @@ +( + graph: ( + nodes: [ + ( + clip: None, + weight: 1.0, + ), + ( + clip: None, + weight: 0.5, + ), + ( + clip: Some(AssetPath("models/animated/Fox.glb#Animation0")), + weight: 1.0, + ), + ( + clip: Some(AssetPath("models/animated/Fox.glb#Animation1")), + weight: 1.0, + ), + ( + clip: Some(AssetPath("models/animated/Fox.glb#Animation2")), + weight: 1.0, + ), + ], + node_holes: [], + edge_property: directed, + edges: [ + Some((0, 1, ())), + Some((0, 2, ())), + Some((1, 3, ())), + Some((1, 4, ())), + ], + ), + root: 0, +) \ No newline at end of file diff --git a/assets/textures/fantasy_ui_borders/border_sheet.png b/assets/textures/fantasy_ui_borders/border_sheet.png new file mode 100644 index 0000000000000..8ee2d2a9b2d34 Binary files /dev/null and b/assets/textures/fantasy_ui_borders/border_sheet.png differ diff --git a/benches/benches/bevy_ecs/world/commands.rs b/benches/benches/bevy_ecs/world/commands.rs index 70cf1351acfcb..2b3d84195aff8 100644 --- a/benches/benches/bevy_ecs/world/commands.rs +++ b/benches/benches/bevy_ecs/world/commands.rs @@ -1,8 +1,8 @@ use bevy_ecs::{ component::Component, entity::Entity, - system::{Command, CommandQueue, Commands}, - world::World, + system::Commands, + world::{Command, CommandQueue, World}, }; use criterion::{black_box, Criterion}; diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index eb8f48c12179b..495ddd7c99d7e 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -13,10 +13,12 @@ keywords = ["bevy"] bevy_app = { path = "../bevy_app", version = "0.14.0-dev" } bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" } bevy_core = { path = "../bevy_core", version = "0.14.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" } bevy_log = { path = "../bevy_log", version = "0.14.0-dev" } bevy_math = { path = "../bevy_math", version = "0.14.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [ "bevy", + "petgraph", ] } bevy_render = { path = "../bevy_render", version = "0.14.0-dev" } bevy_time = { path = "../bevy_time", version = "0.14.0-dev" } @@ -26,8 +28,14 @@ bevy_transform = { path = "../bevy_transform", version = "0.14.0-dev" } bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" } # other +fixedbitset = "0.5" +petgraph = { version = "0.6", features = ["serde-1"] } +ron = "0.8" +serde = "1" sha1_smol = { version = "1.0" } -uuid = { version = "1.7", features = ["v5"] } +thiserror = "1" +thread_local = "1" +uuid = { version = "1.7", features = ["v4"] } [lints] workspace = true diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs new file mode 100644 index 0000000000000..aba53f1d7adfa --- /dev/null +++ b/crates/bevy_animation/src/graph.rs @@ -0,0 +1,400 @@ +//! The animation graph, which allows animations to be blended together. + +use std::io::{self, Write}; +use std::ops::{Index, IndexMut}; + +use bevy_asset::io::Reader; +use bevy_asset::{Asset, AssetId, AssetLoader, AssetPath, AsyncReadExt as _, Handle, LoadContext}; +use bevy_reflect::{Reflect, ReflectSerialize}; +use bevy_utils::BoxedFuture; +use petgraph::graph::{DiGraph, NodeIndex}; +use ron::de::SpannedError; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::AnimationClip; + +/// A graph structure that describes how animation clips are to be blended +/// together. +/// +/// Applications frequently want to be able to play multiple animations at once +/// and to fine-tune the influence that animations have on a skinned mesh. Bevy +/// uses an *animation graph* to store this information. Animation graphs are a +/// directed acyclic graph (DAG) that describes how animations are to be +/// weighted and combined together. Every frame, Bevy evaluates the graph from +/// the root and blends the animations together in a bottom-up fashion to +/// produce the final pose. +/// +/// There are two types of nodes: *blend nodes* and *clip nodes*, both of which +/// can have an associated weight. Blend nodes have no associated animation clip +/// and simply affect the weights of all their descendant nodes. Clip nodes +/// specify an animation clip to play. When a graph is created, it starts with +/// only a single blend node, the root node. +/// +/// For example, consider the following graph: +/// +/// ```text +/// ┌────────────┐ +/// │ │ +/// │ Idle ├─────────────────────┐ +/// │ │ │ +/// └────────────┘ │ +/// │ +/// ┌────────────┐ │ ┌────────────┐ +/// │ │ │ │ │ +/// │ Run ├──┐ ├──┤ Root │ +/// │ │ │ ┌────────────┐ │ │ │ +/// └────────────┘ │ │ Blend │ │ └────────────┘ +/// ├──┤ ├──┘ +/// ┌────────────┐ │ │ 0.5 │ +/// │ │ │ └────────────┘ +/// │ Walk ├──┘ +/// │ │ +/// └────────────┘ +/// ``` +/// +/// In this case, assuming that Idle, Run, and Walk are all playing with weight +/// 1.0, the Run and Walk animations will be equally blended together, then +/// their weights will be halved and finally blended with the Idle animation. +/// Thus the weight of Run and Walk are effectively half of the weight of Idle. +/// +/// Animation graphs are assets and can be serialized to and loaded from [RON] +/// files. Canonically, such files have an `.animgraph.ron` extension. +/// +/// The animation graph implements [RFC 51]. See that document for more +/// information. +/// +/// [RON]: https://github.com/ron-rs/ron +/// +/// [RFC 51]: https://github.com/bevyengine/rfcs/blob/main/rfcs/51-animation-composition.md +#[derive(Asset, Reflect, Clone, Debug, Serialize)] +#[reflect(Serialize, Debug)] +#[serde(into = "SerializedAnimationGraph")] +pub struct AnimationGraph { + /// The `petgraph` data structure that defines the animation graph. + pub graph: AnimationDiGraph, + /// The index of the root node in the animation graph. + pub root: NodeIndex, +} + +/// A type alias for the `petgraph` data structure that defines the animation +/// graph. +pub type AnimationDiGraph = DiGraph; + +/// The index of either an animation or blend node in the animation graph. +/// +/// These indices are the way that [`crate::AnimationPlayer`]s identify +/// particular animations. +pub type AnimationNodeIndex = NodeIndex; + +/// An individual node within an animation graph. +/// +/// If `clip` is present, this is a *clip node*. Otherwise, it's a *blend node*. +/// Both clip and blend nodes can have weights, and those weights are propagated +/// down to descendants. +#[derive(Clone, Reflect, Debug)] +pub struct AnimationGraphNode { + /// The animation clip associated with this node, if any. + /// + /// If the clip is present, this node is an *animation clip node*. + /// Otherwise, this node is a *blend node*. + pub clip: Option>, + + /// The weight of this node. + /// + /// Weights are propagated down to descendants. Thus if an animation clip + /// has weight 0.3 and its parent blend node has weight 0.6, the computed + /// weight of the animation clip is 0.18. + pub weight: f32, +} + +/// An [`AssetLoader`] that can load [`AnimationGraph`]s as assets. +/// +/// The canonical extension for [`AnimationGraph`]s is `.animgraph.ron`. Plain +/// `.animgraph` is supported as well. +#[derive(Default)] +pub struct AnimationGraphAssetLoader; + +/// Various errors that can occur when serializing or deserializing animation +/// graphs to and from RON, respectively. +#[derive(Error, Debug)] +pub enum AnimationGraphLoadError { + /// An I/O error occurred. + #[error("I/O")] + Io(#[from] io::Error), + /// An error occurred in RON serialization or deserialization. + #[error("RON serialization")] + Ron(#[from] ron::Error), + /// An error occurred in RON deserialization, and the location of the error + /// is supplied. + #[error("RON serialization")] + SpannedRon(#[from] SpannedError), +} + +/// A version of [`AnimationGraph`] suitable for serializing as an asset. +/// +/// Animation nodes can refer to external animation clips, and the [`AssetId`] +/// is typically not sufficient to identify the clips, since the +/// [`bevy_asset::AssetServer`] assigns IDs in unpredictable ways. That fact +/// motivates this type, which replaces the `Handle` with an +/// asset path. Loading an animation graph via the [`bevy_asset::AssetServer`] +/// actually loads a serialized instance of this type, as does serializing an +/// [`AnimationGraph`] through `serde`. +#[derive(Serialize, Deserialize)] +pub struct SerializedAnimationGraph { + /// Corresponds to the `graph` field on [`AnimationGraph`]. + pub graph: DiGraph, + /// Corresponds to the `root` field on [`AnimationGraph`]. + pub root: NodeIndex, +} + +/// A version of [`AnimationGraphNode`] suitable for serializing as an asset. +/// +/// See the comments in [`SerializedAnimationGraph`] for more information. +#[derive(Serialize, Deserialize)] +pub struct SerializedAnimationGraphNode { + /// Corresponds to the `clip` field on [`AnimationGraphNode`]. + pub clip: Option, + /// Corresponds to the `weight` field on [`AnimationGraphNode`]. + pub weight: f32, +} + +/// A version of `Handle` suitable for serializing as an asset. +/// +/// This replaces any handle that has a path with an [`AssetPath`]. Failing +/// that, the asset ID is serialized directly. +#[derive(Serialize, Deserialize)] +pub enum SerializedAnimationClip { + /// Records an asset path. + AssetPath(AssetPath<'static>), + /// The fallback that records an asset ID. + /// + /// Because asset IDs can change, this should not be relied upon. Prefer to + /// use asset paths where possible. + AssetId(AssetId), +} + +impl AnimationGraph { + /// Creates a new animation graph with a root node and no other nodes. + pub fn new() -> Self { + let mut graph = DiGraph::default(); + let root = graph.add_node(AnimationGraphNode::default()); + Self { graph, root } + } + + /// A convenience function for creating an [`AnimationGraph`] from a single + /// [`AnimationClip`]. + /// + /// The clip will be a direct child of the root with weight 1.0. Both the + /// graph and the index of the added node are returned as a tuple. + pub fn from_clip(clip: Handle) -> (Self, AnimationNodeIndex) { + let mut graph = Self::new(); + let node_index = graph.add_clip(clip, 1.0, graph.root); + (graph, node_index) + } + + /// Adds an [`AnimationClip`] to the animation graph with the given weight + /// and returns its index. + /// + /// The animation clip will be the child of the given parent. + pub fn add_clip( + &mut self, + clip: Handle, + weight: f32, + parent: AnimationNodeIndex, + ) -> AnimationNodeIndex { + let node_index = self.graph.add_node(AnimationGraphNode { + clip: Some(clip), + weight, + }); + self.graph.add_edge(parent, node_index, ()); + node_index + } + + /// A convenience method to add multiple [`AnimationClip`]s to the animation + /// graph. + /// + /// All of the animation clips will have the same weight and will be + /// parented to the same node. + /// + /// Returns the indices of the new nodes. + pub fn add_clips<'a, I>( + &'a mut self, + clips: I, + weight: f32, + parent: AnimationNodeIndex, + ) -> impl Iterator + 'a + where + I: IntoIterator>, + ::IntoIter: 'a, + { + clips + .into_iter() + .map(move |clip| self.add_clip(clip, weight, parent)) + } + + /// Adds a blend node to the animation graph with the given weight and + /// returns its index. + /// + /// The blend node will be placed under the supplied `parent` node. During + /// animation evaluation, the descendants of this blend node will have their + /// weights multiplied by the weight of the blend. + pub fn add_blend(&mut self, weight: f32, parent: AnimationNodeIndex) -> AnimationNodeIndex { + let node_index = self + .graph + .add_node(AnimationGraphNode { clip: None, weight }); + self.graph.add_edge(parent, node_index, ()); + node_index + } + + /// Adds an edge from the edge `from` to `to`, making `to` a child of + /// `from`. + /// + /// The behavior is unspecified if adding this produces a cycle in the + /// graph. + pub fn add_edge(&mut self, from: NodeIndex, to: NodeIndex) { + self.graph.add_edge(from, to, ()); + } + + /// Removes an edge between `from` and `to` if it exists. + /// + /// Returns true if the edge was successfully removed or false if no such + /// edge existed. + pub fn remove_edge(&mut self, from: NodeIndex, to: NodeIndex) -> bool { + self.graph + .find_edge(from, to) + .map(|edge| self.graph.remove_edge(edge)) + .is_some() + } + + /// Returns the [`AnimationGraphNode`] associated with the given index. + /// + /// If no node with the given index exists, returns `None`. + pub fn get(&self, animation: AnimationNodeIndex) -> Option<&AnimationGraphNode> { + self.graph.node_weight(animation) + } + + /// Returns a mutable reference to the [`AnimationGraphNode`] associated + /// with the given index. + /// + /// If no node with the given index exists, returns `None`. + pub fn get_mut(&mut self, animation: AnimationNodeIndex) -> Option<&mut AnimationGraphNode> { + self.graph.node_weight_mut(animation) + } + + /// Returns an iterator over the [`AnimationGraphNode`]s in this graph. + pub fn nodes(&self) -> impl Iterator { + self.graph.node_indices() + } + + /// Serializes the animation graph to the given [`Write`]r in RON format. + /// + /// If writing to a file, it can later be loaded with the + /// [`AnimationGraphAssetLoader`] to reconstruct the graph. + pub fn save(&self, writer: &mut W) -> Result<(), AnimationGraphLoadError> + where + W: Write, + { + let mut ron_serializer = ron::ser::Serializer::new(writer, None)?; + Ok(self.serialize(&mut ron_serializer)?) + } +} + +impl Index for AnimationGraph { + type Output = AnimationGraphNode; + + fn index(&self, index: AnimationNodeIndex) -> &Self::Output { + &self.graph[index] + } +} + +impl IndexMut for AnimationGraph { + fn index_mut(&mut self, index: AnimationNodeIndex) -> &mut Self::Output { + &mut self.graph[index] + } +} + +impl Default for AnimationGraphNode { + fn default() -> Self { + Self { + clip: None, + weight: 1.0, + } + } +} + +impl Default for AnimationGraph { + fn default() -> Self { + Self::new() + } +} + +impl AssetLoader for AnimationGraphAssetLoader { + type Asset = AnimationGraph; + + type Settings = (); + + type Error = AnimationGraphLoadError; + + fn load<'a>( + &'a self, + reader: &'a mut Reader, + _: &'a Self::Settings, + load_context: &'a mut LoadContext, + ) -> BoxedFuture<'a, Result> { + Box::pin(async move { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + + // Deserialize a `SerializedAnimationGraph` directly, so that we can + // get the list of the animation clips it refers to and load them. + let mut deserializer = ron::de::Deserializer::from_bytes(&bytes)?; + let serialized_animation_graph = + SerializedAnimationGraph::deserialize(&mut deserializer) + .map_err(|err| deserializer.span_error(err))?; + + // Load all `AssetPath`s to convert from a + // `SerializedAnimationGraph` to a real `AnimationGraph`. + Ok(AnimationGraph { + graph: serialized_animation_graph.graph.map( + |_, serialized_node| AnimationGraphNode { + clip: serialized_node.clip.as_ref().map(|clip| match clip { + SerializedAnimationClip::AssetId(asset_id) => Handle::Weak(*asset_id), + SerializedAnimationClip::AssetPath(asset_path) => { + load_context.load(asset_path) + } + }), + weight: serialized_node.weight, + }, + |_, _| (), + ), + root: serialized_animation_graph.root, + }) + }) + } + + fn extensions(&self) -> &[&str] { + &["animgraph", "animgraph.ron"] + } +} + +impl From for SerializedAnimationGraph { + fn from(animation_graph: AnimationGraph) -> Self { + // If any of the animation clips have paths, then serialize them as + // `SerializedAnimationClip::AssetPath` so that the + // `AnimationGraphAssetLoader` can load them. + Self { + graph: animation_graph.graph.map( + |_, node| SerializedAnimationGraphNode { + weight: node.weight, + clip: node.clip.as_ref().map(|clip| match clip.path() { + Some(path) => SerializedAnimationClip::AssetPath(path.clone()), + None => SerializedAnimationClip::AssetId(clip.id()), + }), + }, + |_, _| (), + ), + root: animation_graph.root, + } + } +} diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index bef6c87157c30..d3dbed8406193 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -1,12 +1,15 @@ //! Animation for the game engine Bevy mod animatable; +mod graph; +mod transition; mod util; +use std::cell::RefCell; +use std::collections::BTreeMap; use std::hash::{Hash, Hasher}; use std::iter; use std::ops::{Add, Mul}; -use std::time::Duration; use bevy_app::{App, Plugin, PostUpdate}; use bevy_asset::{Asset, AssetApp, Assets, Handle}; @@ -14,25 +17,36 @@ use bevy_core::Name; use bevy_ecs::entity::MapEntities; use bevy_ecs::prelude::*; use bevy_ecs::reflect::ReflectMapEntities; -use bevy_log::error; use bevy_math::{FloatExt, Quat, Vec3}; use bevy_reflect::Reflect; use bevy_render::mesh::morph::MorphWeights; use bevy_time::Time; use bevy_transform::{prelude::Transform, TransformSystem}; use bevy_utils::hashbrown::HashMap; -use bevy_utils::{NoOpHash, Uuid}; +use bevy_utils::{ + tracing::{error, trace}, + NoOpHash, +}; +use fixedbitset::FixedBitSet; +use graph::{AnimationGraph, AnimationNodeIndex}; +use petgraph::graph::NodeIndex; +use petgraph::Direction; +use prelude::{AnimationGraphAssetLoader, AnimationTransitions}; use sha1_smol::Sha1; +use thread_local::ThreadLocal; +use uuid::Uuid; #[allow(missing_docs)] pub mod prelude { #[doc(hidden)] pub use crate::{ - animatable::*, AnimationClip, AnimationPlayer, AnimationPlugin, Interpolation, Keyframes, - VariableCurve, + animatable::*, graph::*, transition::*, AnimationClip, AnimationPlayer, AnimationPlugin, + Interpolation, Keyframes, VariableCurve, }; } +use crate::transition::{advance_transitions, expire_completed_transitions}; + /// The [UUID namespace] of animation targets (e.g. bones). /// /// [UUID namespace]: https://en.wikipedia.org/wiki/Universally_unique_identifier#Versions_3_and_5_(namespace_name-based) @@ -279,8 +293,17 @@ pub enum RepeatAnimation { Forever, } +/// An animation that an [`AnimationPlayer`] is currently either playing or was +/// playing, but is presently paused. +/// +/// An stopped animation is considered no longer active. #[derive(Debug, Reflect)] -struct PlayingAnimation { +pub struct ActiveAnimation { + /// The factor by which the weight from the [`AnimationGraph`] is multiplied. + weight: f32, + /// The actual weight of this animation this frame, taking the + /// [`AnimationGraph`] into account. + computed_weight: f32, repeat: RepeatAnimation, speed: f32, /// Total time the animation has been played. @@ -291,26 +314,28 @@ struct PlayingAnimation { /// /// Note: This will always be in the range [0.0, animation clip duration] seek_time: f32, - animation_clip: Handle, /// Number of times the animation has completed. /// If the animation is playing in reverse, this increments when the animation passes the start. completions: u32, + paused: bool, } -impl Default for PlayingAnimation { +impl Default for ActiveAnimation { fn default() -> Self { Self { + weight: 1.0, + computed_weight: 1.0, repeat: RepeatAnimation::default(), speed: 1.0, elapsed: 0.0, seek_time: 0.0, - animation_clip: Default::default(), completions: 0, + paused: false, } } } -impl PlayingAnimation { +impl ActiveAnimation { /// Check if the animation has finished, based on its repetition behavior and the number of times it has repeated. /// /// Note: An animation with `RepeatAnimation::Forever` will never finish. @@ -353,38 +378,112 @@ impl PlayingAnimation { } /// Reset back to the initial state as if no time has elapsed. - fn replay(&mut self) { + pub fn replay(&mut self) { self.completions = 0; self.elapsed = 0.0; self.seek_time = 0.0; } -} -/// An animation that is being faded out as part of a transition -struct AnimationTransition { - /// The current weight. Starts at 1.0 and goes to 0.0 during the fade-out. - current_weight: f32, - /// How much to decrease `current_weight` per second - weight_decline_per_sec: f32, - /// The animation that is being faded out - animation: PlayingAnimation, + /// Returns the current weight of this animation. + pub fn weight(&self) -> f32 { + self.weight + } + + /// Sets the weight of this animation. + pub fn set_weight(&mut self, weight: f32) { + self.weight = weight; + } + + /// Pause the animation. + pub fn pause(&mut self) -> &mut Self { + self.paused = true; + self + } + + /// Unpause the animation. + pub fn resume(&mut self) -> &mut Self { + self.paused = false; + self + } + + /// Returns true if this animation is currently paused. + /// + /// Note that paused animations are still [`ActiveAnimation`]s. + #[inline] + pub fn is_paused(&self) -> bool { + self.paused + } + + /// Sets the repeat mode for this playing animation. + pub fn set_repeat(&mut self, repeat: RepeatAnimation) -> &mut Self { + self.repeat = repeat; + self + } + + /// Marks this animation as repeating forever. + pub fn repeat(&mut self) -> &mut Self { + self.set_repeat(RepeatAnimation::Forever) + } + + /// Returns the repeat mode assigned to this active animation. + pub fn repeat_mode(&self) -> RepeatAnimation { + self.repeat + } + + /// Returns the number of times this animation has completed. + pub fn completions(&self) -> u32 { + self.completions + } + + /// Returns true if the animation is playing in reverse. + pub fn is_playback_reversed(&self) -> bool { + self.speed < 0.0 + } + + /// Returns the speed of the animation playback. + pub fn speed(&self) -> f32 { + self.speed + } + + /// Sets the speed of the animation playback. + pub fn set_speed(&mut self, speed: f32) -> &mut Self { + self.speed = speed; + self + } + + /// Returns the amount of time the animation has been playing. + pub fn elapsed(&self) -> f32 { + self.elapsed + } + + /// Returns the seek time of the animation. + /// + /// This is nonnegative and no more than the clip duration. + pub fn seek_time(&self) -> f32 { + self.seek_time + } + + /// Seeks to a specific time in the animation. + pub fn seek_to(&mut self, seek_time: f32) -> &mut Self { + self.seek_time = seek_time; + self + } + + /// Seeks to the beginning of the animation. + pub fn rewind(&mut self) -> &mut Self { + self.seek_time = 0.0; + self + } } /// Animation controls #[derive(Component, Default, Reflect)] #[reflect(Component)] pub struct AnimationPlayer { - paused: bool, - - animation: PlayingAnimation, - - // List of previous animations we're currently transitioning away from. - // Usually this is empty, when transitioning between animations, there is - // one entry. When another animation transition happens while a transition - // is still ongoing, then there can be more than one entry. - // Once a transition is finished, it will be automatically removed from the list - #[reflect(ignore)] - transitions: Vec, + /// We use a `BTreeMap` instead of a `HashMap` here to ensure a consistent + /// ordering when applying the animations. + active_animations: BTreeMap, + blend_weights: HashMap, } /// The components that we might need to read or write during animation of each @@ -397,157 +496,160 @@ struct AnimationTargetContext<'a> { morph_weights: Option>, } -impl AnimationPlayer { - /// Start playing an animation, resetting state of the player. - /// This will use a linear blending between the previous and the new animation to make a smooth transition. - pub fn start(&mut self, handle: Handle) -> &mut Self { - self.animation = PlayingAnimation { - animation_clip: handle, - ..Default::default() - }; - - // We want a hard transition. - // In case any previous transitions are still playing, stop them - self.transitions.clear(); - - self - } +/// Information needed during the traversal of the animation graph in +/// [`advance_animations`]. +#[derive(Default)] +pub struct AnimationGraphEvaluator { + /// The stack used for the depth-first search of the graph. + dfs_stack: Vec, + /// The list of visited nodes during the depth-first traversal. + dfs_visited: FixedBitSet, + /// Accumulated weights for each node. + weights: Vec, +} - /// Start playing an animation, resetting state of the player. - /// This will use a linear blending between the previous and the new animation to make a smooth transition. - pub fn start_with_transition( - &mut self, - handle: Handle, - transition_duration: Duration, - ) -> &mut Self { - let mut animation = PlayingAnimation { - animation_clip: handle, - ..Default::default() - }; - std::mem::swap(&mut animation, &mut self.animation); - - // Add the current transition. If other transitions are still ongoing, - // this will keep those transitions running and cause a transition between - // the output of that previous transition to the new animation. - self.transitions.push(AnimationTransition { - current_weight: 1.0, - weight_decline_per_sec: 1.0 / transition_duration.as_secs_f32(), - animation, - }); +thread_local! { + /// A cached per-thread copy of the graph evaluator. + /// + /// Caching the evaluator lets us save allocation traffic from frame to + /// frame. + static ANIMATION_GRAPH_EVALUATOR: RefCell = + RefCell::new(AnimationGraphEvaluator::default()); +} - self +impl AnimationPlayer { + /// Start playing an animation, restarting it if necessary. + pub fn start(&mut self, animation: AnimationNodeIndex) -> &mut ActiveAnimation { + self.active_animations.entry(animation).or_default() } - /// Start playing an animation, resetting state of the player, unless the requested animation is already playing. - pub fn play(&mut self, handle: Handle) -> &mut Self { - if !self.is_playing_clip(&handle) || self.is_paused() { - self.start(handle); - } - self + /// Start playing an animation, unless the requested animation is already playing. + pub fn play(&mut self, animation: AnimationNodeIndex) -> &mut ActiveAnimation { + let playing_animation = self.active_animations.entry(animation).or_default(); + playing_animation.weight = 1.0; + playing_animation } - /// Start playing an animation, resetting state of the player, unless the requested animation is already playing. - /// This will use a linear blending between the previous and the new animation to make a smooth transition - pub fn play_with_transition( - &mut self, - handle: Handle, - transition_duration: Duration, - ) -> &mut Self { - if !self.is_playing_clip(&handle) || self.is_paused() { - self.start_with_transition(handle, transition_duration); - } + /// Stops playing the given animation, removing it from the list of playing + /// animations. + pub fn stop(&mut self, animation: AnimationNodeIndex) -> &mut Self { + self.active_animations.remove(&animation); self } - /// Handle to the animation clip being played. - pub fn animation_clip(&self) -> &Handle { - &self.animation.animation_clip - } - - /// Check if the given animation clip is being played. - pub fn is_playing_clip(&self, handle: &Handle) -> bool { - self.animation_clip() == handle - } - - /// Check if the playing animation has finished, according to the repetition behavior. - pub fn is_finished(&self) -> bool { - self.animation.is_finished() - } - - /// Sets repeat to [`RepeatAnimation::Forever`]. - /// - /// See also [`Self::set_repeat`]. - pub fn repeat(&mut self) -> &mut Self { - self.animation.repeat = RepeatAnimation::Forever; + /// Stops all currently-playing animations. + pub fn stop_all(&mut self) -> &mut Self { + self.active_animations.clear(); self } - /// Set the repetition behaviour of the animation. - pub fn set_repeat(&mut self, repeat: RepeatAnimation) -> &mut Self { - self.animation.repeat = repeat; - self + /// Iterates through all animations that this [`AnimationPlayer`] is + /// currently playing. + pub fn playing_animations( + &self, + ) -> impl Iterator { + self.active_animations.iter() } - /// Repetition behavior of the animation. - pub fn repeat_mode(&self) -> RepeatAnimation { - self.animation.repeat + /// Iterates through all animations that this [`AnimationPlayer`] is + /// currently playing, mutably. + pub fn playing_animations_mut( + &mut self, + ) -> impl Iterator { + self.active_animations.iter_mut() } - /// Number of times the animation has completed. - pub fn completions(&self) -> u32 { - self.animation.completions + /// Check if the given animation node is being played. + pub fn is_playing_animation(&self, animation: AnimationNodeIndex) -> bool { + self.active_animations.contains_key(&animation) } - /// Check if the animation is playing in reverse. - pub fn is_playback_reversed(&self) -> bool { - self.animation.speed < 0.0 + /// Check if all playing animations have finished, according to the repetition behavior. + pub fn all_finished(&self) -> bool { + self.active_animations + .values() + .all(|playing_animation| playing_animation.is_finished()) } - /// Pause the animation - pub fn pause(&mut self) { - self.paused = true; + /// Check if all playing animations are paused. + #[doc(alias = "is_paused")] + pub fn all_paused(&self) -> bool { + self.active_animations + .values() + .all(|playing_animation| playing_animation.is_paused()) } - /// Unpause the animation - pub fn resume(&mut self) { - self.paused = false; + /// Resume all playing animations. + #[doc(alias = "pause")] + pub fn pause_all(&mut self) -> &mut Self { + for (_, playing_animation) in self.playing_animations_mut() { + playing_animation.pause(); + } + self } - /// Is the animation paused - pub fn is_paused(&self) -> bool { - self.paused + /// Resume all active animations. + #[doc(alias = "resume")] + pub fn resume_all(&mut self) -> &mut Self { + for (_, playing_animation) in self.playing_animations_mut() { + playing_animation.resume(); + } + self } - /// Speed of the animation playback - pub fn speed(&self) -> f32 { - self.animation.speed + /// Rewinds all active animations. + #[doc(alias = "rewind")] + pub fn rewind_all(&mut self) -> &mut Self { + for (_, playing_animation) in self.playing_animations_mut() { + playing_animation.rewind(); + } + self } - /// Set the speed of the animation playback - pub fn set_speed(&mut self, speed: f32) -> &mut Self { - self.animation.speed = speed; + /// Multiplies the speed of all active animations by the given factor. + #[doc(alias = "set_speed")] + pub fn adjust_speeds(&mut self, factor: f32) -> &mut Self { + for (_, playing_animation) in self.playing_animations_mut() { + let new_speed = playing_animation.speed() * factor; + playing_animation.set_speed(new_speed); + } self } - /// Time elapsed playing the animation - pub fn elapsed(&self) -> f32 { - self.animation.elapsed + /// Seeks all active animations forward or backward by the same amount. + /// + /// To seek forward, pass a positive value; to seek negative, pass a + /// negative value. Values below 0.0 or beyond the end of the animation clip + /// are clamped appropriately. + #[doc(alias = "seek_to")] + pub fn seek_all_by(&mut self, amount: f32) -> &mut Self { + for (_, playing_animation) in self.playing_animations_mut() { + let new_time = playing_animation.seek_time(); + playing_animation.seek_to(new_time + amount); + } + self } - /// Seek time inside of the animation. Always within the range [0.0, clip duration]. - pub fn seek_time(&self) -> f32 { - self.animation.seek_time + /// Returns the [`ActiveAnimation`] associated with the given animation + /// node if it's currently playing. + /// + /// If the animation isn't currently active, returns `None`. + pub fn animation(&self, animation: AnimationNodeIndex) -> Option<&ActiveAnimation> { + self.active_animations.get(&animation) } - /// Seek to a specific time in the animation. - pub fn seek_to(&mut self, seek_time: f32) -> &mut Self { - self.animation.seek_time = seek_time; - self + /// Returns a mutable reference to the [`ActiveAnimation`] associated with + /// the given animation node if it's currently active. + /// + /// If the animation isn't currently active, returns `None`. + pub fn animation_mut(&mut self, animation: AnimationNodeIndex) -> Option<&mut ActiveAnimation> { + self.active_animations.get_mut(&animation) } - /// Reset the animation to its initial state, as if no time has elapsed. - pub fn replay(&mut self) { - self.animation.replay(); + /// Returns true if the animation is currently playing or paused, or false + /// if the animation is stopped. + pub fn animation_is_playing(&self, animation: AnimationNodeIndex) -> bool { + self.active_animations.contains_key(&animation) } } @@ -555,46 +657,87 @@ impl AnimationPlayer { pub fn advance_animations( time: Res