-
Notifications
You must be signed in to change notification settings - Fork 14
Asset Promises
AssetPromise is a core architectural piece of the asset loading mechanism in the project's ECS (Entity Component System) framework. It provides a unified, trackable, and cancellable way to request and handle assets that may load asynchronously, such as textures, GLTFs, audio clips, or data from a web request.
At its heart, AssetPromise is a lightweight struct that acts as a handle to an underlying ECS Entity. This entity represents the loading operation itself and holds all its state in various components. This design keeps the promise handle small and cheap to pass around, while the actual loading state is managed by the ECS world and its systems.
An AssetPromise is a generic struct defined as AssetPromise<TAsset, TLoadingIntention>.
-
TAsset: The type of the asset you expect to receive once the promise is fulfilled (e.g.,Texture2DData,GltfContainerAsset,AudioClipData). -
TLoadingIntention: A component that describes what to load and how. This "intention" component holds all the necessary parameters for the loading systems, such as URLs, caching policies, and, crucially, aCancellationTokenSourcefor cancellation. Every intention must implement theIAssetIntentioninterface.
Given the descriptive but long generic types, it's a very common and recommended practice to use using aliases to create shorthand names for specific promise types. This dramatically improves code readability.
// Example from DCL.AvatarRendering.AvatarShape.Systems.AvatarLoaderSystem.cs
using WearablePromise = ECS.StreamableLoading.Common.AssetPromise<DCL.AvatarRendering.Wearables.Components.WearablesResolution, DCL.AvatarRendering.Wearables.Components.Intentions.GetWearablesByPointersIntention>;
using EmotePromise = ECS.StreamableLoading.Common.AssetPromise<DCL.AvatarRendering.Emotes.EmotesResolution, DCL.AvatarRendering.Emotes.GetEmotesByPointersIntention>;The lifecycle of an AssetPromise involves creation, polling for the result, consuming it, and handling cleanup.
There are two main ways to create an AssetPromise:
A) Standard Creation: AssetPromise.Create(...)
This is the most common method. It creates a new entity in the ECS world to represent the loading process.
// Example from DCL/Infrastructure/ECS/SceneLifeCycle/Systems/LoadFixedPointersSystem.cs
var promise = AssetPromise<SceneEntityDefinition, GetSceneDefinition>
.Create(World, new GetSceneDefinition(new CommonLoadingArguments(url), ipfsPath), PartitionComponent.TOP_PRIORITY);-
World: The ECS world where the loading entity will be created. -
new GetSceneDefinition(...): An instance of the loading intention, containing all necessary data. -
PartitionComponent.TOP_PRIORITY: A component that helps prioritize the work of loading systems.
This call creates an entity with the GetSceneDefinition, PartitionComponent, and StreamableLoadingState components. Specialized systems will then query for these entities to execute the loading logic.
B) Finalized Creation: AssetPromise.CreateFinalized(...)
This method creates a promise that is already resolved. It does not create an entity in the world. This is useful for returning a cached asset or an immediate failure without engaging the whole loading system pipeline.
// A finalized promise is "born" consumed and has a result from the start.
var result = new StreamableLoadingResult<MyAsset>(new MyAsset());
AssetPromise<MyAsset, MyIntention> promise = AssetPromise<MyAsset, MyIntention>.CreateFinalized(intention, result);
// promise.IsConsumed is true right after creation.Once a promise is created, the system that performs the loading will eventually add a StreamableLoadingResult<TAsset> component to the promise's entity upon completion. You can then retrieve this result in a few ways.
A) Peeking at the Result: TryGetResult()
This method checks if the result is available without altering the promise or its underlying entity. It's safe to call multiple times. It's useful for systems that need to check the status of a dependency without taking ownership of it.
if (myPromise.TryGetResult(World, out StreamableLoadingResult<MyAsset> result))
{
// The result is ready!
// The promise entity still exists.
if (result.Succeeded)
DoSomethingWith(result.Asset);
}B) Consuming the Result: TryConsume()
This is the primary method for getting the result and taking ownership of it. Upon retrieving the result, it destroys the underlying entity, cleaning up all associated loading state components.
Important: A promise can only be consumed once. Calling TryConsume on an already consumed promise will throw an Exception.
if (myPromise.TryConsume(World, out StreamableLoadingResult<MyAsset> result))
{
// The result is ready and has been transferred to you.
// The promise entity has been destroyed.
if (result.Succeeded)
TakeOwnershipOf(result.Asset);
// The promise is now considered "consumed".
}A helper extension SafeTryConsume exists to simplify cases where a promise might have already been consumed.
For non-ECS systems or async-based logic, you can use UniTask extensions to await a promise.
-
ToUniTaskAsync(world): Awaits the promise and consumes it, returning the modified (and now consumed) promise struct. -
ToUniTaskWithoutDestroyAsync(world): Awaits the promise but does not consume it, making it equivalent to waiting untilTryGetResultreturnstrue.
async UniTask LoadMyAssetAsync(World world, AssetPromise<MyAsset, MyIntention> promise)
{
// Wait for the result and consume the promise
var consumedPromise = await promise.ToUniTaskAsync(world);
if (consumedPromise.Result.Value.Succeeded)
Debug.Log("Asset loaded!");
}There are two ways to interrupt or clean up a promise.
-
ForgetLoading(world): This method is used to explicitly cancel an in-flight loading operation. It triggers theCancellationTokenSourcewithin the loading intention (which should cause the loading system to halt its work) and destroys the promise entity.
// If we no longer need the asset, we can cancel the request.
myPromise.ForgetLoading(World);-
Consume(world): If you no longer care about the result but don't necessarily need to cancel the operation (or it may have already finished), you can simply callConsume. This ensures the promise entity is destroyed, preventing resource leaks. If the promise was already consumed, it does nothing.
// Clean up the loading entity, we don't care about the result anymore.
myPromise.Consume(World);