Skip to main content

Working without Animator

The built-in Animator Controller advances a state machine and fills AnimationToProcessComponent with the clips to sample. A custom ECS system can fill that buffer instead, while keeping Rukhanka Animation's animation and deformation pipeline.

Authoring Preparation​

Set Rig Config Source on Rig Definition Authoring to User Defined and explicitly assign the model's prepared Avatar. You can then remove the Unity Animator component. Set root motion and culling options on the rig definition itself in this mode.

Rig Definition User Mode

Scripted Animator​

Without an authoring Animator Controller, the rig has no controller layer buffer. Your system owns clip selection, playback time, and weights. RigDefinitionBaker still creates the AnimationToProcessComponent buffer; do not add a duplicate buffer in your baker.

Components and Baking​

The following example plays one clip. Put MyAnimatorAuthoring on the same GameObject as RigDefinitionAuthoring, assign a clip, and place the object in a SubScene. For looping playback, enable Loop Time on the imported clip.

using Rukhanka;
using Rukhanka.Hybrid;
using Unity.Entities;
using UnityEngine;

[RequireComponent(typeof(RigDefinitionAuthoring))]
public class MyAnimatorAuthoring : MonoBehaviour
{
public AnimationClip clip;
}

public struct MyAnimatorComponent : IComponentData
{
public float normalizedAnimationTime;
public BlobAssetReference<AnimationClipBlob> clipBlob;
}

The baker uses Editor-only animation conversion APIs. Keep it in the same assembly with the UNITY_EDITOR guard shown below, or in an Editor assembly that references your authoring types and Rukhanka.Hybrid.

#if UNITY_EDITOR
using Rukhanka;
using Rukhanka.Hybrid;
using Unity.Entities;

public class MyAnimatorBaker : Baker<MyAnimatorAuthoring>
{
public override void Bake(MyAnimatorAuthoring authoring)
{
var rig = GetComponent<RigDefinitionAuthoring>();
if (authoring.clip == null || rig == null)
return;

var avatar = rig.GetAvatar();
DependsOn(avatar);
DependsOn(authoring.clip);
var animationBaker = new AnimationClipBaker();
using var animations = animationBaker.BakeAnimations(
this, new[] { authoring.clip }, avatar, authoring.gameObject);
if (animations.Length == 0 || !animations[0].IsCreated)
return;

var clipBlob = animations[0];
var entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent(entity, new MyAnimatorComponent { clipBlob = clipBlob });

// Register the clip separately so other runtime systems can find it by hash.
var databaseEntity = CreateAdditionalEntity(TransformUsageFlags.None);
var records = AddBuffer<NewBlobAssetDatabaseRecord<AnimationClipBlob>>(databaseEntity);
records.Add(new NewBlobAssetDatabaseRecord<AnimationClipBlob>
{
hash = clipBlob.Value.hash,
value = clipBlob
});
}
}
#endif

Baking owns the clip blob's lifetime. Dispose the temporary array returned by BakeAnimations, but do not dispose its blob references from gameplay code.

Animator Controller System​

Advance normalized time by deltaTime / clipLengthInSeconds. Clear the submission buffer once per update, then submit the clips for that update. The system must run before RukhankaAnimationSystemGroup.

using Rukhanka;
using Unity.Burst;
using Unity.Entities;

[UpdateInGroup(typeof(SimulationSystemGroup))]
[UpdateBefore(typeof(RukhankaAnimationSystemGroup))]
public partial struct MyAnimatorSystem : ISystem
{
[BurstCompile]
partial struct AdvanceAnimationJob : IJobEntity
{
public float deltaTime;

void Execute(ref MyAnimatorComponent animator,
DynamicBuffer<AnimationToProcessComponent> animations)
{
ScriptedAnimator.ResetAnimationState(ref animations);
if (!animator.clipBlob.IsCreated || animator.clipBlob.Value.length <= 0)
return;

animator.normalizedAnimationTime += deltaTime / animator.clipBlob.Value.length;
ScriptedAnimator.PlayAnimation(ref animations, animator.clipBlob,
animator.normalizedAnimationTime);
}
}

[BurstCompile]
public void OnUpdate(ref SystemState state)
{
state.Dependency = new AdvanceAnimationJob
{
deltaTime = SystemAPI.Time.DeltaTime
}.ScheduleParallel(state.Dependency);
}
}

PlayAnimation submits a pose at the supplied normalized time; it does not advance time on its own. Continue normalized time across loops so root motion and events can track playback. The clip's baked loop settings determine how the sampling time is wrapped or clamped. ScriptedAnimator also provides helpers for blending clips and blend trees.

Mixed Controller Mode​

Keep the authoring Animator and controller if you want to use states authored in Mecanim while selecting them yourself. ScriptedAnimator.PlayAnimatorState submits a state's motion, including blend trees. Obtain the baked controller and animation blobs from the layer buffer; use GetStateIndexInControllerLayer with a Rukhanka Animation state-name hash to find the state index. Supply the parameters, blob database, layer index, state index, and normalized time on every update.

Choose one owner of each rig's animation submission buffer. For a world using only custom controllers, disable the built-in AnimatorControllerSystem and FillAnimationsFromControllerSystem. In a mixed world, keep them for ordinary rigs and arrange custom submissions after the fill system but before animation processing; otherwise the built-in fill can replace your buffer. A system placed before the outer animation group is suitable for rigs without an authoring Animator, as in the example above.

If you only need a transition to a different authored state, consider ScriptedAnimator.CrossFade instead: it starts a transition once and lets the built-in controller continue advancing it.

Blob Database​

BlobDatabaseSingleton maps hashes to baked animation and avatar-mask blobs. A hash does not bake or register an asset by itself. Register clips through a baker, as above, or add AnimationAssetSetAuthoring to the rig and assign its clips and masks.

Calculate an animation hash during baking with the public BakingUtils.ComputeAnimationHash(AnimationClip, Avatar) overload. Both the clip and avatar must match the baked asset. At runtime, retrieve the blob from the database and check IsCreated before use; a missing entry returns a default blob reference.

The following helper can be called from a system after obtaining the singleton through SystemAPI.TryGetSingleton<BlobDatabaseSingleton>:

using Rukhanka;
using Unity.Entities;

public static class AnimationDatabaseLookup
{
public static bool TryGetAnimation(BlobDatabaseSingleton database, Hash128 hash,
out BlobAssetReference<AnimationClipBlob> clip)
{
clip = database.GetAnimationClipBlob(hash);
return clip.IsCreated;
}
}

Do not dispose database-owned blobs or keep using them after their owning world or scene has unloaded.

Animation Asset Set Authoring

See Scripted Controller for clip selection, manual time control, and blending controls.