Recursive Asset Loading Never Finishes #24344
-
ContextHello, I'm currently making a multiplayer turn-based game similar to chess in which asset files inform the code of which gameplay features to include. I have three types of assets: Cards - contain information about "moves" the player can make. At the start of the game, I need to read and sort all of these assets, so I need to detect when they are done loading. I'm doing this by:
Perhaps problematically, Cards and markets can recursively reference each other. For example, CardA may let the player spawn MarketB which itself sells CardA. At the bottom of this post, I've attached a simplified version of my code that covers only the problem case. It's fairly short (176 lines) and may or may not be helpful to understanding exactly what I'm trying to do. Expected/Desired BehaviorI may be missing something about how assets work; I'm new to programming, rust, bevy, and especially assets. However, here's what I expected/wanted to happen:
Actual BehaviorThe assets never register as being loaded with dependencies, no matter how long I leave the program running. I'm guessing this is because MarketB cannot register as being loaded with dependencies until the CardA is fully loaded with dependencies, but CardA cannot be fully loaded until MarketB is. However, the gameplay concept of a card spawning a market which sells it appears to be logically sound, but I can't think of a way to express this to Bevy. I'm wondering if anyone has ideas on how I can express to Bevy's asset system that: When Asset1's only remaining unloaded dependencies are themselves only waiting on Asset1, then Asset1 and all of its dependencies are fully loaded. Please let me know your thoughts! Thanks so much! Code and Asset FilesCodeuse bevy::{asset::AssetLoader, prelude::*};
use serde::{Deserialize, Serialize};
use thiserror::Error;
fn main() -> AppExit {
App::new()
.add_plugins(DefaultPlugins)
.init_asset::<Card>()
.init_asset::<Market>()
.init_asset::<Pack>()
.init_asset_loader::<CardAssetLoader>()
.init_asset_loader::<MarketAssetLoader>()
.init_asset_loader::<PackAssetLoader>()
.add_systems(Startup, load_packs)
.add_systems(Update, watch_for_pack_load)
.run()
}
#[derive(Asset, TypePath)]
struct Card {
creates_market: Handle<Market>,
}
#[derive(Asset, TypePath)]
struct Market {
sells_card: Handle<Card>,
}
#[derive(Asset, TypePath)]
struct Pack {
markets_included_in_this_game: Vec<Handle<Market>>,
}
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum AssetLoaderError {
/// An [IO](std::io) Error
#[error("Could not load asset: {0}")]
Io(#[from] std::io::Error),
/// A [RON](ron) Error
#[error("Could not parse RON: {0}")]
RonSpannedError(#[from] ron::error::SpannedError),
}
#[derive(Debug, Default, TypePath)]
struct CardAssetLoader;
#[derive(Debug, Default, TypePath)]
struct MarketAssetLoader;
#[derive(Debug, Default, TypePath)]
struct PackAssetLoader;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Deserialize, Serialize)]
struct ProxyCard {
path_of_created_market: String,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Deserialize, Serialize)]
struct ProxyMarket {
path_of_sold_card: String,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Deserialize, Serialize)]
struct ProxyPack {
paths_of_included_markets: Vec<String>,
}
impl AssetLoader for CardAssetLoader {
type Asset = Card;
type Settings = ();
type Error = AssetLoaderError;
async fn load(
&self,
reader: &mut dyn bevy::asset::io::Reader,
_settings: &Self::Settings,
load_context: &mut bevy::asset::LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
#[cfg(debug_assertions)]
println!("began loading a card.");
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let proxy = ron::de::from_bytes::<ProxyCard>(&bytes)?;
Ok(Card {
creates_market: load_context.load(proxy.path_of_created_market),
})
}
fn extensions(&self) -> &[&str] {
&["card.ron"]
}
}
impl AssetLoader for MarketAssetLoader {
type Asset = Market;
type Settings = ();
type Error = AssetLoaderError;
async fn load(
&self,
reader: &mut dyn bevy::asset::io::Reader,
_settings: &Self::Settings,
load_context: &mut bevy::asset::LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
#[cfg(debug_assertions)]
println!("began loading a market.");
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let proxy = ron::de::from_bytes::<ProxyMarket>(&bytes)?;
Ok(Market {
sells_card: load_context.load(proxy.path_of_sold_card),
})
}
fn extensions(&self) -> &[&str] {
&["market.ron"]
}
}
impl AssetLoader for PackAssetLoader {
type Asset = Pack;
type Settings = ();
type Error = AssetLoaderError;
async fn load(
&self,
reader: &mut dyn bevy::asset::io::Reader,
_settings: &Self::Settings,
load_context: &mut bevy::asset::LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
#[cfg(debug_assertions)]
println!("began loading a pack.");
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let proxy = ron::de::from_bytes::<ProxyPack>(&bytes)?;
Ok(Pack {
markets_included_in_this_game: proxy
.paths_of_included_markets
.iter()
.map(|path| load_context.load(path))
.collect::<Vec<_>>(),
})
}
fn extensions(&self) -> &[&str] {
&["pack.ron"]
}
}
#[derive(Debug, Resource)]
struct ActivePacks(Vec<Handle<Pack>>);
fn load_packs(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.insert_resource(ActivePacks(vec![asset_server.load("my_pack.pack.ron")]));
}
fn watch_for_pack_load(asset_server: Res<AssetServer>, packs_to_check: Res<ActivePacks>) {
let is_done = packs_to_check
.0
.iter()
.all(|handle| asset_server.is_loaded_with_dependencies(handle));
if is_done {
#[cfg(debug_assertions)]
info!("Packs (and thus the other assets) finished loading")
}
}Asset FilesFirst, the file "CardA.card.ron" reads Second, the file "MarketB.market.ron" reads Last, the file "my_pack.pack.ron" reads I really appreciate your help. Thanks again! |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
|
There's not much you can really do here with Bevy's asset system - we do indeed assume that there aren't any cycles, which provides a significant performance improvement. We don't need to get the transitive closure of asset dependencies, we can just wait for all direct dependencies to be loaded-with-dependencies. I wouldn't be keen on us trying to fix this. However you can solve this yourself. You can use the VisitAssetDependencies trait to get the dependencies of a particular asset and then compute the transitive closure yourself, ignoring any assets that you've already visited. You'll need to add the |
Beta Was this translation helpful? Give feedback.
There's not much you can really do here with Bevy's asset system - we do indeed assume that there aren't any cycles, which provides a significant performance improvement. We don't need to get the transitive closure of asset dependencies, we can just wait for all direct dependencies to be loaded-with-dependencies. I wouldn't be keen on us trying to fix this.
However you can solve this yourself. You can use the VisitAssetDependencies trait to get the dependencies of a particular asset and then compute the transitive closure yourself, ignoring any assets that you've already visited. You'll need to add the
#[dependency]attribute to your assets' handles, but it's quite reasonable. Unfortunate…