// 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.CameraControls;
using nickmaltbie.OpenKCC.Character.Action;
using nickmaltbie.OpenKCC.Character.Attributes;
using nickmaltbie.OpenKCC.Character.Config;
using nickmaltbie.OpenKCC.Character.Events;
using nickmaltbie.OpenKCC.Utils;
using nickmaltbie.StateMachineUnity;
using nickmaltbie.StateMachineUnity.Attributes;
using nickmaltbie.StateMachineUnity.Event;
using nickmaltbie.StateMachineUnity.Fixed;
using UnityEngine;
using UnityEngine.InputSystem;
using static nickmaltbie.OpenKCC.Character.Animation.HumanoidKCCAnim;
namespace nickmaltbie.OpenKCC.Character
{
///
/// Have a character controller push any dynamic rigidbody it hits
///
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(KCCMovementEngine))]
[DefaultExecutionOrder(1000)]
public class KCCStateMachine : FixedSMAnim, IJumping
{
[Header("Input Controls")]
///
/// Action reference for moving the player.
///
[Tooltip("Action reference for moving the player")]
[SerializeField]
public InputActionReference moveActionReference;
///
/// Action reference for sprinting.
///
[Tooltip("Action reference for moving the player")]
[SerializeField]
public InputActionReference sprintActionReference;
///
/// Action reference for sprinting.
///
[Tooltip("Action reference for player jumping")]
[SerializeField]
public InputActionReference jumpActionReference;
[Header("Movement Settings")]
///
/// Speed of player movement when walking.
///
[Tooltip("Speed of player when walking")]
[SerializeField]
public float walkingSpeed = 7.5f;
///
/// Speed of player when sprinting.
///
[Tooltip("Speed of player when sprinting")]
[SerializeField]
public float sprintSpeed = 10.0f;
///
/// Velocity of player jump.
///
[Tooltip("Velocity of player jump.")]
[SerializeField]
public float jumpVelocity = 6.5f;
///
/// Action reference for jumping.
///
internal JumpAction jumpAction;
///
/// Camera controls associated with the player.
///
protected ICameraControls _cameraControls;
///
/// Movement engine for controlling the kinematic character controller.
///
protected KCCMovementEngine movementEngine;
///
/// Override move action for testing.
///
private InputAction overrideMoveAction;
///
/// Override move action for testing.
///
private InputAction overrideSprintAction;
///
/// Gets the move action associated with this kcc.
///
public InputAction MoveAction
{
get => overrideMoveAction ?? moveActionReference?.action;
set => overrideMoveAction = value;
}
///
/// Gets the move action associated with this kcc.
///
public InputAction SprintAction
{
get => overrideSprintAction ?? sprintActionReference?.action;
set => overrideSprintAction = value;
}
///
/// Current velocity of the player.
///
public Vector3 Velocity { get; protected set; }
///
/// Input movement from player input updated each frame.
///
public Vector3 InputMovement { get; private set; }
///
/// Get the camera controls associated with the state machine.
///
public ICameraControls CameraControls { get => _cameraControls; internal set => _cameraControls = value; }
///
/// Rotation of the plane the player is viewing
///
public Quaternion HorizPlaneView => CameraControls != null ?
CameraControls.PlayerHeading :
Quaternion.Euler(0, transform.eulerAngles.y, 0);
///
/// Idle state for KCC state machine, not moving.
///
[InitialState]
[Animation(IdleAnimState, 0.35f, true)]
[Transition(typeof(StartMoveInput), typeof(WalkingState))]
[Transition(typeof(SteepSlopeEvent), typeof(SlidingState))]
[Transition(typeof(LeaveGroundEvent), typeof(FallingState))]
[Transition(typeof(JumpEvent), typeof(JumpState))]
[MovementSettings]
public class IdleState : State { }
///
/// Jumping state for KCC state machine.
///
[Animation(JumpAnimState, 0.1f, true)]
[TransitionOnAnimationComplete(typeof(FallingState), 0.15f, true)]
[AnimationTransition(typeof(GroundedEvent), typeof(LandingState), 0.35f, true, 0.25f)]
[Transition(typeof(SteepSlopeEvent), typeof(SlidingState))]
[MovementSettings(SpeedConfig = nameof(walkingSpeed))]
public class JumpState : State { }
///
/// Landing state for KCC state machine.
///
[Animation(LandingAnimState, 0.1f, true)]
[TransitionOnAnimationComplete(typeof(IdleState), 0.25f, true)]
[AnimationTransition(typeof(StartMoveInput), typeof(WalkingState), 0.35f, true)]
[AnimationTransition(typeof(JumpEvent), typeof(JumpState), 0.35f, true)]
[Transition(typeof(LeaveGroundEvent), typeof(FallingState))]
[Transition(typeof(SteepSlopeEvent), typeof(SlidingState))]
[MovementSettings(SpeedConfig = nameof(walkingSpeed))]
public class LandingState : State { }
///
/// Walking state for KCC state machine when player is giving
/// some movement input.
///
[Animation(WalkingAnimState, 0.1f, true)]
[Transition(typeof(JumpEvent), typeof(JumpState))]
[Transition(typeof(StopMoveInput), typeof(IdleState))]
[Transition(typeof(SteepSlopeEvent), typeof(SlidingState))]
[Transition(typeof(LeaveGroundEvent), typeof(FallingState))]
[Transition(typeof(StartSprintEvent), typeof(SprintingState))]
[MovementSettings(SpeedConfig = nameof(walkingSpeed))]
public class WalkingState : State { }
///
/// Sprinting state for KCC state machine when player is giving
/// input and performing the sprint action.
///
[Animation(SprintingAnimState, 0.1f, true)]
[Transition(typeof(JumpEvent), typeof(JumpState))]
[Transition(typeof(StopMoveInput), typeof(IdleState))]
[Transition(typeof(SteepSlopeEvent), typeof(SlidingState))]
[Transition(typeof(LeaveGroundEvent), typeof(FallingState))]
[Transition(typeof(StopSprintEvent), typeof(WalkingState))]
[MovementSettings(SpeedConfig = nameof(sprintSpeed))]
public class SprintingState : State { }
///
/// Sliding state for KCC state machine for when the player
/// is moving along a sloped surface too step to walk up.
///
[Animation(SlidingAnimState, 0.35f, true, 0.1f)]
[Transition(typeof(JumpEvent), typeof(JumpState))]
[Transition(typeof(LeaveGroundEvent), typeof(FallingState))]
[AnimationTransition(typeof(GroundedEvent), typeof(LandingState), 0.35f, true, 0.25f)]
[MovementSettings(SpeedConfig = nameof(walkingSpeed))]
public class SlidingState : State { }
///
/// Falling state for KCC state machine when the player has no
/// ground below them.
///
[Animation(FallingAnimState, 0.35f, true)]
[Transition(typeof(JumpEvent), typeof(JumpState))]
[Transition(typeof(SteepSlopeEvent), typeof(SlidingState))]
[AnimationTransition(typeof(GroundedEvent), typeof(LandingState), 0.35f, true, 0.25f)]
[TransitionAfterTime(typeof(LongFallingState), 2.0f)]
[MovementSettings(SpeedConfig = nameof(walkingSpeed))]
public class FallingState : State { }
///
/// Long falling for playing animation when player has been falling
/// for a long period of time.
///
[Animation(LongFallingAnimState, 0.1f, true)]
[Transition(typeof(JumpEvent), typeof(JumpState))]
[Transition(typeof(SteepSlopeEvent), typeof(SlidingState))]
[AnimationTransition(typeof(GroundedEvent), typeof(LandingState), 0.35f, true, 1.0f)]
[MovementSettings(SpeedConfig = nameof(walkingSpeed))]
public class LongFallingState : State { }
///
/// Configure kcc state machine operations.
///
public override void Awake()
{
base.Awake();
jumpAction = new JumpAction()
{
jumpInput = new Input.BufferedInput()
{
inputActionReference = jumpActionReference,
cooldown = 0.25f,
bufferTime = 0.05f,
},
jumpVelocity = jumpVelocity,
maxJumpAngle = 85.0f,
jumpAngleWeightFactor = 0.0f,
};
GetComponent().isKinematic = true;
movementEngine = GetComponent();
_cameraControls = GetComponent();
SetupInputs();
}
///
/// Update the grounded state of the kinematic character controller.
///
public void UpdateGroundedState()
{
if (movementEngine.GroundedState.Falling)
{
RaiseEvent(LeaveGroundEvent.Instance);
}
else if (movementEngine.GroundedState.Sliding)
{
RaiseEvent(SteepSlopeEvent.Instance);
}
else if (movementEngine.GroundedState.StandingOnGround)
{
RaiseEvent(GroundedEvent.Instance);
}
}
///
/// Setup inputs for the KCC
///
public void SetupInputs()
{
jumpAction?.Setup(movementEngine.GroundedState, movementEngine, this);
MoveAction?.Enable();
}
///
public override void FixedUpdate()
{
GetComponent().isKinematic = true;
jumpAction.ApplyJumpIfPossible(movementEngine.GroundedState);
movementEngine.MovePlayer(
GetDesiredMovement() * unityService.fixedDeltaTime,
Velocity * unityService.fixedDeltaTime);
UpdateGroundedState();
// Apply gravity if needed
if (movementEngine.GroundedState.Falling || movementEngine.GroundedState.Sliding)
{
Velocity += Physics.gravity * unityService.fixedDeltaTime;
}
else if (movementEngine.GroundedState.StandingOnGround && !movementEngine.MovingUp(Velocity))
{
Velocity = Vector3.zero;
}
base.FixedUpdate();
}
///
public override void Update()
{
ReadPlayerMovement();
base.Update();
}
///
public void ApplyJump(Vector3 velocity)
{
Velocity = velocity + movementEngine.GetGroundVelocity() + GetDesiredMovement() / 2;
RaiseEvent(JumpEvent.Instance);
}
///
/// The the player's desired velocity for their current input value.
///
/// Vector of player velocity based on input movement rotated by player view
/// and projected onto the ground.
public Vector3 GetDesiredMovement()
{
Vector3 rotatedMovement = HorizPlaneView * InputMovement;
Vector3 projectedMovement = movementEngine.GetProjectedMovement(rotatedMovement);
float speed = MovementSettingsAttribute.GetSpeed(CurrentState, this);
Vector3 scaledMovement = projectedMovement * speed;
return scaledMovement;
}
///
/// Teleport player to a given position.
///
/// Position to teleport player to.
public void TeleportPlayer(Vector3 position)
{
movementEngine.TeleportPlayer(position);
}
///
/// Read the current player input values.
///
public void ReadPlayerMovement()
{
bool denyMovement = PlayerInputUtils.playerMovementState == PlayerInputState.Deny;
Vector2 moveVector = denyMovement ? Vector2.zero : MoveAction?.ReadValue() ?? Vector2.zero;
InputMovement = new Vector3(moveVector.x, 0, moveVector.y);
jumpAction.Update();
float moveX = AttachedAnimator.GetFloat("MoveX");
float moveY = AttachedAnimator.GetFloat("MoveY");
// Get the relative moveX and moveY to include
// the delta in rotation between the avatar's current heading
// and the desired world space input
Vector3 playerHeading = AttachedAnimator.transform.forward;
Vector3 movementDir = HorizPlaneView * InputMovement;
var relative = Quaternion.FromToRotation(playerHeading, movementDir);
Vector3 relativeMovement = relative * Vector3.forward;
moveX = Mathf.Lerp(moveX, relativeMovement.x, 4 * unityService.deltaTime);
moveY = Mathf.Lerp(moveY, relativeMovement.z, 4 * unityService.deltaTime);
AttachedAnimator.SetFloat("MoveX", moveX);
AttachedAnimator.SetFloat("MoveY", moveY);
bool moving = InputMovement.magnitude >= KCCUtils.Epsilon;
IEvent moveEvent = moving ? StartMoveInput.Instance as IEvent : StopMoveInput.Instance as IEvent;
RaiseEvent(moveEvent);
if (moving)
{
if (SprintAction?.IsPressed() ?? false)
{
RaiseEvent(StartSprintEvent.Instance);
}
else
{
RaiseEvent(StopSprintEvent.Instance);
}
}
}
}
}