Utility helper for attaching a GameObject to a bone of a SkinnedModelRenderer. Enables bone objects, retries up to 10 times one frame apart while waiting for the bone to exist, matches the object's world transform to the bone, and optionally destroys the object on failure.
/*
* Utility class for handling models
*/
namespace SWB.Shared;
public class ModelUtil
{
public static void ParentToBone( GameObject gameObject, SkinnedModelRenderer target, string bone, int tries = 0, bool deleteOnFail = true, System.Action<GameObject> onFail = null )
{
if ( !gameObject.IsValid() ) return;
void Fail( string message )
{
Log.Warning( message );
onFail?.Invoke( gameObject );
if ( deleteOnFail && gameObject.IsValid() )
gameObject.Destroy();
}
if ( !target.IsValid() )
{
Fail( $"Could not parent {gameObject} to bone '{bone}' because target renderer is invalid" );
return;
}
// Bone objects are created lazily: models only pay for a bone hierarchy (updated every
// frame on every client) once something actually needs to hang off a bone. The retry
// below waits for them to appear.
if ( !target.CreateBoneObjects )
target.CreateBoneObjects = true;
var holdBoneGO = target.GetBoneObject( bone );
if ( !holdBoneGO.IsValid() || holdBoneGO.Scene is null )
{
// Try again 1 frame later, viewmodel edge case
async void retry()
{
await GameTask.Delay( 1 );
ParentToBone( gameObject, target, bone, tries + 1, deleteOnFail, onFail );
}
if ( tries < 10 )
retry();
else
Fail( $"Could not get bone object from '{bone}' on {target}" );
return;
}
try
{
gameObject.SetParent( holdBoneGO );
}
catch
{
//Log.Info( target.Model.ResourcePath );
//Log.Info( holdBoneGO.Scene );
//Log.Info( holdBoneGO.Scene == null );
Fail( $"Could not parent {gameObject} to {holdBoneGO}" );
return;
}
if ( !gameObject.IsValid() ) return;
gameObject.WorldPosition = holdBoneGO.WorldPosition;
gameObject.WorldRotation = holdBoneGO.WorldRotation;
gameObject.Transform.ClearInterpolation();
}
}