using System; using System.Collections.Generic; using ECSAnimator.Runtime.Product.ECS; using Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Jobs; namespace ECSAnimator.Runtime.Product.Samples.Newcomer { [UpdateInGroup(typeof(CharacterAnimationBridgeSystemGroup))] public partial class NewcomerAnimationBridgeSystem : SystemBase { private sealed class Context : IDisposable { internal int Id; internal CharacterAnimationClient Client; internal NativeArray Targets; internal NativeArray Entities; internal ResolvedAnimationHandle Speed, Direction, Grounded, Attack, Hit, Death; public void Dispose() { if (Targets.IsCreated) Targets.Dispose(); if (Entities.IsCreated) Entities.Dispose(); } } private readonly List _contexts = new List(); private int _nextContextId = 1; internal bool Register(CharacterAnimationClient client, CharacterSpawnResult[] results, out int registration, out string error) { registration = 0; error = String.Empty; if (client == null || !client.IsValid || results == null || results.Length == 0 || _nextContextId <= 0) { error = "Newcomer bridge registration is invalid."; return false; } if (!client.TryGet("Speed", out var speed) || !client.TryGet("Direction", out var direction) || !client.TryGet("Grounded", out var grounded) || !client.TryGet("UpperBody/Attack#1", out var attack) || !client.TryGet("FullBody/Hit#1", out var hit) || !client.TryGet("FullBody/Death#2", out var death)) { error = "Newcomer binding keys do not match this controller."; return false; } var targets = default(NativeArray); var entities = default(NativeArray); try { targets = new NativeArray(results.Length, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); entities = new NativeArray(results.Length, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); for (var index = 0; index < results.Length; index++) { if (!results[index].IsValid) { error = "Newcomer bridge target is invalid."; targets.Dispose(); entities.Dispose(); return false; } targets[index] = results[index].SpawnId; entities[index] = results[index].Entity; } var id = _nextContextId++; _contexts.Add(new Context { Id = id, Client = client, Targets = targets, Entities = entities, Speed = speed, Direction = direction, Grounded = grounded, Attack = attack, Hit = hit, Death = death }); registration = id; return true; } catch (Exception exception) { if (targets.IsCreated) targets.Dispose(); if (entities.IsCreated) entities.Dispose(); error = exception.Message; return false; } } internal void Unregister(int registration) { for (var index = 0; index < _contexts.Count; index++) { if (_contexts[index].Id != registration) continue; Dependency.Complete(); _contexts[index].Dispose(); _contexts.RemoveAt(index); return; } } protected override void OnUpdate() { if (_contexts.Count == 0) return; var inputDependency = Dependency; var outputDependency = default(JobHandle); var intents = GetComponentLookup(true); for (var contextIndex = 0; contextIndex < _contexts.Count; contextIndex++) { var context = _contexts[contextIndex]; if (context.Client == null || !context.Client.IsValid || !context.Targets.IsCreated || context.Targets.Length == 0 || !context.Client.TryReserveTargets( context.Targets.Length, out var writer, out var reservationDependency, out _)) continue; var producer = new BridgeJob { Writer = writer, Targets = context.Targets, Entities = context.Entities, Intents = intents, Speed = context.Speed, Direction = context.Direction, Grounded = context.Grounded, Attack = context.Attack, Hit = context.Hit, Death = context.Death }.Schedule(context.Targets.Length, 64, JobHandle.CombineDependencies(inputDependency, reservationDependency)); context.Client.RegisterProducer(producer); outputDependency = JobHandle.CombineDependencies(outputDependency, producer); } Dependency = JobHandle.CombineDependencies(inputDependency, outputDependency); } [BurstCompile] private struct BridgeJob : IJobParallelFor { internal CharacterAnimationParallelWriter Writer; [ReadOnly] internal NativeArray Targets; [ReadOnly] internal NativeArray Entities; [ReadOnly] internal ComponentLookup Intents; internal ResolvedAnimationHandle Speed, Direction, Grounded, Attack, Hit, Death; public void Execute(int sortKey) { var target = Targets[sortKey]; var entity = Entities[sortKey]; if (!target.IsValid || entity == Entity.Null || !Intents.HasComponent(entity)) return; var intent = Intents[entity]; var output = Writer.Begin(sortKey, target); output.SetFloat(Speed, intent.Speed); output.SetFloat(Direction, intent.Direction); output.SetBool(Grounded, intent.Grounded != 0); // Gameplay policy resolves mutually exclusive full-body requests before // the animation authority: Death > Hit > Attack. No hidden last-write wins. if (intent.Death != 0) output.Play(Death, 0f, intent.DeathRequest); else if (intent.Hit != 0) output.Play(Hit, 0f, intent.HitRequest); else if (intent.Attack != 0) output.Play(Attack, 0f, intent.AttackRequest); } } protected override void OnDestroy() { Dependency.Complete(); for (var index = 0; index < _contexts.Count; index++) _contexts[index].Dispose(); _contexts.Clear(); base.OnDestroy(); } } [UpdateInGroup(typeof(CharacterEventSystemGroup))] public partial class NewcomerAnimationReceiptBridgeSystem : SystemBase { private sealed class Context { internal int Id; internal CharacterAnimationClient Client; internal ResolvedAnimationHandle Attack, Hit, Death; } private readonly List _contexts = new List(); private int _nextContextId = 1; internal bool Register(CharacterAnimationClient client, out int registration) { registration = 0; if (client == null || !client.IsValid || _nextContextId <= 0 || !client.TryGet("UpperBody/Attack#1", out var attack) || !client.TryGet("FullBody/Hit#1", out var hit) || !client.TryGet("FullBody/Death#2", out var death)) return false; var id = _nextContextId++; _contexts.Add(new Context { Id = id, Client = client, Attack = attack, Hit = hit, Death = death }); registration = id; return true; } internal void Unregister(int registration) { for (var index = 0; index < _contexts.Count; index++) if (_contexts[index].Id == registration) { _contexts.RemoveAt(index); return; } } protected override void OnUpdate() { if (_contexts.Count == 0) return; for (var contextIndex = 0; contextIndex < _contexts.Count; contextIndex++) { var context = _contexts[contextIndex]; var client = context.Client; if (client == null || !client.IsValid) continue; if (!client.TryGetReceipts(out var receipts)) continue; for (var index = 0; index < receipts.Length; index++) { var receipt = receipts[index]; if (receipt.Status != CharacterAnimationReceiptStatus.Accepted || !client.TryResolve(receipt.SpawnId, out var entity) || !EntityManager.HasComponent(entity)) continue; var intent = EntityManager.GetComponentData(entity); if (intent.DeathRequest.IsValid && client.Matches(receipt, receipt.SpawnId, intent.DeathRequest, context.Death, CharacterAnimationOperation.PlayNormalized)) { intent.Death = 0; intent.Hit = 0; intent.Attack = 0; } else if (intent.HitRequest.IsValid && client.Matches(receipt, receipt.SpawnId, intent.HitRequest, context.Hit, CharacterAnimationOperation.PlayNormalized)) { intent.Hit = 0; intent.Attack = 0; } else if (intent.AttackRequest.IsValid && client.Matches(receipt, receipt.SpawnId, intent.AttackRequest, context.Attack, CharacterAnimationOperation.PlayNormalized)) intent.Attack = 0; else continue; EntityManager.SetComponentData(entity, intent); } } } } /// Presentation-only event bridge. Damage and skill clocks never depend on it. [UpdateInGroup(typeof(CharacterEventSystemGroup))] public partial class NewcomerAnimationEventBridgeSystem : SystemBase { private sealed class Context { internal int Id; internal ResolvedCharacterBindings Bindings; internal ResolvedAnimationHandle Event; internal CharacterSpawnResult[] Results; } private readonly List _contexts = new List(); private int _nextContextId = 1; internal bool Register(CharacterPreparedAsset prepared, string eventKey, CharacterSpawnResult[] results, out int registration) { registration = 0; if (prepared == null || prepared.Bindings == null || results == null || _nextContextId <= 0 || !prepared.Bindings.TryResolveEvent(eventKey, out var eventHandle)) return false; var id = _nextContextId++; _contexts.Add(new Context { Id = id, Bindings = prepared.Bindings, Event = eventHandle, Results = results }); registration = id; return true; } internal void Unregister(int registration) { for (var index = 0; index < _contexts.Count; index++) if (_contexts[index].Id == registration) { _contexts.RemoveAt(index); return; } } protected override void OnUpdate() { for (var contextIndex = 0; contextIndex < _contexts.Count; contextIndex++) { var context = _contexts[contextIndex]; if (context.Bindings == null || !context.Bindings.IsValid || context.Results == null) continue; for (var resultIndex = 0; resultIndex < context.Results.Length; resultIndex++) { var entity = context.Results[resultIndex].Entity; if (!EntityManager.Exists(entity) || !EntityManager.HasComponent(entity)) continue; var cursor = EntityManager.GetComponentData(entity); if (!CharacterAnimationObservation.TryGetEventOccurrences(EntityManager, entity, context.Bindings, context.Event, out var events)) continue; for (var ordinal = 0; events.MoveNext(out var occurrence, out _); ordinal++) { if (occurrence.Generation < cursor.LastGeneration || occurrence.Generation == cursor.LastGeneration && ordinal <= cursor.LastOrdinal) continue; UnityEngine.Debug.Log("ECSAnimator sample event generation " + occurrence.Generation); cursor.LastGeneration = occurrence.Generation; cursor.LastOrdinal = ordinal; } EntityManager.SetComponentData(entity, cursor); } } } } }