ECS:使用BlobAsset实现Animation动画
将一个方块的AnimationClip数据导出,然后以BlobAsset的方式在JobSystem中并行计算,以提升效率:
(1)编辑态,将AnimationClip转为SriptableObject数据,进行存储;
(2)运行时,将ScriptableObject数据转换为BlobAsset对象,供ComponentData使用;
(3)在JobSystem中使用BlobAsset驱动Entity的Matrix(T R S)修改。
编辑态:
记录动画数据的SriptableObject:
[CreateAssetMenu(menuName = "Test/CreaetAnimationData")] public class AnimationData : ScriptableObject { public float frameDelta; public int frameCount; public List<Vector3> positions; public List<Vector3> eulers; public List<Vector3> scales; }
通过编辑器进行数据转换(fps30的数据采样率):
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; public class ConvertAnimationDataEditor : EditorWindow { private static EditorWindow window; [MenuItem("Test/ConverAnimationData")] static void Execute() { if (window == null) window = (ConvertAnimationDataEditor)GetWindow(typeof(ConvertAnimationDataEditor)); window.minSize = new Vector2(300, 200); window.name = "动画转换工具"; window.Show(); } private AnimationClip clip; private AnimationData animAsset; private void OnGUI() { using (new GUILayout.HorizontalScope("box")) { GUILayout.Label("AnimClip:", GUILayout.Width(60f)); clip = EditorGUILayout.ObjectField(clip, typeof(AnimationClip), false) as AnimationClip; } using (new GUILayout.HorizontalScope()) { GUILayout.Label("SaveAsset:", GUILayout.Width(60f)); animAsset = EditorGUILayout.ObjectField(animAsset, typeof(AnimationData), false) as AnimationData; } if(GUILayout.Button("Save")) { Save(); } } private void Save() { var path = AssetDatabase.GetAssetPath(animAsset); var asset = AssetDatabase.LoadAssetAtPath<AnimationData>(path); asset.frameDelta = 1f / 30f; asset.frameCount = Mathf.CeilToInt(clip.length / asset.frameDelta); asset.positions = new List<Vector3>(asset.frameCount); asset.scales = new List<Vector3>(asset.frameCount); for(int i = 0; i < asset.frameCount; ++i) { asset.positions.Add(Vector3.zero); asset.scales.Add(Vector3.one); } foreach (var binding in AnimationUtility.GetCurveBindings(clip)) { AnimationCurve curve = AnimationUtility.GetEditorCurve(clip, binding); string propName = binding.propertyName; float timer = 0f; float maxTime = clip.length; int index = 0; while(timer < maxTime && index < asset.frameCount) { switch (propName) { case "m_LocalPosition.x": { var pos = asset.positions[index]; pos.x = GetValue(curve.keys, timer); asset.positions[index] = pos; } break; case "m_LocalPosition.y": { var pos = asset.positions[index]; pos.y = GetValue(curve.keys, timer); asset.positions[index] = pos; } break; case "m_LocalPosition.z": { var pos = asset.positions[index]; pos.z = GetValue(curve.keys, timer); asset.positions[index] = pos; } break; case "m_LocalScale.x": { var scale = animAsset.scales[index]; scale.x = GetValue(curve.keys, timer); animAsset.scales[index] = scale; } break; case "m_LocalScale.y": { var scale = asset.scales[index]; scale.y = GetValue(curve.keys, timer); asset.scales[index] = scale; } break; case "m_LocalScale.z": { var scale = asset.scales[index]; scale.z = GetValue(curve.keys, timer); asset.scales[index] = scale; } break; } timer += asset.frameDelta; index++; } } EditorUtility.SetDirty(asset); AssetDatabase.SaveAssets(); } private float GetValue(Keyframe[] frames, float time) { int pre = 0; int next = 0; for(int i = 0; i < frames.Length; ++i) { var frame = frames[i]; if(time <= frame.time) { next = i; break; } } pre = Mathf.Max(0, next - 1); var preFrame = frames[pre]; var nextFrame = frames[next]; if(pre == next) return nextFrame.time; float ret = preFrame.value + (nextFrame.value - preFrame.value) * (time - preFrame.time) / (nextFrame.time - preFrame.time); return ret; } }
导出以后就会生成一个记录了动画数据的asset:
运行时:
运行时记录动画数据的结构体:
using Unity.Entities; using Unity.Mathematics; public struct AnimationBlobAsset { public float frameDelta; public int frameCount; public BlobArray<float3> positions; public BlobArray<float3> eulers; public BlobArray<float3> scales; }
注意使用的是Unity.Mathematics.float3而不是vector3,这样可以利用burst的simd优化。
将ScriptableObject对象的数据转化成为BlobAsset:
private BlobAssetReference<AnimationBlobAsset> animationBlob; public void RegisterBlobAsset() { AnimationData data = ...; // Asset资源加载 using (BlobBuilder blobBuilder = new BlobBuilder(Allocator.Temp)) { ref AnimationBlobAsset asset = ref blobBuilder.ConstructRoot<AnimationBlobAsset>(); BlobBuilderArray<float3> positions = blobBuilder.Allocate(ref asset.positions, data.frameCount); BlobBuilderArray<float3> scales = blobBuilder.Allocate(ref asset.scales, data.frameCount); asset.frameDelta = data.frameDelta; asset.frameCount = data.frameCount; for (int i = 0; i < data.frameCount; ++i) { positions[i] = new float3(data.positions[i]); scales[i] = new float3(data.scales[i]); } animationBlob = blobBuilder.CreateBlobAssetReference<AnimationBlobAsset>(Allocator.Persistent); } }
上面的代码,就是创建BlobAsset的一个流程。
AnimationComponent数据的定义:
using Unity.Entities; using Unity.Mathematics; public struct Animation : IComponentData { public BlobAssetReference<AnimationBlobAsset> animBlobRef; public float timer; public int frame; public float3 localPosition; }
创建Entity时,给AnimationComponent设置数据:
// 设置ComponentData属性 entityManager.SetComponentData(entity, new Animation() { animBlobRef = animationBlob, timer = 0f, frame = 0, }); // ...
System的实现:
using Unity.Entities; using Unity.Jobs; using Unity.Transforms; using Unity.Burst; public partial class AnimationSystem : JobComponentSystem { protected override JobHandle OnUpdate(JobHandle inputDeps) { AnimateJob job = new AnimateJob() { deltaTime = UnityEngine.Time.deltaTime, }; JobHandle handle = job.Schedule(this, inputDeps); return handle; } } public partial class AnimationSystem : JobComponentSystem { [RequireComponentTag(typeof(Arrive))] [BurstCompile] private struct AnimateJob: IJobForEach<Animation, NonUniformScale> { public float deltaTime; public void Execute(ref Animation anim, ref NonUniformScale scale) { ref AnimationBlobAsset blob = ref anim.animBlobRef.Value; anim.timer += deltaTime; if(anim.timer < blob.frameDelta) return; while(anim.timer > blob.frameDelta) { anim.timer -= blob.frameDelta; anim.frame = (anim.frame + 1) % blob.frameCount; } anim.localPosition = blob.positions[anim.frame]; scale.Value = blob.scales[anim.frame]; } } }