// Copyright (C) 2023 Nicholas Maltbie // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and // associated documentation files (the "Software"), to deal in the Software without restriction, // including without limitation the rights to use, copy, modify, merge, publish, distribute, // sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System.Collections.Generic; using System.Linq; using nickmaltbie.OpenKCC.Animation; using nickmaltbie.OpenKCC.Tests.TestCommon; using nickmaltbie.OpenKCC.Utils; using nickmaltbie.TestUtilsUnity; using nickmaltbie.TestUtilsUnity.Tests.TestCommon; using NUnit.Framework; using UnityEditor.Animations; using UnityEngine; namespace nickmaltbie.OpenKCC.Tests.EditMode.Animation { /// /// Basic tests for in edit mode. /// [TestFixture] public class HumanoidFootIKTests : TestBase { /// /// Animation state for testing avatar. /// private const string AnimState = "AnimState"; /// /// Set of bones in basic humanoid avatar. /// private static readonly (HumanBodyBones, HumanBodyBones)[] RequiredBones = new[] { (HumanBodyBones.Hips, HumanBodyBones.Hips), (HumanBodyBones.Spine, HumanBodyBones.Hips), (HumanBodyBones.Chest, HumanBodyBones.Spine), (HumanBodyBones.UpperChest, HumanBodyBones.Chest), (HumanBodyBones.Neck, HumanBodyBones.UpperChest), (HumanBodyBones.Head, HumanBodyBones.Neck), (HumanBodyBones.LeftUpperLeg, HumanBodyBones.Hips), (HumanBodyBones.LeftLowerLeg, HumanBodyBones.LeftUpperLeg), (HumanBodyBones.LeftFoot, HumanBodyBones.LeftLowerLeg), (HumanBodyBones.LeftToes, HumanBodyBones.LeftFoot), (HumanBodyBones.RightUpperLeg, HumanBodyBones.Hips), (HumanBodyBones.RightLowerLeg, HumanBodyBones.RightUpperLeg), (HumanBodyBones.RightFoot, HumanBodyBones.RightLowerLeg), (HumanBodyBones.RightToes, HumanBodyBones.RightFoot), (HumanBodyBones.LeftShoulder, HumanBodyBones.UpperChest), (HumanBodyBones.LeftUpperArm, HumanBodyBones.LeftShoulder), (HumanBodyBones.LeftLowerArm, HumanBodyBones.LeftUpperArm), (HumanBodyBones.LeftHand, HumanBodyBones.LeftLowerArm), (HumanBodyBones.RightShoulder, HumanBodyBones.UpperChest), (HumanBodyBones.RightUpperArm, HumanBodyBones.RightShoulder), (HumanBodyBones.RightLowerArm, HumanBodyBones.RightUpperArm), (HumanBodyBones.RightHand, HumanBodyBones.RightLowerArm), }; private Dictionary avatarBones; private MockUnityService unityServiceMock; private MockRaycastHelper raycastHelperMock; private HumanoidFootIK footIK; private Animator animator; [SetUp] public void SetUp() { // Create game object for the avatar GameObject go = CreateGameObject(); go.name = "Avatar Base"; animator = go.AddComponent(); footIK = go.AddComponent(); // Setup animation controller. var controller = new AnimatorController(); controller.AddLayer("base"); AnimatorStateMachine rootStateMachine = controller.layers[0].stateMachine; controller.AddParameter(FootTarget.LeftFootIKWeight, AnimatorControllerParameterType.Float); controller.AddParameter(FootTarget.RightFootIKWeight, AnimatorControllerParameterType.Float); rootStateMachine.AddState(AnimState); animator.runtimeAnimatorController = controller; // Attach mocks to the object. unityServiceMock = new MockUnityService(); raycastHelperMock = new MockRaycastHelper(); footIK.unityService = unityServiceMock; footIK.raycastHelper = raycastHelperMock; footIK.LeftFootTarget.unityService = unityServiceMock; footIK.RightFootTarget.unityService = unityServiceMock; // setup the avatar for the player avatarBones = RequiredBones.Select(set => set.Item1).ToDictionary( bone => bone, bone => { GameObject created = CreateGameObject(); created.name = bone.ToString(); created.transform.SetParent(go.transform); return created.transform; }); // Set the hierarchy of bones foreach ((HumanBodyBones, HumanBodyBones) set in RequiredBones) { if (set.Item1 != set.Item2) { avatarBones[set.Item1].SetParent(avatarBones[set.Item2]); } } var description = new HumanDescription() { human = avatarBones.Values.Select(bone => new HumanBone { boneName = bone.name, humanName = bone.name }).ToArray(), skeleton = avatarBones.Values.Select(bone => new SkeletonBone { name = bone.name }).Append(new SkeletonBone { name = go.name }).ToArray(), upperArmTwist = 0.1f, lowerArmTwist = 0.1f, upperLegTwist = 0.1f, lowerLegTwist = 0.1f, armStretch = 0.1f, legStretch = 0.1f, feetSpacing = 0.1f, hasTranslationDoF = false, }; Avatar avatar = AvatarBuilder.BuildHumanAvatar(go, description); animator.avatar = avatar; // Configure objects footIK.Awake(); animator.StartPlayback(); animator.Play(AnimState, 0); } /// /// Test to verify constants for foot ik. /// [Test] public void Validate_HumanoidFootIK_Constants() { Assert.IsTrue(HumanoidFootIK.Feet.Contains(Foot.LeftFoot)); Assert.IsTrue(HumanoidFootIK.Feet.Contains(Foot.RightFoot)); } /// /// Validate that the /// will properly translate foot targets to simulate player standing on moving /// ground. /// [Test] public void Validate_HumanoidFootIK_UpdateFootPlacement() { footIK.correctForOverlap = false; GameObject ground = CreateGameObject(); ground.name = "ground"; BoxCollider box = ground.AddComponent(); GameObject ground2 = CreateGameObject(); ground2.name = "ground2"; BoxCollider box2 = ground2.AddComponent(); // Ground one of the two feet footIK.LeftFootTarget.ReleaseFoot(); footIK.RightFootTarget.StartStride(Vector3.forward, Quaternion.identity, ground, Vector3.forward, Vector3.up, false); Assert.AreEqual(FootState.Released, footIK.LeftFootTarget.State); Assert.AreEqual(FootState.Grounded, footIK.RightFootTarget.State); // Assert that the foot targets are expected values. Assert.AreEqual(Vector3.zero, footIK.LeftFootTarget.TargetFootPosition); Assert.AreEqual(Vector3.forward, footIK.RightFootTarget.TargetFootPosition); // Set the player as standing on something var mockHit = new MockRaycastHit() { normal = Vector3.up, collider = box, }; int calls = 0; raycastHelperMock.OnDoRaycastInDirection = (Vector3 pos, Vector3 dir, float dist, out IRaycastHit hit, int layerMask, QueryTriggerInteraction queryTriggerInteraction) => { calls++; mockHit.point = pos; hit = mockHit; return true; }; // Move player feet one unit to the right footIK.UpdateFeetPositions(Vector3.right); // Verify that a check for the right foot was performed Assert.IsTrue(calls >= 1); // After grounding the foot, trigger the update foot placement // This update foot placement should only be applied to the grounded foot (right foot) Assert.AreEqual(Vector3.zero, footIK.LeftFootTarget.TargetFootPosition); Assert.AreEqual(Vector3.forward + Vector3.right, footIK.RightFootTarget.TargetFootPosition); Assert.AreEqual(FootState.Released, footIK.LeftFootTarget.State); Assert.AreEqual(FootState.Grounded, footIK.RightFootTarget.State); // Trigger another update, but have the player's moved foot be on a new ground mockHit.collider = box2; footIK.UpdateFeetPositions(Vector3.right); Assert.AreEqual(Vector3.zero, footIK.LeftFootTarget.TargetFootPosition); Assert.AreEqual(Vector3.forward + Vector3.right + Vector3.right, footIK.RightFootTarget.TargetFootPosition); Assert.AreEqual(FootState.Released, footIK.LeftFootTarget.State); Assert.AreEqual(FootState.Grounded, footIK.RightFootTarget.State); // Trigger another update, but have the player's moved foot be no longer supported raycastHelperMock.OnDoRaycastInDirection = (Vector3 pos, Vector3 dir, float dist, out IRaycastHit hit, int layerMask, QueryTriggerInteraction queryTriggerInteraction) => { hit = default; return false; }; footIK.UpdateFeetPositions(Vector3.forward); Assert.AreEqual(FootState.Released, footIK.LeftFootTarget.State); Assert.AreEqual(FootState.Released, footIK.RightFootTarget.State); // Also do a zero delta to verify skip footIK.UpdateFeetPositions(Vector3.zero); Assert.AreEqual(FootState.Released, footIK.LeftFootTarget.State); Assert.AreEqual(FootState.Released, footIK.RightFootTarget.State); } /// /// Validate that the feet positions use the overlap position when updating position not /// the animator position. /// [Test] public void Validate_HumanoidFootIK_UpdateFootPositionWhenOverlap() { footIK.RightFootTarget.StartStride(Vector3.forward, Quaternion.identity, CreateGameObject(), Vector3.forward, Vector3.up, false); var overlapHit = new MockRaycastHit() { normal = Vector3.up, point = Vector3.forward + Vector3.up }; var otherHit = new MockRaycastHit() { normal = Vector3.up, point = Vector3.forward + Vector3.up }; int calls = 0; raycastHelperMock.OnDoRaycastInDirection = (Vector3 pos, Vector3 dir, float dist, out IRaycastHit hit, int layerMask, QueryTriggerInteraction queryTriggerInteraction) => { if (calls == 0) { hit = overlapHit; calls++; return true; } hit = otherHit; calls++; return true; }; footIK.UpdateFootPosition(Foot.RightFoot, Vector3.forward); Assert.IsTrue(calls >= 2); } /// /// Validate that the /// will trigger updates for both feet. When triggering update, if one /// foot is grounded and the other is released. if the threshold value is exceeded, /// they should change states /// [Test] public void Validate_HumanoidFootIK_BasicUpdateChangeState() { // Set one foot as grounded, one as released footIK.LeftFootTarget.ReleaseFoot(); footIK.RightFootTarget.StartStride(Vector3.forward, Quaternion.identity, CreateGameObject(), Vector3.forward, Vector3.up, false); Assert.AreEqual(FootState.Released, footIK.LeftFootTarget.State); Assert.AreEqual(FootState.Grounded, footIK.RightFootTarget.State); unityServiceMock.deltaTime = 0.1f; // Lerp the weights until they are within expected values for (int i = 0; i < 100 && footIK.RightFootTarget.FootIKWeight <= 0.95f; i++) { UnityEngine.Debug.Log(footIK.RightFootTarget.FootIKWeight); footIK.RightFootTarget.LerpFootIKWeight(); } UnityEngine.Debug.Log(footIK.RightFootTarget.FootIKWeight); Assert.IsTrue(footIK.RightFootTarget.FootIKWeight >= 0.95f); // Flip the foot thresholds animator.SetFloat(FootTarget.LeftFootIKWeight, 1.0f); animator.SetFloat(FootTarget.RightFootIKWeight, 0.0f); // Set the player as standing on something var mockHit = new MockRaycastHit() { normal = Vector3.up }; int calls = 0; raycastHelperMock.OnDoRaycastInDirection = (Vector3 pos, Vector3 dir, float dist, out IRaycastHit hit, int layerMask, QueryTriggerInteraction queryTriggerInteraction) => { hit = mockHit; calls++; return true; }; // Trigger the update feet method footIK.OnAnimatorIK(0); // Verify that a check for the feet were performed Assert.IsTrue(calls >= 1); // Feet should flip states Assert.AreEqual(FootState.Grounded, footIK.LeftFootTarget.State); Assert.AreEqual(FootState.Released, footIK.RightFootTarget.State); } /// /// Validate that the /// will trigger updates for both feet. When triggering update, if /// a foot is grounded but desired position is too far away, /// foot should be released. /// [Test] public void Validate_HumanoidFootIK_BasicUpdateReleaseStepWhenGrounded() { // Set one foot as grounded, one as released footIK.LeftFootTarget.StartStride(Vector3.zero, Quaternion.identity, CreateGameObject(), Vector3.forward, Vector3.up, false); footIK.RightFootTarget.StartStride(Vector3.zero, Quaternion.identity, CreateGameObject(), Vector3.forward, Vector3.up, false); Assert.AreEqual(FootState.Grounded, footIK.LeftFootTarget.State); Assert.AreEqual(FootState.Grounded, footIK.RightFootTarget.State); unityServiceMock.deltaTime = 0.1f; // Lerp the weights until they are within expected values for (int i = 0; i < 100 && (footIK.RightFootTarget.FootIKWeight <= 0.95f || footIK.LeftFootTarget.FootIKWeight <= 0.95f); i++) { footIK.RightFootTarget.LerpFootIKWeight(); footIK.LeftFootTarget.LerpFootIKWeight(); } Assert.IsTrue(footIK.RightFootTarget.FootIKWeight >= 0.95f); Assert.IsTrue(footIK.RightFootTarget.FootIKWeight >= 0.95f); // Set so both feet are no longer mid stride unityServiceMock.time = 100.0f; Assert.IsFalse(footIK.RightFootTarget.MidStride); Assert.IsFalse(footIK.RightFootTarget.MidStride); // set booth feet as remain grounded animator.SetFloat(FootTarget.LeftFootIKWeight, 1.0f); animator.SetFloat(FootTarget.RightFootIKWeight, 1.0f); // Set the player as standing on something // For the left foot, have the foot be too far away and trigger // the foot to release // move hips forward, like a lot forward avatarBones[HumanBodyBones.Hips].transform.position += Vector3.forward * 100; var mockHit = new MockRaycastHit { normal = Vector3.up }; raycastHelperMock.OnDoRaycastInDirection = (Vector3 pos, Vector3 dir, float dist, out IRaycastHit hit, int layerMask, QueryTriggerInteraction queryTriggerInteraction) => { mockHit.point = pos; hit = mockHit; return true; }; // when manually updating the feet, feet should be released because // it's way too far form original position footIK.OnAnimatorIK(0); Assert.AreEqual(FootState.Released, footIK.LeftFootTarget.State); Assert.AreEqual(FootState.Released, footIK.RightFootTarget.State); } /// /// Validate that the /// will trigger updates for both feet. When triggering update, if /// a foot is grounded but desired rotation is beyond the current rotation. /// [Test] public void Validate_HumanoidFootIK_BasicUpdateBumpStepWhenGrounded() { // Set one foot as grounded, one as released footIK.LeftFootTarget.StartStride(Vector3.zero, Quaternion.identity, CreateGameObject(), Vector3.forward, Vector3.up, false); footIK.RightFootTarget.StartStride(Vector3.zero, Quaternion.identity, CreateGameObject(), Vector3.forward, Vector3.up, false); Assert.AreEqual(FootState.Grounded, footIK.LeftFootTarget.State); Assert.AreEqual(FootState.Grounded, footIK.RightFootTarget.State); unityServiceMock.deltaTime = 0.1f; // Lerp the weights until they are within expected values for (int i = 0; i < 100 && (footIK.RightFootTarget.FootIKWeight <= 0.95f || footIK.LeftFootTarget.FootIKWeight <= 0.95f); i++) { footIK.RightFootTarget.LerpFootIKWeight(); footIK.LeftFootTarget.LerpFootIKWeight(); } Assert.IsTrue(footIK.RightFootTarget.FootIKWeight >= 0.95f); Assert.IsTrue(footIK.LeftFootTarget.FootIKWeight >= 0.95f); // Set so both feet are no longer mid stride unityServiceMock.time = 100.0f; Assert.IsFalse(footIK.RightFootTarget.MidStride); Assert.IsFalse(footIK.LeftFootTarget.MidStride); // set booth feet as remain grounded animator.SetFloat(FootTarget.LeftFootIKWeight, 1.0f); animator.SetFloat(FootTarget.RightFootIKWeight, 1.0f); // Set the player as standing on something // move hips rotated around completely. avatarBones[HumanBodyBones.Hips].transform.rotation *= Quaternion.Euler(0, 180, 0); var mockHit = new MockRaycastHit() { normal = Vector3.up }; raycastHelperMock.OnDoRaycastInDirection = (Vector3 pos, Vector3 dir, float dist, out IRaycastHit hit, int layerMask, QueryTriggerInteraction queryTriggerInteraction) => { mockHit.point = pos; hit = mockHit; return true; }; // when manually updating the feet, feet should be updated to new position // and rotation footIK.OnAnimatorIK(0); // Only of of the feet should lift because we don't want to hop in place Assert.AreEqual(FootState.Grounded, footIK.LeftFootTarget.State); Assert.AreEqual(FootState.Grounded, footIK.RightFootTarget.State); Assert.IsTrue(footIK.LeftFootTarget.MidStride ^ footIK.RightFootTarget.MidStride); Assert.IsTrue(footIK.LeftFootTarget.UseBump ^ footIK.RightFootTarget.UseBump); Assert.IsFalse(footIK.CanTakeStride); } /// /// Validate that the /// will trigger update the stride target until the foot is placed. /// [Test] public void Validate_HumanoidFootIK_UpdateTargetMidStride() { // Set one foot as grounded, one as released footIK.RightFootTarget.StartStride(Vector3.zero, Quaternion.identity, CreateGameObject(), Vector3.forward, Vector3.up, true); Assert.AreEqual(FootState.Released, footIK.LeftFootTarget.State); Assert.AreEqual(FootState.Grounded, footIK.RightFootTarget.State); unityServiceMock.deltaTime = 0.05f; // Lerp the weights until they are within expected values for (int i = 0; i < 100 && footIK.RightFootTarget.FootIKWeight <= 0.05f; i++) { footIK.RightFootTarget.LerpFootIKWeight(); } Assert.IsTrue(footIK.RightFootTarget.FootIKWeight >= 0.05f); // Right foot should still be mid stride Assert.IsTrue(footIK.RightFootTarget.MidStride); Assert.IsTrue(footIK.RightFootTarget.UseBump); // set foot as remain grounded animator.SetFloat(FootTarget.RightFootIKWeight, 1.0f); // Set the player as standing on something // move hips rotated around completely. avatarBones[HumanBodyBones.Hips].transform.position = Vector3.forward; var mockHit = new MockRaycastHit() { normal = Vector3.up, }; raycastHelperMock.OnDoRaycastInDirection = (Vector3 pos, Vector3 dir, float dist, out IRaycastHit hit, int layerMask, QueryTriggerInteraction queryTriggerInteraction) => { mockHit.point = pos + Vector3.forward * 0.01f; hit = mockHit; return true; }; // when manually updating the feet, feet should be moved slightly forward footIK.OnAnimatorIK(0); footIK.OnAnimatorIK(0); footIK.OnAnimatorIK(0); Assert.IsTrue(Vector3.Dot(footIK.RightFootTarget.TargetFootPosition, Vector3.forward) > 0.05f); Assert.AreEqual(FootState.Grounded, footIK.RightFootTarget.State); } /// /// Validate that the /// will trigger updates for both feet. When triggering update, if one /// foot is grounded and the other is released. As long as teh /// feet remain within their threshold, they should not have any /// change in state. /// /// The right foot target should also attempt to check /// if it's still grounded calling the /// /// at least once. /// [Test] public void Validate_HumanoidFootIK_BasicUpdateMaintainState() { // Set one foot as grounded, one as released footIK.LeftFootTarget.ReleaseFoot(); footIK.RightFootTarget.StartStride(Vector3.forward, Quaternion.identity, CreateGameObject(), Vector3.forward, Vector3.up, false); Assert.AreEqual(FootState.Released, footIK.LeftFootTarget.State); Assert.AreEqual(FootState.Grounded, footIK.RightFootTarget.State); // Set the right foot as over grounded threshold and left foot as under animator.SetFloat(FootTarget.LeftFootIKWeight, 0.0f); animator.SetFloat(FootTarget.RightFootIKWeight, 1.0f); // Set the player as standing on something var mockHit = new MockRaycastHit { normal = Vector3.up, }; int calls = 0; raycastHelperMock.OnDoRaycastInDirection = (Vector3 pos, Vector3 dir, float dist, out IRaycastHit hit, int layerMask, QueryTriggerInteraction queryTriggerInteraction) => { hit = mockHit; calls++; return true; }; // Trigger the update feet method footIK.OnAnimatorIK(0); // Verify that a check for the right foot was performed Assert.IsTrue(calls >= 1); // Feet should remain in same state Assert.AreEqual(FootState.Released, footIK.LeftFootTarget.State); Assert.AreEqual(FootState.Grounded, footIK.RightFootTarget.State); } /// /// Validate the when the configuration values for the /// /// are updated and /// is performed (such as when a change is detected by the unity editor), /// that these new values are properly synchronized /// with the and /// /// of the /// for each property modified. /// [Test] public void Validate_HumanoidFootIK_ConfigUpdates() { Assert.AreEqual(footIK.stepHeight, footIK.LeftFootTarget.StrideHeight); Assert.AreEqual(footIK.stepHeight, footIK.RightFootTarget.StrideHeight); Assert.AreEqual(footIK.strideTime, footIK.LeftFootTarget.StrideTime); Assert.AreEqual(footIK.strideTime, footIK.RightFootTarget.StrideTime); Assert.AreEqual(footIK.footGroundedHeight, footIK.LeftFootTarget.FootGroundedHeight); Assert.AreEqual(footIK.footGroundedHeight, footIK.RightFootTarget.FootGroundedHeight); footIK.stepHeight *= 2; footIK.strideTime *= 2; footIK.footGroundedHeight *= 2; footIK.OnValidate(); Assert.AreEqual(footIK.stepHeight, footIK.LeftFootTarget.StrideHeight); Assert.AreEqual(footIK.stepHeight, footIK.RightFootTarget.StrideHeight); Assert.AreEqual(footIK.strideTime, footIK.LeftFootTarget.StrideTime); Assert.AreEqual(footIK.strideTime, footIK.RightFootTarget.StrideTime); Assert.AreEqual(footIK.footGroundedHeight, footIK.LeftFootTarget.FootGroundedHeight); Assert.AreEqual(footIK.footGroundedHeight, footIK.RightFootTarget.FootGroundedHeight); } /// /// Validate that the /// returns the expected foot target, either right or left (or null if improperly specified). /// [Test] public void Validate_HumanoidFootIK_GetFootTargets() { Assert.AreEqual(footIK.LeftFootTarget, footIK.GetFootTarget(Foot.LeftFoot)); Assert.AreEqual(footIK.RightFootTarget, footIK.GetFootTarget(Foot.RightFoot)); Assert.IsNull(footIK.GetFootTarget(Foot.Unspecified)); } /// /// Tests to ensure that Validate that the /// performs as expected. /// /// Should left foot be grounded. /// Should right foot be grounded. [Test] public void Validate_HumanoidFootIK_GetTargetHipOffset([Values] bool leftGrounded, [Values] bool rightGrounded) { GameObject floor = CreateGameObject(); unityServiceMock.deltaTime = 1.0f; // Setup each foot in expected grounded state. if (leftGrounded) { footIK.LeftFootTarget.StartStride(Vector3.down * 1.25f, Quaternion.identity, floor, Vector3.forward, Vector3.up, false); for (int i = 0; i < 10; i++) { footIK.LeftFootTarget.LerpFootIKWeight(); } } else { footIK.LeftFootTarget.ReleaseFoot(); } if (rightGrounded) { footIK.RightFootTarget.StartStride(Vector3.down * 1.45f, Quaternion.identity, floor, Vector3.forward, Vector3.up, false); for (int i = 0; i < 10; i++) { footIK.RightFootTarget.LerpFootIKWeight(); } } else { footIK.RightFootTarget.ReleaseFoot(); } unityServiceMock.time = 100.0f; avatarBones[HumanBodyBones.RightFoot].transform.position = Vector3.zero; avatarBones[HumanBodyBones.LeftFoot].transform.position = Vector3.zero; // When getting hip offset, should be some large value if either left foot // or right foot is not grounded float offset = footIK.GetTargetHipOffset(); if (leftGrounded && rightGrounded) { TestUtils.AssertInBounds(-0.55f, offset); } else if (leftGrounded && !rightGrounded) { TestUtils.AssertInBounds(-0.35f, offset); } else if (!leftGrounded && rightGrounded) { TestUtils.AssertInBounds(-0.55f, offset); } else { Assert.AreEqual(0, offset); } } /// /// Validate that the /// properly invokes the /// for the mocked delta time values. /// [Test] public void Validate_HumanoidFootIK_LerpUpdates() { unityServiceMock.deltaTime = 0.01f; footIK.OnValidate(); // Ground the left foot footIK.LeftFootTarget.StartStride(Vector3.forward, Quaternion.identity, CreateGameObject(), Vector3.forward, Vector3.up, false); float previousLeftFootWeight = footIK.LeftFootTarget.FootIKWeight; footIK.Update(); // Assert that the footIK will lerp as part of the update for (int i = 0; i < 10; i++) { footIK.Update(); float leftFootWeight = footIK.LeftFootTarget.FootIKWeight; // Assert that the right foot is unaffected // and the left foot weight is increasing. Assert.AreEqual(0, footIK.RightFootTarget.FootIKWeight); Assert.IsTrue(leftFootWeight >= previousLeftFootWeight); previousLeftFootWeight = leftFootWeight; } } } }