I needed a way to generate some ID for assets (including nested ones) in edit-time. So far I’ve been using a hash of GlobalObjectId, but now it seems to me that using instance ID as it is, without any hashing, would be even better.
This what docs have to say about instance ID persistence:
And this is what I believed for the longest time, treating instance ID as a volatile and session-dependent.
However, here comes LazyLoadReference - based on the source code, it is merely a wrapper for instance ID with some IsPersistent checks for safety. There are no native components to this, no magic. For this type to work, both in the editor and in a build, across multiple sessions, it requires instance ID to remain the same. This means that instance ID is persistentfor persistent objects and can be used to identify those objects safely.
Am I missing something? Are there any other reasons to stay away from instance ID in this situation?
It seems there’s some magic involved after all. Despite LazyLoadReference being merely a wrapper over instance ID, it is serialized as full reference (guid, fileId, type). So it seems the internal instance ID is assigned as LazyLoadReference is being deserialized.
With this component in, instance ID can (and seems it does) change for each session, be it editor or build.
// Notes:
// - *** The memory layout of this struct must be identical to the native type: AssetReferenceMemoryLayout. ***
// - Not using bindings file as we don't want a wrapper class in this situation. but it must mirror it's native counter part to a 'T'.
//----------------------------------------------------------------------------------------------------------------------
So there is most likely “some native magic” happening
I often use the asset id, specifically from GlobalObjectId, for various things as well. And this is how I do it.
First I created a SerializableGuid struct that mirrors System.Guid:
Note that there is also a ConfigAttribute in it like so:
public class ConfigAttribute : System.Attribute
{
public bool AllowZero;
/// <summary>
/// Attempts to make the guid match the guid associated with the asset this is on.
/// Note this only works if it's on an asset that exists on disk (ScriptableObject, Prefab).
/// Also it means guids will match across all scripts that are on a prefab with this flagged. (since the prefab only has one guid)
/// Really... this should be mainly done with ScriptableObjects only, Prefab's are just a
/// sort of bonus but with caveats.
///
/// New instances created via 'Instantiate' or 'CreateInstance' will not get anything.
/// This is editor time only for assets on disk!
/// </summary>
public bool LinkToAsset;
/// <summary>
/// Attempts to make the guid match the targetObjectId & targetPrefabId of the GlobalObjectId from the object
/// for the upper and lower portions of the guid respectively. If LinkToAsset is true, that takes precedance to this.
/// </summary>
public bool LinkToGlobalObjectId;
}
And then I have the PropertyDrawer for this struct that not only draws the guid correctly (rather than as a bunch of byte fields), but also a postprocessor that will associate my assets for me automatically:
And then I can simply link it up like so:
This works pretty nice when intermingling with Addressables, since Addressables can be configured to work with asset guid’s as well (and if I recall correctly defaults to that behaviour).
Well when not using addressables it’s an obvious gain.
As for when using addressables. So what I mean about them intermingling well is that in the addressables system you have different options for what to use as the ids, with the guids being one of them. So now when using the guids these and the addressable id are the same exact value.
Thing is, and this may have changed since the version of Addressables I’ve been using in my last projects, the guid id is not actually like made obvious through the API. Like it uses the guid internally, and you can even load assets using the guid as a key, but like there’s no actual obvious reference to the id through the API to read at runtime. You could in theory yank it out of an ‘AssetReference’ of course… but what if I’m in a context that doesn’t have that.
So… for example in one of my games we have a terrarium with a bonsai tree in it. And then you the player can fill that terrarium with little toys you collect through gameplay: https://humaniquarium.com/
When we save the game I just enumerate all the objects in the scene and write a token for each object that includes its position, rotation, some stats associated with it on a per object basis, and… this guid. This guid is easily read from a component on the root of the asset which is used to both know its a saveable item AND what its associated guid is without having to do any magic against the addressables (also… I can easily turn addressables on/off, we had cross-platform operation in mind and some platforms we’d turn addressables off on for various reasons that I can’t really get into here and now). Now when I had to load it I had an ‘AssetCatalog’ which I could push the guid into, it would then ask addressables (or whatever asset database I used on that platform) to load the object for me, and return it.
Oh I should also mention another side effect that relates to Addressables, and anything ‘AssetBundle’ based thing as well.
And you can see this in my ‘ProxyMediator’:
/// <summary>
/// Addressables/AssetBundles break ProxyMediator's original implementation. Since these loaded assets will create new instances of the mediator.
/// This lookup table allows us to link all of these mediators together. A refcount is used to destory the hook when the last mediator for that guid
/// is destroyed.
/// </summary>
So there’s this oddity about Addressables and AssetBundles that makes sense when you think about it… but might surprise you the first time you run into it.
A given asset from an assetbundle/addressable group is NOT the same as one from another assetbundle/addressable group/the core project. Even though in the editor they might be the same asset.
So this ‘ProxyMediator’ that I have is intended for transmitting messages around my game. Lets say I have a ‘ItemAvatar’ prefab and a ‘ItemUI’ prefab. The avatar is the 3d representation of the item, and the ItemUI is some ui that always sits in the upper right corner and displays some basic info about an item that you’ve clicked on in the scene. Thing is… these 2 prefabs are completely unrelated, they don’t actually have references to one another. So what I might do is have an ‘t_OnClickItem’ script attached to the ItemAvatar which then signals a ‘ProxyMediator’ asset I’ve named “p.ItemClicked”. Then in my ItemUI I have a ‘t_OnProxyMediatorSignaled’ that listens to the ‘ProxyMediator’ for its ‘OnTriggered’ event and updates the screen (the onclickitem passes the reference to the ItemAvatar as a property of the ProxyMediator, think like how EventArgs can have properties on them).
So both prefabs just reference this ‘p.ItemClicked’ asset in my assets folder and that’s what relates the 2 prefabs together without needing to reference one another.
BUT… when we introduce Addressables, or AssetBundles. And lets say the ItemAvatar in question is from my “X-mas DLC Package” addressable group, and the ItemUI is just part of the ‘core game’. They’re actually in 2 completely different domains. This means that the p.ItemClicked in the ItemUI is NOT the same p.ItemClicked in the ItemAvatar. They’er actually distinctly different assets that happen to be named the same thing, have the same configuration.
This is actually a problem that effects AssetBundles all together. If you have to bundles/groups that contain the SAME assets (directly or indirectly… note a prefab that references another prefab, that daisy chained asset ends up in the bundle/group). Then when you load both bundle/group, you actually end up loading TWO distinct prefabs into memory that just happen to look the same.
So back at my ProxyMediator. By associating this ‘guid’ with it… instead of doing an ‘object.ReferenceEquals / ==’ comparison of them determining if they’re the same object… since they might not be. I can instead compare the guids and assume they’re the same thing!
I’ve also just rolled my own Guids for any assets I want to re-locate later. Though I ended up using a class, AssetGUID, that just wraps around a string populated by System.Guid.NewGuid.ToString(), and gets converted back and cached to a System.Guid when needed. Any object that expresses a unique ID implements an IAssetGUID interface that exposes the Guid.
Then there’s a bunch of systems that work around this interface for automating the saving and retrieval of assets, particularly useful for save/load systems.
Would much rather work with my of ID system, knowing it won’t change out from under me.
I use exactly the same system, just my interface is named IUniqueIdentifier.
I use it with a super useful IdentifierService.Get<T>(Guid uniqueIdentifier) where T : IUniqueIdentifier where all IUniqueIdentifiers register when instantiated and unregister when destroyed. Very useful for loading/saving and multiplayer.