using System.Collections; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; using Unity.Netcode; using Unity.Netcode.Transports.UTP; using Colosseum.Player; namespace Colosseum.Tests { /// /// 피격 반응 애니메이션의 속도 배율 반영을 검증하는 PlayMode 테스트입니다. /// public class HitReactionAnimationSpeedTests { private const string AnimatorControllerResourcePath = "AC_Player_Default_PlayModeTest"; private const string HitStateName = "Hit"; private const string HitSpeedMultiplierParam = "HitSpeedMultiplier"; private const float AnimationSampleWindow = 0.08f; private GameObject networkManagerObject; private NetworkManager networkManager; private GameObject playerObject; private HitReactionController hitReactionController; private Animator animator; [UnitySetUp] public IEnumerator SetUp() { CleanupTestObjects(); networkManagerObject = new GameObject("TestNetworkManager"); networkManager = networkManagerObject.AddComponent(); UnityTransport transport = networkManagerObject.AddComponent(); transport.SetConnectionData("127.0.0.1", AllocateTestPort()); networkManager.NetworkConfig = new NetworkConfig { NetworkTransport = transport, }; Assert.IsTrue(networkManager.StartHost(), "테스트용 호스트 시작에 실패했습니다."); yield return null; RuntimeAnimatorController controller = Resources.Load(AnimatorControllerResourcePath); Assert.NotNull(controller, $"테스트용 AnimatorController를 찾을 수 없습니다: Resources/{AnimatorControllerResourcePath}"); playerObject = new GameObject("HitReactionTestPlayer"); playerObject.SetActive(false); playerObject.transform.position = Vector3.zero; playerObject.AddComponent(); hitReactionController = playerObject.AddComponent(); GameObject visualObject = new GameObject("Visual"); visualObject.transform.SetParent(playerObject.transform, false); animator = visualObject.AddComponent(); animator.runtimeAnimatorController = controller; animator.applyRootMotion = false; playerObject.SetActive(true); yield return null; NetworkObject networkObject = playerObject.GetComponent(); networkObject.SpawnWithOwnership(networkManager.LocalClientId); yield return null; Assert.NotNull(hitReactionController, "HitReactionController를 생성하지 못했습니다."); Assert.NotNull(animator, "테스트용 Animator를 생성하지 못했습니다."); } [UnityTearDown] public IEnumerator TearDown() { if (networkManager != null && networkManager.IsListening) { networkManager.Shutdown(); } yield return null; CleanupTestObjects(); yield return null; } [UnityTest] public IEnumerator ApplyStagger_ReusesHitAnimationAndReflectsSpeedMultiplier() { float slowDelta = 0f; yield return MeasureHitPlaybackDelta( () => hitReactionController.ApplyStagger(0.4f, true, 0.5f), 0.5f, value => slowDelta = value); float fastDelta = 0f; yield return MeasureHitPlaybackDelta( () => hitReactionController.ApplyStagger(0.4f, true, 2f), 2f, value => fastDelta = value); Assert.Greater(slowDelta, 0.01f, "느린 경직 재생에서 normalizedTime이 전진하지 않았습니다."); Assert.Greater(fastDelta, slowDelta * 1.75f, $"빠른 경직 재생 전진량이 충분하지 않습니다. slow={slowDelta:F3}, fast={fastDelta:F3}"); } [UnityTest] public IEnumerator ApplyKnockback_AlsoAppliesHitAnimationSpeedMultiplier() { float measuredDelta = 0f; yield return MeasureHitPlaybackDelta( () => hitReactionController.ApplyKnockback(Vector3.back * 3f, 0.25f, true, 1.6f), 1.6f, value => measuredDelta = value); Assert.Greater(measuredDelta, 0.01f, "넉백에서 Hit 애니메이션이 재생되지 않았습니다."); } private IEnumerator MeasureHitPlaybackDelta(System.Action applyReaction, float expectedSpeedMultiplier, System.Action setMeasuredDelta) { hitReactionController.ClearHitReactionState(); animator.Rebind(); animator.Update(0f); yield return null; applyReaction.Invoke(); yield return WaitForHitState(); float actualMultiplier = animator.GetFloat(HitSpeedMultiplierParam); Assert.AreEqual(expectedSpeedMultiplier, actualMultiplier, 0.01f, "Hit 속도 배율 파라미터가 기대값과 다릅니다."); float startNormalizedTime = animator.GetCurrentAnimatorStateInfo(0).normalizedTime; yield return new WaitForSeconds(AnimationSampleWindow); float endNormalizedTime = animator.GetCurrentAnimatorStateInfo(0).normalizedTime; setMeasuredDelta(endNormalizedTime - startNormalizedTime); yield return null; } private IEnumerator WaitForHitState() { int hitStateHash = Animator.StringToHash(HitStateName); for (int frame = 0; frame < 120; frame++) { AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0); if (!animator.IsInTransition(0) && stateInfo.shortNameHash == hitStateHash) { yield break; } yield return null; } Assert.Fail("Hit 상태 진입을 시간 내에 확인하지 못했습니다."); } private static ushort AllocateTestPort() { return (ushort)Random.Range(20000, 40000); } private void CleanupTestObjects() { if (playerObject != null) { Object.DestroyImmediate(playerObject); playerObject = null; } foreach (Camera camera in Object.FindObjectsByType(FindObjectsSortMode.None)) { if (camera != null && camera.gameObject.name == "PlayerCamera") { Object.DestroyImmediate(camera.gameObject); } } if (networkManagerObject != null) { Object.DestroyImmediate(networkManagerObject); networkManagerObject = null; networkManager = null; } } } }