// 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;
using nickmaltbie.OpenKCC.CameraControls;
using nickmaltbie.OpenKCC.Character;
using nickmaltbie.OpenKCC.Character.Action;
using nickmaltbie.OpenKCC.Character.Attributes;
using nickmaltbie.OpenKCC.Character.Config;
using nickmaltbie.OpenKCC.Character.Events;
using nickmaltbie.OpenKCC.netcode.Utils;
using nickmaltbie.OpenKCC.Utils;
using nickmaltbie.OpenKCC.Utils.ColliderCast;
using nickmaltbie.StateMachineUnity;
using nickmaltbie.StateMachineUnity.Attributes;
using nickmaltbie.StateMachineUnity.Event;
using nickmaltbie.StateMachineUnity.netcode;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.InputSystem;
namespace nickmaltbie.OpenKCC.MoleKCCSample
{
public class AvatarOffsetAttribute : Attribute
{
public string OffsetValue;
}
public class DiggingParticlesEnabled : Attribute { }
///
/// Have a character controller push any dynamic rigidbody it hits
///
[RequireComponent(typeof(MoleParticles))]
[RequireComponent(typeof(MoleMovementEngine))]
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(SphereColliderCast))]
[DefaultExecutionOrder(1000)]
public class MoleCharacter : NetworkSMAnim, IJumping
{
[SerializeField]
public Vector3 avatarDiggingOffset = new Vector3(0, 0, 0);
[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 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;
///
/// Velocity of player jump.
///
[Tooltip("Velocity of player jump.")]
[SerializeField]
public float jumpVelocity = 6.5f;
[Tooltip("Threshold angle for deciding between up and down")]
[SerializeField]
[Range(0, 90)]
public float upFlipThreshold = 80.0f;
private bool previousFacingUp = true;
///
/// Action reference for jumping.
///
internal JumpAction jumpAction;
///
/// Override move action for testing.
///
private InputAction overrideMoveAction;
///
/// Gets the move action associated with this kcc.
///
public InputAction MoveAction
{
get => overrideMoveAction ?? moveActionReference?.action;
set => overrideMoveAction = value;
}
private NetworkVariable relativeUp = new NetworkVariable(
Vector3.up,
readPerm: NetworkVariableReadPermission.Everyone,
writePerm: NetworkVariableWritePermission.Owner);
private Vector3 defaultOffset;
///
/// Time in which the player has been falling.
///
public float FallingTime { get; private set; }
///
/// Player velocity in world space.
///
public Vector3 Velocity { get; protected set; }
///
/// Input movement from player input updated each frame.
///
public Vector3 InputMovement { get; private set; }
///
/// Movement engine for controlling the kinematic character controller.
///
protected MoleMovementEngine movementEngine;
///
/// Relative parent configuration for following the ground.
///
public RelativeParentConfig relativeParentConfig { get; protected set; } = new RelativeParentConfig();
///
/// Position from previous frame for rotating mole.
///
private Vector3 previousPosition;
[InitialState]
[Animation("Idle")]
[Transition(typeof(StartMoveInput), typeof(WalkingState))]
[Transition(typeof(LeaveGroundEvent), typeof(FallingState))]
[Transition(typeof(JumpEvent), typeof(JumpState))]
public class IdleState : State { }
[Animation("Jumping")]
[TransitionOnAnimationComplete(typeof(FallingState), 0.15f, true)]
[Transition(typeof(GroundedEvent), typeof(IdleState))]
[MovementSettings(SpeedConfig = nameof(walkingSpeed))]
public class JumpState : State { }
[Animation("Digging")]
[DiggingParticlesEnabled]
[AvatarOffsetAttribute(OffsetValue = nameof(avatarDiggingOffset))]
[Transition(typeof(JumpEvent), typeof(JumpState))]
[Transition(typeof(StopMoveInput), typeof(IdleState))]
[Transition(typeof(LeaveGroundEvent), typeof(FallingState))]
[MovementSettings(SpeedConfig = nameof(walkingSpeed))]
public class WalkingState : State { }
[Animation("Walking")]
[Transition(typeof(JumpEvent), typeof(JumpState))]
[Transition(typeof(GroundedEvent), typeof(IdleState))]
[MovementSettings(SpeedConfig = nameof(walkingSpeed))]
public class FallingState : State { }
///
/// Update the grounded state of the kinematic character controller.
///
public void UpdateGroundedState()
{
if (movementEngine.GroundedState.Falling)
{
RaiseEvent(LeaveGroundEvent.Instance);
}
else if (movementEngine.GroundedState.StandingOnGround)
{
RaiseEvent(GroundedEvent.Instance);
}
}
///
public override void Awake()
{
base.Awake();
jumpAction = new JumpAction()
{
jumpInput = new Input.BufferedInput()
{
inputActionReference = jumpActionReference,
cooldown = 0.25f,
bufferTime = 0.05f,
},
jumpVelocity = jumpVelocity,
maxJumpAngle = 180.0f,
jumpAngleWeightFactor = 0.5f,
};
}
///
/// Configure kcc state machine operations.
///
public override void Start()
{
base.Start();
defaultOffset = AttachedAnimator.transform.localPosition;
GetComponent().isKinematic = true;
movementEngine = GetComponent();
SetupInputs();
}
///
/// Setup inputs for the KCC
///
public void SetupInputs()
{
if (IsOwner)
{
jumpAction?.Setup(movementEngine.GroundedState, movementEngine, this);
MoveAction?.Enable();
}
}
public override void LateUpdate()
{
relativeParentConfig.FollowGround(transform);
transform.position += movementEngine.ColliderCast.PushOutOverlapping(transform.position, transform.rotation, movementEngine.MaxPushSpeed * unityService.deltaTime);
base.LateUpdate();
}
///
/// 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()
{
// If the player is not on the ground, simply have them move
// along the plane perpendicular to gravity.
Vector3 planeNormal = movementEngine.Up;
// Compare the direction the player is looking with
// the surface the mole is standing on.
IManagedCamera camera = GetComponent();
float yaw = camera.Yaw;
float dot = Vector3.Dot(Vector3.Project(planeNormal, Vector3.up), Vector3.up);
bool facingUp = dot > 0 || Mathf.Approximately(dot, 0);
if (previousFacingUp != facingUp)
{
float angle = Vector3.Angle(planeNormal, previousFacingUp ? Vector3.up : Vector3.down);
if (Mathf.Abs(angle % 90) <= upFlipThreshold && !Mathf.Approximately(angle, 180))
{
facingUp = previousFacingUp;
}
}
previousFacingUp = facingUp;
Vector3 worldUp = facingUp ? Vector3.up : Vector3.down;
if (Mathf.Approximately(dot, -1))
{
yaw *= -1;
}
var planeRotation = Quaternion.FromToRotation(worldUp, planeNormal);
var playerRotation = Quaternion.AngleAxis(yaw, worldUp);
Vector3 movementForward = planeRotation * playerRotation * Vector3.forward;
Debug.DrawRay(transform.position, planeNormal, Color.green, 0);
Debug.DrawRay(transform.position, movementForward, Color.blue, 0);
// Apply the player's two axis movement to the movement input
var movementRotation = Quaternion.LookRotation(movementForward, planeNormal);
Vector3 axisMovement = movementRotation * InputMovement;
// Scale movement by speed
float speed = MovementSettingsAttribute.GetSpeed(CurrentState, this);
return speed * axisMovement;
}
public override void FixedUpdate()
{
GetComponent().isKinematic = true;
if (IsOwner)
{
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)
{
Velocity = Vector3.zero;
}
relativeUp.Value = movementEngine.Up;
}
if (movementEngine.GroundedState.StandingOnGround)
{
// Set the player's rotation to follow the ground
var rotation = Quaternion.FromToRotation(Vector3.up, relativeUp.Value);
transform.rotation = Quaternion.Lerp(transform.rotation, rotation, 10 * unityService.deltaTime);
}
else
{
// or Velocity if not grounded
transform.rotation = Quaternion.Lerp(
transform.rotation,
Quaternion.FromToRotation(Vector3.up, (transform.position - previousPosition).normalized),
10 * unityService.deltaTime);
}
previousPosition = transform.position;
GetComponent()?.UpdateState(relativeParentConfig);
base.FixedUpdate();
}
///
public override void Update()
{
if (IsOwner)
{
ReadPlayerMovement();
}
if (!IsOwner)
{
// movementEngine.SetNormal(relativeUp.Value);
movementEngine.CheckGrounded(false, false);
}
bool particlesEnabled = Attribute.GetCustomAttribute(CurrentState, typeof(DiggingParticlesEnabled)) != null;
MoleParticles digging = GetComponent();
digging.DrawParticles = particlesEnabled;
if (Attribute.GetCustomAttribute(CurrentState, typeof(AvatarOffsetAttribute)) is AvatarOffsetAttribute avatarOffset)
{
AttachedAnimator.transform.localPosition = (Vector3)this.EvaluateMember(avatarOffset.OffsetValue);
}
else
{
AttachedAnimator.transform.localPosition = defaultOffset;
}
base.Update();
}
///
public void ApplyJump(Vector3 velocity)
{
if (IsOwner)
{
Velocity = velocity + movementEngine.GetGroundVelocity();
RaiseEvent(JumpEvent.Instance);
}
}
///
/// 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();
bool moving = InputMovement.magnitude >= KCCUtils.Epsilon;
IEvent moveEvent = moving ? StartMoveInput.Instance as IEvent : StopMoveInput.Instance as IEvent;
RaiseEvent(moveEvent);
}
}
}