bevy/crates/bevy_asset/src/id.rs
Patrick Walton dfdf2b9ea4
Implement the AnimationGraph, allowing for multiple animations to be blended together. (#11989)
This is an implementation of RFC #51:
https://github.com/bevyengine/rfcs/blob/main/rfcs/51-animation-composition.md

Note that the implementation strategy is different from the one outlined
in that RFC, because two-phase animation has now landed.

# Objective

Bevy needs animation blending. The RFC for this is [RFC 51].

## Solution

This is an implementation of the RFC. Note that the implementation
strategy is different from the one outlined there, because two-phase
animation has now landed.

This is just a draft to get the conversation started. Currently we're
missing a few things:

- [x] A fully-fleshed-out mechanism for transitions
- [x] A serialization format for `AnimationGraph`s
- [x] Examples are broken, other than `animated_fox`
- [x] Documentation

---

## Changelog

### Added

* The `AnimationPlayer` has been reworked to support blending multiple
animations together through an `AnimationGraph`, and as such will no
longer function unless a `Handle<AnimationGraph>` has been added to the
entity containing the player. See [RFC 51] for more details.

* Transition functionality has moved from the `AnimationPlayer` to a new
component, `AnimationTransitions`, which works in tandem with the
`AnimationGraph`.

## Migration Guide

* `AnimationPlayer`s can no longer play animations by themselves and
need to be paired with a `Handle<AnimationGraph>`. Code that was using
`AnimationPlayer` to play animations will need to create an
`AnimationGraph` asset first, add a node for the clip (or clips) you
want to play, and then supply the index of that node to the
`AnimationPlayer`'s `play` method.

* The `AnimationPlayer::play_with_transition()` method has been removed
and replaced with the `AnimationTransitions` component. If you were
previously using `AnimationPlayer::play_with_transition()`, add all
animations that you were playing to the `AnimationGraph`, and create an
`AnimationTransitions` component to manage the blending between them.

[RFC 51]:
https://github.com/bevyengine/rfcs/blob/main/rfcs/51-animation-composition.md

---------

Co-authored-by: Rob Parrett <robparrett@gmail.com>
2024-03-07 20:22:42 +00:00

513 lines
16 KiB
Rust

use crate::{Asset, AssetIndex};
use bevy_reflect::Reflect;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use std::{
any::TypeId,
fmt::{Debug, Display},
hash::Hash,
marker::PhantomData,
};
use thiserror::Error;
/// A unique runtime-only identifier for an [`Asset`]. This is cheap to [`Copy`]/[`Clone`] and is not directly tied to the
/// lifetime of the Asset. This means it _can_ point to an [`Asset`] that no longer exists.
///
/// For an identifier tied to the lifetime of an asset, see [`Handle`](`crate::Handle`).
///
/// For an "untyped" / "generic-less" id, see [`UntypedAssetId`].
#[derive(Reflect, Serialize, Deserialize)]
pub enum AssetId<A: Asset> {
/// A small / efficient runtime identifier that can be used to efficiently look up an asset stored in [`Assets`]. This is
/// the "default" identifier used for assets. The alternative(s) (ex: [`AssetId::Uuid`]) will only be used if assets are
/// explicitly registered that way.
///
/// [`Assets`]: crate::Assets
Index {
index: AssetIndex,
#[reflect(ignore)]
marker: PhantomData<fn() -> A>,
},
/// A stable-across-runs / const asset identifier. This will only be used if an asset is explicitly registered in [`Assets`]
/// with one.
///
/// [`Assets`]: crate::Assets
Uuid { uuid: Uuid },
}
impl<A: Asset> AssetId<A> {
/// The uuid for the default [`AssetId`]. It is valid to assign a value to this in [`Assets`](crate::Assets)
/// and by convention (where appropriate) assets should support this pattern.
pub const DEFAULT_UUID: Uuid = Uuid::from_u128(200809721996911295814598172825939264631);
/// This asset id _should_ never be valid. Assigning a value to this in [`Assets`](crate::Assets) will
/// produce undefined behavior, so don't do it!
pub const INVALID_UUID: Uuid = Uuid::from_u128(108428345662029828789348721013522787528);
/// Returns an [`AssetId`] with [`Self::INVALID_UUID`], which _should_ never be assigned to.
#[inline]
pub const fn invalid() -> Self {
Self::Uuid {
uuid: Self::INVALID_UUID,
}
}
/// Converts this to an "untyped" / "generic-less" [`Asset`] identifier that stores the type information
/// _inside_ the [`UntypedAssetId`].
#[inline]
pub fn untyped(self) -> UntypedAssetId {
self.into()
}
#[inline]
pub(crate) fn internal(self) -> InternalAssetId {
match self {
AssetId::Index { index, .. } => InternalAssetId::Index(index),
AssetId::Uuid { uuid } => InternalAssetId::Uuid(uuid),
}
}
}
impl<A: Asset> Default for AssetId<A> {
fn default() -> Self {
AssetId::Uuid {
uuid: Self::DEFAULT_UUID,
}
}
}
impl<A: Asset> Clone for AssetId<A> {
fn clone(&self) -> Self {
*self
}
}
impl<A: Asset> Copy for AssetId<A> {}
impl<A: Asset> Display for AssetId<A> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(self, f)
}
}
impl<A: Asset> Debug for AssetId<A> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AssetId::Index { index, .. } => {
write!(
f,
"AssetId<{}>{{ index: {}, generation: {}}}",
std::any::type_name::<A>(),
index.index,
index.generation
)
}
AssetId::Uuid { uuid } => {
write!(
f,
"AssetId<{}>{{uuid: {}}}",
std::any::type_name::<A>(),
uuid
)
}
}
}
}
impl<A: Asset> Hash for AssetId<A> {
#[inline]
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.internal().hash(state);
TypeId::of::<A>().hash(state);
}
}
impl<A: Asset> PartialEq for AssetId<A> {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.internal().eq(&other.internal())
}
}
impl<A: Asset> Eq for AssetId<A> {}
impl<A: Asset> PartialOrd for AssetId<A> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<A: Asset> Ord for AssetId<A> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.internal().cmp(&other.internal())
}
}
impl<A: Asset> From<AssetIndex> for AssetId<A> {
#[inline]
fn from(value: AssetIndex) -> Self {
Self::Index {
index: value,
marker: PhantomData,
}
}
}
impl<A: Asset> From<Uuid> for AssetId<A> {
#[inline]
fn from(value: Uuid) -> Self {
Self::Uuid { uuid: value }
}
}
/// An "untyped" / "generic-less" [`Asset`] identifier that behaves much like [`AssetId`], but stores the [`Asset`] type
/// information at runtime instead of compile-time. This increases the size of the type, but it enables storing asset ids
/// across asset types together and enables comparisons between them.
#[derive(Debug, Copy, Clone)]
pub enum UntypedAssetId {
/// A small / efficient runtime identifier that can be used to efficiently look up an asset stored in [`Assets`]. This is
/// the "default" identifier used for assets. The alternative(s) (ex: [`UntypedAssetId::Uuid`]) will only be used if assets are
/// explicitly registered that way.
///
/// [`Assets`]: crate::Assets
Index { type_id: TypeId, index: AssetIndex },
/// A stable-across-runs / const asset identifier. This will only be used if an asset is explicitly registered in [`Assets`]
/// with one.
///
/// [`Assets`]: crate::Assets
Uuid { type_id: TypeId, uuid: Uuid },
}
impl UntypedAssetId {
/// Converts this to a "typed" [`AssetId`] without checking the stored type to see if it matches the target `A` [`Asset`] type.
/// This should only be called if you are _absolutely certain_ the asset type matches the stored type. And even then, you should
/// consider using [`UntypedAssetId::typed_debug_checked`] instead.
#[inline]
pub fn typed_unchecked<A: Asset>(self) -> AssetId<A> {
match self {
UntypedAssetId::Index { index, .. } => AssetId::Index {
index,
marker: PhantomData,
},
UntypedAssetId::Uuid { uuid, .. } => AssetId::Uuid { uuid },
}
}
/// Converts this to a "typed" [`AssetId`]. When compiled in debug-mode it will check to see if the stored type
/// matches the target `A` [`Asset`] type. When compiled in release-mode, this check will be skipped.
///
/// # Panics
///
/// Panics if compiled in debug mode and the [`TypeId`] of `A` does not match the stored [`TypeId`].
#[inline]
pub fn typed_debug_checked<A: Asset>(self) -> AssetId<A> {
debug_assert_eq!(
self.type_id(),
TypeId::of::<A>(),
"The target AssetId<{}>'s TypeId does not match the TypeId of this UntypedAssetId",
std::any::type_name::<A>()
);
self.typed_unchecked()
}
/// Converts this to a "typed" [`AssetId`].
///
/// # Panics
///
/// Panics if the [`TypeId`] of `A` does not match the stored type id.
#[inline]
pub fn typed<A: Asset>(self) -> AssetId<A> {
let Ok(id) = self.try_typed() else {
panic!(
"The target AssetId<{}>'s TypeId does not match the TypeId of this UntypedAssetId",
std::any::type_name::<A>()
)
};
id
}
/// Try to convert this to a "typed" [`AssetId`].
#[inline]
pub fn try_typed<A: Asset>(self) -> Result<AssetId<A>, UntypedAssetIdConversionError> {
AssetId::try_from(self)
}
/// Returns the stored [`TypeId`] of the referenced [`Asset`].
#[inline]
pub fn type_id(&self) -> TypeId {
match self {
UntypedAssetId::Index { type_id, .. } | UntypedAssetId::Uuid { type_id, .. } => {
*type_id
}
}
}
#[inline]
pub(crate) fn internal(self) -> InternalAssetId {
match self {
UntypedAssetId::Index { index, .. } => InternalAssetId::Index(index),
UntypedAssetId::Uuid { uuid, .. } => InternalAssetId::Uuid(uuid),
}
}
}
impl Display for UntypedAssetId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut writer = f.debug_struct("UntypedAssetId");
match self {
UntypedAssetId::Index { index, type_id } => {
writer
.field("type_id", type_id)
.field("index", &index.index)
.field("generation", &index.generation);
}
UntypedAssetId::Uuid { uuid, type_id } => {
writer.field("type_id", type_id).field("uuid", uuid);
}
}
writer.finish()
}
}
impl PartialEq for UntypedAssetId {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.type_id() == other.type_id() && self.internal().eq(&other.internal())
}
}
impl Eq for UntypedAssetId {}
impl Hash for UntypedAssetId {
#[inline]
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.internal().hash(state);
self.type_id().hash(state);
}
}
impl PartialOrd for UntypedAssetId {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
if self.type_id() != other.type_id() {
None
} else {
Some(self.internal().cmp(&other.internal()))
}
}
}
/// An asset id without static or dynamic types associated with it.
/// This exist to support efficient type erased id drop tracking. We
/// could use [`UntypedAssetId`] for this, but the [`TypeId`] is unnecessary.
///
/// Do not _ever_ use this across asset types for comparison.
/// [`InternalAssetId`] contains no type information and will happily collide
/// with indices across types.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)]
pub(crate) enum InternalAssetId {
Index(AssetIndex),
Uuid(Uuid),
}
impl InternalAssetId {
#[inline]
pub(crate) fn typed<A: Asset>(self) -> AssetId<A> {
match self {
InternalAssetId::Index(index) => AssetId::Index {
index,
marker: PhantomData,
},
InternalAssetId::Uuid(uuid) => AssetId::Uuid { uuid },
}
}
#[inline]
pub(crate) fn untyped(self, type_id: TypeId) -> UntypedAssetId {
match self {
InternalAssetId::Index(index) => UntypedAssetId::Index { index, type_id },
InternalAssetId::Uuid(uuid) => UntypedAssetId::Uuid { uuid, type_id },
}
}
}
impl From<AssetIndex> for InternalAssetId {
fn from(value: AssetIndex) -> Self {
Self::Index(value)
}
}
impl From<Uuid> for InternalAssetId {
fn from(value: Uuid) -> Self {
Self::Uuid(value)
}
}
// Cross Operations
impl<A: Asset> PartialEq<UntypedAssetId> for AssetId<A> {
#[inline]
fn eq(&self, other: &UntypedAssetId) -> bool {
TypeId::of::<A>() == other.type_id() && self.internal().eq(&other.internal())
}
}
impl<A: Asset> PartialEq<AssetId<A>> for UntypedAssetId {
#[inline]
fn eq(&self, other: &AssetId<A>) -> bool {
other.eq(self)
}
}
impl<A: Asset> PartialOrd<UntypedAssetId> for AssetId<A> {
#[inline]
fn partial_cmp(&self, other: &UntypedAssetId) -> Option<std::cmp::Ordering> {
if TypeId::of::<A>() != other.type_id() {
None
} else {
Some(self.internal().cmp(&other.internal()))
}
}
}
impl<A: Asset> PartialOrd<AssetId<A>> for UntypedAssetId {
#[inline]
fn partial_cmp(&self, other: &AssetId<A>) -> Option<std::cmp::Ordering> {
Some(other.partial_cmp(self)?.reverse())
}
}
impl<A: Asset> From<AssetId<A>> for UntypedAssetId {
#[inline]
fn from(value: AssetId<A>) -> Self {
let type_id = TypeId::of::<A>();
match value {
AssetId::Index { index, .. } => UntypedAssetId::Index { type_id, index },
AssetId::Uuid { uuid } => UntypedAssetId::Uuid { type_id, uuid },
}
}
}
impl<A: Asset> TryFrom<UntypedAssetId> for AssetId<A> {
type Error = UntypedAssetIdConversionError;
#[inline]
fn try_from(value: UntypedAssetId) -> Result<Self, Self::Error> {
let found = value.type_id();
let expected = TypeId::of::<A>();
match value {
UntypedAssetId::Index { index, type_id } if type_id == expected => Ok(AssetId::Index {
index,
marker: PhantomData,
}),
UntypedAssetId::Uuid { uuid, type_id } if type_id == expected => {
Ok(AssetId::Uuid { uuid })
}
_ => Err(UntypedAssetIdConversionError::TypeIdMismatch { expected, found }),
}
}
}
/// Errors preventing the conversion of to/from an [`UntypedAssetId`] and an [`AssetId`].
#[derive(Error, Debug, PartialEq, Clone)]
#[non_exhaustive]
pub enum UntypedAssetIdConversionError {
/// Caused when trying to convert an [`UntypedAssetId`] into an [`AssetId`] of the wrong type.
#[error("This UntypedAssetId is for {found:?} and cannot be converted into an AssetId<{expected:?}>")]
TypeIdMismatch { expected: TypeId, found: TypeId },
}
#[cfg(test)]
mod tests {
use super::*;
type TestAsset = ();
const UUID_1: Uuid = Uuid::from_u128(123);
const UUID_2: Uuid = Uuid::from_u128(456);
/// Simple utility to directly hash a value using a fixed hasher
fn hash<T: Hash>(data: &T) -> u64 {
use std::hash::Hasher;
let mut hasher = bevy_utils::AHasher::default();
data.hash(&mut hasher);
hasher.finish()
}
/// Typed and Untyped `AssetIds` should be equivalent to each other and themselves
#[test]
fn equality() {
let typed = AssetId::<TestAsset>::Uuid { uuid: UUID_1 };
let untyped = UntypedAssetId::Uuid {
type_id: TypeId::of::<TestAsset>(),
uuid: UUID_1,
};
assert_eq!(Ok(typed), AssetId::try_from(untyped));
assert_eq!(UntypedAssetId::from(typed), untyped);
assert_eq!(typed, untyped);
}
/// Typed and Untyped `AssetIds` should be orderable amongst each other and themselves
#[test]
fn ordering() {
assert!(UUID_1 < UUID_2);
let typed_1 = AssetId::<TestAsset>::Uuid { uuid: UUID_1 };
let typed_2 = AssetId::<TestAsset>::Uuid { uuid: UUID_2 };
let untyped_1 = UntypedAssetId::Uuid {
type_id: TypeId::of::<TestAsset>(),
uuid: UUID_1,
};
let untyped_2 = UntypedAssetId::Uuid {
type_id: TypeId::of::<TestAsset>(),
uuid: UUID_2,
};
assert!(typed_1 < typed_2);
assert!(untyped_1 < untyped_2);
assert!(UntypedAssetId::from(typed_1) < untyped_2);
assert!(untyped_1 < UntypedAssetId::from(typed_2));
assert!(AssetId::try_from(untyped_1).unwrap() < typed_2);
assert!(typed_1 < AssetId::try_from(untyped_2).unwrap());
assert!(typed_1 < untyped_2);
assert!(untyped_1 < typed_2);
}
/// Typed and Untyped `AssetIds` should be equivalently hashable to each other and themselves
#[test]
fn hashing() {
let typed = AssetId::<TestAsset>::Uuid { uuid: UUID_1 };
let untyped = UntypedAssetId::Uuid {
type_id: TypeId::of::<TestAsset>(),
uuid: UUID_1,
};
assert_eq!(
hash(&typed),
hash(&AssetId::<TestAsset>::try_from(untyped).unwrap())
);
assert_eq!(hash(&UntypedAssetId::from(typed)), hash(&untyped));
assert_eq!(hash(&typed), hash(&untyped));
}
/// Typed and Untyped `AssetIds` should be interchangeable
#[test]
fn conversion() {
let typed = AssetId::<TestAsset>::Uuid { uuid: UUID_1 };
let untyped = UntypedAssetId::Uuid {
type_id: TypeId::of::<TestAsset>(),
uuid: UUID_1,
};
assert_eq!(Ok(typed), AssetId::try_from(untyped));
assert_eq!(UntypedAssetId::from(typed), untyped);
}
}