// 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 nickmaltbie.OpenKCC.Animation;
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 FootTarget in edit mode.
///
[TestFixture]
public class FootTargetTests : TestBase
{
private const string AnimState = "AnimState";
private MockUnityService unityServiceMock;
private FootTarget leftFootTarget, rightFootTarget;
private Animator animator;
[SetUp]
public void SetUp()
{
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 = CreateGameObject().AddComponent();
animator.runtimeAnimatorController = controller;
leftFootTarget = new FootTarget(Foot.LeftFoot, animator, 0.05f, 0.25f, 0.15f);
rightFootTarget = new FootTarget(Foot.RightFoot, animator, 0.05f, 0.25f, 0.15f);
unityServiceMock = new MockUnityService();
leftFootTarget.unityService = unityServiceMock;
rightFootTarget.unityService = unityServiceMock;
animator.StartPlayback();
animator.Play(AnimState, 0);
}
[Test]
public void Validate_FootTarget_SmoothValue()
{
int total = 100;
// Check that the value is constantly increasing by some non zero value
// between 0 and 1
for (int sample = 0; sample < total; sample++)
{
float i = (float)sample / total;
float j = (float)(sample + 1) / total;
float f_i = FootTarget.SmoothValue(i);
float f_j = FootTarget.SmoothValue(j);
Assert.IsTrue(f_j > f_i);
}
}
[Test]
public void Verify_FootTarget_LerpedMovement()
{
// Set foot stride time to 1 second and time step to 0.1 seconds
var footTarget = new FootTarget(Foot.LeftFoot, animator, 0.25f, 1.0f, 0.1f);
unityServiceMock.deltaTime = 0.1f;
unityServiceMock.time = 0.0f;
footTarget.unityService = unityServiceMock;
// Assert that foot is released
footTarget.ReleaseFoot();
// Start a stride and check that foot will lock from position to stride position
GameObject floor = CreateGameObject();
footTarget.UpdateStrideTarget(Vector3.zero, Quaternion.identity, Vector3.up, true);
footTarget.StartStride(Vector3.forward, Quaternion.identity, floor, Vector3.forward, Vector3.up, false);
// Assert that the foot is grounded and now mid stride
Assert.IsTrue(footTarget.State == FootState.Grounded);
Assert.IsTrue(footTarget.MidStride);
float previousWeight = footTarget.FootIKWeight;
// It should take 10 updates to fully transition to finished position
// Assert that foot is moving towards target position now.
for (int i = 0; i < 10; i++)
{
unityServiceMock.time = i / 10.0f;
footTarget.LerpFootIKWeight();
// Just assert that the weight is increasing
Assert.IsTrue(previousWeight <= footTarget.FootIKWeight);
previousWeight = footTarget.FootIKWeight;
}
// After 10 updates, foot should be fully grounded and weight should be close to 1.0f
while (footTarget.MidStride)
{
footTarget.LerpFootIKWeight();
}
Assert.IsFalse(footTarget.MidStride);
unityServiceMock.time = 2.0f;
footTarget.LerpFootIKWeight();
TestUtils.AssertInBounds(footTarget.FootIKTargetPos(), new Vector3(0, 0.1f, 1.0f));
TestUtils.AssertInBounds(Quaternion.Angle(footTarget.FootIKTargetRot(), Quaternion.identity), 0, 1.0f);
TestUtils.AssertInBounds(footTarget.FootIKWeight, FootTarget.ThresholdIKWeightGrounded, 0.01f);
}
[Test]
public void Validate_FootTarget_UpdateStrideTarget()
{
// If updating with force, should snap right away.
leftFootTarget.UpdateStrideTarget(Vector3.forward, Quaternion.identity, Vector3.up, true);
Assert.AreEqual(Vector3.forward, leftFootTarget.TargetFootPosition);
Assert.AreEqual(Quaternion.identity, leftFootTarget.TargetFootRotation);
leftFootTarget.UpdateStrideTarget(Vector3.zero, Quaternion.identity, Vector3.up, true);
Assert.AreEqual(Vector3.zero, leftFootTarget.TargetFootPosition);
Assert.AreEqual(Quaternion.identity, leftFootTarget.TargetFootRotation);
// Measure distance and ensure it keeps dropping
float previousDelta = Vector3.Distance(leftFootTarget.TargetFootPosition, Vector3.forward);
for (int i = 0; i < 100; i++)
{
leftFootTarget.UpdateStrideTarget(Vector3.forward, Quaternion.identity, Vector3.up, false);
float newDelta = Vector3.Distance(leftFootTarget.TargetFootPosition, Vector3.forward);
Assert.IsTrue(newDelta <= previousDelta);
previousDelta = newDelta;
}
}
[Test]
public void Validate_FootTarget_StrideState()
{
GameObject floor = CreateGameObject();
unityServiceMock.deltaTime = 0.05f;
unityServiceMock.time = 0.0f;
// Foot should start off released and able to update stride target
Assert.AreEqual(FootState.Released, leftFootTarget.State);
Assert.IsTrue(leftFootTarget.CanUpdateStrideTarget());
Assert.AreEqual(0.0f, leftFootTarget.FootIKWeight);
// Start basic action to ground foot
leftFootTarget.StartStride(Vector3.forward, Quaternion.identity, floor, Vector3.forward, Vector3.up, false);
Assert.IsFalse(leftFootTarget.CanUpdateStrideTarget());
// Assert that the foot is now mid stride and remains mid stride until
// the foot ik weight drops below the threshold ik weight grounded
while (leftFootTarget.FootIKWeight >= FootTarget.ThresholdIKWeightGrounded)
{
Assert.IsTrue(leftFootTarget.MidStride);
Assert.IsFalse(leftFootTarget.CanUpdateStrideTarget());
leftFootTarget.LerpFootIKWeight();
unityServiceMock.time += 0.05f;
}
// Once the foot is above threshold, it should be fully grounded
while (leftFootTarget.MidStride)
{
leftFootTarget.LerpFootIKWeight();
unityServiceMock.time += 0.05f;
}
Assert.AreEqual(FootState.Grounded, leftFootTarget.State);
Assert.IsFalse(leftFootTarget.MidStride);
Assert.IsFalse(leftFootTarget.CanUpdateStrideTarget());
// Now take a small bump step and assert that we are mid
// stride as long as the stride time is greater than zero
Quaternion previousRotation = leftFootTarget.FootIKTargetRot();
leftFootTarget.StartStride(Vector3.left, Quaternion.LookRotation(Vector3.left, Vector3.up), floor, Vector3.left, Vector3.up, true);
while (leftFootTarget.RemainingStrideTime > 0)
{
bool expectedStrideUpdate = leftFootTarget.RemainingStrideTime >= leftFootTarget.StrideTime * FootTarget.ThresholdFractionStrideNoUpdate;
Assert.IsTrue(leftFootTarget.MidStride);
Assert.AreEqual(expectedStrideUpdate, leftFootTarget.CanUpdateStrideTarget());
leftFootTarget.LerpFootIKWeight();
unityServiceMock.time += 0.01f;
// Also... assert check our delta rotation
Assert.IsTrue(Quaternion.Angle(previousRotation, leftFootTarget.FootIKTargetRot()) >= 0);
previousRotation = leftFootTarget.FootIKTargetRot();
}
Assert.IsFalse(leftFootTarget.MidStride);
Assert.IsFalse(leftFootTarget.CanUpdateStrideTarget());
// Assert that we are close to our target rotation
Assert.IsTrue(Quaternion.Angle(Quaternion.LookRotation(Vector3.left, Vector3.up), leftFootTarget.FootIKTargetRot()) <= 5.0f);
// Finally release the foot and assert it's mid stride until
// foot IK Weight is below a specific value.
leftFootTarget.ReleaseFoot();
Assert.IsTrue(leftFootTarget.MidStride);
Assert.IsFalse(leftFootTarget.CanUpdateStrideTarget());
while (leftFootTarget.MidStride)
{
leftFootTarget.LerpFootIKWeight();
unityServiceMock.time += 0.01f;
if (leftFootTarget.FootIKWeight <= FootTarget.ThresholdIKWeightReleased)
{
Assert.IsTrue(leftFootTarget.CanUpdateStrideTarget());
}
}
}
[Test]
public void Validate_FootTarget_GetAnimationCurveValue()
{
for (float sample = 0; sample <= 1.0f; sample += 0.001f)
{
float rightValue = 1 - sample;
float leftValue = sample;
animator.SetFloat(FootTarget.RightFootIKWeight, rightValue);
animator.SetFloat(FootTarget.LeftFootIKWeight, leftValue);
Assert.AreEqual(rightValue, rightFootTarget.GetFootAnimationWeight());
Assert.AreEqual(leftValue, leftFootTarget.GetFootAnimationWeight());
// Also verify thresholds
Assert.AreEqual(leftValue >= FootTarget.ThresholdGroundedStateChange, leftFootTarget.OverGroundThreshold());
Assert.AreEqual(leftValue <= FootTarget.ThresholdGroundedStateChange, leftFootTarget.UnderReleaseThreshold());
Assert.AreEqual(rightValue >= FootTarget.ThresholdGroundedStateChange, rightFootTarget.OverGroundThreshold());
Assert.AreEqual(rightValue <= FootTarget.ThresholdGroundedStateChange, rightFootTarget.UnderReleaseThreshold());
}
}
}
}