Whenever you’re making a save system or you’re making cross-scene references, you might come across a need for a unique ID for all your GameObjects. You might think Unity offers a solution for this, but there’s no clean solution offered yet.
Internally, each GameObject does have a UUID, but this value changes every run. This basically makes this value unusable.
Unity provides a git repository called guid-based-reference (https://github.com/Unity-Technologies/guid-based-reference) that provides this feature, but this comes with multiple disadvantages.
- A prefab instance’s Guid shows up as modified compared to the prefab. If you accidentally apply all changes, then this library will die.
- Guids are illegible. Wouldn’t it be better to mention an important GameObject as
BossCharacter
rather than2c138012-f543-4caa-bcd4-e4a64bbeb623
? - Small memory inefficiencies. There are extra 8 bytes per instance to store a reference to a byte array. The sad part is that this array is only used during Unity’s serialization/deserialization.
The Solution
Here’s how CloudCanard’s UUID system handles these issues.
First, we have a lightweight serializable UUID struct EntityUuid
.
[Serializable] | |
[StructLayout(LayoutKind.Explicit)] | |
public struct EntityUuid : IComparable, IComparable<EntityUuid>, IComparable<Guid>, IEquatable<EntityUuid>, IEquatable<Guid> | |
{ | |
[SerializeField] | |
[FieldOffset(0)] | |
private long _firstHalf; | |
[SerializeField] | |
[FieldOffset(8)] | |
private long _secondHalf; | |
[FieldOffset(0)] | |
private readonly Guid _guid; | |
public EntityUuid(string explicitName) | |
{ | |
if (explicitName == null) | |
{ | |
CreateProceduralUuid(); | |
} | |
else | |
{ | |
CreateExplicitUuid(explicitName) | |
} | |
} | |
// … | |
} |
Serialization works automatically via FieldOffset
s, which make the serializable part share the same memory location as the Guid
. This only works because the Guid
is also a 16 byte-long struct.
EntityUuid
‘s very last bit is used to indicate whether it is Explicit
or Procedural
.
Procedural
UUIDs consist of 127 random bits. Explicit
UUIDs spell out a name. The idea is that some GameObjects are too important to have a random bitstring as its ID. CloudCanards has a debug terminal that can target specific GameObjects by their UUID. For some GameObjects like Yanna, typing Yanna
is a lot more intuitive than typing random hex.
The complexity of designing a UUID system comes from being able to duplicate GameObjects in the scene. We want the designers to freely copy-paste prefabs around the scene without breaking the rule that no two GameObject share a UUID. At the same time, we want to ensure that the newly duplicated GameObject gets a new UUID and not the previously existing GameObject.
We do this by detecting the (1)
that gets appended at the end of a GameObject name. For GameObject with procedural UUIDs, their name format is some name @guid-tostring-value@
. If the name is some name
or some name @guid-tostring-value@ (1)
, then it is replaced with some name @some-other-guid@
. Note that the GameObject name change is not marked as part of a prefab modification.
[DisallowMultipleComponent] | |
[ExecuteAlways] | |
public class Entity : MonoBehaviour | |
{ | |
private static readonly Dictionary<EntityUuid, Entity> Entities = new Dictionary<EntityUuid, Entity>(); | |
public EntityUuid Uuid; | |
private void Awake() | |
{ | |
if (Application.isPlaying) | |
{ | |
Uuid = ParseUuidFromName(); | |
Entities.Add(Uuid, this); | |
} | |
} | |
public EntityUuid ParseUuidFromName() | |
{ | |
if (Uuid.IsExplicit) | |
{ | |
return Uuid; | |
} | |
var entityName = gameObject.name; | |
if (entityName.EndsWith("@")) | |
{ | |
var startIndex = entityName.LastIndexOf('@', entityName.Length – 2) + 1; | |
var uuidStr = entityName.Substring(startIndex, entityName.Length – startIndex – 1); | |
return EntityUuid.Parse(uuidStr); | |
} | |
var uuid = Uuid; | |
if (Entities.ContainsKey(Uuid)) | |
uuid = new EntityUuid(null); | |
gameObject.name = entityName + " @" + uuid + "@"; | |
return uuid; | |
} | |
#if UNITY_EDITOR | |
private void Start() | |
{ | |
if (!Application.isPlaying) | |
{ | |
ValidateName(); | |
Entities[Uuid] = this; | |
} | |
} | |
#endif | |
private void ValidateName() | |
{ | |
#if UNITY_EDITOR | |
if (Application.isPlaying) | |
return; | |
if (Uuid.IsExplicit) | |
return; | |
var go = gameObject; | |
if (go.IsPrefabObject()) | |
return; | |
var prevName = go.name; | |
if (Regex.IsMatch(prevName, @"^.* @[A-Za-z0-9\-]+@ \(\d+\)$")) | |
{ | |
var updatedName = prevName.Substring(0, prevName.LastIndexOf('@', | |
prevName.LastIndexOf('@') – 1) – 1) + " @" + | |
new EntityUuid(null) + "@"; | |
go.name = updatedName; | |
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(go.scene); | |
} | |
else if (!prevName.EndsWith("@")) | |
{ | |
go.name = prevName + " @" + new EntityUuid(null) + "@"; | |
} | |
#endif | |
} | |
private void OnDestroy() | |
{ | |
if (Application.isPlaying) | |
{ | |
Entities.Remove(Uuid); | |
} | |
else | |
{ | |
Entities.Remove(ParseUuidFromName()); | |
} | |
} | |
public override string ToString() | |
{ | |
return Uuid.ToString(); | |
} | |
// … | |
} |
Hopefully, you found this helpful! There are some other systems in play here, such as a custom drawer that can edit Explicit UUIDs in prefab mode, but I left them out for brevity.