Move and consolidate many third-party plugin files and metadata from various locations (notably Assets/Third Parties/Plugins 1 and scattered Opsive/Photon folders) into a unified Assets/Plugins directory. Includes DOTween, PrimeTween, Native/BackroomsNoise, Sirenix/Odin Inspector, and Opsive UltimateCharacterController/shared libs, plus updates to several .meta files and removal of obsolete installer/legacy files. This standardizes plugin layout and cleans up duplicate/obsolete assets.
453 lines
21 KiB
C#
453 lines
21 KiB
C#
/// ---------------------------------------------
|
|
/// Ultimate Character Controller
|
|
/// Copyright (c) Opsive. All Rights Reserved.
|
|
/// https://www.opsive.com
|
|
/// ---------------------------------------------
|
|
|
|
namespace Opsive.UltimateCharacterController.Character
|
|
{
|
|
using Opsive.Shared.Events;
|
|
using Opsive.Shared.Game;
|
|
using Opsive.Shared.Utility;
|
|
using Opsive.UltimateCharacterController.Audio;
|
|
#if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER
|
|
using Opsive.UltimateCharacterController.Networking;
|
|
#endif
|
|
using Opsive.UltimateCharacterController.StateSystem;
|
|
using Opsive.UltimateCharacterController.SurfaceSystem;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// The CharacterFootEffects component will detect when a footstep has occurred.
|
|
/// </summary>
|
|
public class CharacterFootEffects : StateBehavior
|
|
{
|
|
/// <summary>
|
|
/// Specifies the properties of a foot.
|
|
/// </summary>
|
|
[System.Serializable]
|
|
public struct Foot
|
|
{
|
|
[Tooltip("A reference to the foot Transform. This reference should be pointing in the character's forward direction so the foot is placed correctly.")]
|
|
[SerializeField] private Transform m_Object;
|
|
#pragma warning disable 0649
|
|
[Tooltip("The grouping of the foot. If for example the character is a dog, the two left feet will be in the first group while the right feet are in the second group.")]
|
|
[SerializeField] private int m_Group;
|
|
#pragma warning restore 0649
|
|
[Tooltip("Should the footprint be flipped for this foot?")]
|
|
[SerializeField] private bool m_FlippedFootprint;
|
|
|
|
public Transform Object { get { return m_Object; } set { m_Object = value; } }
|
|
public int Group { get { return m_Group; } }
|
|
public bool FlippedFootprint { get { return m_FlippedFootprint; } set { m_FlippedFootprint = value; } }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Specifies how the footsteps are placed.
|
|
/// </summary>
|
|
public enum FootstepPlacementMode
|
|
{
|
|
BodyStep, // The footsteps are determined by the vertical height of the character's feet.
|
|
Trigger, // The footsteps are placed by a trigger on each foot.
|
|
FixedInterval, // The footsteps are placed at a regular interval while the character is moving.
|
|
CameraBob, // The footsteps are place according to the camera's bob.
|
|
None
|
|
}
|
|
|
|
[Tooltip("The Surface Impact triggered when there is a footstep.")]
|
|
[SerializeField] protected SurfaceImpact m_SurfaceImpact;
|
|
[Tooltip("Specifies how the footsteps are placed.")]
|
|
[SerializeField] protected FootstepPlacementMode m_FootstepMode;
|
|
[Tooltip("The character's feet. Only used with the BodyStep and Trigger placement modes.")]
|
|
[SerializeField] protected Foot[] m_Feet;
|
|
[Tooltip("If using the BodyStep mode, specifies the number of frames that the foot must be moving in order for it to be checked if it is down.")]
|
|
[SerializeField] protected int m_MoveDirectionFrameCount = 7;
|
|
[Tooltip("Specifies an offset for when a raycast is cast to determine if the character's foot is considered down.")]
|
|
[SerializeField] protected float m_FootOffset = 0.07f;
|
|
[Tooltip("If using the FixedInterval mode, specifies how often the footsteps occur when the character is moving.")]
|
|
[SerializeField] protected float m_Interval = 0.3f;
|
|
[Tooltip("If using the CameraBob mode, specifies the minimum time that must elapse before another footstep occurs.")]
|
|
[SerializeField] protected float m_MinBobInterval = 0.2f;
|
|
|
|
public FootstepPlacementMode FootstepMode { get { return m_FootstepMode; }
|
|
set
|
|
{
|
|
if (m_FootstepMode != value) {
|
|
m_FootstepMode = value;
|
|
if (Application.isPlaying) {
|
|
PrepareVerticalOffsetLists();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
public SurfaceImpact SurfaceImpact { get { return m_SurfaceImpact; } set { m_SurfaceImpact = value; } }
|
|
[NonSerialized] public Foot[] Feet { get { return m_Feet; } set { m_Feet = value; } }
|
|
public int MoveDirectionFrameCount { get { return m_MoveDirectionFrameCount; } set { m_MoveDirectionFrameCount = value; } }
|
|
public float FootOffset { get { return m_FootOffset; } set { m_FootOffset = value; } }
|
|
public float Interval { get { return m_Interval; } set { m_Interval = value; } }
|
|
public float MinBobInterval { get { return m_MinBobInterval; } set { m_MinBobInterval = value; } }
|
|
|
|
private GameObject m_GameObject;
|
|
private Transform m_Transform;
|
|
private UltimateCharacterLocomotion m_CharacterLocomotion;
|
|
private CharacterLayerManager m_CharacterLayerManager;
|
|
private ILookSource m_LookSource;
|
|
#if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER
|
|
private INetworkInfo m_NetworkInfo;
|
|
private Vector3 m_PreviousPosition;
|
|
#endif
|
|
|
|
private List<List<Transform>> m_FeetGrouping = new List<List<Transform>>();
|
|
private HashSet<Transform> m_FlippedFootprints = new HashSet<Transform>();
|
|
private float[] m_VerticalOffset;
|
|
private float[] m_LastVerticalOffset;
|
|
private int[] m_UpCount;
|
|
private int[] m_DownCount;
|
|
private Transform m_LastFootDown;
|
|
private float m_LastFootstepTime;
|
|
private int m_FootstepGroupIndex;
|
|
|
|
/// <summary>
|
|
/// Initialize the default values.
|
|
/// </summary>
|
|
protected override void Awake()
|
|
{
|
|
base.Awake();
|
|
|
|
m_GameObject = gameObject;
|
|
m_Transform = transform;
|
|
m_CharacterLocomotion = m_GameObject.GetCachedComponent<UltimateCharacterLocomotion>();
|
|
m_CharacterLayerManager = m_GameObject.GetCachedComponent<CharacterLayerManager>();
|
|
#if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER
|
|
m_NetworkInfo = m_GameObject.GetCachedComponent<INetworkInfo>();
|
|
m_PreviousPosition = m_Transform.position;
|
|
#endif
|
|
|
|
if (m_Feet == null) {
|
|
InitializeHumanoidFeet();
|
|
}
|
|
|
|
if (m_Feet != null && m_Feet.Length != 0) {
|
|
for (int i = 0; i < m_Feet.Length; ++i) {
|
|
if (m_Feet[i].Object == null) {
|
|
continue;
|
|
}
|
|
|
|
// The FeetGrouping list should be at least the size of the current group index.
|
|
while (m_Feet[i].Group >= m_FeetGrouping.Count) {
|
|
m_FeetGrouping.Add(new List<Transform>());
|
|
}
|
|
m_FeetGrouping[m_Feet[i].Group].Add(m_Feet[i].Object);
|
|
// The Transform should only be added to the set if the footprint is flipped. If the Transform is not in the set then the footprint is not flipped.
|
|
if (m_Feet[i].FlippedFootprint) {
|
|
m_FlippedFootprints.Add(m_Feet[i].Object);
|
|
}
|
|
|
|
// Footstep sounds are played from the feet.
|
|
AudioManager.Register(m_Feet[i].Object.gameObject, 0.05f);
|
|
}
|
|
} else {
|
|
m_FeetGrouping.Add(new List<Transform>());
|
|
m_FeetGrouping[0].Add(m_Transform);
|
|
}
|
|
|
|
if (m_FootstepMode == FootstepPlacementMode.Trigger || m_FootstepMode == FootstepPlacementMode.CameraBob || m_FootstepMode == FootstepPlacementMode.None) {
|
|
// The component doesn't need to be enabled if using a trigger - the FootstepTrigger component will detect the footstep. The CameraBob will enable the component
|
|
// when the look source is attached.
|
|
enabled = false;
|
|
} else if (m_Feet != null) {
|
|
PrepareVerticalOffsetLists();
|
|
}
|
|
|
|
EventHandler.RegisterEvent<ILookSource>(m_GameObject, "OnCharacterAttachLookSource", OnAttachLookSource);
|
|
EventHandler.RegisterEvent<bool>(m_GameObject, "OnCharacterMoving", OnMoving);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to initialize the feet if the character is a humanoid.
|
|
/// </summary>
|
|
public void InitializeHumanoidFeet()
|
|
{
|
|
if (m_Feet != null) {
|
|
return;
|
|
}
|
|
|
|
// Add the humanoid feet if the character is a humanoid.
|
|
var animator = gameObject.GetComponent<Animator>();
|
|
if (animator != null) {
|
|
var head = animator.GetBoneTransform(HumanBodyBones.Head);
|
|
if (head != null) {
|
|
m_Feet = new CharacterFootEffects.Foot[2];
|
|
// Try to use the toes if the bones have been mapped.
|
|
var toe = animator.GetBoneTransform(HumanBodyBones.LeftToes);
|
|
if (toe != null) {
|
|
m_Feet[0].Object = toe;
|
|
} else {
|
|
m_Feet[0].Object = animator.GetBoneTransform(HumanBodyBones.LeftFoot);
|
|
}
|
|
if (m_Feet[0].Object != null && m_Feet[0].Object.gameObject.GetComponent<AudioSource>() == null) {
|
|
var audioSource = m_Feet[0].Object.gameObject.AddComponent<AudioSource>();
|
|
audioSource.volume = 0.4f;
|
|
audioSource.playOnAwake = false;
|
|
audioSource.spatialBlend = 1;
|
|
audioSource.maxDistance = 20;
|
|
}
|
|
toe = animator.GetBoneTransform(HumanBodyBones.RightToes);
|
|
if (toe != null) {
|
|
m_Feet[1].Object = toe;
|
|
} else {
|
|
m_Feet[1].Object = animator.GetBoneTransform(HumanBodyBones.RightFoot);
|
|
}
|
|
if (m_Feet[1].Object != null && m_Feet[1].Object.gameObject.GetComponent<AudioSource>() == null) {
|
|
var audioSource = m_Feet[1].Object.gameObject.AddComponent<AudioSource>();
|
|
audioSource.volume = 0.4f;
|
|
audioSource.playOnAwake = false;
|
|
audioSource.spatialBlend = 1;
|
|
audioSource.maxDistance = 20;
|
|
}
|
|
m_Feet[1].FlippedFootprint = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A new ILookSource object has been attached to the character.
|
|
/// </summary>
|
|
/// <param name="lookSource">The ILookSource object attached to the character.</param>
|
|
private void OnAttachLookSource(ILookSource lookSource)
|
|
{
|
|
if (m_LookSource == null) {
|
|
EventHandler.RegisterEvent<bool>(m_GameObject, "OnCharacterChangePerspectives", OnChangePerspectives);
|
|
}
|
|
|
|
m_LookSource = lookSource;
|
|
|
|
if (m_LookSource == null) {
|
|
EventHandler.UnregisterEvent<bool>(m_GameObject, "OnCharacterChangePerspectives", OnChangePerspectives);
|
|
}
|
|
|
|
PrepareVerticalOffsetLists();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The character perspective between first and third person has changed.
|
|
/// </summary>
|
|
/// <param name="firstPersonPerspective">Is the character in a first person perspective?</param>
|
|
private void OnChangePerspectives(bool firstPersonPerspective)
|
|
{
|
|
PrepareVerticalOffsetLists();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the vertical offset arrays.
|
|
/// </summary>
|
|
private void PrepareVerticalOffsetLists()
|
|
{
|
|
if (m_FootstepMode == FootstepPlacementMode.Trigger || m_FootstepMode == FootstepPlacementMode.FixedInterval || m_FootstepMode == FootstepPlacementMode.None) {
|
|
return;
|
|
}
|
|
|
|
var count = m_FootstepMode == FootstepPlacementMode.CameraBob ? 1 : m_Feet.Length;
|
|
if (m_VerticalOffset == null) {
|
|
m_VerticalOffset = new float[count];
|
|
m_LastVerticalOffset = new float[count];
|
|
} else if (m_VerticalOffset.Length != count){
|
|
System.Array.Resize(ref m_VerticalOffset, count);
|
|
System.Array.Resize(ref m_LastVerticalOffset, count);
|
|
} else {
|
|
// Return early if the array lenth is the same. The arrays are already setup for the current footstep mode.
|
|
return;
|
|
}
|
|
|
|
// Setup the default values.
|
|
if (m_FootstepMode == FootstepPlacementMode.CameraBob) {
|
|
m_LastVerticalOffset[0] = m_VerticalOffset[0] = m_Transform.InverseTransformPoint(m_LookSource.LookPosition()).y;
|
|
} else { // Body Step.
|
|
if (m_UpCount == null) {
|
|
m_UpCount = new int[count];
|
|
m_DownCount = new int[count];
|
|
} else if (m_UpCount.Length != count) {
|
|
System.Array.Resize(ref m_UpCount, count);
|
|
System.Array.Resize(ref m_DownCount, count);
|
|
}
|
|
for (int i = 0; i < m_Feet.Length; ++i) {
|
|
m_LastVerticalOffset[i] = m_VerticalOffset[i] = m_Transform.InverseTransformPoint(m_Feet[i].Object.position).y;
|
|
m_UpCount[i] = 0;
|
|
}
|
|
}
|
|
enabled = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detect the footstep.
|
|
/// </summary>
|
|
private void FixedUpdate()
|
|
{
|
|
if (m_FootstepMode == FootstepPlacementMode.None) {
|
|
return;
|
|
}
|
|
|
|
#if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER
|
|
if (m_NetworkInfo != null && !m_NetworkInfo.IsLocalPlayer()) {
|
|
var velocity = (m_PreviousPosition - m_Transform.position) / Time.deltaTime;
|
|
m_PreviousPosition = m_Transform.position;
|
|
|
|
var localVelocity = m_Transform.InverseTransformDirection(velocity);
|
|
localVelocity.y = 0;
|
|
// The character has to be moving in order to place a footstep.
|
|
if (localVelocity.sqrMagnitude <= 0.1f) {
|
|
return;
|
|
}
|
|
|
|
// The character should be grounded.
|
|
RaycastHit raycastHit = new RaycastHit();
|
|
if (!m_CharacterLocomotion.SingleCast(-m_Transform.up * (m_CharacterLocomotion.SkinWidth + m_CharacterLocomotion.ColliderSpacing), Vector3.zero, m_CharacterLayerManager.SolidObjectLayers, ref raycastHit)) {
|
|
return;
|
|
}
|
|
} else {
|
|
#endif
|
|
// The character has to be grounded and moving in order to be able to place footsteps.
|
|
if (!m_CharacterLocomotion.Grounded || !m_CharacterLocomotion.Moving) {
|
|
return;
|
|
}
|
|
#if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER
|
|
}
|
|
#endif
|
|
|
|
switch (m_FootstepMode) {
|
|
case FootstepPlacementMode.BodyStep:
|
|
DetectBodyStep();
|
|
break;
|
|
case FootstepPlacementMode.FixedInterval:
|
|
UpdateFixedInterval();
|
|
break;
|
|
case FootstepPlacementMode.CameraBob:
|
|
DetectCameraBob();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A body step is detected when the foot touches the ground.
|
|
/// </summary>
|
|
private void DetectBodyStep()
|
|
{
|
|
for (int i = 0; i < m_Feet.Length; ++i) {
|
|
var verticalOffset = m_Transform.InverseTransformPoint(m_Feet[i].Object.position).y;
|
|
// A footstep can be detected when the foot is moving down after moving up for a minimum number of frames.
|
|
if (verticalOffset < m_LastVerticalOffset[i]) {
|
|
// The downward foot must have moved in the same direction for at least the specified number of frames.
|
|
if ((m_UpCount[i] > (m_MoveDirectionFrameCount / (m_CharacterLocomotion.TimeScale * Time.timeScale)) || m_DownCount[i] > (m_MoveDirectionFrameCount / (m_CharacterLocomotion.TimeScale * Time.timeScale))) &&
|
|
m_Feet[i].Object != m_LastFootDown && FootStep(m_Feet[i].Object, m_Feet[i].FlippedFootprint)) {
|
|
m_LastFootDown = m_Feet[i].Object;
|
|
m_LastFootstepTime = Time.time;
|
|
for (int j = 0; j < m_UpCount.Length; ++j) {
|
|
m_UpCount[j] = 0;
|
|
m_DownCount[j] = 0;
|
|
}
|
|
} else {
|
|
m_DownCount[i]++;
|
|
}
|
|
} else if (verticalOffset > m_LastVerticalOffset[i]) {
|
|
m_UpCount[i]++;
|
|
}
|
|
m_LastVerticalOffset[i] = verticalOffset;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Places a group footstep at a fixed interval.
|
|
/// </summary>
|
|
private void UpdateFixedInterval()
|
|
{
|
|
// Don't place a footstep if there was recently a footstep.
|
|
if (m_LastFootstepTime + m_Interval * m_CharacterLocomotion.TimeScale > Time.time) {
|
|
return;
|
|
}
|
|
|
|
// Place a footprint with the current group.
|
|
GroupFootStep();
|
|
m_LastFootstepTime = Time.time;
|
|
}
|
|
|
|
/// <summary>
|
|
/// A camera bob is detected when the look source point is at its lowest point relative to the character's transform.
|
|
/// </summary>
|
|
private void DetectCameraBob()
|
|
{
|
|
// In order for the lowest point to be detected the look source position must be decreasing. When the look source position
|
|
// starts to increase again then it was at the lowest point.
|
|
var verticalOffset = m_Transform.InverseTransformPoint(m_LookSource.LookPosition()).y;
|
|
if (m_LastVerticalOffset[0] > m_VerticalOffset[0] && verticalOffset > m_VerticalOffset[0] && m_LastFootstepTime + (m_MinBobInterval * m_CharacterLocomotion.TimeScale) < Time.time) {
|
|
GroupFootStep();
|
|
m_LastFootstepTime = Time.time;
|
|
}
|
|
m_LastVerticalOffset[0] = m_VerticalOffset[0];
|
|
m_VerticalOffset[0] = verticalOffset;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The active group index caused a footstep.
|
|
/// </summary>
|
|
private void GroupFootStep()
|
|
{
|
|
var grouping = m_FeetGrouping[m_FootstepGroupIndex];
|
|
for (int i = 0; i < grouping.Count; ++i) {
|
|
FootStep(grouping[i], m_FlippedFootprints.Contains(grouping[i]));
|
|
}
|
|
// Move to the next index which will cause the footstep.
|
|
m_FootstepGroupIndex = (m_FootstepGroupIndex + 1) % m_FeetGrouping.Count;
|
|
}
|
|
|
|
/// <summary>
|
|
/// A footstep has occurred. Notify the SurfaceManager.
|
|
/// </summary>
|
|
/// <param name="foot">The foot which caused the footstep.</param>
|
|
/// <param name="flipFootprint">Should the footprint be flipped?</param>
|
|
/// <returns>True if the footstep was successfully planted.</returns>
|
|
public virtual bool FootStep(Transform foot, bool flipFootprint)
|
|
{
|
|
// A RaycastHit is required for the SurfaceManager.
|
|
RaycastHit hit;
|
|
if (Physics.Raycast(foot.position + m_Transform.up * 0.1f, -m_Transform.up, out hit, 0.11f + m_FootOffset, m_CharacterLayerManager.IgnoreInvisibleCharacterWaterLayers, QueryTriggerInteraction.Ignore)) {
|
|
SurfaceManager.SpawnEffect(hit, m_SurfaceImpact, m_CharacterLocomotion.GravityDirection, m_CharacterLocomotion.TimeScale, foot.gameObject, m_Transform.forward, flipFootprint);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The character has started to or stopped moving.
|
|
/// </summary>
|
|
/// <param name="moving">Is the character moving?</param>
|
|
private void OnMoving(bool moving)
|
|
{
|
|
// When the character starts to move reset the footstep time so footsteps don't appear immediately after the character starts to moving.
|
|
if (moving) {
|
|
m_LastFootstepTime = Time.time;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Callback when the StateManager has changed the active state on the current object.
|
|
/// </summary>
|
|
public override void StateChange()
|
|
{
|
|
base.StateChange();
|
|
|
|
// The component doesn't need to be active if the footsteps are being triggered from a trigger.
|
|
enabled = m_FootstepMode != FootstepPlacementMode.Trigger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The character has been destroyed.
|
|
/// </summary>
|
|
private void OnDestroy()
|
|
{
|
|
EventHandler.UnregisterEvent<bool>(m_GameObject, "OnCharacterMoving", OnMoving);
|
|
EventHandler.UnregisterEvent<ILookSource>(m_GameObject, "OnCharacterAttachLookSource", OnAttachLookSource);
|
|
EventHandler.UnregisterEvent<bool>(m_GameObject, "OnCharacterChangePerspectives", OnChangePerspectives);
|
|
}
|
|
}
|
|
} |