using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; using Doozy.Runtime.Common.Attributes; using Doozy.Runtime.Common.Events; using Doozy.Runtime.Common.Extensions; using Doozy.Runtime.Common.Utils; using Doozy.Runtime.Mody; using Doozy.Runtime.Reactor; using Doozy.Runtime.Signals; using TMPro; using UnityEngine; using UnityEngine.EventSystems; // ReSharper disable MemberCanBePrivate.Global // ReSharper disable PartialTypeWithSinglePart // ReSharper disable UnusedMember.Local namespace Doozy.Runtime.UIManager.Components { /// /// Stepper component that can be used to increment or decrement a value. /// Has category/name id identifier. /// [AddComponentMenu("Doozy/UI/Components/UI Stepper")] public partial class UIStepper : MonoBehaviour, IBeginDragHandler, IEndDragHandler, IDragHandler { private const float TOLERANCE = 0.0001f; private const float DRAG_DISTANCE = 20f; private const float WAIT_BEFORE_STARTING = 0.6f; private const float WAIT_TIME = 0.4f; private const float WAIT_TIME_MIN = 0.04f; private const float WAIT_TIME_REDUCTION = 0.4f; #if UNITY_EDITOR [UnityEditor.MenuItem("GameObject/Doozy/UI/Components/UIStepper", false, 8)] private static void CreateComponent(UnityEditor.MenuCommand menuCommand) { GameObjectUtils.AddToScene("UIStepper", false, true); } #endif /// UISteppers database public static HashSet database { get; private set; } = new HashSet(); [ExecuteOnReload] private static void OnReload() { database ??= new HashSet(); } [ClearOnReload] private static SignalStream s_stream; /// UIStepper signal stream public static SignalStream stream => s_stream ??= SignalsService.GetStream(UISelectable.k_StreamCategory, nameof(UIStepper)); /// All steppers that are active and enabled public static IEnumerable availableSteppers => database.Where(item => item.isActiveAndEnabled); /// UIStepper identifier public UIStepperId Id; /// /// Drag direction for a UIStepper /// public enum Direction { /// /// Value increases when dragging to the right and decreases when dragging to the left /// Horizontal, /// /// Value increases when dragging up and decreases when dragging down /// Vertical } /// /// The initial and maximum time in seconds to wait before the value starts to auto repeat (increment/decrement) /// public float AutoRepeatWaitTime = WAIT_TIME; /// /// When the stepper is auto-repeating, the wait time between each increase/decrease will be reduced by multiplying the remaining wait time with this value until it reaches AutoRepeatMinWaitTime limit. /// This reduction makes the stepper feel more responsive and less laggy. /// public float AutoRepeatWaitTimeReduction = WAIT_TIME_REDUCTION; /// The minimum wait time between each increase/decrease when the stepper is auto-repeating public float AutoRepeatMinWaitTime = WAIT_TIME_MIN; [SerializeField] private UIButton MinusButton; /// Reference to a UIButton that will be used to decrease the value of the stepper public UIButton minusButton { get => MinusButton; private set { if (MinusButton != null) { MinusButton.pressedState.stateEvent.Event.RemoveListener(OnMinusButtonClicked); MinusButton.onPointerDownEvent.RemoveListener(OnMinusButtonDown); MinusButton.onPointerUpEvent.RemoveListener(OnMinusButtonUp); } if (value != null) { value.pressedState.stateEvent.Event.AddListener(OnMinusButtonClicked); value.onPointerDownEvent.AddListener(OnMinusButtonDown); value.onPointerUpEvent.AddListener(OnMinusButtonUp); } MinusButton = value; } } [SerializeField] private UIButton PlusButton; /// Reference to a UIButton that will be used to increase the value of the stepper public UIButton plusButton { get => PlusButton; private set { if (PlusButton != null) { PlusButton.pressedState.stateEvent.Event.RemoveListener(OnPlusButtonClicked); PlusButton.onPointerDownEvent.RemoveListener(OnPlusButtonDown); PlusButton.onPointerUpEvent.RemoveListener(OnPlusButtonUp); } if (value != null) { value.pressedState.stateEvent.Event.AddListener(OnPlusButtonClicked); value.onPointerDownEvent.AddListener(OnPlusButtonDown); value.onPointerUpEvent.AddListener(OnPlusButtonUp); } PlusButton = value; } } [SerializeField] private UIButton ResetButton; /// Reference to a UIButton that will reset the stepper value to its default value public UIButton resetButton { get => ResetButton; private set { if (ResetButton != null) { ResetButton.pressedState.stateEvent.Event.RemoveListener(OnResetButtonClicked); } if (value != null) { value.pressedState.stateEvent.Event.AddListener(OnResetButtonClicked); } ResetButton = value; } } [SerializeField] private TMP_Text TargetLabel; /// Reference to the value label that displays the current value public TMP_Text targetLabel { get => TargetLabel; private set { TargetLabel = value; UpdateValueLabel(); } } [SerializeField] private float MinValue; /// The minimum value that the stepper can have public float minValue { get => MinValue; private set { MinValue = value.Round(ValuePrecision); MaxValue = MinValue > MaxValue ? MinValue : MaxValue; if (Value < MinValue) { SetValue(MinValue); } UpdateTargetProgressorMinMax(); UpdateTargetProgressorValue(); UpdateTargetProgressorMinMax(); UpdateTargetSliderValue(Value); } } [SerializeField] private float MaxValue = 1f; /// The maximum value that the stepper can reach public float maxValue { get => MaxValue; private set { MaxValue = value.Round(ValuePrecision); MinValue = MaxValue < MinValue ? MaxValue : MinValue; if (Value > MaxValue) { SetValue(MaxValue); } UpdateTargetProgressorMinMax(); UpdateTargetProgressorValue(); UpdateTargetProgressorMinMax(); UpdateTargetSliderValue(Value); } } [SerializeField] private float Value; /// The current value of the stepper public float value { get => Value; set => SetValue(value); } [SerializeField] private float DefaultValue; /// Reset value for the stepper public float defaultValue { get => DefaultValue; set => DefaultValue = Mathf.Clamp(value, minValue, maxValue); } [SerializeField] private float Step = 0.1f; /// Value by which the stepper will increase or decrease when the value is changed public float step { get => Step; private set { Step = value; SetValue(NearestStep(value)); UpdateTargetSliderValue(value); stepValueChanged = true; } } [SerializeField] private UISlider TargetSlider; /// /// Reference to a UISlider that will be updated when the stepper value changes. /// The slider value also updates the stepper value, when it changes. /// public UISlider targetSlider { get => TargetSlider; private set { if (TargetSlider != null) { TargetSlider.OnValueChanged.RemoveListener(OnTargetSliderValueChanged); OnValueChanged.RemoveListener(UpdateTargetSliderValue); } if (value != null) { value.OnValueChanged.AddListener(OnTargetSliderValueChanged); OnValueChanged.AddListener(UpdateTargetSliderValue); } TargetSlider = value; UpdateTargetSliderMinMax(); UpdateTargetSliderValue(Value); } } [SerializeField] private Progressor TargetProgressor; /// Reference to a Progressor that will be updated when the stepper value changes public Progressor targetProgressor { get => TargetProgressor; private set { TargetProgressor = value; if (value == null) return; UpdateTargetProgressorMinMax(); UpdateTargetProgressorValue(); } } /// /// When true, the stepper will update the target progressor value with SetValueAt instead of PlayToValue. /// Basically, if true, the progressor will not animate when the stepper value changes. /// public bool InstantProgressorUpdate = true; /// Reset the stepper value to its default value OnEnable public bool ResetValueOnEnable = true; /// Number of decimal places to round the value to, if the step is not a whole number public int ValuePrecision = 2; /// /// Enable or disable the drag functionality for this stepper. /// public bool DragEnabled; /// /// Reference to the RectTransform used as the drag handle. /// [SerializeField] private RectTransform DragHandle; public RectTransform dragHandle { get => DragHandle; set { DragHandle = value; if (DragHandle) { m_DragHandleInitialPosition = DragHandle.anchoredPosition; } } } [SerializeField] private Direction DragDirection = Direction.Horizontal; /// Drag direction for the Drag Handle to increase or decrease the value public Direction dragDirection { get => DragDirection; private set => DragDirection = value; } /// /// During a drag operation, this is the maximum distance the drag handle can be moved from the initial position. /// This also affects the speed of the value change (inversely proportional). /// The longer the distance the finer the control the user has to change the value. /// public float MaxDragDistance = DRAG_DISTANCE; /// /// Fired when the value changed. /// Returns the new value. /// public FloatEvent OnValueChanged = new FloatEvent(); /// /// Fired when the value increases. /// Returns the difference between the new and old value. /// Example: if the previous value was 0.5 and the new value is 0.7, the returned value will be 0.2 /// public FloatEvent OnValueIncremented = new FloatEvent(); /// /// Fired when the value decreases. /// Returns the difference between the new and old value. /// Example: if the previous value was 10 and the new value is 5, the returned value will be -5 /// public FloatEvent OnValueDecremented = new FloatEvent(); /// Fired when the value was reset public ModyEvent OnValueReset = new ModyEvent(); /// Fired when the value has reached the minimum value public ModyEvent OnValueReachedMin = new ModyEvent(); /// Fired when the value has reached the maximum value public ModyEvent OnValueReachedMax = new ModyEvent(); /// /// Coroutine called when the user is holding down the plus button. /// It's used to auto-repeat the increment action. /// private Coroutine autoIncrementCoroutine { get; set; } /// /// Coroutine called when the user is holding down the minus button. /// It's used to auto-repeat the decrement action. /// private Coroutine autoDecrementCoroutine { get; set; } /// /// Flag to indicate if the stepper is currently auto-repeating an increment action. /// private bool autoIncrementing { get; set; } /// /// Flag to indicate if the stepper is currently auto-repeating the decrement action. /// private bool autoDecrementing { get; set; } /// /// During a drag operation, this is the distance travelled from the start of the drag. /// It is used to determine the direction of the drag and the speed of the value change (the further the drag, the faster the value change). /// private float draggedDistance => DragDirection switch { Direction.Horizontal => dragHandle.anchoredPosition.x - m_DragHandleInitialPosition.x, Direction.Vertical => dragHandle.anchoredPosition.y - m_DragHandleInitialPosition.y, _ => throw new ArgumentOutOfRangeException() }; /// /// During a drag operation, this is the time in seconds that the stepper will wait before changing the value (incrementing or decrementing). /// The value will change depending on how far, from the initial position, the drag area has been dragged. /// The further the drag area is dragged, the faster the value will change (thus this value will be lower). /// private float dragWaitTime { get { float distance = Mathf.Abs(draggedDistance); distance = Mathf.Clamp(distance, 0, MaxDragDistance); float dragRatio = 1f - distance / MaxDragDistance; float waitTime = Mathf.Clamp(dragRatio * AutoRepeatWaitTime, AutoRepeatMinWaitTime, AutoRepeatWaitTime); return waitTime; } } /// /// Initial position of the drag handle when the drag operation starts. /// private Vector2 m_DragHandleInitialPosition; /// /// Coroutine called when the user is dragging the drag handle in the increment direction (right or up). /// It's used to drag-repeat the decrement action. /// private Coroutine dragIncrementCoroutine { get; set; } /// /// Coroutine called when the user is dragging the drag handle in the decrement direction (left or down). /// It's used to drag-repeat the decrement action. /// private Coroutine dragDecrementCoroutine { get; set; } /// /// Flag to indicate if the stepper is currently drag-repeating an increment action. /// private bool dragIncrementing { get; set; } /// /// Flag to indicate if the stepper is currently drag-repeating the decrement action. /// private bool dragDecrementing { get; set; } /// /// Flag to indicate if the stepper DragHandle can be dragged. /// private bool canDrag { get; set; } /// /// Flag to indicate if the stepper DragHandle is currently being dragged. /// private bool isDragging { get; set; } /// /// Flag to indicate that the stepper's step vale has changed. /// private bool stepValueChanged { get; set; } protected UIStepper() { Id = new UIStepperId(); } protected virtual void OnValidate() { SetValue(value); } protected virtual void Awake() { database.Add(this); } protected virtual void Start() { UpdateValueLabel(); } protected virtual void OnEnable() { database.Remove(null); if (!Application.isPlaying) return; if (minusButton != null) { minusButton.pressedState.stateEvent.Event.AddListener(OnMinusButtonClicked); minusButton.onPointerUpEvent.AddListener(OnMinusButtonUp); minusButton.onPointerDownEvent.AddListener(OnMinusButtonDown); } if (plusButton != null) { plusButton.pressedState.stateEvent.Event.AddListener(OnPlusButtonClicked); plusButton.onPointerUpEvent.AddListener(OnPlusButtonUp); plusButton.onPointerDownEvent.AddListener(OnPlusButtonDown); } if (resetButton != null) { resetButton.pressedState.stateEvent.Event.AddListener(OnResetButtonClicked); } if (TargetSlider) { TargetSlider.OnValueChanged.AddListener(OnTargetSliderValueChanged); OnValueChanged.AddListener(UpdateTargetSliderValue); } UpdateTargetProgressorMinMax(); UpdateTargetSliderMinMax(); if (ResetValueOnEnable) { ResetValue(); } else { UpdateTargetProgressorValue(); UpdateTargetSliderValue(value); } if (dragHandle) { m_DragHandleInitialPosition = dragHandle.anchoredPosition; } canDrag = true; isDragging = false; } protected virtual void OnDisable() { database.Remove(null); if (!Application.isPlaying) return; if (plusButton != null) { plusButton.pressedState.stateEvent.Event.RemoveListener(OnPlusButtonClicked); plusButton.onPointerUpEvent.RemoveListener(OnPlusButtonUp); plusButton.onPointerDownEvent.RemoveListener(OnPlusButtonDown); } if (minusButton != null) { minusButton.pressedState.stateEvent.Event.RemoveListener(OnMinusButtonClicked); minusButton.onPointerUpEvent.RemoveListener(OnMinusButtonUp); minusButton.onPointerDownEvent.RemoveListener(OnMinusButtonDown); } if (resetButton != null) { resetButton.pressedState.stateEvent.Event.RemoveListener(OnResetButtonClicked); } autoIncrementing = false; if (autoIncrementCoroutine != null) { StopCoroutine(autoIncrementCoroutine); } autoDecrementing = false; if (autoDecrementCoroutine != null) { StopCoroutine(autoDecrementCoroutine); } dragIncrementing = false; if (dragIncrementCoroutine != null) { StopCoroutine(dragIncrementCoroutine); } dragDecrementing = false; if (dragDecrementCoroutine != null) { StopCoroutine(dragDecrementCoroutine); } UpdateTargetProgressorValue(); UpdateTargetSliderValue(value); if (TargetSlider) { TargetSlider.OnValueChanged.RemoveListener(OnTargetSliderValueChanged); OnValueChanged.RemoveListener(UpdateTargetSliderValue); } } protected virtual void OnDestroy() { database.Remove(null); database.Remove(this); } protected virtual void OnMinusButtonClicked() { DecrementValue(); canDrag = true; } protected virtual void OnPlusButtonClicked() { IncrementValue(); canDrag = true; } protected virtual void OnPlusButtonDown() { StopAutoIncrement(); if (isDragging) return; canDrag = false; StartAutoIncrement(); } protected virtual void OnPlusButtonUp() { StopAutoIncrement(); canDrag = true; } protected virtual void OnMinusButtonDown() { StopAutoDecrement(); if (isDragging) return; canDrag = false; StartAutoDecrement(); } protected virtual void OnMinusButtonUp() { StopAutoDecrement(); canDrag = true; } protected virtual void OnResetButtonClicked() { ResetValue(); } protected virtual void OnTargetSliderValueChanged(float sliderValue) { float nearestStepValue = NearestStep(sliderValue); TargetSlider.SetValueWithoutNotify(nearestStepValue); SetValue(nearestStepValue); } /// Reset the current int or float value, depending on the stepper's value type, to the default value. public void ResetValue() { SetValue(defaultValue); OnValueReset.Execute(); } /// Set the current float value to the given value /// New value public void SetValue(float newValue) { bool valueChanged = Math.Abs(Value - newValue) > TOLERANCE; //check if the value has changed Value = Mathf.Clamp(newValue, minValue, maxValue).Round(ValuePrecision); //set the new value if (stepValueChanged) { Value = NearestStep(Value); stepValueChanged = false; } UpdateValueLabel(); //update the value label if (valueChanged) { OnValueChanged.Invoke(Value); //invoke the OnValueChanged event only if the value has changed if (InstantProgressorUpdate) { UpdateTargetProgressorValue(); } else { PlayTargetProgressorValue(); } UpdateTargetSliderValue(Value); stream.SendSignal(new UIStepperSignalData(Id.Category, Id.Name, StepperState.ValueChanged, this)); } if (Value <= minValue) { //value is equal to the min value //invoke the OnValueReachedMin event if the value is equal to the min value //disable the minus button if the value is equal to the min value OnValueReachedMin.Execute(); stream.SendSignal(new UIStepperSignalData(Id.Category, Id.Name, StepperState.ReachedMinValue, this)); if (minusButton != null && minusButton.interactable) { minusButton.interactable = false; } } else { //value is not equal to the min value //enable the minus button if the value is not equal to the min value if (minusButton != null && !minusButton.interactable) { minusButton.interactable = true; } } if (Value >= maxValue) { //value is equal to the max value //invoke the OnValueReachedMax event if the value is equal to the max value //disable the plus button if the value is equal to the max value OnValueReachedMax.Execute(); stream.SendSignal(new UIStepperSignalData(Id.Category, Id.Name, StepperState.ReachedMaxValue, this)); if (plusButton != null && plusButton.interactable) { plusButton.interactable = false; } } else { //value is not equal to the max value //enable the plus button if the value is not equal to the max value if (plusButton != null && !plusButton.interactable) { plusButton.interactable = true; } } } /// Increment the value by the step value public void IncrementValue() { IncrementValue(step); } /// Increment the value /// Increment value public void IncrementValue(float increment) { bool currentValueIsMax = Math.Abs(value - maxValue) < TOLERANCE; //we need to know this before we increment the value if (!currentValueIsMax) { OnValueIncremented.Invoke(increment); //invoke the OnValueIncremented event only if the value was not already at max //stream.SendSignal(new UIStepperSignalData(Id.Category, Id.Name, StepperState.ValueIncremented, this)); } SetValue(value + increment); //increment value } /// Decrement the value by the step value public void DecrementValue() { DecrementValue(step); } /// Decrement the value /// Decrement value public void DecrementValue(float decrement) { bool currentValueIsMin = Math.Abs(value - minValue) < TOLERANCE; //we need to know this before we decrement the value if (!currentValueIsMin) { OnValueDecremented.Invoke(-decrement); //invoke the OnValueDecremented event only if the value was not already at min //stream.SendSignal(new UIStepperSignalData(Id.Category, Id.Name, StepperState.ValueDecremented, this)); } SetValue(value - decrement); //decrement value } /// Update the value label text with the current value public void UpdateValueLabel() { if (targetLabel == null) return; targetLabel.text = value.ToString(CultureInfo.InvariantCulture); } private void UpdateTargetProgressorMinMax() { if (!targetProgressor) return; targetProgressor.fromValue = minValue; targetProgressor.toValue = maxValue; targetProgressor.SetValueAt(value); } private void UpdateTargetProgressorValue() { if (!targetProgressor) return; targetProgressor.SetValueAt(value); } private void PlayTargetProgressorValue() { if (!targetProgressor) return; targetProgressor.PlayToValue(value); } private void UpdateTargetSliderMinMax() { if (targetSlider == null) return; targetSlider.minValue = minValue; targetSlider.maxValue = maxValue; } private void UpdateTargetSliderValue(float newValue) { if (targetSlider == null) return; targetSlider.SetValueWithoutNotify(newValue); } #region Chainable Methods /// Reference a new UIButton to the minus button /// The UIButton to reference public T SetMinusButton(UIButton button) where T : UIStepper { minusButton = button; return (T)this; } /// Reference a new UIButton to the plus button /// The UIButton to reference public T SetPlusButton(UIButton button) where T : UIStepper { plusButton = button; return (T)this; } /// Reference a new TMP_Text to the value label /// The TMP_Text to reference public T SetValueLabel(TMP_Text label) where T : UIStepper { targetLabel = label; return (T)this; } /// Reference a new UIButton to the reset button /// The UIButton to reference public T SetResetButton(UIButton button) where T : UIStepper { resetButton = button; return (T)this; } /// Set a new min value /// The new min value public T SetMinValue(float newMinValue) where T : UIStepper { minValue = newMinValue; return (T)this; } /// Set a new max value /// The new max value public T SetMaxValue(float newMaxValue) where T : UIStepper { maxValue = newMaxValue; return (T)this; } /// Set a new default value (reset value) /// The new default value (reset value) public T SetDefaultValue(float newResetValue) where T : UIStepper { defaultValue = newResetValue; return (T)this; } /// Set a new target progressor to update when the value changes /// The new target progressor to update when the value changes public T SetTargetProgressor(Progressor progressor) where T : UIStepper { targetProgressor = progressor; return (T)this; } /// Set a new direction for the stepper /// The new direction for the stepper public T SetStepperDirection(Direction direction) where T : UIStepper { DragDirection = direction; return (T)this; } #endregion /// /// Flag used to determine if a drag can occur /// private bool cannotDrag => !DragEnabled || !canDrag; public void OnBeginDrag(PointerEventData eventData) { if (cannotDrag) return; isDragging = true; // m_DragHandleInitialPosition = DragHandle.anchoredPosition; } public void OnEndDrag(PointerEventData eventData) { if (cannotDrag) return; isDragging = false; dragHandle.anchoredPosition = m_DragHandleInitialPosition; } public void OnDrag(PointerEventData eventData) { if (cannotDrag) return; if (!isDragging) return; float iX = m_DragHandleInitialPosition.x; float iY = m_DragHandleInitialPosition.y; switch (DragDirection) { case Direction.Horizontal: float x = dragHandle.anchoredPosition.x + eventData.delta.x; x = Mathf.Clamp(x, iX - MaxDragDistance, iX + MaxDragDistance); dragHandle.anchoredPosition = new Vector2(x, m_DragHandleInitialPosition.y); break; case Direction.Vertical: float y = dragHandle.anchoredPosition.y + eventData.delta.y; y = Mathf.Clamp(y, iY - MaxDragDistance, iY + MaxDragDistance); dragHandle.anchoredPosition = new Vector2(m_DragHandleInitialPosition.x, y); break; default: throw new ArgumentOutOfRangeException(); } } private void LateUpdate() { if (!isDragging) { if (dragIncrementing) StopDragIncrement(); if (dragDecrementing) StopDragDecrement(); return; } if (autoIncrementing) StopAutoIncrement(); if (autoDecrementing) StopAutoDecrement(); if (draggedDistance > 0) { if (dragDecrementing) StopDragDecrement(); if (dragIncrementing) return; StartDragIncrement(); } else { if (dragIncrementing) StopDragIncrement(); if (dragDecrementing) return; StartDragDecrement(); } } #region Increment private bool CanIncrementValue() => value < maxValue; #region Auto Increment private void StartAutoIncrement() { StopAutoIncrement(); autoIncrementCoroutine = StartCoroutine(AutoIncrementValue()); } private void StopAutoIncrement() { autoIncrementing = false; if (autoIncrementCoroutine == null) return; StopCoroutine(autoIncrementCoroutine); } private IEnumerator AutoIncrementValue() { autoIncrementing = true; yield return new WaitForSecondsRealtime(WAIT_BEFORE_STARTING); float waitTime = AutoRepeatWaitTime; while (CanIncrementValue()) { IncrementValue(); yield return new WaitForSecondsRealtime(waitTime); waitTime = Mathf.Clamp(waitTime * AutoRepeatWaitTimeReduction, AutoRepeatMinWaitTime, AutoRepeatWaitTime); } autoIncrementing = false; autoIncrementCoroutine = null; } #endregion #region Drag Increment private void StartDragIncrement() { StopDragIncrement(); dragIncrementCoroutine = StartCoroutine(DragIncrementValue()); } private void StopDragIncrement() { dragIncrementing = false; if (dragIncrementCoroutine == null) return; StopCoroutine(dragIncrementCoroutine); } private IEnumerator DragIncrementValue() { dragIncrementing = true; while (CanIncrementValue()) { IncrementValue(); yield return new WaitForSecondsRealtime(dragWaitTime); } dragIncrementing = false; dragIncrementCoroutine = null; } #endregion #endregion #region Decrement private bool CanDecrementValue() => value > minValue; #region Auto Decrement private void StartAutoDecrement() { StopAutoDecrement(); autoDecrementCoroutine = StartCoroutine(AutoDecrementValue()); } private void StopAutoDecrement() { autoDecrementing = false; if (autoDecrementCoroutine == null) return; StopCoroutine(autoDecrementCoroutine); } private IEnumerator AutoDecrementValue() { autoDecrementing = true; yield return new WaitForSecondsRealtime(WAIT_BEFORE_STARTING); float waitTime = AutoRepeatWaitTime; while (CanDecrementValue()) { DecrementValue(); yield return new WaitForSecondsRealtime(waitTime); waitTime = Mathf.Clamp(waitTime * AutoRepeatWaitTimeReduction, AutoRepeatMinWaitTime, AutoRepeatWaitTime); } autoDecrementing = false; autoDecrementCoroutine = null; } #endregion #region Drag Decrement private void StartDragDecrement() { StopDragDecrement(); dragDecrementCoroutine = StartCoroutine(DragDecrementValue()); } private void StopDragDecrement() { dragDecrementing = false; if (dragDecrementCoroutine == null) return; StopCoroutine(dragDecrementCoroutine); } private IEnumerator DragDecrementValue() { dragDecrementing = true; while (CanDecrementValue()) { DecrementValue(); yield return new WaitForSecondsRealtime(dragWaitTime); } dragDecrementing = false; dragDecrementCoroutine = null; } #endregion #endregion /// Get the nearest value to the given value that is a multiple of the step size. /// Value that has not been corrected. private float NearestStep(float uncorrectedValue) { if (Step == 0) return uncorrectedValue; return (int)Math.Round(uncorrectedValue / (double)step, MidpointRounding.AwayFromZero) * step; } } }