using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Unity.Cinemachine
{
///
/// This interface identifies a behaviour that can drive IInputAxisOwners.
///
public interface IInputAxisController
{
///
/// 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.
///
void SynchronizeControllers();
#if UNITY_EDITOR
///
/// Available in Editor only. Used to check if a controller synchronization is necessary.
/// Normally we should have one controller per IInputAxisOwner axis.
///
/// True if there is one controller defined per IInputAxisOwner axis,
/// false if there is a mismatch
bool ControllersAreValid();
#endif
}
/// Use special property drawer for a list of InputAxisControllerBase.Controller objects
internal class InputAxisControllerManagerAttribute : PropertyAttribute {}
[Serializable]
internal class InputAxisControllerManager where T : IInputAxisReader, new ()
{
[NonReorderable]
public List.Controller> Controllers = new ();
/// Axes are dynamically discovered by querying behaviours implementing
///
readonly List m_Axes = new ();
readonly List m_AxisOwners = new ();
readonly List 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
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
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 s_AxisTargetsCache = new ();
#endif
///
/// Creates default controllers for an axis.
/// Override this if the default axis controllers do not fit your axes.
///
/// Description of the axis whose default controller needs to be set.
/// Controller to drive the axis.
public delegate void DefaultInitializer(
in IInputAxisOwner.AxisDescriptor axis, InputAxisControllerBase.Controller controller);
///
/// Create missing controllers (in their default state) and remove any that
/// are no longer relevant.
///
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.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.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.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);
}
}
/// Read all the controllers and process their input.
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);
}
}
}
///
/// 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.
///
/// The axis reader that will read the inputs.
[ExecuteAlways]
[SaveDuringPlay]
public abstract class InputAxisControllerBase : MonoBehaviour, IInputAxisController where T : IInputAxisReader, new ()
{
/// 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.
[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;
/// If set, input will not be processed while the Cinemachine Camera is
/// participating in a blend.
[HideIfNoComponent(typeof(CinemachineVirtualCameraBase))]
[Tooltip("If set, input will not be processed while the Cinemachine Camera is "
+ "participating in a blend.")]
public bool SuppressInputWhileBlending = true;
///
/// 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.
///
public bool IgnoreTimeScale;
///
/// Each discovered axis will get a Controller to drive it in Update().
///
[Serializable]
public class Controller
{
/// Identifies this axis in the inspector
[HideInInspector] public string Name;
/// Identifies this owner of the axis controlled by this controller
[HideInInspector] public UnityEngine.Object Owner;
///
/// When enabled, this controller will drive the input axis.
///
[Tooltip("When enabled, this controller will drive the input axis")]
public bool Enabled = true;
/// The input axis reader to read the value from the user
[HideFoldout]
public T Input;
/// The current value of the input
public float InputValue;
/// Drives the input axis value based on input value
[HideFoldout]
public DefaultInputAxisDriver Driver;
}
[Header("Driven Axes")]
[InputAxisControllerManager]
[SerializeField] internal InputAxisControllerManager m_ControllerManager = new ();
/// This list is dynamically populated based on the discovered axes
public List Controllers
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => m_ControllerManager.Controllers;
}
/// Editor only: Called by Unity when the component is serialized
/// or the inspector is changed.
protected virtual void OnValidate() => m_ControllerManager.Validate();
/// Called by Unity when the component is reset.
protected virtual void Reset()
{
ScanRecursively = true;
SuppressInputWhileBlending = true;
m_ControllerManager.Reset();
SynchronizeControllers();
}
/// Called by Unity when the inspector component is enabled
protected virtual void OnEnable() => SynchronizeControllers();
/// Called by Unity when the inspector component is disabled
protected virtual void OnDisable() => m_ControllerManager.OnDisable();
#if UNITY_EDITOR
///
public bool ControllersAreValid() => m_ControllerManager.ControllersAreValid(gameObject, ScanRecursively);
#endif
///
/// 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.
///
public void SynchronizeControllers() => m_ControllerManager.CreateControllers(
gameObject, ScanRecursively, enabled, InitializeControllerDefaultsForAxis);
///
/// Creates default controllers for an axis.
/// Override this if the default axis controllers do not fit your axes.
///
/// Description of the axis whose default controller needs to be set.
/// Controller to drive the axis.
protected virtual void InitializeControllerDefaultsForAxis(
in IInputAxisOwner.AxisDescriptor axis, Controller controller) {}
/// Read all the controllers and process their input.
/// Default implementation calls UpdateControllers(IgnoreTimeScale ? Time.unscaledDeltaTime : Time.deltaTime)
protected void UpdateControllers()
{
UpdateControllers(IgnoreTimeScale ? Time.unscaledDeltaTime : Time.deltaTime);
}
/// Read all the controllers and process their input.
/// The time interval for which to process the input
protected void UpdateControllers(float deltaTime)
{
if (SuppressInputWhileBlending
&& TryGetComponent(out var vcam)
&& vcam.IsParticipatingInBlend())
return;
m_ControllerManager.UpdateControllers(this, deltaTime);
}
}
}