// 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.
// So, this file seems to encounter some errors with validation
// specifically in unity 2020, will investigate later.
// TODO: Fix flaky behavior for unity version 2020.
# if UNITY_2021_1_OR_NEWER
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using nickmaltbie.openkcc.Tests.netcode.TestCommon;
using nickmaltbie.OpenKCC.Character;
using nickmaltbie.OpenKCC.Character.Action;
using nickmaltbie.OpenKCC.Character.Animation;
using nickmaltbie.OpenKCC.Environment.MovingGround;
using nickmaltbie.OpenKCC.Input;
using nickmaltbie.OpenKCC.netcode.Character;
using nickmaltbie.OpenKCC.netcode.Utils;
using nickmaltbie.OpenKCC.Utils.ColliderCast;
using nickmaltbie.TestUtilsUnity.Tests.TestCommon;
using NUnit.Framework;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
using UnityEngine.TestTools;
using static nickmaltbie.OpenKCC.netcode.Character.NetworkKCC;
namespace nickmaltbie.openkcc.Tests.netcode.Runtime.Character
{
public class MovingGroundConveyer : MonoBehaviour, IMovingGround
{
public Vector3 push = Vector3.forward;
public bool AvoidTransferMomentum()
{
return false;
}
public float GetMovementWeight(Vector3 point, Vector3 playerVelocity)
{
return 1.0f;
}
public float GetTransferMomentumWeight(Vector3 point, Vector3 playerVelocity)
{
return 1.0f;
}
public Vector3 GetVelocityAtPoint(Vector3 point)
{
return push;
}
public bool ShouldAttach()
{
return false;
}
}
///
/// Simple tests meant to be run in PlayMode
///
[TestFixture]
public class NetworkKCCTests : NetcodeInputRuntimeTest
{
public const string MoveControlName = "Move";
public const string JumpControlName = "Jump";
public const string SprintControlName = "Sprint";
protected override int NumberOfClients => 2;
protected override IEnumerable RequiredComponents => new[] { typeof(CapsuleColliderCast), typeof(ClientNetworkTransform) };
private GameObject floor;
[OneTimeSetUp]
public override void OneTimeSetup()
{
base.OneTimeSetup();
}
[OneTimeTearDown]
public override void OneTimeTearDown()
{
base.OneTimeTearDown();
}
[UnityTearDown]
public override IEnumerator UnityTearDown()
{
yield return base.UnityTearDown();
UnityEngine.Object.Destroy(floor);
}
[UnitySetUp]
public override IEnumerator UnitySetUp()
{
yield return base.UnitySetUp();
// Spawn a floor below the players.
floor = GameObject.CreatePrimitive(PrimitiveType.Cube);
floor.name = "floor";
floor.transform.localScale = new Vector3(100, 1, 100);
floor.transform.position -= Vector3.up * 0.65f;
}
public override void SetupClient(NetworkKCC e, int objectIdx, int clientIdx)
{
e.TeleportPlayer(Vector3.right * clientIdx * 2 + Vector3.up * 0.025f);
e.GetComponent().enabled = objectIdx == clientIdx;
}
///
/// Validation basic transitions between idle state, walking state,
/// back to idle, then to jumping state for a networked KCC
/// and verify that these states are propagated to other clients
/// as well.
///
/// Enumerator representing state of test.
[UnityTest]
public IEnumerator Validate_NetworkKCC_Move_Transition()
{
yield return SetupPlayersInIdleState();
for (int i = 0; i <= NumberOfClients; i++)
{
NetworkKCC kcc = GetAttachedNetworkBehaviour(i, i);
input.Set(GetTestableNetworkBehaviour(i, i).GetControl(MoveControlName), Vector2.up);
yield return TestUtils.WaitUntil(() => typeof(WalkingState) == kcc.CurrentState);
UnityEngine.Debug.Log("Validated WalkingState");
input.Set(GetTestableNetworkBehaviour(i, i).GetControl(SprintControlName), 1.0f);
yield return TestUtils.WaitUntil(() => typeof(SprintingState) == kcc.CurrentState);
UnityEngine.Debug.Log("Validated SprintingState");
input.Set(GetTestableNetworkBehaviour(i, i).GetControl(MoveControlName), Vector2.zero);
yield return TestUtils.WaitUntil(() => typeof(IdleState) == kcc.CurrentState);
UnityEngine.Debug.Log("Validated IdleState");
input.Set(GetTestableNetworkBehaviour(i, i).GetControl(JumpControlName), 1.0f);
yield return TestUtils.WaitUntil(() => typeof(JumpState) == kcc.CurrentState);
UnityEngine.Debug.Log("Validated JumpState");
}
}
///
/// Spawn a set of players on a "MovingGroundConveyer" and verify
/// that the players move with the MovingGroundConveyer's baked
/// velocity.
///
/// Assert that when the players jump, they maintain some velocity
/// in the direction of the MovingGroundConveyer's motion as well.
///
/// Enumerator representing state of test.
[UnityTest]
public IEnumerator Validate_NetworkKCC_MovingGround()
{
yield return SetupPlayersInIdleState();
// Attach a moving ground conveyer to the ground
MovingGroundConveyer conveyer = floor.AddComponent();
// Assert that the players are pushed forward in some direction of the moving
// ground push by one unit. First save their initial positions
var initialPos = Enumerable.Range(0, NumberOfClients + 1)
.Select(i => GetObject(i, i).transform.position)
.ToList();
// Wait until the delta between the player and their current position is at least
// one unit in the push direction.
yield return TestUtils.WaitUntil(() => ForAllPlayers(player =>
{
Vector3 initial = initialPos[(int)player.OwnerClientId];
Vector3 delta = player.transform.position - initial;
float alongLine = Vector3.Dot(Vector3.Project(delta, conveyer.push), conveyer.push.normalized);
return alongLine >= 1.0f;
}));
// Have the players jump and assert they retain some forward momentum
ForEachTestableOwner((player, _) => input.Set(player.GetControl(JumpControlName), 1.0f));
yield return TestUtils.WaitUntil(() =>
Enumerable.Range(0, NumberOfClients + 1).All(
i =>
{
NetworkKCC networkKCC = GetAttachedNetworkBehaviour(i, i);
return Vector3.Project(networkKCC.Velocity, conveyer.push.normalized).magnitude >= 1.0;
}));
}
///
/// Create a set of players and verify that they transition
/// to sliding state on the owner as well as client
/// machines. Achieve this by rotating the floor and setting
/// the max walk angle for the players to be less than that of
/// the floor rotation.
///
/// Enumerator representing state of test.
[UnityTest]
public IEnumerator Validate_NetworkKCC_Sliding()
{
SetupPlayersInIdleState();
floor.transform.position = Vector3.down * 3;
yield return new WaitForFixedUpdate();
floor.transform.rotation = Quaternion.Euler(10, 0, 0);
// Adjust each player's max walk angle to 5 degrees
ForAllPlayers(player =>
{
player.GetComponent().maxWalkAngle = 5;
return true;
});
// Wait until players are in sliding state
yield return TestUtils.WaitUntil(() => ForAllPlayers(player => typeof(SlidingState) == player.CurrentState));
}
protected IEnumerator SetupPlayersInIdleState()
{
ForEachOwner((player, i) => player.TeleportPlayer(Vector3.right * i * 2 + Vector3.up));
SetupInputs();
yield return TestUtils.WaitUntil(() => ForAllPlayers(player =>
{
UnityEngine.Debug.Log($"Checking current state: {player.CurrentState}");
return typeof(IdleState) == player.CurrentState || typeof(LandingState) == player.CurrentState;
}));
}
public override void SetupInputs(Gamepad gamepad, TestableNetworkBehaviour b, NetworkKCC networkKCC)
{
b.SetupControl(MoveControlName, gamepad.leftStick);
b.SetupControl(JumpControlName, gamepad.aButton);
b.SetupControl(SprintControlName, gamepad.bButton);
var jumpInputAction = new InputAction("jumpAction", InputActionType.Button, b.GetControl(JumpControlName).path);
var sprintInputAction = new InputAction("sprintAction", InputActionType.Value, b.GetControl(SprintControlName).path);
var moveInputAction = new InputAction("moveAction", InputActionType.Value, b.GetControl(MoveControlName).path);
// Enable inputs
jumpInputAction.Enable();
moveInputAction.Enable();
sprintInputAction.Enable();
// Configure jump action
var jumpInput = new BufferedInput();
jumpInput.InputAction = jumpInputAction;
jumpInput.cooldown = 1.0f;
jumpInput.bufferTime = 3.0f;
var jumpAction = new JumpAction();
jumpAction.jumpInput = jumpInput;
// Setup actions
networkKCC.jumpAction = jumpAction;
networkKCC.MoveAction = moveInputAction;
networkKCC.SprintAction = sprintInputAction;
networkKCC.SetupInputs();
}
public override void SetupPrefab(GameObject go)
{
// Setup the rigidbody
Rigidbody rb = go.GetComponent();
rb.useGravity = false;
rb.isKinematic = true;
// Setup animation controller.
Animator anim = go.AddComponent();
var controller = new AnimatorController();
controller.AddLayer("base");
AnimatorStateMachine rootStateMachine = controller.layers[0].stateMachine;
controller.AddParameter("MoveX", AnimatorControllerParameterType.Float);
controller.AddParameter("MoveY", AnimatorControllerParameterType.Float);
rootStateMachine.AddState(HumanoidKCCAnim.IdleAnimState);
rootStateMachine.AddState(HumanoidKCCAnim.JumpAnimState);
rootStateMachine.AddState(HumanoidKCCAnim.LandingAnimState);
rootStateMachine.AddState(HumanoidKCCAnim.WalkingAnimState);
rootStateMachine.AddState(HumanoidKCCAnim.SlidingAnimState);
rootStateMachine.AddState(HumanoidKCCAnim.FallingAnimState);
rootStateMachine.AddState(HumanoidKCCAnim.SprintingAnimState);
rootStateMachine.AddState(HumanoidKCCAnim.LongFallingAnimState);
anim.runtimeAnimatorController = controller;
// setup capsule collider
CapsuleCollider capsuleCollider = go.AddComponent();
capsuleCollider.center = new Vector3(0, 1, 0);
capsuleCollider.height = 2.0f;
capsuleCollider.radius = 0.5f;
}
}
}
#endif