DynmaicBone最新版本使用了多线程,30人同屏情况下消耗为6ms左右,如下图:
这个消耗依旧挺大,会使50帧的游戏降10帧左右。
使用 job system + burst 优化后的消耗为 0.05 ms,如下图:
优化方案参考如下:
https://blog.csdn.net/tangyin025/article/details/109683573
https://zhuanlan.zhihu.com/p/113367281
本人已将其整合到项目中,提供简略版的DynmaicBone,即DynamicBoneFast,源码分享如下:
DynamicBoneFast.cs
1 using Unity.Collections; 2 using Unity.Mathematics; 3 using UnityEngine; 4 5 [AddComponentMenu("Dynamic Bone/Dynamic Bone Fast")] 6 public class DynamicBoneFast : MonoBehaviour 7 { 8 #region Particle & HeadInfo 9 10 public struct Particle 11 { 12 public int m_ParentIndex; 13 public int m_ChildCount; 14 public float m_Damping; 15 public float m_Elasticity; 16 public float m_Stiffness; 17 public float m_Inert; 18 19 public float3 m_EndOffset; 20 public float3 m_InitLocalPosition; 21 public quaternion m_InitLocalRotation; 22 23 // [ta] 24 public int index; 25 public float3 tmpWorldPosition; 26 public float3 tmpPrevWorldPosition; 27 public float3 localPosition; 28 public quaternion localRotation; 29 public float3 parentScale; 30 public int isRootParticle; 31 32 //for output 33 public float3 worldPosition; 34 public quaternion worldRotation; 35 } 36 37 // [ta] 38 public struct HeadInfo 39 { 40 int m_HeadIndex; 41 42 public float m_UpdateRate; 43 public Vector3 m_ObjectMove; 44 public int m_particleCount; 45 public int m_jobDataOffset; 46 public int m_ParticleLoopCount; 47 48 public float3 m_RootParentBoneWorldPos; 49 public quaternion m_RootParentBoneWorldRot; 50 51 public int HeadIndex 52 { 53 get => m_HeadIndex; 54 set => m_HeadIndex = value; 55 } 56 } 57 58 #endregion 59 60 public const int MAX_TRANSFORM_LIMIT = 10; 61 62 public Transform m_Root; 63 public float m_UpdateRate = 60.0f; 64 [Range(0, 1)] 65 public float m_Damping = 0.1f; 66 [Range(0, 1)] 67 public float m_Elasticity = 0.1f; 68 [Range(0, 1)] 69 public float m_Stiffness = 0.1f; 70 [Range(0, 1)] 71 public float m_Inert = 0; 72 73 public bool m_DistantDisable = false; 74 public Transform m_ReferenceObject = null; 75 public float m_DistanceToObject = 20; 76 77 public NativeArray<Particle> m_Particles; 78 public Transform[] m_particleTransformArr; 79 private int m_ParticleCount; 80 public Transform m_rootParentTransform; 81 public HeadInfo m_headInfo; 82 bool m_IsInited; 83 84 85 public HeadInfo ResetHeadIndexAndDataOffset(int headIndex) 86 { 87 m_headInfo.HeadIndex = headIndex; 88 m_headInfo.m_jobDataOffset = headIndex * MAX_TRANSFORM_LIMIT; 89 90 return m_headInfo; 91 } 92 public void ClearJobData() 93 { 94 if (m_Particles.IsCreated) 95 { 96 m_Particles.Dispose(); 97 } 98 99 m_particleTransformArr = null; 100 m_IsInited = false; 101 } 102 103 void Awake() 104 { 105 Init(); 106 } 107 108 void Init() 109 { 110 if (m_Root == null) 111 return; 112 113 m_headInfo = new HeadInfo(); 114 m_headInfo.m_UpdateRate = this.m_UpdateRate; 115 m_headInfo.m_ObjectMove = Vector3.zero; 116 m_headInfo.m_particleCount = 0; 117 118 m_Particles = new NativeArray<Particle>(MAX_TRANSFORM_LIMIT, Allocator.Persistent); 119 m_particleTransformArr = new Transform[MAX_TRANSFORM_LIMIT]; 120 m_ParticleCount = 0; 121 122 SetupParticles(); 123 124 m_IsInited = true; 125 } 126 127 public void BeginWork() 128 { 129 if (!m_IsInited) 130 Init(); 131 DoEnable(); 132 } 133 134 void DoEnable() 135 { 136 if (m_IsInited) 137 DynamicBoneFastMgr.Singleton.OnEnter(this); 138 } 139 140 void DoDisable() 141 { 142 DynamicBoneFastMgr.Singleton.OnExit(this); 143 } 144 145 void OnEnable() 146 { 147 if (!m_IsInited) 148 Init(); 149 DoEnable(); 150 } 151 152 void OnDisable() 153 { 154 InitTransforms(); 155 DoDisable(); 156 } 157 158 void OnDestroy() 159 { 160 DoDisable(); 161 } 162 163 void SetupParticles() 164 { 165 if (m_Root == null) 166 return; 167 168 AppendParticles(m_Root, -1, 0); 169 UpdateParameters(); 170 171 m_headInfo.m_particleCount = m_ParticleCount; 172 m_rootParentTransform = m_Root.parent; 173 } 174 175 void AppendParticles(Transform b, int parentIndex, float boneLength) 176 { 177 var p = new Particle(); 178 p.index = m_ParticleCount++; 179 p.m_ParentIndex = parentIndex; 180 181 if (b != null) 182 { 183 p.tmpWorldPosition = p.tmpPrevWorldPosition = b.position; 184 p.m_InitLocalPosition = b.localPosition; 185 p.m_InitLocalRotation = b.localRotation; 186 187 // [ta] 188 p.localPosition = b.localPosition; 189 p.localRotation = b.localRotation; 190 p.parentScale = b.parent.lossyScale; 191 p.isRootParticle = parentIndex == -1 ? 1 : 0; 192 } 193 else // end bone 194 { 195 Transform pb = m_particleTransformArr[parentIndex]; 196 p.m_EndOffset = pb.InverseTransformPoint(transform.position + pb.position); 197 p.tmpWorldPosition = p.tmpPrevWorldPosition = pb.TransformPoint(p.m_EndOffset); 198 p.m_InitLocalPosition = Vector3.zero; 199 p.m_InitLocalRotation = Quaternion.identity; 200 } 201 202 if (parentIndex >= 0) 203 { 204 float dis = math.distance(m_particleTransformArr[parentIndex].position, p.tmpWorldPosition); 205 boneLength += dis; 206 ++p.m_ChildCount; 207 } 208 209 m_Particles[p.index] = p; 210 m_particleTransformArr[p.index] = b; 211 212 int index = p.index; 213 214 if (b != null) 215 { 216 for (int i = 0; i < b.childCount; ++i) 217 { 218 Transform child = b.GetChild(i); 219 AppendParticles(child, index, boneLength); 220 } 221 } 222 } 223 224 public void UpdateParameters() 225 { 226 if (m_Root == null) 227 return; 228 229 for (int i = 0; i < m_ParticleCount; ++i) 230 { 231 Particle p = m_Particles[i]; 232 p.m_Damping = m_Damping; 233 p.m_Elasticity = m_Elasticity; 234 p.m_Stiffness = m_Stiffness; 235 p.m_Inert = m_Inert; 236 p.m_Damping = Mathf.Clamp01(p.m_Damping); 237 p.m_Elasticity = Mathf.Clamp01(p.m_Elasticity); 238 p.m_Stiffness = Mathf.Clamp01(p.m_Stiffness); 239 p.m_Inert = Mathf.Clamp01(p.m_Inert); 240 241 m_Particles[i] = p; 242 } 243 } 244 245 void InitTransforms() 246 { 247 for (int i = 0; i < m_ParticleCount; ++i) 248 { 249 Particle p = m_Particles[i]; 250 Transform trans = m_particleTransformArr[p.index]; 251 if (trans != null) 252 { 253 p.localPosition = p.m_InitLocalPosition; 254 p.localRotation = p.m_InitLocalRotation; 255 } 256 } 257 } 258 259 }
DynamicBoneFastMgr.cs
1 using System.Collections.Generic; 2 using Tools; 3 using Unity.Burst; 4 using Unity.Collections; 5 using Unity.Jobs; 6 using Unity.Mathematics; 7 using UnityEngine; 8 using UnityEngine.Jobs; 9 10 public class DynamicBoneFastMgr : SingletonMonoBehaviour<DynamicBoneFastMgr> 11 { 12 #region Job 13 14 [BurstCompile] 15 struct RootPosApplyJob : IJobParallelForTransform 16 { 17 public NativeArray<DynamicBoneFast.HeadInfo> ParticleHeadInfo; 18 19 public void Execute(int index, TransformAccess transform) 20 { 21 DynamicBoneFast.HeadInfo headInfo = ParticleHeadInfo[index]; 22 headInfo.m_RootParentBoneWorldPos = transform.position; 23 headInfo.m_RootParentBoneWorldRot = transform.rotation; 24 25 ParticleHeadInfo[index] = headInfo; 26 } 27 } 28 29 [BurstCompile] 30 struct PrepareParticleJob : IJob 31 { 32 [ReadOnly] 33 public NativeArray<DynamicBoneFast.HeadInfo> ParticleHeadInfo; 34 public NativeArray<DynamicBoneFast.Particle> ParticleInfo; 35 public int HeadCount; 36 37 public void Execute() 38 { 39 for (int i = 0; i < HeadCount; i++) 40 { 41 DynamicBoneFast.HeadInfo curHeadInfo = ParticleHeadInfo[i]; 42 43 float3 parentPosition = curHeadInfo.m_RootParentBoneWorldPos; 44 quaternion parentRotation = curHeadInfo.m_RootParentBoneWorldRot; 45 46 for (int j = 0; j < curHeadInfo.m_particleCount; j++) 47 { 48 int pIdx = curHeadInfo.m_jobDataOffset + j; 49 DynamicBoneFast.Particle p = ParticleInfo[pIdx]; 50 51 var localPosition = p.localPosition * p.parentScale; 52 var localRotation = p.localRotation; 53 var worldPosition = parentPosition + math.mul(parentRotation, localPosition); 54 var worldRotation = math.mul(parentRotation, localRotation); 55 56 p.worldPosition = worldPosition; 57 p.worldRotation = worldRotation; 58 59 parentPosition = worldPosition; 60 parentRotation = worldRotation; 61 62 ParticleInfo[pIdx] = p; 63 } 64 } 65 } 66 } 67 68 [BurstCompile] 69 struct UpdateParticles1Job : IJobParallelFor 70 { 71 [ReadOnly] 72 public NativeArray<DynamicBoneFast.HeadInfo> ParticleHeadInfo; 73 public NativeArray<DynamicBoneFast.Particle> ParticleInfo; 74 public int HeadCount; 75 76 public void Execute(int index) 77 { 78 int headIndex = index / DynamicBoneFast.MAX_TRANSFORM_LIMIT; 79 DynamicBoneFast.HeadInfo curHeadInfo = ParticleHeadInfo[headIndex]; 80 int singleId = index % DynamicBoneFast.MAX_TRANSFORM_LIMIT; 81 82 if (singleId >= curHeadInfo.m_particleCount) 83 return; 84 85 int pIdx = curHeadInfo.m_jobDataOffset + (index % DynamicBoneFast.MAX_TRANSFORM_LIMIT); 86 87 DynamicBoneFast.Particle p = ParticleInfo[pIdx]; 88 89 if (p.m_ParentIndex >= 0) 90 { 91 float3 ev = p.tmpWorldPosition - p.tmpPrevWorldPosition; 92 float3 evrmove = curHeadInfo.m_ObjectMove * p.m_Inert; 93 p.tmpPrevWorldPosition = p.tmpWorldPosition + evrmove; 94 95 float edamping = p.m_Damping; 96 float3 tmp = ev * (1 - edamping) + evrmove; 97 p.tmpWorldPosition += tmp; 98 } 99 else 100 { 101 p.tmpPrevWorldPosition = p.tmpWorldPosition; 102 p.tmpWorldPosition = p.worldPosition; 103 } 104 105 ParticleInfo[pIdx] = p; 106 } 107 } 108 109 [BurstCompile] 110 struct UpdateParticle2Job : IJobParallelFor 111 { 112 [ReadOnly] 113 public NativeArray<DynamicBoneFast.HeadInfo> ParticleHeadInfo; 114 public NativeArray<DynamicBoneFast.Particle> ParticleInfo; 115 public int HeadCount; 116 public float DeltaTime; 117 118 public void Execute(int index) 119 { 120 if (index % DynamicBoneFast.MAX_TRANSFORM_LIMIT == 0) 121 return; 122 123 int headIndex = index / DynamicBoneFast.MAX_TRANSFORM_LIMIT; 124 DynamicBoneFast.HeadInfo curHeadInfo = ParticleHeadInfo[headIndex]; 125 126 int singleId = index % DynamicBoneFast.MAX_TRANSFORM_LIMIT; 127 128 if (singleId >= curHeadInfo.m_particleCount) 129 return; 130 131 int pIdx = curHeadInfo.m_jobDataOffset + (index % DynamicBoneFast.MAX_TRANSFORM_LIMIT); 132 133 DynamicBoneFast.Particle p = ParticleInfo[pIdx]; 134 int p0Idx = curHeadInfo.m_jobDataOffset + p.m_ParentIndex; 135 DynamicBoneFast.Particle p0 = ParticleInfo[p0Idx]; 136 137 float3 ePos = p.worldPosition; 138 float3 ep0Pos = p0.worldPosition; 139 140 float erestLen = math.distance(ep0Pos, ePos); 141 142 float stiffness = p.m_Stiffness; 143 if (stiffness > 0 || p.m_Elasticity > 0) 144 { 145 float4x4 em0 = float4x4.TRS(p0.tmpWorldPosition, p0.worldRotation, p.parentScale); 146 float3 erestPos = math.mul(em0, new float4(p.localPosition.xyz, 1)).xyz; 147 float3 ed = erestPos - p.tmpWorldPosition; 148 float3 eStepElasticity = ed * p.m_Elasticity * curHeadInfo.m_UpdateRate * DeltaTime; 149 p.tmpWorldPosition += eStepElasticity; 150 151 if (stiffness > 0) 152 { 153 float len = math.distance(erestPos, p.tmpWorldPosition); 154 float maxlen = erestLen * (1 - stiffness) * 2; 155 if (len > maxlen) 156 { 157 float3 max = ed * ((len - maxlen) / len); 158 p.tmpWorldPosition += max; 159 } 160 } 161 } 162 163 float3 edd = p0.tmpWorldPosition - p.tmpWorldPosition; 164 float eleng = math.distance(p0.tmpWorldPosition, p.tmpWorldPosition); 165 if (eleng > 0) 166 { 167 float3 tmp = edd * ((eleng - erestLen) / eleng); 168 p.tmpWorldPosition += tmp; 169 } 170 171 ParticleInfo[pIdx] = p; 172 } 173 } 174 175 [BurstCompile] 176 struct ApplyParticleToTransform : IJobParallelFor 177 { 178 [ReadOnly] 179 public NativeArray<DynamicBoneFast.HeadInfo> ParticleHeadInfo; 180 public NativeArray<DynamicBoneFast.Particle> ParticleInfo; 181 public int HeadCount; 182 183 public void Execute(int index) 184 { 185 if (index % DynamicBoneFast.MAX_TRANSFORM_LIMIT == 0) 186 return; 187 188 int headIndex = index / DynamicBoneFast.MAX_TRANSFORM_LIMIT; 189 190 DynamicBoneFast.HeadInfo curHeadInfo = ParticleHeadInfo[headIndex]; 191 int singleId = index % DynamicBoneFast.MAX_TRANSFORM_LIMIT; 192 193 if (singleId >= curHeadInfo.m_particleCount) 194 return; 195 196 int pIdx = curHeadInfo.m_jobDataOffset + (index % DynamicBoneFast.MAX_TRANSFORM_LIMIT); 197 198 DynamicBoneFast.Particle p = ParticleInfo[pIdx]; 199 int p0Idx = curHeadInfo.m_jobDataOffset + p.m_ParentIndex; 200 DynamicBoneFast.Particle p0 = ParticleInfo[p0Idx]; 201 202 if (p0.m_ChildCount <= 1) 203 { 204 float3 ev = p.localPosition; 205 float3 ev2 = p.tmpWorldPosition - p0.tmpWorldPosition; 206 207 float4x4 epm = float4x4.TRS(p.worldPosition, p.worldRotation, p.parentScale); 208 209 var worldV = math.mul(epm, new float4(ev, 0)).xyz; 210 Quaternion erot = Quaternion.FromToRotation(worldV, ev2); 211 var eoutputRot = math.mul(erot, p.worldRotation); 212 p0.worldRotation = eoutputRot; 213 } 214 215 p.worldPosition = p.tmpWorldPosition; 216 217 ParticleInfo[pIdx] = p; 218 ParticleInfo[p0Idx] = p0; 219 } 220 } 221 222 [BurstCompile] 223 struct FinalJob : IJobParallelForTransform 224 { 225 [ReadOnly] 226 public NativeArray<DynamicBoneFast.Particle> ParticleInfo; 227 228 public void Execute(int index, TransformAccess transform) 229 { 230 transform.rotation = ParticleInfo[index].worldRotation; 231 transform.position = ParticleInfo[index].worldPosition; 232 } 233 } 234 235 #endregion 236 237 238 List<DynamicBoneFast> m_DynamicBoneList; 239 NativeList<DynamicBoneFast.Particle> m_ParticleInfoList; 240 NativeList<DynamicBoneFast.HeadInfo> m_HeadInfoList; 241 242 TransformAccessArray m_headRootTransform; 243 TransformAccessArray m_particleTransformArr; 244 int m_DbDataLen; 245 JobHandle m_lastJobHandle; 246 247 Queue<DynamicBoneFast> m_loadingQueue = new Queue<DynamicBoneFast>(); 248 Queue<DynamicBoneFast> m_removeQueue = new Queue<DynamicBoneFast>(); 249 250 251 void Awake() 252 { 253 m_DynamicBoneList = new List<DynamicBoneFast>(); 254 m_ParticleInfoList = new NativeList<DynamicBoneFast.Particle>(Allocator.Persistent); 255 m_HeadInfoList = new NativeList<DynamicBoneFast.HeadInfo>(Allocator.Persistent); 256 m_particleTransformArr = new TransformAccessArray(200 * DynamicBoneFast.MAX_TRANSFORM_LIMIT, 64); 257 m_headRootTransform = new TransformAccessArray(200, 64); 258 } 259 260 void UpdateQueue() 261 { 262 while (m_loadingQueue.Count > 0) 263 { 264 DynamicBoneFast target = m_loadingQueue.Dequeue(); 265 int index = m_DynamicBoneList.IndexOf(target); 266 if (index != -1) 267 continue; 268 269 m_DynamicBoneList.Add(target); 270 target.m_headInfo.m_jobDataOffset = m_ParticleInfoList.Length; 271 target.m_headInfo.HeadIndex = m_HeadInfoList.Length; 272 m_HeadInfoList.Add(target.m_headInfo); 273 m_ParticleInfoList.AddRange(target.m_Particles); 274 m_headRootTransform.Add(target.m_rootParentTransform); 275 276 for (int i = 0; i < DynamicBoneFast.MAX_TRANSFORM_LIMIT; i++) 277 m_particleTransformArr.Add(target.m_particleTransformArr[i]); 278 279 m_DbDataLen++; 280 } 281 282 while (m_removeQueue.Count > 0) 283 { 284 DynamicBoneFast target = m_removeQueue.Dequeue(); 285 int index = m_DynamicBoneList.IndexOf(target); 286 if (index != -1) 287 { 288 m_DynamicBoneList.RemoveAt(index); 289 290 int curHeadIndex = target.m_headInfo.HeadIndex; 291 292 //是否是队列中末尾对象 293 bool isEndTarget = curHeadIndex == m_HeadInfoList.Length - 1; 294 if (isEndTarget) 295 { 296 m_HeadInfoList.RemoveAtSwapBack(curHeadIndex); 297 m_headRootTransform.RemoveAtSwapBack(curHeadIndex); 298 299 for (int i = DynamicBoneFast.MAX_TRANSFORM_LIMIT - 1; i >= 0; i--) 300 { 301 int dataOffset = curHeadIndex * DynamicBoneFast.MAX_TRANSFORM_LIMIT + i; 302 m_ParticleInfoList.RemoveAtSwapBack(dataOffset); 303 m_particleTransformArr.RemoveAtSwapBack(dataOffset); 304 } 305 } 306 else 307 { 308 //将最末列的HeadInfo 索引设置为当前将要移除的HeadInfo 索引 309 DynamicBoneFast lastTarget = m_DynamicBoneList[m_DynamicBoneList.Count - 1]; 310 m_DynamicBoneList.RemoveAt(m_DynamicBoneList.Count - 1); 311 m_DynamicBoneList.Insert(index, lastTarget); 312 313 DynamicBoneFast.HeadInfo lastHeadInfo = lastTarget.ResetHeadIndexAndDataOffset(curHeadIndex); 314 m_HeadInfoList.RemoveAtSwapBack(curHeadIndex); 315 m_HeadInfoList[curHeadIndex] = lastHeadInfo; 316 m_headRootTransform.RemoveAtSwapBack(curHeadIndex); 317 318 for (int i = DynamicBoneFast.MAX_TRANSFORM_LIMIT - 1; i >= 0; i--) 319 { 320 int dataOffset = curHeadIndex * DynamicBoneFast.MAX_TRANSFORM_LIMIT + i; 321 m_ParticleInfoList.RemoveAtSwapBack(dataOffset); 322 m_particleTransformArr.RemoveAtSwapBack(dataOffset); 323 } 324 } 325 326 m_DbDataLen--; 327 } 328 329 target.ClearJobData(); 330 } 331 } 332 333 public void OnEnter(DynamicBoneFast target) 334 { 335 m_loadingQueue.Enqueue(target); 336 } 337 338 public void OnExit(DynamicBoneFast target) 339 { 340 m_removeQueue.Enqueue(target); 341 } 342 343 void LateUpdate() 344 { 345 if (!m_lastJobHandle.IsCompleted) 346 return; 347 348 m_lastJobHandle.Complete(); 349 350 UpdateQueue(); 351 352 if (m_DbDataLen == 0) 353 return; 354 355 var dataArrLength = m_DbDataLen * DynamicBoneFast.MAX_TRANSFORM_LIMIT; 356 357 var rootJob = new RootPosApplyJob 358 { 359 ParticleHeadInfo = m_HeadInfoList 360 }; 361 var rootHandle = rootJob.Schedule(m_headRootTransform); 362 363 var prepareJob = new PrepareParticleJob 364 { 365 ParticleHeadInfo = m_HeadInfoList, 366 ParticleInfo = m_ParticleInfoList, 367 HeadCount = m_DbDataLen 368 }; 369 var prepareHandle = prepareJob.Schedule(rootHandle); 370 371 var update1Job = new UpdateParticles1Job 372 { 373 ParticleHeadInfo = m_HeadInfoList, 374 ParticleInfo = m_ParticleInfoList, 375 HeadCount = m_DbDataLen 376 }; 377 var update1Handle = update1Job.Schedule(dataArrLength, DynamicBoneFast.MAX_TRANSFORM_LIMIT, prepareHandle); 378 379 var update2Job = new UpdateParticle2Job 380 { 381 ParticleHeadInfo = m_HeadInfoList, 382 ParticleInfo = m_ParticleInfoList, 383 HeadCount = m_DbDataLen, 384 DeltaTime = Time.deltaTime, 385 }; 386 var update2Handle = update2Job.Schedule(dataArrLength, DynamicBoneFast.MAX_TRANSFORM_LIMIT, update1Handle); 387 388 var appTransJob = new ApplyParticleToTransform 389 { 390 ParticleHeadInfo = m_HeadInfoList, 391 ParticleInfo = m_ParticleInfoList, 392 HeadCount = m_DbDataLen 393 }; 394 395 var appTransHandle = appTransJob.Schedule(dataArrLength, DynamicBoneFast.MAX_TRANSFORM_LIMIT, update2Handle); 396 var finalJob = new FinalJob 397 { 398 ParticleInfo = m_ParticleInfoList, 399 }; 400 var finalHandle = finalJob.Schedule(m_particleTransformArr, appTransHandle); 401 402 m_lastJobHandle = finalHandle; 403 404 JobHandle.ScheduleBatchedJobs(); 405 } 406 407 void OnDestroy() 408 { 409 if (m_particleTransformArr.isCreated) 410 { 411 m_particleTransformArr.Dispose(); 412 } 413 414 if (m_ParticleInfoList.IsCreated) 415 { 416 m_ParticleInfoList.Dispose(); 417 } 418 419 if (m_HeadInfoList.IsCreated) 420 { 421 m_HeadInfoList.Dispose(); 422 } 423 424 if (m_headRootTransform.isCreated) 425 { 426 m_headRootTransform.Dispose(); 427 } 428 } 429 }
转载请注明出处:https://www.cnblogs.com/jietian331/p/17154522.html
标签:DynamicBoneFast,index,int,MAX,void,Unity,DynamicBone,优化,public From: https://www.cnblogs.com/jietian331/p/17154522.html