392 lines
17 KiB
C#
392 lines
17 KiB
C#
|
|
using System;
|
||
|
|
using System.Collections.Generic;
|
||
|
|
using System.Runtime.CompilerServices;
|
||
|
|
using UnityEngine;
|
||
|
|
|
||
|
|
namespace Unity.Cinemachine
|
||
|
|
{
|
||
|
|
/// <summary>
|
||
|
|
/// This interface identifies a behaviour that can drive IInputAxisOwners.
|
||
|
|
/// </summary>
|
||
|
|
public interface IInputAxisController
|
||
|
|
{
|
||
|
|
/// <summary>
|
||
|
|
/// Called by editor only. Normally we should have one controller per
|
||
|
|
/// IInputAxisOwner axis. This will scan the object for IInputAxisOwner
|
||
|
|
/// behaviours, create missing controllers (in their
|
||
|
|
/// default state), and remove any that are no longer relevant.
|
||
|
|
/// </summary>
|
||
|
|
void SynchronizeControllers();
|
||
|
|
|
||
|
|
#if UNITY_EDITOR
|
||
|
|
/// <summary>
|
||
|
|
/// Available in Editor only. Used to check if a controller synchronization is necessary.
|
||
|
|
/// Normally we should have one controller per IInputAxisOwner axis.
|
||
|
|
/// </summary>
|
||
|
|
/// <returns>True if there is one controller defined per IInputAxisOwner axis,
|
||
|
|
/// false if there is a mismatch</returns>
|
||
|
|
bool ControllersAreValid();
|
||
|
|
#endif
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>Use special property drawer for a list of InputAxisControllerBase.Controller objects</summary>
|
||
|
|
internal class InputAxisControllerManagerAttribute : PropertyAttribute {}
|
||
|
|
|
||
|
|
[Serializable]
|
||
|
|
internal class InputAxisControllerManager<T> where T : IInputAxisReader, new ()
|
||
|
|
{
|
||
|
|
[NonReorderable]
|
||
|
|
public List<InputAxisControllerBase<T>.Controller> Controllers = new ();
|
||
|
|
|
||
|
|
/// Axes are dynamically discovered by querying behaviours implementing <see cref="IInputAxisOwner"/>
|
||
|
|
/// </summary>
|
||
|
|
readonly List<IInputAxisOwner.AxisDescriptor> m_Axes = new ();
|
||
|
|
readonly List<IInputAxisOwner> m_AxisOwners = new ();
|
||
|
|
readonly List<IInputAxisResetSource> m_AxisResetters = new ();
|
||
|
|
|
||
|
|
/// Call from owner's OnValidate()
|
||
|
|
public void Validate()
|
||
|
|
{
|
||
|
|
for (int i = 0; i < Controllers.Count; ++i)
|
||
|
|
if (Controllers[i] != null)
|
||
|
|
Controllers[i].Driver.Validate();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Call from owner's OnDisable() to shut down <summary>
|
||
|
|
public void OnDisable()
|
||
|
|
{
|
||
|
|
for (int i = 0; i < m_AxisResetters.Count; ++i)
|
||
|
|
if ((m_AxisResetters[i] as UnityEngine.Object) != null)
|
||
|
|
m_AxisResetters[i].UnregisterResetHandler(OnResetInput);
|
||
|
|
m_Axes.Clear();
|
||
|
|
m_AxisOwners.Clear();
|
||
|
|
m_AxisResetters.Clear();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Call from owner's OnDisable() to shut down <summary>
|
||
|
|
public void Reset()
|
||
|
|
{
|
||
|
|
OnDisable();
|
||
|
|
Controllers.Clear();
|
||
|
|
}
|
||
|
|
|
||
|
|
void OnResetInput()
|
||
|
|
{
|
||
|
|
for (int i = 0; i < Controllers.Count; ++i)
|
||
|
|
Controllers[i].Driver.Reset(ref m_Axes[i].DrivenAxis());
|
||
|
|
}
|
||
|
|
|
||
|
|
#if UNITY_EDITOR
|
||
|
|
public bool ControllersAreValid(GameObject root, bool scanRecursively)
|
||
|
|
{
|
||
|
|
s_AxisTargetsCache.Clear();
|
||
|
|
if (scanRecursively)
|
||
|
|
root.GetComponentsInChildren(s_AxisTargetsCache);
|
||
|
|
else
|
||
|
|
root.GetComponents(s_AxisTargetsCache);
|
||
|
|
var count = s_AxisTargetsCache.Count;
|
||
|
|
bool isValid = count == m_AxisOwners.Count;
|
||
|
|
for (int i = 0; isValid && i < count; ++i)
|
||
|
|
if (s_AxisTargetsCache[i] != m_AxisOwners[i])
|
||
|
|
isValid = false;
|
||
|
|
return isValid;
|
||
|
|
}
|
||
|
|
static readonly List<IInputAxisOwner> s_AxisTargetsCache = new ();
|
||
|
|
#endif
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Creates default controllers for an axis.
|
||
|
|
/// Override this if the default axis controllers do not fit your axes.
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="axis">Description of the axis whose default controller needs to be set.</param>
|
||
|
|
/// <param name="controller">Controller to drive the axis.</param>
|
||
|
|
public delegate void DefaultInitializer(
|
||
|
|
in IInputAxisOwner.AxisDescriptor axis, InputAxisControllerBase<T>.Controller controller);
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Create missing controllers (in their default state) and remove any that
|
||
|
|
/// are no longer relevant.
|
||
|
|
/// </summary>
|
||
|
|
public void CreateControllers(
|
||
|
|
GameObject root, bool scanRecursively, bool enabled, DefaultInitializer defaultInitializer)
|
||
|
|
{
|
||
|
|
OnDisable();
|
||
|
|
if (scanRecursively)
|
||
|
|
root.GetComponentsInChildren(m_AxisOwners);
|
||
|
|
else
|
||
|
|
root.GetComponents(m_AxisOwners);
|
||
|
|
|
||
|
|
// Trim excess controllers
|
||
|
|
for (int i = Controllers.Count - 1; i >= 0; --i)
|
||
|
|
if (!m_AxisOwners.Contains(Controllers[i].Owner as IInputAxisOwner))
|
||
|
|
Controllers.RemoveAt(i);
|
||
|
|
|
||
|
|
// Rebuild the controller list, recycling existing ones to preserve the settings
|
||
|
|
List<InputAxisControllerBase<T>.Controller> newControllers = new();
|
||
|
|
for (int j = 0; j < m_AxisOwners.Count; ++j)
|
||
|
|
{
|
||
|
|
var t = m_AxisOwners[j];
|
||
|
|
var startIndex = m_Axes.Count;
|
||
|
|
t.GetInputAxes(m_Axes);
|
||
|
|
for (int i = startIndex; i < m_Axes.Count; ++i)
|
||
|
|
{
|
||
|
|
int controllerIndex = GetControllerIndex(Controllers, t, m_Axes[i].Name);
|
||
|
|
if (controllerIndex < 0)
|
||
|
|
{
|
||
|
|
var c = new InputAxisControllerBase<T>.Controller
|
||
|
|
{
|
||
|
|
Enabled = true,
|
||
|
|
Name = m_Axes[i].Name,
|
||
|
|
Owner = t as UnityEngine.Object,
|
||
|
|
Input = new T()
|
||
|
|
};
|
||
|
|
defaultInitializer?.Invoke(m_Axes[i], c);
|
||
|
|
newControllers.Add(c);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
newControllers.Add(Controllers[controllerIndex]);
|
||
|
|
Controllers.RemoveAt(controllerIndex);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Controllers = newControllers;
|
||
|
|
|
||
|
|
if (enabled)
|
||
|
|
RegisterResetHandlers(root, scanRecursively);
|
||
|
|
|
||
|
|
static int GetControllerIndex(
|
||
|
|
List<InputAxisControllerBase<T>.Controller> list, IInputAxisOwner owner, string axisName)
|
||
|
|
{
|
||
|
|
for (int i = 0; i < list.Count; ++i)
|
||
|
|
if (list[i].Owner as IInputAxisOwner == owner && list[i].Name == axisName)
|
||
|
|
return i;
|
||
|
|
return -1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void RegisterResetHandlers(GameObject root, bool scanRecursively)
|
||
|
|
{
|
||
|
|
// Rebuild the resetter list and register with them
|
||
|
|
m_AxisResetters.Clear();
|
||
|
|
if (scanRecursively)
|
||
|
|
root.GetComponentsInChildren(m_AxisResetters);
|
||
|
|
else
|
||
|
|
root.GetComponents(m_AxisResetters);
|
||
|
|
for (int i = 0; i < m_AxisResetters.Count; ++i)
|
||
|
|
{
|
||
|
|
m_AxisResetters[i].UnregisterResetHandler(OnResetInput);
|
||
|
|
m_AxisResetters[i].RegisterResetHandler(OnResetInput);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>Read all the controllers and process their input.</summary>
|
||
|
|
public void UpdateControllers(UnityEngine.Object context, float deltaTime)
|
||
|
|
{
|
||
|
|
for (int i = 0; i < Controllers.Count; ++i)
|
||
|
|
{
|
||
|
|
var c = Controllers[i];
|
||
|
|
if (!c.Enabled || c.Input == null)
|
||
|
|
continue;
|
||
|
|
var hint = i < m_Axes.Count ? m_Axes[i].Hint : 0;
|
||
|
|
if (c.Input != null)
|
||
|
|
c.InputValue = c.Input.GetValue(context, hint);
|
||
|
|
|
||
|
|
c.Driver.ProcessInput(ref m_Axes[i].DrivenAxis(), c.InputValue, deltaTime);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
int GetControllerIndex(string axisName)
|
||
|
|
{
|
||
|
|
for (int i = 0; i < Controllers.Count; ++i)
|
||
|
|
{
|
||
|
|
var c = Controllers[i];
|
||
|
|
if (c.Name == axisName)
|
||
|
|
return i;
|
||
|
|
}
|
||
|
|
return -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Get the controller for a given axis name. The axis name is the name displayed
|
||
|
|
/// for the axis foldout on the inspector.
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="axisName">The name of the axis, as it appears in the inspector.</param>
|
||
|
|
/// <returns>The first Controller object with the matching axis name, or null if not found.</returns>
|
||
|
|
public InputAxisControllerBase<T>.Controller GetController(string axisName)
|
||
|
|
{
|
||
|
|
int i = GetControllerIndex(axisName);
|
||
|
|
return i < 0 ? null : Controllers[i];
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Triggers recentering for a given axis, and also cancels any input currently in progrress for that axis.
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="axisName">The name of the axis, as it appears in the inspector.</param>
|
||
|
|
/// <returns>True if the axis was found and recentering triggered, false otherwise</returns>
|
||
|
|
public bool TriggerRecentering(string axisName)
|
||
|
|
{
|
||
|
|
int i = GetControllerIndex(axisName);
|
||
|
|
if (i >= 0)
|
||
|
|
{
|
||
|
|
var c = Controllers[i];
|
||
|
|
c.Driver.CancelCurrentInput(ref m_Axes[i].DrivenAxis());
|
||
|
|
m_Axes[i].DrivenAxis().TriggerRecentering();
|
||
|
|
}
|
||
|
|
return i >= 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// This is a base class for a behaviour that is used to drive IInputAxisOwner behaviours,
|
||
|
|
/// which it discovers dynamically. It is the bridge between the input system and
|
||
|
|
/// Cinemachine cameras that require user input. Add it to a Cinemachine camera that needs it.
|
||
|
|
/// If you want to read inputs from a third-party source, then you must specialize this class
|
||
|
|
/// with an appropriate implementation of IInputAxisReader.
|
||
|
|
/// </summary>
|
||
|
|
/// <typeparam name="T">The axis reader that will read the inputs.</typeparam>
|
||
|
|
[ExecuteAlways]
|
||
|
|
[SaveDuringPlay]
|
||
|
|
public abstract class InputAxisControllerBase<T> : MonoBehaviour, IInputAxisController where T : IInputAxisReader, new ()
|
||
|
|
{
|
||
|
|
/// <summary>If set, a recursive search for IInputAxisOwners behaviours will be performed.
|
||
|
|
/// Otherwise, only behaviours attached directly to this GameObject will be considered,
|
||
|
|
/// and child objects will be ignored.</summary>
|
||
|
|
[Tooltip("If set, a recursive search for IInputAxisOwners behaviours will be performed. "
|
||
|
|
+ "Otherwise, only behaviours attached directly to this GameObject will be considered, "
|
||
|
|
+ "and child objects will be ignored")]
|
||
|
|
public bool ScanRecursively = true;
|
||
|
|
|
||
|
|
/// <summary>If set, input will not be processed while the Cinemachine Camera is
|
||
|
|
/// participating in a blend.</summary>
|
||
|
|
[HideIfNoComponent(typeof(CinemachineVirtualCameraBase))]
|
||
|
|
[Tooltip("If set, input will not be processed while the Cinemachine Camera is "
|
||
|
|
+ "participating in a blend.")]
|
||
|
|
public bool SuppressInputWhileBlending = true;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// If set, then input will be processed using unscaled deltaTime, and not scaled deltaTime.
|
||
|
|
/// This allows input to continue even when the timescale is set to 0.
|
||
|
|
/// </summary>
|
||
|
|
public bool IgnoreTimeScale;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Each discovered axis will get a Controller to drive it in Update().
|
||
|
|
/// </summary>
|
||
|
|
[Serializable]
|
||
|
|
public class Controller
|
||
|
|
{
|
||
|
|
/// <summary>Identifies this axis in the inspector</summary>
|
||
|
|
[HideInInspector] public string Name;
|
||
|
|
|
||
|
|
/// <summary>Identifies this owner of the axis controlled by this controller</summary>
|
||
|
|
[HideInInspector] public UnityEngine.Object Owner;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// When enabled, this controller will drive the input axis.
|
||
|
|
/// </summary>
|
||
|
|
[Tooltip("When enabled, this controller will drive the input axis")]
|
||
|
|
public bool Enabled = true;
|
||
|
|
|
||
|
|
/// <summary>The input axis reader to read the value from the user</summary>
|
||
|
|
[HideFoldout]
|
||
|
|
public T Input;
|
||
|
|
|
||
|
|
/// <summary>The current value of the input</summary>
|
||
|
|
public float InputValue;
|
||
|
|
|
||
|
|
/// <summary>Drives the input axis value based on input value</summary>
|
||
|
|
[HideFoldout]
|
||
|
|
public DefaultInputAxisDriver Driver;
|
||
|
|
}
|
||
|
|
|
||
|
|
[Header("Driven Axes")]
|
||
|
|
[InputAxisControllerManager]
|
||
|
|
[SerializeField, NoSaveDuringPlay] internal InputAxisControllerManager<T> m_ControllerManager = new ();
|
||
|
|
|
||
|
|
/// <summary>This list is dynamically populated based on the discovered axes</summary>
|
||
|
|
public List<Controller> Controllers
|
||
|
|
{
|
||
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
|
|
get => m_ControllerManager.Controllers;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>Editor only: Called by Unity when the component is serialized
|
||
|
|
/// or the inspector is changed.</summary>
|
||
|
|
protected virtual void OnValidate() => m_ControllerManager.Validate();
|
||
|
|
|
||
|
|
/// <summary>Called by Unity when the component is reset.</summary>
|
||
|
|
protected virtual void Reset()
|
||
|
|
{
|
||
|
|
ScanRecursively = true;
|
||
|
|
SuppressInputWhileBlending = true;
|
||
|
|
m_ControllerManager.Reset();
|
||
|
|
SynchronizeControllers();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>Called by Unity when the inspector component is enabled</summary>
|
||
|
|
protected virtual void OnEnable() => SynchronizeControllers();
|
||
|
|
|
||
|
|
/// <summary>Called by Unity when the inspector component is disabled</summary>
|
||
|
|
protected virtual void OnDisable() => m_ControllerManager.OnDisable();
|
||
|
|
|
||
|
|
#if UNITY_EDITOR
|
||
|
|
/// <inheritdoc />
|
||
|
|
public bool ControllersAreValid() => m_ControllerManager.ControllersAreValid(gameObject, ScanRecursively);
|
||
|
|
#endif
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Normally we should have one controller per IInputAxisOwner axis.
|
||
|
|
/// This will create missing controllers (in their default state) and remove any that
|
||
|
|
/// are no longer relevant. This is costly - do not call it every frame.
|
||
|
|
/// </summary>
|
||
|
|
public void SynchronizeControllers() => m_ControllerManager.CreateControllers(
|
||
|
|
gameObject, ScanRecursively, enabled, InitializeControllerDefaultsForAxis);
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Creates default controllers for an axis.
|
||
|
|
/// Override this if the default axis controllers do not fit your axes.
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="axis">Description of the axis whose default controller needs to be set.</param>
|
||
|
|
/// <param name="controller">Controller to drive the axis.</param>
|
||
|
|
protected virtual void InitializeControllerDefaultsForAxis(
|
||
|
|
in IInputAxisOwner.AxisDescriptor axis, Controller controller) {}
|
||
|
|
|
||
|
|
/// <summary>Read all the controllers and process their input.
|
||
|
|
/// Default implementation calls UpdateControllers(IgnoreTimeScale ? Time.unscaledDeltaTime : Time.deltaTime)</summary>
|
||
|
|
protected void UpdateControllers()
|
||
|
|
{
|
||
|
|
UpdateControllers(IgnoreTimeScale ? Time.unscaledDeltaTime : Time.deltaTime);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>Read all the controllers and process their input.</summary>
|
||
|
|
/// <param name="deltaTime">The time interval for which to process the input</param>
|
||
|
|
protected void UpdateControllers(float deltaTime)
|
||
|
|
{
|
||
|
|
if (SuppressInputWhileBlending
|
||
|
|
&& TryGetComponent<CinemachineVirtualCameraBase>(out var vcam)
|
||
|
|
&& vcam.IsParticipatingInBlend())
|
||
|
|
return;
|
||
|
|
|
||
|
|
m_ControllerManager.UpdateControllers(this, deltaTime);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Get the controller for a given axis name. The axis name is the name displayed
|
||
|
|
/// for the axis foldout on the inspector.
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="axisName">The name of the axis, as it appears in the inspector.</param>
|
||
|
|
/// <returns>The first Controller object with the matching axis name, or null if not found.</returns>
|
||
|
|
public Controller GetController(string axisName) => m_ControllerManager.GetController(axisName);
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Triggers recentering for a given axis, and also cancels any input currently in progress for that axis.
|
||
|
|
/// This ensures that the recentering begins immediately.
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="axisName">The name of the axis, as it appears in the inspector.</param>
|
||
|
|
/// <returns>True if the axis was found and recentering triggered, false otherwise</returns>
|
||
|
|
public bool TriggerRecentering(string axisName) => m_ControllerManager.TriggerRecentering(axisName);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|