chore: initial commit

This commit is contained in:
2026-05-08 11:04:00 +08:00
commit f55d2a57c3
6278 changed files with 866081 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 244b68853eb429a4a8fabd54b720495a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,598 @@
using System;
using System.Collections;
using System.Collections.Generic;
using MoreMountains.Tools;
using UnityEngine;
using Random = UnityEngine.Random;
namespace MoreMountains.Feedbacks
{
/// <summary>
/// A base class, meant to be extended, defining a Feedback. A Feedback is an action triggered by a MMFeedbacks, usually in reaction to the player's input or actions,
/// to help communicate both emotion and legibility, improving game feel.
/// To create a new feedback, extend this class and override its Custom methods, declared at the end of this class. You can look at the many examples for reference.
/// </summary>
[AddComponentMenu("")]
[System.Serializable]
[ExecuteAlways]
public abstract class MMFeedback : MonoBehaviour
{
/// whether or not this feedback is active
[Tooltip("whether or not this feedback is active")]
public bool Active = true;
/// the name of this feedback to display in the inspector
[Tooltip("the name of this feedback to display in the inspector")]
public string Label = "MMFeedback";
/// the chance of this feedback happening (in percent : 100 : happens all the time, 0 : never happens, 50 : happens once every two calls, etc)
[Tooltip("the chance of this feedback happening (in percent : 100 : happens all the time, 0 : never happens, 50 : happens once every two calls, etc)")]
[Range(0,100)]
public float Chance = 100f;
/// a number of timing-related values (delay, repeat, etc)
[Tooltip("a number of timing-related values (delay, repeat, etc)")]
public MMFeedbackTiming Timing;
/// the Owner of the feedback, as defined when calling the Initialization method
public GameObject Owner { get; set; }
[HideInInspector]
/// whether or not this feedback is in debug mode
public bool DebugActive = false;
/// set this to true if your feedback should pause the execution of the feedback sequence
public virtual IEnumerator Pause { get { return null; } }
/// if this is true, this feedback will wait until all previous feedbacks have run
public virtual bool HoldingPause { get { return false; } }
/// if this is true, this feedback will wait until all previous feedbacks have run, then run all previous feedbacks again
public virtual bool LooperPause { get { return false; } }
/// if this is true, this feedback will pause and wait until Resume() is called on its parent MMFeedbacks to resume execution
public virtual bool ScriptDrivenPause { get; set; }
/// if this is a positive value, the feedback will auto resume after that duration if it hasn't been resumed via script already
public virtual float ScriptDrivenPauseAutoResume { get; set; }
/// if this is true, this feedback will wait until all previous feedbacks have run, then run all previous feedbacks again
public virtual bool LooperStart { get { return false; } }
/// an overridable color for your feedback, that can be redefined per feedback. White is the only reserved color, and the feedback will revert to
/// normal (light or dark skin) when left to White
#if UNITY_EDITOR
public virtual Color FeedbackColor { get { return Color.white; } }
#endif
/// returns true if this feedback is in cooldown at this time (and thus can't play), false otherwise
public virtual bool InCooldown { get { return (Timing.CooldownDuration > 0f) && (FeedbackTime - _lastPlayTimestamp < Timing.CooldownDuration); } }
/// if this is true, this feedback is currently playing
public virtual bool IsPlaying { get; set; }
/// the time (or unscaled time) based on the selected Timing settings
public float FeedbackTime
{
get
{
if (Timing.TimescaleMode == TimescaleModes.Scaled)
{
return Time.time;
}
else
{
return Time.unscaledTime;
}
}
}
/// the delta time (or unscaled delta time) based on the selected Timing settings
public float FeedbackDeltaTime
{
get
{
if (Timing.TimescaleMode == TimescaleModes.Scaled)
{
return Time.deltaTime;
}
else
{
return Time.unscaledDeltaTime;
}
}
}
/// <summary>
/// The total duration of this feedback :
/// total = initial delay + duration * (number of repeats + delay between repeats)
/// </summary>
public float TotalDuration
{
get
{
if ((Timing != null) && (!Timing.ContributeToTotalDuration))
{
return 0f;
}
float totalTime = 0f;
if (Timing == null)
{
return 0f;
}
if (Timing.InitialDelay != 0)
{
totalTime += ApplyTimeMultiplier(Timing.InitialDelay);
}
totalTime += FeedbackDuration;
if (Timing.NumberOfRepeats > 0)
{
float delayBetweenRepeats = ApplyTimeMultiplier(Timing.DelayBetweenRepeats);
totalTime += (Timing.NumberOfRepeats * FeedbackDuration) + (Timing.NumberOfRepeats * delayBetweenRepeats);
}
return totalTime;
}
}
// the timestamp at which this feedback was last played
public virtual float FeedbackStartedAt { get { return _lastPlayTimestamp; } }
// the perceived duration of the feedback, to be used to display its progress bar, meant to be overridden with meaningful data by each feedback
public virtual float FeedbackDuration { get { return 0f; } set { } }
/// whether or not this feedback is playing right now
public virtual bool FeedbackPlaying { get { return ((FeedbackStartedAt > 0f) && (Time.time - FeedbackStartedAt < FeedbackDuration)); } }
public virtual MMChannelData ChannelData(int channel) => _channelData.Set(MMChannelModes.Int, channel, null);
protected float _lastPlayTimestamp = -1f;
protected int _playsLeft;
protected bool _initialized = false;
protected Coroutine _playCoroutine;
protected Coroutine _infinitePlayCoroutine;
protected Coroutine _sequenceCoroutine;
protected Coroutine _repeatedPlayCoroutine;
protected int _sequenceTrackID = 0;
protected MMFeedbacks _hostMMFeedbacks;
protected float _beatInterval;
protected bool BeatThisFrame = false;
protected int LastBeatIndex = 0;
protected int CurrentSequenceIndex = 0;
protected float LastBeatTimestamp = 0f;
protected bool _isHostMMFeedbacksNotNull;
protected MMChannelData _channelData;
protected virtual void OnEnable()
{
_hostMMFeedbacks = this.gameObject.GetComponent<MMFeedbacks>();
_isHostMMFeedbacksNotNull = _hostMMFeedbacks != null;
}
/// <summary>
/// Initializes the feedback and its timing related variables
/// </summary>
/// <param name="owner"></param>
public virtual void Initialization(GameObject owner)
{
_initialized = true;
Owner = owner;
_playsLeft = Timing.NumberOfRepeats + 1;
_hostMMFeedbacks = this.gameObject.GetComponent<MMFeedbacks>();
_channelData = new MMChannelData(MMChannelModes.Int, 0, null);
SetInitialDelay(Timing.InitialDelay);
SetDelayBetweenRepeats(Timing.DelayBetweenRepeats);
SetSequence(Timing.Sequence);
CustomInitialization(owner);
}
/// <summary>
/// Plays the feedback
/// </summary>
/// <param name="position"></param>
/// <param name="feedbacksIntensity"></param>
public virtual void Play(Vector3 position, float feedbacksIntensity = 1.0f)
{
if (!Active)
{
return;
}
if (!_initialized)
{
Debug.LogWarning("The " + this + " feedback is being played without having been initialized. Call Initialization() first.");
}
// we check the cooldown
if (InCooldown)
{
return;
}
if (Timing.InitialDelay > 0f)
{
_playCoroutine = StartCoroutine(PlayCoroutine(position, feedbacksIntensity));
}
else
{
_lastPlayTimestamp = FeedbackTime;
RegularPlay(position, feedbacksIntensity);
}
}
/// <summary>
/// An internal coroutine delaying the initial play of the feedback
/// </summary>
/// <param name="position"></param>
/// <param name="feedbacksIntensity"></param>
/// <returns></returns>
protected virtual IEnumerator PlayCoroutine(Vector3 position, float feedbacksIntensity = 1.0f)
{
if (Timing.TimescaleMode == TimescaleModes.Scaled)
{
yield return MMFeedbacksCoroutine.WaitFor(Timing.InitialDelay);
}
else
{
yield return MMFeedbacksCoroutine.WaitForUnscaled(Timing.InitialDelay);
}
_lastPlayTimestamp = FeedbackTime;
RegularPlay(position, feedbacksIntensity);
}
/// <summary>
/// Triggers delaying coroutines if needed
/// </summary>
/// <param name="position"></param>
/// <param name="feedbacksIntensity"></param>
protected virtual void RegularPlay(Vector3 position, float feedbacksIntensity = 1.0f)
{
if (Chance == 0f)
{
return;
}
if (Chance != 100f)
{
// determine the odds
float random = Random.Range(0f, 100f);
if (random > Chance)
{
return;
}
}
if (Timing.UseIntensityInterval)
{
if ((feedbacksIntensity < Timing.IntensityIntervalMin) || (feedbacksIntensity >= Timing.IntensityIntervalMax))
{
return;
}
}
if (Timing.RepeatForever)
{
_infinitePlayCoroutine = StartCoroutine(InfinitePlay(position, feedbacksIntensity));
return;
}
if (Timing.NumberOfRepeats > 0)
{
_repeatedPlayCoroutine = StartCoroutine(RepeatedPlay(position, feedbacksIntensity));
return;
}
if (Timing.Sequence == null)
{
CustomPlayFeedback(position, feedbacksIntensity);
}
else
{
_sequenceCoroutine = StartCoroutine(SequenceCoroutine(position, feedbacksIntensity));
}
}
/// <summary>
/// Internal coroutine used for repeated play without end
/// </summary>
/// <param name="position"></param>
/// <param name="feedbacksIntensity"></param>
/// <returns></returns>
protected virtual IEnumerator InfinitePlay(Vector3 position, float feedbacksIntensity = 1.0f)
{
while (true)
{
_lastPlayTimestamp = FeedbackTime;
if (Timing.Sequence == null)
{
CustomPlayFeedback(position, feedbacksIntensity);
if (Timing.TimescaleMode == TimescaleModes.Scaled)
{
yield return MMFeedbacksCoroutine.WaitFor(Timing.DelayBetweenRepeats);
}
else
{
yield return MMFeedbacksCoroutine.WaitForUnscaled(Timing.DelayBetweenRepeats);
}
}
else
{
_sequenceCoroutine = StartCoroutine(SequenceCoroutine(position, feedbacksIntensity));
float delay = ApplyTimeMultiplier(Timing.DelayBetweenRepeats) + Timing.Sequence.Length;
if (Timing.TimescaleMode == TimescaleModes.Scaled)
{
yield return MMFeedbacksCoroutine.WaitFor(delay);
}
else
{
yield return MMFeedbacksCoroutine.WaitForUnscaled(delay);
}
}
}
}
/// <summary>
/// Internal coroutine used for repeated play
/// </summary>
/// <param name="position"></param>
/// <param name="feedbacksIntensity"></param>
/// <returns></returns>
protected virtual IEnumerator RepeatedPlay(Vector3 position, float feedbacksIntensity = 1.0f)
{
while (_playsLeft > 0)
{
_lastPlayTimestamp = FeedbackTime;
_playsLeft--;
if (Timing.Sequence == null)
{
CustomPlayFeedback(position, feedbacksIntensity);
if (Timing.TimescaleMode == TimescaleModes.Scaled)
{
yield return MMFeedbacksCoroutine.WaitFor(Timing.DelayBetweenRepeats);
}
else
{
yield return MMFeedbacksCoroutine.WaitForUnscaled(Timing.DelayBetweenRepeats);
}
}
else
{
_sequenceCoroutine = StartCoroutine(SequenceCoroutine(position, feedbacksIntensity));
float delay = ApplyTimeMultiplier(Timing.DelayBetweenRepeats) + Timing.Sequence.Length;
if (Timing.TimescaleMode == TimescaleModes.Scaled)
{
yield return MMFeedbacksCoroutine.WaitFor(delay);
}
else
{
yield return MMFeedbacksCoroutine.WaitForUnscaled(delay);
}
}
}
_playsLeft = Timing.NumberOfRepeats + 1;
}
/// <summary>
/// A coroutine used to play this feedback on a sequence
/// </summary>
/// <param name="position"></param>
/// <param name="feedbacksIntensity"></param>
/// <returns></returns>
protected virtual IEnumerator SequenceCoroutine(Vector3 position, float feedbacksIntensity = 1.0f)
{
yield return null;
float timeStartedAt = FeedbackTime;
float lastFrame = FeedbackTime;
BeatThisFrame = false;
LastBeatIndex = 0;
CurrentSequenceIndex = 0;
LastBeatTimestamp = 0f;
if (Timing.Quantized)
{
while (CurrentSequenceIndex < Timing.Sequence.QuantizedSequence[0].Line.Count)
{
_beatInterval = 60f / Timing.TargetBPM;
if ((FeedbackTime - LastBeatTimestamp >= _beatInterval) || (LastBeatTimestamp == 0f))
{
BeatThisFrame = true;
LastBeatIndex = CurrentSequenceIndex;
LastBeatTimestamp = FeedbackTime;
for (int i = 0; i < Timing.Sequence.SequenceTracks.Count; i++)
{
if (Timing.Sequence.QuantizedSequence[i].Line[CurrentSequenceIndex].ID == Timing.TrackID)
{
CustomPlayFeedback(position, feedbacksIntensity);
}
}
CurrentSequenceIndex++;
}
yield return null;
}
}
else
{
while (FeedbackTime - timeStartedAt < Timing.Sequence.Length)
{
foreach (MMSequenceNote item in Timing.Sequence.OriginalSequence.Line)
{
if ((item.ID == Timing.TrackID) && (item.Timestamp >= lastFrame) && (item.Timestamp <= FeedbackTime - timeStartedAt))
{
CustomPlayFeedback(position, feedbacksIntensity);
}
}
lastFrame = FeedbackTime - timeStartedAt;
yield return null;
}
}
}
/// <summary>
/// Stops all feedbacks from playing. Will stop repeating feedbacks, and call custom stop implementations
/// </summary>
/// <param name="position"></param>
/// <param name="feedbacksIntensity"></param>
public virtual void Stop(Vector3 position, float feedbacksIntensity = 1.0f)
{
if (_playCoroutine != null) { StopCoroutine(_playCoroutine); }
if (_infinitePlayCoroutine != null) { StopCoroutine(_infinitePlayCoroutine); }
if (_repeatedPlayCoroutine != null) { StopCoroutine(_repeatedPlayCoroutine); }
if (_sequenceCoroutine != null) { StopCoroutine(_sequenceCoroutine); }
_lastPlayTimestamp = 0f;
_playsLeft = Timing.NumberOfRepeats + 1;
if (Timing.InterruptsOnStop)
{
CustomStopFeedback(position, feedbacksIntensity);
}
}
/// <summary>
/// Calls this feedback's custom reset
/// </summary>
public virtual void ResetFeedback()
{
_playsLeft = Timing.NumberOfRepeats + 1;
CustomReset();
}
/// <summary>
/// Use this method to change this feedback's sequence at runtime
/// </summary>
/// <param name="newSequence"></param>
public virtual void SetSequence(MMSequence newSequence)
{
Timing.Sequence = newSequence;
if (Timing.Sequence != null)
{
for (int i = 0; i < Timing.Sequence.SequenceTracks.Count; i++)
{
if (Timing.Sequence.SequenceTracks[i].ID == Timing.TrackID)
{
_sequenceTrackID = i;
}
}
}
}
/// <summary>
/// Use this method to specify a new delay between repeats at runtime
/// </summary>
/// <param name="delay"></param>
public virtual void SetDelayBetweenRepeats(float delay)
{
Timing.DelayBetweenRepeats = delay;
}
/// <summary>
/// Use this method to specify a new initial delay at runtime
/// </summary>
/// <param name="delay"></param>
public virtual void SetInitialDelay(float delay)
{
Timing.InitialDelay = delay;
}
/// <summary>
/// Returns a new value of the normalized time based on the current play direction of this feedback
/// </summary>
/// <param name="normalizedTime"></param>
/// <returns></returns>
protected virtual float ApplyDirection(float normalizedTime)
{
return NormalPlayDirection ? normalizedTime : 1 - normalizedTime;
}
/// <summary>
/// Returns true if this feedback should play normally, or false if it should play in rewind
/// </summary>
public virtual bool NormalPlayDirection
{
get
{
switch (Timing.PlayDirection)
{
case MMFeedbackTiming.PlayDirections.FollowMMFeedbacksDirection:
return (_hostMMFeedbacks.Direction == MMFeedbacks.Directions.TopToBottom);
case MMFeedbackTiming.PlayDirections.AlwaysNormal:
return true;
case MMFeedbackTiming.PlayDirections.AlwaysRewind:
return false;
case MMFeedbackTiming.PlayDirections.OppositeMMFeedbacksDirection:
return !(_hostMMFeedbacks.Direction == MMFeedbacks.Directions.TopToBottom);
}
return true;
}
}
/// <summary>
/// Returns true if this feedback should play in the current parent MMFeedbacks direction, according to its MMFeedbacksDirectionCondition setting
/// </summary>
public virtual bool ShouldPlayInThisSequenceDirection
{
get
{
switch (Timing.MMFeedbacksDirectionCondition)
{
case MMFeedbackTiming.MMFeedbacksDirectionConditions.Always:
return true;
case MMFeedbackTiming.MMFeedbacksDirectionConditions.OnlyWhenForwards:
return (_hostMMFeedbacks.Direction == MMFeedbacks.Directions.TopToBottom);
case MMFeedbackTiming.MMFeedbacksDirectionConditions.OnlyWhenBackwards:
return (_hostMMFeedbacks.Direction == MMFeedbacks.Directions.BottomToTop);
}
return true;
}
}
/// <summary>
/// Returns the t value at which to evaluate a curve at the end of this feedback's play time
/// </summary>
protected virtual float FinalNormalizedTime
{
get
{
return NormalPlayDirection ? 1f : 0f;
}
}
/// <summary>
/// Applies the host MMFeedbacks' time multiplier to this feedback
/// </summary>
/// <param name="duration"></param>
/// <returns></returns>
protected virtual float ApplyTimeMultiplier(float duration)
{
if (_isHostMMFeedbacksNotNull)
{
return _hostMMFeedbacks.ApplyTimeMultiplier(duration);
}
return duration;
}
/// <summary>
/// This method describes all custom initialization processes the feedback requires, in addition to the main Initialization method
/// </summary>
/// <param name="owner"></param>
protected virtual void CustomInitialization(GameObject owner) { }
/// <summary>
/// This method describes what happens when the feedback gets played
/// </summary>
/// <param name="position"></param>
/// <param name="feedbacksIntensity"></param>
protected abstract void CustomPlayFeedback(Vector3 position, float feedbacksIntensity = 1.0f);
/// <summary>
/// This method describes what happens when the feedback gets stopped
/// </summary>
/// <param name="position"></param>
/// <param name="feedbacksIntensity"></param>
protected virtual void CustomStopFeedback(Vector3 position, float feedbacksIntensity = 1.0f) { }
/// <summary>
/// This method describes what happens when the feedback gets reset
/// </summary>
protected virtual void CustomReset() { }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1b1d3a32ffe67b94187272c15134cae5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,924 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using MoreMountains.Feedbacks;
using System.Linq;
using MoreMountains.Tools;
using UnityEditor.Experimental;
using UnityEngine.Events;
using Random = UnityEngine.Random;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace MoreMountains.Feedbacks
{
/// <summary>
/// A collection of MMFeedback, meant to be played altogether.
/// This class provides a custom inspector to add and customize feedbacks, and public methods to trigger them, stop them, etc.
/// You can either use it on its own, or bind it from another class and trigger it from there.
/// </summary>
[AddComponentMenu("")]
public class MMFeedbacks : MonoBehaviour
{
/// the possible directions MMFeedbacks can be played
public enum Directions { TopToBottom, BottomToTop }
/// the possible SafeModes (will perform checks to make sure no serialization error has damaged them)
/// - nope : no safety
/// - editor only : performs checks on enable
/// - runtime only : performs checks on Awake
/// - full : performs both editor and runtime checks, recommended setting
public enum SafeModes { Nope, EditorOnly, RuntimeOnly, Full }
/// a list of MMFeedback to trigger
public List<MMFeedback> Feedbacks = new List<MMFeedback>();
/// the possible initialization modes. If you use Script, you'll have to initialize manually by calling the Initialization method and passing it an owner
/// Otherwise, you can have this component initialize itself at Awake or Start, and in this case the owner will be the MMFeedbacks itself
public enum InitializationModes { Script, Awake, Start }
/// the chosen initialization mode
[Tooltip("the chosen initialization modes. If you use Script, you'll have to initialize manually by calling the " +
"Initialization method and passing it an owner. Otherwise, you can have this component initialize " +
"itself at Awake or Start, and in this case the owner will be the MMFeedbacks itself")]
public InitializationModes InitializationMode = InitializationModes.Start;
/// if you set this to true, the system will make changes to ensure that initialization always happens before play
[Tooltip("if you set this to true, the system will make changes to ensure that initialization always happens before play")]
public bool AutoInitialization = true;
/// the selected safe mode
[Tooltip("the selected safe mode")]
public SafeModes SafeMode = SafeModes.Full;
/// the selected direction
[Tooltip("the selected direction these feedbacks should play in")]
public Directions Direction = Directions.TopToBottom;
/// whether or not this MMFeedbacks should invert its direction when all feedbacks have played
[Tooltip("whether or not this MMFeedbacks should invert its direction when all feedbacks have played")]
public bool AutoChangeDirectionOnEnd = false;
/// whether or not to play this feedbacks automatically on Start
[Tooltip("whether or not to play this feedbacks automatically on Start")]
public bool AutoPlayOnStart = false;
/// whether or not to play this feedbacks automatically on Enable
[Tooltip("whether or not to play this feedbacks automatically on Enable")]
public bool AutoPlayOnEnable = false;
/// if this is true, all feedbacks within that player will work on the specified ForcedTimescaleMode, regardless of their individual settings
[Tooltip("if this is true, all feedbacks within that player will work on the specified ForcedTimescaleMode, regardless of their individual settings")]
public bool ForceTimescaleMode = false;
/// the time scale mode all feedbacks on this player should work on, if ForceTimescaleMode is true
[Tooltip("the time scale mode all feedbacks on this player should work on, if ForceTimescaleMode is true")]
[MMFCondition("ForceTimescaleMode", true)]
public TimescaleModes ForcedTimescaleMode = TimescaleModes.Unscaled;
/// a time multiplier that will be applied to all feedback durations (initial delay, duration, delay between repeats...)
[Tooltip("a time multiplier that will be applied to all feedback durations (initial delay, duration, delay between repeats...)")]
public float DurationMultiplier = 1f;
/// a multiplier to apply to all timescale operations (1: normal, less than 1: slower operations, higher than 1: faster operations)
[Tooltip("a multiplier to apply to all timescale operations (1: normal, less than 1: slower operations, higher than 1: faster operations)")]
public float TimescaleMultiplier = 1f;
/// if this is true, will expose a RandomDurationMultiplier. The final duration of each feedback will be : their base duration * DurationMultiplier * a random value between RandomDurationMultiplier.x and RandomDurationMultiplier.y
[Tooltip("if this is true, will expose a RandomDurationMultiplier. The final duration of each feedback will be : their base duration * DurationMultiplier * a random value between RandomDurationMultiplier.x and RandomDurationMultiplier.y")]
public bool RandomizeDuration = false;
/// if RandomizeDuration is true, the min (x) and max (y) values for the random duration multiplier
[Tooltip("if RandomizeDuration is true, the min (x) and max (y) values for the random duration multiplier")]
[MMCondition("RandomizeDuration", true)]
public Vector2 RandomDurationMultiplier = new Vector2(0.5f, 1.5f);
/// if this is true, more editor-only, detailed info will be displayed per feedback in the duration slot
[Tooltip("if this is true, more editor-only, detailed info will be displayed per feedback in the duration slot")]
public bool DisplayFullDurationDetails = false;
/// the timescale at which the player itself will operate. This notably impacts sequencing and pauses duration evaluation.
[Tooltip("the timescale at which the player itself will operate. This notably impacts sequencing and pauses duration evaluation.")]
public TimescaleModes PlayerTimescaleMode = TimescaleModes.Unscaled;
/// if this is true, this feedback will only play if its distance to RangeCenter is lower or equal to RangeDistance
[Tooltip("if this is true, this feedback will only play if its distance to RangeCenter is lower or equal to RangeDistance")]
public bool OnlyPlayIfWithinRange = false;
/// when in OnlyPlayIfWithinRange mode, the transform to consider as the center of the range
[Tooltip("when in OnlyPlayIfWithinRange mode, the transform to consider as the center of the range")]
public Transform RangeCenter;
/// when in OnlyPlayIfWithinRange mode, the distance to the center within which the feedback will play
[Tooltip("when in OnlyPlayIfWithinRange mode, the distance to the center within which the feedback will play")]
public float RangeDistance = 5f;
/// when in OnlyPlayIfWithinRange mode, whether or not to modify the intensity of feedbacks based on the RangeFallOff curve
[Tooltip("when in OnlyPlayIfWithinRange mode, whether or not to modify the intensity of feedbacks based on the RangeFallOff curve")]
public bool UseRangeFalloff = false;
/// the animation curve to use to define falloff (on the x 0 represents the range center, 1 represents the max distance to it)
[Tooltip("the animation curve to use to define falloff (on the x 0 represents the range center, 1 represents the max distance to it)")]
[MMFCondition("UseRangeFalloff", true)]
public AnimationCurve RangeFalloff = new AnimationCurve(new Keyframe(0f, 1f), new Keyframe(1f, 0f));
/// the values to remap the falloff curve's y axis' 0 and 1
[Tooltip("the values to remap the falloff curve's y axis' 0 and 1")]
[MMFVector("Zero","One")]
public Vector2 RemapRangeFalloff = new Vector2(0f, 1f);
/// whether or not to ignore MMSetFeedbackRangeCenterEvent, used to set the RangeCenter from anywhere
[Tooltip("whether or not to ignore MMSetFeedbackRangeCenterEvent, used to set the RangeCenter from anywhere")]
public bool IgnoreRangeEvents = false;
/// a duration, in seconds, during which triggering a new play of this MMFeedbacks after it's been played once will be impossible
[Tooltip("a duration, in seconds, during which triggering a new play of this MMFeedbacks after it's been played once will be impossible")]
public float CooldownDuration = 0f;
/// a duration, in seconds, to delay the start of this MMFeedbacks' contents play
[Tooltip("a duration, in seconds, to delay the start of this MMFeedbacks' contents play")]
public float InitialDelay = 0f;
/// whether this player can be played or not, useful to temporarily prevent play from another class, for example
[Tooltip("whether this player can be played or not, useful to temporarily prevent play from another class, for example")]
public bool CanPlay = true;
/// if this is true, you'll be able to trigger a new Play while this feedback is already playing, otherwise you won't be able to
[Tooltip("if this is true, you'll be able to trigger a new Play while this feedback is already playing, otherwise you won't be able to")]
public bool CanPlayWhileAlreadyPlaying = true;
/// the chance of this sequence happening (in percent : 100 : happens all the time, 0 : never happens, 50 : happens once every two calls, etc)
[Tooltip("the chance of this sequence happening (in percent : 100 : happens all the time, 0 : never happens, 50 : happens once every two calls, etc)")]
[Range(0,100)]
public float ChanceToPlay = 100f;
/// the intensity at which to play this feedback. That value will be used by most feedbacks to tune their amplitude. 1 is normal, 0.5 is half power, 0 is no effect.
/// Note that what this value controls depends from feedback to feedback, don't hesitate to check the code to see what it does exactly.
[Tooltip("the intensity at which to play this feedback. That value will be used by most feedbacks to tune their amplitude. 1 is normal, 0.5 is half power, 0 is no effect." +
"Note that what this value controls depends from feedback to feedback, don't hesitate to check the code to see what it does exactly.")]
public float FeedbacksIntensity = 1f;
/// a number of UnityEvents that can be triggered at the various stages of this MMFeedbacks
[Tooltip("a number of UnityEvents that can be triggered at the various stages of this MMFeedbacks")]
public MMFeedbacksEvents Events;
/// a global switch used to turn all feedbacks on or off globally
[Tooltip("a global switch used to turn all feedbacks on or off globally")]
public static bool GlobalMMFeedbacksActive = true;
[HideInInspector]
/// whether or not this MMFeedbacks is in debug mode
public bool DebugActive = false;
/// whether or not this MMFeedbacks is playing right now - meaning it hasn't been stopped yet.
/// if you don't stop your MMFeedbacks it'll remain true of course
public bool IsPlaying { get; protected set; }
/// if this MMFeedbacks is playing the time since it started playing
public virtual float ElapsedTime => IsPlaying ? GetTime() - _lastStartAt : 0f;
/// the amount of times this MMFeedbacks has been played
public int TimesPlayed { get; protected set; }
/// whether or not the execution of this MMFeedbacks' sequence is being prevented and waiting for a Resume() call
public bool InScriptDrivenPause { get; set; }
/// true if this MMFeedbacks contains at least one loop
public bool ContainsLoop { get; set; }
/// true if this feedback should change play direction next time it's played
public bool ShouldRevertOnNextPlay { get; set; }
/// true if this player is forcing unscaled mode
public bool ForcingUnscaledTimescaleMode { get { return (ForceTimescaleMode && ForcedTimescaleMode == TimescaleModes.Unscaled); } }
/// The total duration (in seconds) of all the active feedbacks in this MMFeedbacks
public virtual float TotalDuration
{
get
{
float total = 0f;
foreach (MMFeedback feedback in Feedbacks)
{
if ((feedback != null) && (feedback.Active))
{
if (total < feedback.TotalDuration)
{
total = feedback.TotalDuration;
}
}
}
return ComputedInitialDelay + total;
}
}
public virtual float GetTime() { return (PlayerTimescaleMode == TimescaleModes.Scaled) ? Time.time : Time.unscaledTime; }
public virtual float GetDeltaTime() { return (PlayerTimescaleMode == TimescaleModes.Scaled) ? Time.deltaTime : Time.unscaledDeltaTime; }
public virtual float ComputedInitialDelay => ApplyTimeMultiplier(InitialDelay);
protected float _startTime = 0f;
protected float _holdingMax = 0f;
protected float _lastStartAt = -float.MaxValue;
protected int _lastStartFrame = -1;
protected bool _pauseFound = false;
protected float _totalDuration = 0f;
protected bool _shouldStop = false;
protected const float _smallValue = 0.001f;
protected float _randomDurationMultiplier = 1f;
protected float _lastOnEnableFrame = -1;
#region INITIALIZATION
/// <summary>
/// On Awake we initialize our feedbacks if we're in auto mode
/// </summary>
protected virtual void Awake()
{
// if our MMFeedbacks is in AutoPlayOnEnable mode, we add a little helper to it that will re-enable it if needed if the parent game object gets turned off and on again
if (AutoPlayOnEnable)
{
MMFeedbacksEnabler enabler = GetComponent<MMFeedbacksEnabler>();
if (enabler == null)
{
enabler = this.gameObject.AddComponent<MMFeedbacksEnabler>();
}
enabler.TargetMMFeedbacks = this;
}
if ((InitializationMode == InitializationModes.Awake) && (Application.isPlaying))
{
Initialization(this.gameObject);
}
CheckForLoops();
}
/// <summary>
/// On Start we initialize our feedbacks if we're in auto mode
/// </summary>
protected virtual void Start()
{
if ((InitializationMode == InitializationModes.Start) && (Application.isPlaying))
{
Initialization(this.gameObject);
}
if (AutoPlayOnStart && Application.isPlaying)
{
PlayFeedbacks();
}
CheckForLoops();
}
/// <summary>
/// On Enable we initialize our feedbacks if we're in auto mode
/// </summary>
protected virtual void OnEnable()
{
if (AutoPlayOnEnable && Application.isPlaying)
{
PlayFeedbacks();
}
}
/// <summary>
/// Initializes the MMFeedbacks, setting this MMFeedbacks as the owner
/// </summary>
public virtual void Initialization(bool forceInitIfPlaying = false)
{
Initialization(this.gameObject);
}
/// <summary>
/// A public method to initialize the feedback, specifying an owner that will be used as the reference for position and hierarchy by feedbacks
/// </summary>
/// <param name="owner"></param>
/// <param name="feedbacksOwner"></param>
public virtual void Initialization(GameObject owner)
{
if ((SafeMode == MMFeedbacks.SafeModes.RuntimeOnly) || (SafeMode == MMFeedbacks.SafeModes.Full))
{
AutoRepair();
}
IsPlaying = false;
TimesPlayed = 0;
_lastStartAt = -float.MaxValue;
for (int i = 0; i < Feedbacks.Count; i++)
{
if (Feedbacks[i] != null)
{
Feedbacks[i].Initialization(owner);
}
}
}
#endregion
#region PLAY
/// <summary>
/// Plays all feedbacks using the MMFeedbacks' position as reference, and no attenuation
/// </summary>
public virtual void PlayFeedbacks()
{
PlayFeedbacksInternal(this.transform.position, FeedbacksIntensity);
}
/// <summary>
/// Plays all feedbacks and awaits until completion
/// </summary>
/// <param name="position"></param>
/// <param name="feedbacksIntensity"></param>
/// <param name="forceRevert"></param>
public virtual async System.Threading.Tasks.Task PlayFeedbacksTask(Vector3 position, float feedbacksIntensity = 1.0f, bool forceRevert = false)
{
PlayFeedbacks(position, feedbacksIntensity, forceRevert);
while (IsPlaying)
{
await System.Threading.Tasks.Task.Yield();
}
}
/// <summary>
/// Plays all feedbacks and awaits until completion
/// </summary>
public virtual async System.Threading.Tasks.Task PlayFeedbacksTask()
{
PlayFeedbacks();
while (IsPlaying)
{
await System.Threading.Tasks.Task.Yield();
}
}
/// <summary>
/// Plays all feedbacks, specifying a position and intensity. The position may be used by each Feedback and taken into account to spark a particle or play a sound for example.
/// The feedbacks intensity is a factor that can be used by each Feedback to lower its intensity, usually you'll want to define that attenuation based on time or distance (using a lower
/// intensity value for feedbacks happening further away from the Player).
/// Additionally you can force the feedback to play in reverse, ignoring its current condition
/// </summary>
/// <param name="position"></param>
/// <param name="feedbacksOwner"></param>
/// <param name="feedbacksIntensity"></param>
public virtual void PlayFeedbacks(Vector3 position, float feedbacksIntensity = 1.0f, bool forceRevert = false)
{
PlayFeedbacksInternal(position, feedbacksIntensity, forceRevert);
}
/// <summary>
/// Plays all feedbacks using the MMFeedbacks' position as reference, and no attenuation, and in reverse (from bottom to top)
/// </summary>
public virtual void PlayFeedbacksInReverse()
{
PlayFeedbacksInternal(this.transform.position, FeedbacksIntensity, true);
}
/// <summary>
/// Plays all feedbacks using the MMFeedbacks' position as reference, and no attenuation, and in reverse (from bottom to top)
/// </summary>
public virtual void PlayFeedbacksInReverse(Vector3 position, float feedbacksIntensity = 1.0f, bool forceRevert = false)
{
PlayFeedbacksInternal(position, feedbacksIntensity, forceRevert);
}
/// <summary>
/// Plays all feedbacks in the sequence, but only if this MMFeedbacks is playing in reverse order
/// </summary>
public virtual void PlayFeedbacksOnlyIfReversed()
{
if ( (Direction == Directions.BottomToTop && !ShouldRevertOnNextPlay)
|| ((Direction == Directions.TopToBottom) && ShouldRevertOnNextPlay) )
{
PlayFeedbacks();
}
}
/// <summary>
/// Plays all feedbacks in the sequence, but only if this MMFeedbacks is playing in reverse order
/// </summary>
public virtual void PlayFeedbacksOnlyIfReversed(Vector3 position, float feedbacksIntensity = 1.0f, bool forceRevert = false)
{
if ( (Direction == Directions.BottomToTop && !ShouldRevertOnNextPlay)
|| ((Direction == Directions.TopToBottom) && ShouldRevertOnNextPlay) )
{
PlayFeedbacks(position, feedbacksIntensity, forceRevert);
}
}
/// <summary>
/// Plays all feedbacks in the sequence, but only if this MMFeedbacks is playing in normal order
/// </summary>
public virtual void PlayFeedbacksOnlyIfNormalDirection()
{
if (Direction == Directions.TopToBottom)
{
PlayFeedbacks();
}
}
/// <summary>
/// Plays all feedbacks in the sequence, but only if this MMFeedbacks is playing in normal order
/// </summary>
public virtual void PlayFeedbacksOnlyIfNormalDirection(Vector3 position, float feedbacksIntensity = 1.0f, bool forceRevert = false)
{
if (Direction == Directions.TopToBottom)
{
PlayFeedbacks(position, feedbacksIntensity, forceRevert);
}
}
/// <summary>
/// A public coroutine you can call externally when you want to yield in a coroutine of yours until the MMFeedbacks has stopped playing
/// typically : yield return myFeedback.PlayFeedbacksCoroutine(this.transform.position, 1.0f, false);
/// </summary>
/// <param name="position">The position at which the MMFeedbacks should play</param>
/// <param name="feedbacksIntensity">The intensity of the feedback</param>
/// <param name="forceRevert">Whether or not the MMFeedbacks should play in reverse or not</param>
/// <returns></returns>
public virtual IEnumerator PlayFeedbacksCoroutine(Vector3 position, float feedbacksIntensity = 1.0f, bool forceRevert = false)
{
PlayFeedbacks(position, feedbacksIntensity, forceRevert);
while (IsPlaying)
{
yield return null;
}
}
#endregion
#region SEQUENCE
/// <summary>
/// An internal method used to play feedbacks, shouldn't be called externally
/// </summary>
/// <param name="position"></param>
/// <param name="feedbacksIntensity"></param>
protected virtual void PlayFeedbacksInternal(Vector3 position, float feedbacksIntensity, bool forceRevert = false)
{
if (!CanPlay)
{
return;
}
if (IsPlaying && !CanPlayWhileAlreadyPlaying)
{
return;
}
if (!EvaluateChance())
{
return;
}
// if we have a cooldown we prevent execution if needed
if (CooldownDuration > 0f)
{
if (GetTime() - _lastStartAt < CooldownDuration)
{
return;
}
}
// if all MMFeedbacks are disabled globally, we stop and don't play
if (!GlobalMMFeedbacksActive)
{
return;
}
if (!this.gameObject.activeInHierarchy)
{
return;
}
if (ShouldRevertOnNextPlay)
{
Revert();
ShouldRevertOnNextPlay = false;
}
if (forceRevert)
{
Direction = (Direction == Directions.BottomToTop) ? Directions.TopToBottom : Directions.BottomToTop;
}
ResetFeedbacks();
this.enabled = true;
TimesPlayed++;
IsPlaying = true;
_startTime = GetTime();
_lastStartAt = _startTime;
_totalDuration = TotalDuration;
CheckForPauses();
if (ComputedInitialDelay > 0f)
{
StartCoroutine(HandleInitialDelayCo(position, feedbacksIntensity, forceRevert));
}
else
{
PreparePlay(position, feedbacksIntensity, forceRevert);
}
}
protected virtual void PreparePlay(Vector3 position, float feedbacksIntensity, bool forceRevert = false)
{
Events.TriggerOnPlay(this);
_holdingMax = 0f;
CheckForPauses();
if (!_pauseFound)
{
PlayAllFeedbacks(position, feedbacksIntensity, forceRevert);
}
else
{
// if at least one pause was found
StartCoroutine(PausedFeedbacksCo(position, feedbacksIntensity));
}
}
protected virtual void CheckForPauses()
{
_pauseFound = false;
for (int i = 0; i < Feedbacks.Count; i++)
{
if (Feedbacks[i] != null)
{
if ((Feedbacks[i].Pause != null) && (Feedbacks[i].Active) && (Feedbacks[i].ShouldPlayInThisSequenceDirection))
{
_pauseFound = true;
}
if ((Feedbacks[i].HoldingPause == true) && (Feedbacks[i].Active) && (Feedbacks[i].ShouldPlayInThisSequenceDirection))
{
_pauseFound = true;
}
}
}
}
protected virtual void PlayAllFeedbacks(Vector3 position, float feedbacksIntensity, bool forceRevert = false)
{
// if no pause was found, we just play all feedbacks at once
for (int i = 0; i < Feedbacks.Count; i++)
{
if (FeedbackCanPlay(Feedbacks[i]))
{
Feedbacks[i].Play(position, feedbacksIntensity);
}
}
}
protected virtual IEnumerator HandleInitialDelayCo(Vector3 position, float feedbacksIntensity, bool forceRevert = false)
{
IsPlaying = true;
yield return MMFeedbacksCoroutine.WaitFor(ComputedInitialDelay);
PreparePlay(position, feedbacksIntensity, forceRevert);
}
protected virtual void Update()
{
if (_shouldStop)
{
if (HasFeedbackStillPlaying())
{
return;
}
IsPlaying = false;
Events.TriggerOnComplete(this);
ApplyAutoRevert();
this.enabled = false;
_shouldStop = false;
}
if (IsPlaying)
{
if (!_pauseFound)
{
if (GetTime() - _startTime > _totalDuration)
{
_shouldStop = true;
}
}
}
else
{
this.enabled = false;
}
}
/// <summary>
/// Returns true if feedbacks are still playing
/// </summary>
/// <returns></returns>
public virtual bool HasFeedbackStillPlaying()
{
int count = Feedbacks.Count;
for (int i = 0; i < count; i++)
{
if ((Feedbacks[i] != null) && (Feedbacks[i].IsPlaying))
{
return true;
}
}
return false;
}
/// <summary>
/// A coroutine used to handle the sequence of feedbacks if pauses are involved
/// </summary>
/// <param name="position"></param>
/// <param name="feedbacksIntensity"></param>
/// <returns></returns>
protected virtual IEnumerator PausedFeedbacksCo(Vector3 position, float feedbacksIntensity)
{
yield return null;
}
#endregion
#region STOP
/// <summary>
/// Stops all further feedbacks from playing, without stopping individual feedbacks
/// </summary>
public virtual void StopFeedbacks()
{
StopFeedbacks(true);
}
/// <summary>
/// Stops all feedbacks from playing, with an option to also stop individual feedbacks
/// </summary>
public virtual void StopFeedbacks(bool stopAllFeedbacks = true)
{
StopFeedbacks(this.transform.position, 1.0f, stopAllFeedbacks);
}
/// <summary>
/// Stops all feedbacks from playing, specifying a position and intensity that can be used by the Feedbacks
/// </summary>
/// <param name="position"></param>
/// <param name="feedbacksIntensity"></param>
public virtual void StopFeedbacks(Vector3 position, float feedbacksIntensity = 1.0f, bool stopAllFeedbacks = true)
{
if (stopAllFeedbacks)
{
for (int i = 0; i < Feedbacks.Count; i++)
{
if (Feedbacks[i] != null)
{
Feedbacks[i].Stop(position, feedbacksIntensity);
}
}
}
IsPlaying = false;
StopAllCoroutines();
}
#endregion
#region CONTROLS
/// <summary>
/// Calls each feedback's Reset method if they've defined one. An example of that can be resetting the initial color of a flickering renderer.
/// </summary>
public virtual void ResetFeedbacks()
{
for (int i = 0; i < Feedbacks.Count; i++)
{
if ((Feedbacks[i] != null) && (Feedbacks[i].Active))
{
Feedbacks[i].ResetFeedback();
}
}
IsPlaying = false;
}
/// <summary>
/// Changes the direction of this MMFeedbacks
/// </summary>
public virtual void Revert()
{
Events.TriggerOnRevert(this);
Direction = (Direction == Directions.BottomToTop) ? Directions.TopToBottom : Directions.BottomToTop;
}
/// <summary>
/// Use this method to authorize or prevent this player from being played
/// </summary>
/// <param name="newState"></param>
public virtual void SetCanPlay(bool newState)
{
CanPlay = newState;
}
/// <summary>
/// Pauses execution of a sequence, which can then be resumed by calling ResumeFeedbacks()
/// </summary>
public virtual void PauseFeedbacks()
{
Events.TriggerOnPause(this);
InScriptDrivenPause = true;
}
/// <summary>
/// Resumes execution of a sequence if a script driven pause is in progress
/// </summary>
public virtual void ResumeFeedbacks()
{
Events.TriggerOnResume(this);
InScriptDrivenPause = false;
}
#endregion
#region MODIFICATION
public virtual MMFeedback AddFeedback(System.Type feedbackType, bool add = true)
{
MMFeedback newFeedback;
#if UNITY_EDITOR
if (!Application.isPlaying)
{
newFeedback = Undo.AddComponent(this.gameObject, feedbackType) as MMFeedback;
}
else
{
newFeedback = this.gameObject.AddComponent(feedbackType) as MMFeedback;
}
#else
newFeedback = this.gameObject.AddComponent(feedbackType) as MMFeedback;
#endif
newFeedback.hideFlags = HideFlags.HideInInspector;
newFeedback.Label = FeedbackPathAttribute.GetFeedbackDefaultName(feedbackType);
AutoRepair();
return newFeedback;
}
public virtual void RemoveFeedback(int id)
{
#if UNITY_EDITOR
if (!Application.isPlaying)
{
Undo.DestroyObjectImmediate(Feedbacks[id]);
}
else
{
DestroyImmediate(Feedbacks[id]);
}
#else
DestroyImmediate(Feedbacks[id]);
#endif
Feedbacks.RemoveAt(id);
AutoRepair();
}
#endregion MODIFICATION
#region HELPERS
/// <summary>
/// Evaluates the chance of this feedback to play, and returns true if this feedback can play, false otherwise
/// </summary>
/// <returns></returns>
protected virtual bool EvaluateChance()
{
if (ChanceToPlay == 0f)
{
return false;
}
if (ChanceToPlay != 100f)
{
// determine the odds
float random = Random.Range(0f, 100f);
if (random > ChanceToPlay)
{
return false;
}
}
return true;
}
/// <summary>
/// Checks whether or not this MMFeedbacks contains one or more looper feedbacks
/// </summary>
protected virtual void CheckForLoops()
{
ContainsLoop = false;
for (int i = 0; i < Feedbacks.Count; i++)
{
if (Feedbacks[i] != null)
{
if (Feedbacks[i].LooperPause && Feedbacks[i].Active)
{
ContainsLoop = true;
return;
}
}
}
}
/// <summary>
/// This will return true if the conditions defined in the specified feedback's Timing section allow it to play in the current play direction of this MMFeedbacks
/// </summary>
/// <param name="feedback"></param>
/// <returns></returns>
protected bool FeedbackCanPlay(MMFeedback feedback)
{
if (feedback == null)
{
return false;
}
if (feedback.Timing == null)
{
return false;
}
if (feedback.Timing.MMFeedbacksDirectionCondition == MMFeedbackTiming.MMFeedbacksDirectionConditions.Always)
{
return true;
}
else if (((Direction == Directions.TopToBottom) && (feedback.Timing.MMFeedbacksDirectionCondition == MMFeedbackTiming.MMFeedbacksDirectionConditions.OnlyWhenForwards))
|| ((Direction == Directions.BottomToTop) && (feedback.Timing.MMFeedbacksDirectionCondition == MMFeedbackTiming.MMFeedbacksDirectionConditions.OnlyWhenBackwards)))
{
return true;
}
return false;
}
/// <summary>
/// Readies the MMFeedbacks to revert direction on the next play
/// </summary>
protected virtual void ApplyAutoRevert()
{
if (AutoChangeDirectionOnEnd)
{
ShouldRevertOnNextPlay = true;
}
}
/// <summary>
/// Applies this feedback's time multiplier to a duration (in seconds)
/// </summary>
/// <param name="duration"></param>
/// <returns></returns>
public virtual float ApplyTimeMultiplier(float duration)
{
return duration * Mathf.Clamp(DurationMultiplier, _smallValue, Single.MaxValue);
}
/// <summary>
/// Unity sometimes has serialization issues.
/// This method fixes that by fixing any bad sync that could happen.
/// </summary>
public virtual void AutoRepair()
{
List<Component> components = components = new List<Component>();
components = this.gameObject.GetComponents<Component>().ToList();
foreach (Component component in components)
{
if (component is MMFeedback)
{
bool found = false;
for (int i = 0; i < Feedbacks.Count; i++)
{
if (Feedbacks[i] == (MMFeedback)component)
{
found = true;
break;
}
}
if (!found)
{
Feedbacks.Add((MMFeedback)component);
}
}
}
}
#endregion
#region EVENTS
/// <summary>
/// On Disable we stop all feedbacks
/// </summary>
protected virtual void OnDisable()
{
/*if (IsPlaying)
{
StopFeedbacks();
StopAllCoroutines();
}*/
}
/// <summary>
/// On validate, we make sure our DurationMultiplier remains positive
/// </summary>
protected virtual void OnValidate()
{
DurationMultiplier = Mathf.Clamp(DurationMultiplier, _smallValue, Single.MaxValue);
}
/// <summary>
/// On Destroy, removes all feedbacks from this MMFeedbacks to avoid any leftovers
/// </summary>
protected virtual void OnDestroy()
{
IsPlaying = false;
#if UNITY_EDITOR
if (!Application.isPlaying)
{
// we remove all binders
foreach (MMFeedback feedback in Feedbacks)
{
EditorApplication.delayCall += () =>
{
DestroyImmediate(feedback);
};
}
}
#endif
}
#endregion EVENTS
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 841b72de2996c5c40bfb394f3d0e0a98
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,28 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace MoreMountains.Feedbacks
{
/// <summary>
/// A helper class added automatically by MMFeedbacks if they're in AutoPlayOnEnable mode
/// This lets them play again should their parent game object be disabled/enabled
/// </summary>
[AddComponentMenu("")]
public class MMFeedbacksEnabler : MonoBehaviour
{
/// the MMFeedbacks to pilot
public MMFeedbacks TargetMMFeedbacks { get; set; }
/// <summary>
/// On enable, we re-enable (and thus play) our MMFeedbacks if needed
/// </summary>
protected virtual void OnEnable()
{
if ((TargetMMFeedbacks != null) && !TargetMMFeedbacks.enabled && TargetMMFeedbacks.AutoPlayOnEnable)
{
TargetMMFeedbacks.enabled = true;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dd5c9de2e9b0d6540b318450df3fb297
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8f397bae44366904cb741a56fb7cc568
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,14 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 09e29c2242d13d64480d58af86fcb50f, type: 3}
m_Name: ChannelA
m_EditorClassIdentifier:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6ba165fd91edb434aa2f1cd6f6b05885
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,14 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 09e29c2242d13d64480d58af86fcb50f, type: 3}
m_Name: ChannelB
m_EditorClassIdentifier:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7471a7ccb4c5bdb4e9c9d5b51f6db888
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,14 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 09e29c2242d13d64480d58af86fcb50f, type: 3}
m_Name: ChannelC
m_EditorClassIdentifier:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 277be439bdc9329468f9e6d0799d54be
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,92 @@
using System;
using UnityEngine;
namespace MoreMountains.Feedbacks
{
/// <summary>
/// The possible modes used to identify a channel, either via an int or a MMChannel scriptable object
/// </summary>
public enum MMChannelModes
{
Int,
MMChannel
}
/// <summary>
/// A data structure used to pass channel information
/// </summary>
[Serializable]
public class MMChannelData
{
public MMChannelModes MMChannelMode;
public int Channel;
public MMChannel MMChannelDefinition;
public MMChannelData(MMChannelModes mode, int channel, MMChannel channelDefinition)
{
MMChannelMode = mode;
Channel = channel;
MMChannelDefinition = channelDefinition;
}
}
/// <summary>
/// Extensions class for MMChannelData
/// </summary>
public static class MMChannelDataExtensions
{
public static MMChannelData Set(this MMChannelData data, MMChannelModes mode, int channel, MMChannel channelDefinition)
{
data.MMChannelMode = mode;
data.Channel = channel;
data.MMChannelDefinition = channelDefinition;
return data;
}
}
/// <summary>
/// A scriptable object you can create assets from, to identify Channels, used mostly (but not only) in feedbacks and shakers,
/// to determine a channel of communication, usually between emitters and receivers
/// </summary>
[CreateAssetMenu(menuName = "MoreMountains/MMChannel", fileName = "MMChannel")]
public class MMChannel : ScriptableObject
{
public static bool Match(MMChannelData dataA, MMChannelData dataB)
{
if (dataA.MMChannelMode != dataB.MMChannelMode)
{
return false;
}
if (dataA.MMChannelMode == MMChannelModes.Int)
{
return dataA.Channel == dataB.Channel;
}
else
{
return dataA.MMChannelDefinition == dataB.MMChannelDefinition;
}
}
public static bool Match(MMChannelData dataA, MMChannelModes modeB, int channelB, MMChannel channelDefinitionB)
{
if (dataA == null)
{
return true;
}
if (dataA.MMChannelMode != modeB)
{
return false;
}
if (dataA.MMChannelMode == MMChannelModes.Int)
{
return dataA.Channel == channelB;
}
else
{
return dataA.MMChannelDefinition == channelDefinitionB;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 09e29c2242d13d64480d58af86fcb50f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 29c391bec7aa5dc4e914145af8159c87
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1466e386bacf73e428d7e19707b9e185
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,22 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace MoreMountains.Feedbacks
{
[Serializable]
public class MMF_Button
{
public delegate void ButtonMethod();
public string ButtonText;
public ButtonMethod TargetMethod;
public MMF_Button(string buttonText, ButtonMethod method)
{
ButtonText = buttonText;
TargetMethod = method;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ecda383e35cc53e4ca2b9d5857dbd8f4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6da43522623d4704e979466dc7650b65
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,54 @@
using System.Collections;
using System.Collections.Generic;
using MoreMountains.Feedbacks;
using UnityEngine;
#if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER
using UnityEngine.InputSystem;
#endif
namespace MoreMountains.Feedbacks
{
/// <summary>
/// Add this debug component to a MMF Player, and you'll be able to play it at runtime at the press of a (customisable) key, useful when tweaking or debugging your feedbacks
/// </summary>
public class MMF_PlayerDebugInput : MonoBehaviour
{
#if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER
/// the button used to cause a debug play of this feedback
public Key PlayKey = Key.P;
#else
/// the button used to cause a debug play of this feedback
public KeyCode PlayButton = KeyCode.P;
#endif
protected MMF_Player _player;
/// <summary>
/// On Awake we store our MMF Player
/// </summary>
protected virtual void Awake()
{
_player = this.gameObject.GetComponent<MMF_Player>();
}
/// <summary>
/// On Update, we play our feedback if the right button is pressed
/// </summary>
protected virtual void Update()
{
bool keyPressed = false;
#if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER
keyPressed = Keyboard.current[PlayKey].wasPressedThisFrame;
#else
keyPressed = Input.GetKeyDown(PlayButton);
#endif
if (keyPressed)
{
_player.PlayFeedbacks();
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4474026fd06a48748b3ac7eef71e802d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,28 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace MoreMountains.Feedbacks
{
/// <summary>
/// A helper class added automatically by MMFeedbacks if they're in AutoPlayOnEnable mode
/// This lets them play again should their parent game object be disabled/enabled
/// </summary>
[AddComponentMenu("")]
public class MMF_PlayerEnabler : MonoBehaviour
{
/// the MMFeedbacks to pilot
public virtual MMF_Player TargetMmfPlayer { get; set; }
/// <summary>
/// On enable, we re-enable (and thus play) our MMFeedbacks if needed
/// </summary>
protected virtual void OnEnable()
{
if ((TargetMmfPlayer != null) && !TargetMmfPlayer.enabled && TargetMmfPlayer.AutoPlayOnEnable)
{
TargetMmfPlayer.enabled = true;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 109f4af2c917dc74aa07733bafe2f548
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,123 @@
using System.Collections;
using System.Collections.Generic;
using MoreMountains.Tools;
using UnityEngine;
namespace MoreMountains.Feedbacks
{
/// <summary>
/// A class collecting target acquisition settings
/// </summary>
[System.Serializable]
public class MMFeedbackTargetAcquisition
{
public enum Modes { None, Self, AnyChild, ChildAtIndex, Parent, FirstReferenceHolder, PreviousReferenceHolder, ClosestReferenceHolder, NextReferenceHolder, LastReferenceHolder }
/// the selected mode for target acquisition
/// None : nothing will happen
/// Self : the target will be picked on the MMF Player's game object
/// AnyChild : the target will be picked on any of the MMF Player's child objects
/// ChildAtIndex : the target will be picked on the child at index X of the MMF Player
/// Parent : the target will be picked on the first parent where a matching target is found
/// Various reference holders : the target will be picked on the specified reference holder in the list (either the first one, previous : first one found before this feedback in the list, closest in any direction from this feedback, the next one found, or the last one in the list)
[Tooltip("the selected mode for target acquisition\n"+
"None : nothing will happen\n"+
"Self : the target will be picked on the MMF Player's game object\n"+
"AnyChild : the target will be picked on any of the MMF Player's child objects\n"+
"ChildAtIndex : the target will be picked on the child at index X of the MMF Player\n"+
"Parent : the target will be picked on the first parent where a matching target is found\n"+
"Various reference holders : the target will be picked on the specified reference holder in the list " +
"(either the first one, previous : first one found before this feedback in the list, closest in any direction from this feedback, the next one found, or the last one in the list)")]
public Modes Mode = Modes.None;
[MMFEnumCondition("Mode", (int)Modes.ChildAtIndex)]
public int ChildIndex = 0;
private static MMF_ReferenceHolder _referenceHolder;
public static MMF_ReferenceHolder GetReferenceHolder(MMFeedbackTargetAcquisition settings, MMF_Player owner, int currentFeedbackIndex)
{
switch (settings.Mode)
{
case Modes.FirstReferenceHolder:
return owner.GetFeedbackOfType<MMF_ReferenceHolder>(MMF_Player.AccessMethods.First, currentFeedbackIndex);
case Modes.PreviousReferenceHolder:
return owner.GetFeedbackOfType<MMF_ReferenceHolder>(MMF_Player.AccessMethods.Previous, currentFeedbackIndex);
case Modes.ClosestReferenceHolder:
return owner.GetFeedbackOfType<MMF_ReferenceHolder>(MMF_Player.AccessMethods.Closest, currentFeedbackIndex);
case Modes.NextReferenceHolder:
return owner.GetFeedbackOfType<MMF_ReferenceHolder>(MMF_Player.AccessMethods.Next, currentFeedbackIndex);
case Modes.LastReferenceHolder:
return owner.GetFeedbackOfType<MMF_ReferenceHolder>(MMF_Player.AccessMethods.Last, currentFeedbackIndex);
}
return null;
}
public static GameObject FindAutomatedTargetGameObject(MMFeedbackTargetAcquisition settings, MMF_Player owner, int currentFeedbackIndex)
{
if (owner.FeedbacksList[currentFeedbackIndex].ForcedReferenceHolder != null)
{
return owner.FeedbacksList[currentFeedbackIndex].ForcedReferenceHolder.GameObjectReference;
}
_referenceHolder = GetReferenceHolder(settings, owner, currentFeedbackIndex);
switch (settings.Mode)
{
case Modes.Self:
return owner.gameObject;
case Modes.ChildAtIndex:
return owner.transform.GetChild(settings.ChildIndex).gameObject;
case Modes.AnyChild:
return owner.transform.GetChild(0).gameObject;
case Modes.Parent:
return owner.transform.parent.gameObject;
case Modes.FirstReferenceHolder:
case Modes.PreviousReferenceHolder:
case Modes.ClosestReferenceHolder:
case Modes.NextReferenceHolder:
case Modes.LastReferenceHolder:
return _referenceHolder?.GameObjectReference;
}
return null;
}
public static T FindAutomatedTarget<T>(MMFeedbackTargetAcquisition settings, MMF_Player owner, int currentFeedbackIndex)
{
if (owner.FeedbacksList[currentFeedbackIndex].ForcedReferenceHolder != null)
{
return owner.FeedbacksList[currentFeedbackIndex].ForcedReferenceHolder.GameObjectReference.GetComponent<T>();
}
_referenceHolder = GetReferenceHolder(settings, owner, currentFeedbackIndex);
switch (settings.Mode)
{
case Modes.Self:
return owner.GetComponent<T>();
case Modes.ChildAtIndex:
return owner.transform.GetChild(settings.ChildIndex).gameObject.GetComponent<T>();
case Modes.AnyChild:
for (int i = 0; i < owner.transform.childCount; i++)
{
if (owner.transform.GetChild(i).GetComponent<T>() != null)
{
return owner.transform.GetChild(i).GetComponent<T>();
}
}
return owner.GetComponentInChildren<T>();
case Modes.Parent:
return owner.transform.parent.GetComponentInParent<T>();
case Modes.FirstReferenceHolder:
case Modes.PreviousReferenceHolder:
case Modes.ClosestReferenceHolder:
case Modes.NextReferenceHolder:
case Modes.LastReferenceHolder:
return (_referenceHolder != null)
? _referenceHolder.GameObjectReference.GetComponent<T>()
: default(T);
}
return default(T);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a40118e4d239fe146bb900366085611f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,137 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace MoreMountains.Feedbacks
{
/// the possible modes for the timescale
public enum TimescaleModes { Scaled, Unscaled }
/// <summary>
/// A class collecting delay, cooldown and repeat values, to be used to define the behaviour of each MMFeedback
/// </summary>
[System.Serializable]
public class MMFeedbackTiming
{
/// the possible ways this feedback can play based on the host MMFeedbacks' directions
public enum MMFeedbacksDirectionConditions { Always, OnlyWhenForwards, OnlyWhenBackwards };
/// the possible ways this feedback can play
public enum PlayDirections { FollowMMFeedbacksDirection, OppositeMMFeedbacksDirection, AlwaysNormal, AlwaysRewind }
[Header("Timescale")]
/// whether we're working on scaled or unscaled time
[Tooltip("whether we're working on scaled or unscaled time")]
public TimescaleModes TimescaleMode = TimescaleModes.Scaled;
[Header("Exceptions")]
/// if this is true, holding pauses won't wait for this feedback to finish
[Tooltip("if this is true, holding pauses won't wait for this feedback to finish")]
public bool ExcludeFromHoldingPauses = false;
/// whether to count this feedback in the parent MMFeedbacks(Player) total duration or not
[Tooltip("whether to count this feedback in the parent MMFeedbacks(Player) total duration or not")]
public bool ContributeToTotalDuration = true;
[Header("Delays")]
/// the initial delay to apply before playing the delay (in seconds)
[Tooltip("the initial delay to apply before playing the delay (in seconds)")]
public float InitialDelay = 0f;
/// the cooldown duration mandatory between two plays
[Tooltip("the cooldown duration mandatory between two plays")]
public float CooldownDuration = 0f;
[Header("Stop")]
/// if this is true, this feedback will interrupt itself when Stop is called on its parent MMFeedbacks, otherwise it'll keep running
[Tooltip("if this is true, this feedback will interrupt itself when Stop is called on its parent MMFeedbacks, otherwise it'll keep running")]
public bool InterruptsOnStop = true;
[Header("Repeat")]
/// the repeat mode, whether the feedback should be played once, multiple times, or forever
[Tooltip("the repeat mode, whether the feedback should be played once, multiple times, or forever")]
public int NumberOfRepeats = 0;
/// if this is true, the feedback will be repeated forever
[Tooltip("if this is true, the feedback will be repeated forever")]
public bool RepeatForever = false;
/// the delay (in seconds) between two firings of this feedback. This doesn't include the duration of the feedback.
[Tooltip("the delay (in seconds) between two firings of this feedback. This doesn't include the duration of the feedback.")]
public float DelayBetweenRepeats = 1f;
[Header("PlayCount")]
/// the number of times this feedback's been played since its initialization (or last reset if SetPlayCountToZeroOnReset is true)
[Tooltip("the number of times this feedback's been played since its initialization (or last reset if SetPlayCountToZeroOnReset is true)")]
[MMFReadOnly]
public int PlayCount = 0;
/// whether or not to limit the amount of times this feedback can be played. beyond that amount, it won't play anymore
[Tooltip("whether or not to limit the amount of times this feedback can be played. beyond that amount, it won't play anymore")]
public bool LimitPlayCount = false;
/// if LimitPlayCount is true, the maximum amount of times this feedback can be played
[Tooltip("if LimitPlayCount is true, the maximum amount of times this feedback can be played")]
[MMFCondition("LimitPlayCount", true)]
public int MaxPlayCount = 3;
/// if LimitPlayCount is true, whether or not to reset the play count to zero when the feedback is reset
[Tooltip("if LimitPlayCount is true, whether or not to reset the play count to zero when the feedback is reset")]
[MMFCondition("LimitPlayCount", true)]
public bool SetPlayCountToZeroOnReset = false;
[Header("Play Direction")]
/// this defines how this feedback should play when the host MMFeedbacks is played :
/// - always (default) : this feedback will always play
/// - OnlyWhenForwards : this feedback will only play if the host MMFeedbacks is played in the top to bottom direction (forwards)
/// - OnlyWhenBackwards : this feedback will only play if the host MMFeedbacks is played in the bottom to top direction (backwards)
[Tooltip("this defines how this feedback should play when the host MMFeedbacks is played :" +
"- always (default) : this feedback will always play" +
"- OnlyWhenForwards : this feedback will only play if the host MMFeedbacks is played in the top to bottom direction (forwards)" +
"- OnlyWhenBackwards : this feedback will only play if the host MMFeedbacks is played in the bottom to top direction (backwards)")]
public MMFeedbacksDirectionConditions MMFeedbacksDirectionCondition = MMFeedbacksDirectionConditions.Always;
/// this defines the way this feedback will play. It can play in its normal direction, or in rewind (a sound will play backwards,
/// an object normally scaling up will scale down, a curve will be evaluated from right to left, etc)
/// - BasedOnMMFeedbacksDirection : will play normally when the host MMFeedbacks is played forwards, in rewind when it's played backwards
/// - OppositeMMFeedbacksDirection : will play in rewind when the host MMFeedbacks is played forwards, and normally when played backwards
/// - Always Normal : will always play normally, regardless of the direction of the host MMFeedbacks
/// - Always Rewind : will always play in rewind, regardless of the direction of the host MMFeedbacks
[Tooltip("this defines the way this feedback will play. It can play in its normal direction, or in rewind (a sound will play backwards," +
" an object normally scaling up will scale down, a curve will be evaluated from right to left, etc)" +
"- BasedOnMMFeedbacksDirection : will play normally when the host MMFeedbacks is played forwards, in rewind when it's played backwards" +
"- OppositeMMFeedbacksDirection : will play in rewind when the host MMFeedbacks is played forwards, and normally when played backwards" +
"- Always Normal : will always play normally, regardless of the direction of the host MMFeedbacks" +
"- Always Rewind : will always play in rewind, regardless of the direction of the host MMFeedbacks")]
public PlayDirections PlayDirection = PlayDirections.FollowMMFeedbacksDirection;
[Header("Intensity")]
/// if this is true, intensity will be constant, even if the parent MMFeedbacks is played at a lower intensity
[Tooltip("if this is true, intensity will be constant, even if the parent MMFeedbacks is played at a lower intensity")]
public bool ConstantIntensity = false;
/// if this is true, this feedback will only play if its intensity is higher or equal to IntensityIntervalMin and lower than IntensityIntervalMax
[Tooltip("if this is true, this feedback will only play if its intensity is higher or equal to IntensityIntervalMin and lower than IntensityIntervalMax")]
public bool UseIntensityInterval = false;
/// the minimum intensity required for this feedback to play
[Tooltip("the minimum intensity required for this feedback to play")]
[MMFCondition("UseIntensityInterval", true)]
public float IntensityIntervalMin = 0f;
/// the maximum intensity required for this feedback to play
[Tooltip("the maximum intensity required for this feedback to play")]
[MMFCondition("UseIntensityInterval", true)]
public float IntensityIntervalMax = 0f;
[Header("Sequence")]
/// A MMSequence to use to play these feedbacks on
[Tooltip("A MMSequence to use to play these feedbacks on")]
public MMSequence Sequence;
/// The MMSequence's TrackID to consider
[Tooltip("The MMSequence's TrackID to consider")]
public int TrackID = 0;
/// whether or not to use the quantized version of the target sequence
[Tooltip("whether or not to use the quantized version of the target sequence")]
public bool Quantized = false;
/// if using the quantized version of the target sequence, the BPM to apply to the sequence when playing it
[Tooltip("if using the quantized version of the target sequence, the BPM to apply to the sequence when playing it")]
[MMFCondition("Quantized", true)]
public int TargetBPM = 120;
/// from any class, you can set UseScriptDrivenTimescale:true, from there, instead of looking at Time.time, Time.deltaTime (or their unscaled equivalents), this feedback will compute time based on the values you feed them via ScriptDrivenDeltaTime and ScriptDrivenTime
public virtual bool UseScriptDrivenTimescale { get; set; }
/// the value this feedback should use for delta time
public virtual float ScriptDrivenDeltaTime { get; set; }
/// the value this feedback should use for time
public virtual float ScriptDrivenTime { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 293c62544284dea419e338a8524a7fb4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,55 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace MoreMountains.Feedbacks
{
/// <summary>
/// Coroutine helpers
/// </summary>
public static class MMFeedbacksCoroutine
{
/// <summary>
/// Waits for the specified amount of frames
/// use : yield return MMCoroutine.WaitFor(1);
/// </summary>
/// <param name="frameCount"></param>
/// <returns></returns>
public static IEnumerator WaitForFrames(int frameCount)
{
while (frameCount > 0)
{
frameCount--;
yield return null;
}
}
/// <summary>
/// Waits for the specified amount of seconds (using regular time)
/// use : yield return MMCoroutine.WaitFor(1f);
/// </summary>
/// <param name="seconds"></param>
/// <returns></returns>
public static IEnumerator WaitFor(float seconds)
{
for (float timer = 0f; timer < seconds; timer += Time.deltaTime)
{
yield return null;
}
}
/// <summary>
/// Waits for the specified amount of seconds (using unscaled time)
/// use : yield return MMCoroutine.WaitForUnscaled(1f);
/// </summary>
/// <param name="seconds"></param>
/// <returns></returns>
public static IEnumerator WaitForUnscaled(float seconds)
{
for (float timer = 0f; timer < seconds; timer += Time.unscaledDeltaTime)
{
yield return null;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fff95d1eb5038764b9fdf57b6818aa6f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,306 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
namespace MoreMountains.Feedbacks
{
/// <summary>
/// Events triggered by a MMFeedbacks when playing a series of feedbacks
/// - play : when a MMFeedbacks starts playing
/// - pause : when a holding pause is met
/// - resume : after a holding pause resumes
/// - revert : when a MMFeedbacks reverts its play direction
/// - complete : when a MMFeedbacks has played its last feedback
///
/// to listen to these events :
///
/// public virtual void OnMMFeedbacksEvent(MMFeedbacks source, EventTypes type)
/// {
/// // do something
/// }
///
/// protected virtual void OnEnable()
/// {
/// MMFeedbacksEvent.Register(OnMMFeedbacksEvent);
/// }
///
/// protected virtual void OnDisable()
/// {
/// MMFeedbacksEvent.Unregister(OnMMFeedbacksEvent);
/// }
///
/// </summary>
public struct MMFeedbacksEvent
{
static private event Delegate OnEvent;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void RuntimeInitialization() { OnEvent = null; }
static public void Register(Delegate callback) { OnEvent += callback; }
static public void Unregister(Delegate callback) { OnEvent -= callback; }
public enum EventTypes { Play, Pause, Resume, Revert, Complete, SkipToTheEnd, RestoreInitialValues, Loop, Enable, Disable, InitializationComplete }
public delegate void Delegate(MMFeedbacks source, EventTypes type);
static public void Trigger(MMFeedbacks source, EventTypes type)
{
OnEvent?.Invoke(source, type);
}
}
/// <summary>
/// An event used to set the RangeCenter on all feedbacks that listen for it
/// </summary>
public struct MMSetFeedbackRangeCenterEvent
{
static private event Delegate OnEvent;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void RuntimeInitialization() { OnEvent = null; }
static public void Register(Delegate callback) { OnEvent += callback; }
static public void Unregister(Delegate callback) { OnEvent -= callback; }
public delegate void Delegate(Transform newCenter);
static public void Trigger(Transform newCenter)
{
OnEvent?.Invoke(newCenter);
}
}
/// <summary>
/// A subclass of MMFeedbacks, contains UnityEvents that can be played,
/// </summary>
[Serializable]
public class MMFeedbacksEvents
{
/// whether or not this MMFeedbacks should fire MMFeedbacksEvents
[Tooltip("whether or not this MMFeedbacks should fire MMFeedbacksEvents")]
public bool TriggerMMFeedbacksEvents = false;
/// whether or not this MMFeedbacks should fire Unity Events
[Tooltip("whether or not this MMFeedbacks should fire Unity Events")]
public bool TriggerUnityEvents = true;
/// This event will fire every time this MMFeedbacks gets played
[Tooltip("This event will fire every time this MMFeedbacks gets played")]
public UnityEvent OnPlay;
/// This event will fire every time this MMFeedbacks starts a holding pause
[Tooltip("This event will fire every time this MMFeedbacks starts a holding pause")]
public UnityEvent OnPause;
/// This event will fire every time this MMFeedbacks resumes after a holding pause
[Tooltip("This event will fire every time this MMFeedbacks resumes after a holding pause")]
public UnityEvent OnResume;
/// This event will fire every time this MMFeedbacks reverts its play direction
[Tooltip("This event will fire every time this MMFeedbacks reverts its play direction")]
public UnityEvent OnRevert;
/// This event will fire every time this MMFeedbacks plays its last MMFeedback
[Tooltip("This event will fire every time this MMFeedbacks plays its last MMFeedback")]
public UnityEvent OnComplete;
/// This event will fire every time this MMFeedbacks gets restored to its initial values
[Tooltip("This event will fire every time this MMFeedbacks gets restored to its initial values")]
public UnityEvent OnRestoreInitialValues;
/// This event will fire every time this MMFeedbacks gets skipped to the end
[Tooltip("This event will fire every time this MMFeedbacks gets skipped to the end")]
public UnityEvent OnSkipToTheEnd;
/// This event will fire after the MMF Player is done initializing
[Tooltip("This event will fire after the MMF Player is done initializing")]
public UnityEvent OnInitializationComplete;
/// This event will fire every time this MMFeedbacks' game object gets enabled
[Tooltip("This event will fire every time this MMFeedbacks' game object gets enabled")]
public UnityEvent OnEnable;
/// This event will fire every time this MMFeedbacks' game object gets disabled
[Tooltip("This event will fire every time this MMFeedbacks' game object gets disabled")]
public UnityEvent OnDisable;
public virtual bool OnPlayIsNull { get; protected set; }
public virtual bool OnPauseIsNull { get; protected set; }
public virtual bool OnResumeIsNull { get; protected set; }
public virtual bool OnRevertIsNull { get; protected set; }
public virtual bool OnCompleteIsNull { get; protected set; }
public virtual bool OnRestoreInitialValuesIsNull { get; protected set; }
public virtual bool OnSkipToTheEndIsNull { get; protected set; }
public virtual bool OnInitializationCompleteIsNull { get; protected set; }
public virtual bool OnEnableIsNull { get; protected set; }
public virtual bool OnDisableIsNull { get; protected set; }
/// <summary>
/// On init we store for each event whether or not we have one to invoke
/// </summary>
public virtual void Initialization()
{
OnPlayIsNull = OnPlay == null;
OnPauseIsNull = OnPause == null;
OnResumeIsNull = OnResume == null;
OnRevertIsNull = OnRevert == null;
OnCompleteIsNull = OnComplete == null;
OnRestoreInitialValuesIsNull = OnRestoreInitialValues == null;
OnSkipToTheEndIsNull = OnSkipToTheEnd == null;
OnInitializationCompleteIsNull = OnInitializationComplete == null;
OnEnableIsNull = OnEnable == null;
OnDisableIsNull = OnDisable == null;
}
/// <summary>
/// Fires Play events if needed
/// </summary>
/// <param name="source"></param>
public virtual void TriggerOnPlay(MMFeedbacks source)
{
if (!OnPlayIsNull && TriggerUnityEvents)
{
OnPlay.Invoke();
}
if (TriggerMMFeedbacksEvents)
{
MMFeedbacksEvent.Trigger(source, MMFeedbacksEvent.EventTypes.Play);
}
}
/// <summary>
/// Fires pause events if needed
/// </summary>
/// <param name="source"></param>
public virtual void TriggerOnPause(MMFeedbacks source)
{
if (!OnPauseIsNull && TriggerUnityEvents)
{
OnPause.Invoke();
}
if (TriggerMMFeedbacksEvents)
{
MMFeedbacksEvent.Trigger(source, MMFeedbacksEvent.EventTypes.Pause);
}
}
/// <summary>
/// Fires resume events if needed
/// </summary>
/// <param name="source"></param>
public virtual void TriggerOnResume(MMFeedbacks source)
{
if (!OnResumeIsNull && TriggerUnityEvents)
{
OnResume.Invoke();
}
if (TriggerMMFeedbacksEvents)
{
MMFeedbacksEvent.Trigger(source, MMFeedbacksEvent.EventTypes.Resume);
}
}
/// <summary>
/// Fires revert events if needed
/// </summary>
/// <param name="source"></param>
public virtual void TriggerOnRevert(MMFeedbacks source)
{
if (!OnRevertIsNull && TriggerUnityEvents)
{
OnRevert.Invoke();
}
if (TriggerMMFeedbacksEvents)
{
MMFeedbacksEvent.Trigger(source, MMFeedbacksEvent.EventTypes.Revert);
}
}
/// <summary>
/// Fires complete events if needed
/// </summary>
/// <param name="source"></param>
public virtual void TriggerOnComplete(MMFeedbacks source)
{
if (!OnCompleteIsNull && TriggerUnityEvents)
{
OnComplete.Invoke();
}
if (TriggerMMFeedbacksEvents)
{
MMFeedbacksEvent.Trigger(source, MMFeedbacksEvent.EventTypes.Complete);
}
}
/// <summary>
/// Fires skip events if needed
/// </summary>
/// <param name="source"></param>
public virtual void TriggerOnSkipToTheEnd(MMFeedbacks source)
{
if (!OnSkipToTheEndIsNull && TriggerUnityEvents)
{
OnSkipToTheEnd.Invoke();
}
if (TriggerMMFeedbacksEvents)
{
MMFeedbacksEvent.Trigger(source, MMFeedbacksEvent.EventTypes.SkipToTheEnd);
}
}
public virtual void TriggerOnInitializationComplete(MMFeedbacks source)
{
if (!OnInitializationCompleteIsNull && TriggerUnityEvents)
{
OnInitializationComplete.Invoke();
}
if (TriggerMMFeedbacksEvents)
{
MMFeedbacksEvent.Trigger(source, MMFeedbacksEvent.EventTypes.InitializationComplete);
}
}
/// <summary>
/// Fires revert events if needed
/// </summary>
/// <param name="source"></param>
public virtual void TriggerOnRestoreInitialValues(MMFeedbacks source)
{
if (!OnRestoreInitialValuesIsNull && TriggerUnityEvents)
{
OnRestoreInitialValues.Invoke();
}
if (TriggerMMFeedbacksEvents)
{
MMFeedbacksEvent.Trigger(source, MMFeedbacksEvent.EventTypes.RestoreInitialValues);
}
}
/// <summary>
/// Fires enable events if needed
/// </summary>
/// <param name="source"></param>
public virtual void TriggerOnEnable(MMF_Player source)
{
if (!OnEnableIsNull && TriggerUnityEvents)
{
OnEnable.Invoke();
}
if (TriggerMMFeedbacksEvents)
{
MMFeedbacksEvent.Trigger(source, MMFeedbacksEvent.EventTypes.Enable);
}
}
/// <summary>
/// Fires disable events if needed
/// </summary>
/// <param name="source"></param>
public virtual void TriggerOnDisable(MMF_Player source)
{
if (!OnDisableIsNull && TriggerUnityEvents)
{
OnDisable.Invoke();
}
if (TriggerMMFeedbacksEvents)
{
MMFeedbacksEvent.Trigger(source, MMFeedbacksEvent.EventTypes.Disable);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 716ed1aa8290ba04ba69b411a3d8554a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,568 @@
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;
using System.Linq;
using System.Reflection;
using MoreMountains.Tools;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace MoreMountains.Feedbacks
{
[AddComponentMenu("")]
public class MMFeedbacksHelpers : MonoBehaviour
{
/// <summary>
/// Remaps a value x in interval [A,B], to the proportional value in interval [C,D]
/// </summary>
/// <param name="x">The value to remap.</param>
/// <param name="A">the minimum bound of interval [A,B] that contains the x value</param>
/// <param name="B">the maximum bound of interval [A,B] that contains the x value</param>
/// <param name="C">the minimum bound of target interval [C,D]</param>
/// <param name="D">the maximum bound of target interval [C,D]</param>
public static float Remap(float x, float A, float B, float C, float D)
{
float remappedValue = C + (x - A) / (B - A) * (D - C);
return remappedValue;
}
/// <summary>
/// A helper used to migrate values from an AnimationCurve field to a MMTweenType, useful when updating
/// old feedbacks to use them without losing legacy values
/// </summary>
/// <param name="oldCurve"></param>
/// <param name="newTweenType"></param>
/// <param name="owner"></param>
public static void MigrateCurve(AnimationCurve oldCurve, MMTweenType newTweenType, MMF_Player owner)
{
if ((oldCurve.keys.Length > 0) && (!newTweenType.Initialized))
{
newTweenType.Curve = oldCurve;
newTweenType.MMTweenDefinitionType = MMTweenDefinitionTypes.AnimationCurve;
oldCurve = null;
newTweenType.Initialized = true;
#if UNITY_EDITOR
UnityEditor.Undo.RecordObject(owner, "Ports animation curve to tween system");
#endif
}
}
}
public class MMFReadOnlyAttribute : PropertyAttribute { }
[System.AttributeUsage(System.AttributeTargets.Field)]
public class MMFInspectorButtonAttribute : PropertyAttribute
{
public readonly string MethodName;
public MMFInspectorButtonAttribute(string MethodName)
{
this.MethodName = MethodName;
}
}
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Class | AttributeTargets.Struct, Inherited = true)]
public class MMFEnumConditionAttribute : PropertyAttribute
{
public string ConditionEnum = "";
public bool Hidden = false;
BitArray bitArray = new BitArray(32);
public bool ContainsBitFlag(int enumValue)
{
return bitArray.Get(enumValue);
}
public MMFEnumConditionAttribute(string conditionBoolean, params int[] enumValues)
{
this.ConditionEnum = conditionBoolean;
this.Hidden = true;
for (int i = 0; i < enumValues.Length; i++)
{
bitArray.Set(enumValues[i], true);
}
}
}
#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(MMFInspectorButtonAttribute))]
public class MMFInspectorButtonPropertyDrawer : PropertyDrawer
{
private MethodInfo _eventMethodInfo = null;
public override void OnGUI(Rect position, SerializedProperty prop, GUIContent label)
{
MMFInspectorButtonAttribute inspectorButtonAttribute = (MMFInspectorButtonAttribute)attribute;
float buttonLength = position.width;
Rect buttonRect = new Rect(position.x + (position.width - buttonLength) * 0.5f, position.y, buttonLength, position.height);
if (GUI.Button(buttonRect, inspectorButtonAttribute.MethodName))
{
System.Type eventOwnerType = prop.serializedObject.targetObject.GetType();
string eventName = inspectorButtonAttribute.MethodName;
if (_eventMethodInfo == null)
{
_eventMethodInfo = eventOwnerType.GetMethod(eventName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
}
if (_eventMethodInfo != null)
{
_eventMethodInfo.Invoke(prop.serializedObject.targetObject, null);
}
else
{
Debug.LogWarning(string.Format("InspectorButton: Unable to find method {0} in {1}", eventName, eventOwnerType));
}
}
}
}
#endif
public class MMFInformationAttribute : PropertyAttribute
{
public enum InformationType { Error, Info, None, Warning }
#if UNITY_EDITOR
public string Message;
public MessageType Type;
public bool MessageAfterProperty;
public MMFInformationAttribute(string message, InformationType type, bool messageAfterProperty)
{
this.Message = message;
if (type == InformationType.Error) { this.Type = UnityEditor.MessageType.Error; }
if (type == InformationType.Info) { this.Type = UnityEditor.MessageType.Info; }
if (type == InformationType.Warning) { this.Type = UnityEditor.MessageType.Warning; }
if (type == InformationType.None) { this.Type = UnityEditor.MessageType.None; }
this.MessageAfterProperty = messageAfterProperty;
}
#else
public MMFInformationAttribute(string message, InformationType type, bool messageAfterProperty)
{
}
#endif
}
public class MMFHiddenAttribute : PropertyAttribute { }
[AttributeUsage(System.AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Class | AttributeTargets.Struct, Inherited = true)]
public class MMFConditionAttribute : PropertyAttribute
{
public string ConditionBoolean = "";
public bool Hidden = false;
public bool Negative = false;
public MMFConditionAttribute(string conditionBoolean)
{
this.ConditionBoolean = conditionBoolean;
this.Hidden = false;
this.Negative = false;
}
public MMFConditionAttribute(string conditionBoolean, bool hideInInspector)
{
this.ConditionBoolean = conditionBoolean;
this.Hidden = hideInInspector;
this.Negative = false;
}
public MMFConditionAttribute(string conditionBoolean, bool hideInInspector, bool negative)
{
this.ConditionBoolean = conditionBoolean;
this.Hidden = hideInInspector;
this.Negative = negative;
}
}
public class MMFVectorAttribute : PropertyAttribute
{
public readonly string[] Labels;
public MMFVectorAttribute(params string[] labels)
{
Labels = labels;
}
}
#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(MMFVectorAttribute))]
public class MMVectorLabelsAttributeDrawer : PropertyDrawer
{
protected static readonly GUIContent[] originalLabels = new GUIContent[] { new GUIContent("X"), new GUIContent("Y"), new GUIContent("Z"), new GUIContent("W") };
protected const int padding = 375;
public override float GetPropertyHeight(SerializedProperty property, GUIContent guiContent)
{
int ratio = (padding > Screen.width) ? 2 : 1;
return ratio * base.GetPropertyHeight(property, guiContent);
}
public override void OnGUI(Rect rect, SerializedProperty property, GUIContent guiContent)
{
MMFVectorAttribute vector = (MMFVectorAttribute)attribute;
if (property.propertyType == SerializedPropertyType.Vector2)
{
float[] fieldArray = new float[] { property.vector2Value.x, property.vector2Value.y };
fieldArray = DrawFields(rect, fieldArray, ObjectNames.NicifyVariableName(property.name), EditorGUI.FloatField, vector, guiContent);
property.vector2Value = new Vector2(fieldArray[0], fieldArray[1]);
}
else if (property.propertyType == SerializedPropertyType.Vector3)
{
float[] fieldArray = new float[] { property.vector3Value.x, property.vector3Value.y, property.vector3Value.z };
fieldArray = DrawFields(rect, fieldArray, ObjectNames.NicifyVariableName(property.name), EditorGUI.FloatField, vector, guiContent);
property.vector3Value = new Vector3(fieldArray[0], fieldArray[1], fieldArray[2]);
}
else if (property.propertyType == SerializedPropertyType.Vector4)
{
float[] fieldArray = new float[] { property.vector4Value.x, property.vector4Value.y, property.vector4Value.z, property.vector4Value.w };
fieldArray = DrawFields(rect, fieldArray, ObjectNames.NicifyVariableName(property.name), EditorGUI.FloatField, vector, guiContent);
property.vector4Value = new Vector4(fieldArray[0], fieldArray[1], fieldArray[2]);
}
else if (property.propertyType == SerializedPropertyType.Vector2Int)
{
int[] fieldArray = new int[] { property.vector2IntValue.x, property.vector2IntValue.y };
fieldArray = DrawFields(rect, fieldArray, ObjectNames.NicifyVariableName(property.name), EditorGUI.IntField, vector, guiContent);
property.vector2IntValue = new Vector2Int(fieldArray[0], fieldArray[1]);
}
else if (property.propertyType == SerializedPropertyType.Vector3Int)
{
int[] array = new int[] { property.vector3IntValue.x, property.vector3IntValue.y, property.vector3IntValue.z };
array = DrawFields(rect, array, ObjectNames.NicifyVariableName(property.name), EditorGUI.IntField, vector, guiContent);
property.vector3IntValue = new Vector3Int(array[0], array[1], array[2]);
}
}
protected T[] DrawFields<T>(Rect rect, T[] vector, string mainLabel, System.Func<Rect, GUIContent, T, T> fieldDrawer, MMFVectorAttribute vectors, GUIContent originalGuiContent)
{
T[] result = vector;
bool shortSpace = (Screen.width < padding);
Rect mainLabelRect = rect;
mainLabelRect.width = EditorGUIUtility.labelWidth;
if (shortSpace)
{
mainLabelRect.height *= 0.5f;
}
Rect fieldRect = rect;
if (shortSpace)
{
fieldRect.height *= 0.5f;
fieldRect.y += fieldRect.height;
fieldRect.width = rect.width / vector.Length;
}
else
{
fieldRect.x += mainLabelRect.width;
fieldRect.width = (rect.width - mainLabelRect.width) / vector.Length;
}
GUIContent mainLabelContent = new GUIContent();
mainLabelContent.text = mainLabel;
mainLabelContent.tooltip = originalGuiContent.tooltip;
EditorGUI.LabelField(mainLabelRect, mainLabelContent);
for (int i = 0; i < vector.Length; i++)
{
GUIContent label = vectors.Labels.Length > i ? new GUIContent(vectors.Labels[i]) : originalLabels[i];
Vector2 labelSize = EditorStyles.label.CalcSize(label);
EditorGUIUtility.labelWidth = Mathf.Max(labelSize.x + 5, 0.3f * fieldRect.width);
result[i] = fieldDrawer(fieldRect, label, vector[i]);
fieldRect.x += fieldRect.width;
}
EditorGUIUtility.labelWidth = 0;
return result;
}
}
#endif
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public class MMFHiddenPropertiesAttribute : Attribute
{
public string[] PropertiesNames;
public MMFHiddenPropertiesAttribute(params string[] propertiesNames)
{
PropertiesNames = propertiesNames;
}
}
/// <summary>
/// An attribute used to group inspector fields under common dropdowns
/// Implementation inspired by Rodrigo Prinheiro's work, available at https://github.com/RodrigoPrinheiro/unityFoldoutAttribute
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Class | AttributeTargets.Struct, Inherited = true)]
public class MMFInspectorGroupAttribute : PropertyAttribute
{
public string GroupName;
public bool GroupAllFieldsUntilNextGroupAttribute;
public int GroupColorIndex;
public bool RequiresSetup;
public bool ClosedByDefault;
public MMFInspectorGroupAttribute(string groupName, bool groupAllFieldsUntilNextGroupAttribute = false, int groupColorIndex = 24, bool requiresSetup = false, bool closedByDefault = false)
{
if (groupColorIndex > 139) { groupColorIndex = 139; }
this.GroupName = groupName;
this.GroupAllFieldsUntilNextGroupAttribute = groupAllFieldsUntilNextGroupAttribute;
this.GroupColorIndex = groupColorIndex;
this.RequiresSetup = requiresSetup;
this.ClosedByDefault = closedByDefault;
}
}
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true, Inherited = true)]
public class TmpAttribute : PropertyAttribute
{
/// <summary>
/// <para>The header text.</para>
/// </summary>
/// <footer><a href="https://docs.unity3d.com/2019.4/Documentation/ScriptReference/30_search.html?q=HeaderAttribute.header">`HeaderAttribute.header` on docs.unity3d.com</a></footer>
public readonly string header;
/// <summary>
/// <para>Add a header above some fields in the Inspector.</para>
/// </summary>
/// <param name="header">The header text.</param>
/// <footer><a href="https://docs.unity3d.com/2019.4/Documentation/ScriptReference/30_search.html?q=HeaderAttribute">`HeaderAttribute` on docs.unity3d.com</a></footer>
public TmpAttribute(string header) => this.header = header;
}
public static class MMFeedbackStaticMethods
{
static List<Component> m_ComponentCache = new List<Component>();
/// <summary>
/// Grabs a component without allocating memory uselessly
/// </summary>
/// <param name="this"></param>
/// <param name="componentType"></param>
/// <returns></returns>
public static Component GetComponentNoAlloc(this GameObject @this, System.Type componentType)
{
@this.GetComponents(componentType, m_ComponentCache);
var component = m_ComponentCache.Count > 0 ? m_ComponentCache[0] : null;
m_ComponentCache.Clear();
return component;
}
public static Type MMFGetTypeByName(string name)
{
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (Type type in assembly.GetTypes())
{
if (type.Name == name)
{
return type;
}
}
}
return null;
}
/// <summary>
/// Grabs a component without allocating memory uselessly
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="this"></param>
/// <returns></returns>
public static T MMFGetComponentNoAlloc<T>(this GameObject @this) where T : Component
{
@this.GetComponents(typeof(T), m_ComponentCache);
Component component = m_ComponentCache.Count > 0 ? m_ComponentCache[0] : null;
m_ComponentCache.Clear();
return component as T;
}
#if UNITY_EDITOR
/// <summary>
/// Returns the object value of a target serialized property
/// </summary>
/// <param name="property"></param>
/// <returns></returns>
public static object MMFGetObjectValue(this SerializedProperty property)
{
if (property == null)
{
return null;
}
string propertyPath = property.propertyPath.Replace(".Array.data[", "[");
object targetObject = property.serializedObject.targetObject;
var elements = propertyPath.Split('.');
foreach (var element in elements)
{
if (!element.Contains("["))
{
targetObject = MMFGetPropertyValue(targetObject, element);
}
else
{
string elementName = element.Substring(0, element.IndexOf("["));
int elementIndex = System.Convert.ToInt32(element.Substring(element.IndexOf("[")).Replace("[", "").Replace("]", ""));
targetObject = MMFGetPropertyValue(targetObject, elementName, elementIndex);
}
}
return targetObject;
}
private static object MMFGetPropertyValue(object source, string propertyName)
{
if (source == null)
{
return null;
}
Type propertyType = source.GetType();
while (propertyType != null)
{
FieldInfo fieldInfo = propertyType.GetField(propertyName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
if (fieldInfo != null)
{
return fieldInfo.GetValue(source);
}
PropertyInfo propertyInfo = propertyType.GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.IgnoreCase | BindingFlags.Instance);
if (propertyInfo != null)
{
return propertyInfo.GetValue(source, null);
}
propertyType = propertyType.BaseType;
}
return null;
}
private static object MMFGetPropertyValue(object source, string propertyName, int index)
{
var enumerable = MMFGetPropertyValue(source, propertyName) as System.Collections.IEnumerable;
if (enumerable == null)
{
return null;
}
var enumerator = enumerable.GetEnumerator();
for (int i = 0; i <= index; i++)
{
if (!enumerator.MoveNext())
{
return null;
}
}
return enumerator.Current;
}
#endif
}
/// <summary>
/// Atttribute used to mark feedback class.
/// The provided path is used to sort the feedback list displayed in the feedback manager dropdown
/// </summary>
[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class FeedbackPathAttribute : System.Attribute
{
public string Path;
public string Name;
public FeedbackPathAttribute(string path)
{
Path = path;
Name = path.Split('/').Last();
}
static public string GetFeedbackDefaultName(System.Type type)
{
FeedbackPathAttribute attribute = type.GetCustomAttributes(false).OfType<FeedbackPathAttribute>().FirstOrDefault();
return attribute != null ? attribute.Name : type.Name;
}
static public string GetFeedbackDefaultPath(System.Type type)
{
FeedbackPathAttribute attribute = type.GetCustomAttributes(false).OfType<FeedbackPathAttribute>().FirstOrDefault();
return attribute != null ? attribute.Path : null;
}
}
/// <summary>
/// Atttribute used to mark feedback class.
/// The contents allow you to specify a help text for each feedback
/// </summary>
[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class FeedbackHelpAttribute : System.Attribute
{
public string HelpText;
public FeedbackHelpAttribute(string helpText)
{
HelpText = helpText;
}
static public string GetFeedbackHelpText(System.Type type)
{
FeedbackHelpAttribute attribute = type.GetCustomAttributes(false).OfType<FeedbackHelpAttribute>().FirstOrDefault();
return attribute != null ? attribute.HelpText : "";
}
}
public static class MMF_FieldInfo
{
public static Dictionary<int, List<FieldInfo>> FieldInfoList = new Dictionary<int, List<FieldInfo>>();
public static int GetFieldInfo(MMF_Feedback target, out List<FieldInfo> fieldInfoList)
{
Type targetType = target.GetType();
int targetTypeHashCode = targetType.GetHashCode();
if (!FieldInfoList.TryGetValue(targetTypeHashCode, out fieldInfoList))
{
IList<Type> typeTree = targetType.GetBaseTypes();
fieldInfoList = target.GetType().GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.NonPublic)
.OrderByDescending(x => typeTree.IndexOf(x.DeclaringType))
.ToList();
FieldInfoList.Add(targetTypeHashCode, fieldInfoList);
}
return fieldInfoList.Count;
}
public static int GetFieldInfo(UnityEngine.Object target, out List<FieldInfo> fieldInfoList)
{
Type targetType = target.GetType();
int targetTypeHashCode = targetType.GetHashCode();
if (!FieldInfoList.TryGetValue(targetTypeHashCode, out fieldInfoList))
{
IList<Type> typeTree = targetType.GetBaseTypes();
fieldInfoList = target.GetType().GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.NonPublic)
.OrderByDescending(x => typeTree.IndexOf(x.DeclaringType))
.ToList();
FieldInfoList.Add(targetTypeHashCode, fieldInfoList);
}
return fieldInfoList.Count;
}
public static IList<Type> GetBaseTypes(this Type t)
{
var types = new List<Type>();
while (t.BaseType != null)
{
types.Add(t);
t = t.BaseType;
}
return types;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0542c719094b3924cbee2c7229b1f4eb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,34 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using MoreMountains.Feedbacks;
namespace MoreMountains.Feedbacks
{
public class MMFeedbacksInspectorColors : MonoBehaviour
{
public static Color32 GameObjectColor = new Color32(76, 174, 80, 255);
public static Color32 PostProcessColor = new Color32(254, 234, 59, 255);
public static Color32 RendererColor = new Color32(254, 151, 0, 255);
public static Color32 TransformColor = new Color32(134, 209, 243, 255);
public static Color32 CameraColor = new Color32(237, 0, 0, 255);
public static Color32 SoundsColor = new Color32(155, 39, 175, 255);
public static Color32 EventsColor = new Color32(232, 30, 99, 255);
public static Color32 SceneColor = new Color32(232, 30, 99, 255);
public static Color32 TimeColor = new Color32(240, 172, 172, 255);
public static Color32 LightColor = new Color32(254, 192, 7, 255);
public static Color32 ParticlesColor = new Color32(0, 149, 135, 255);
public static Color32 UIColor = new Color32(225, 2, 65, 255);
public static Color32 TMPColor = new Color32(135, 206, 250, 255);
public static Color32 HapticsColor = new Color32(61, 206, 250, 255);
public static Color32 FeedbacksColor = new Color32(105, 32, 133, 255);
public static Color32 AnimationColor = new Color32(200, 48, 128, 255);
public static Color32 SpringColor = new Color32(221, 230, 128, 255);
public static Color32 PauseColor = new Color32(98, 115, 0, 255);
public static Color32 HoldingPauseColor = new Color32(0, 114, 61, 255);
public static Color32 LooperColor = new Color32(12, 100, 128, 255);
public static Color32 DebugColor = new Color32(255, 0, 0, 255);
public static Color32 LooperStartColor = new Color32(166, 75, 5, 255);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 97e6ede6fceb23f40805fef166a6f181
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,365 @@
using System;
using MoreMountains.Tools;
using UnityEngine;
namespace MoreMountains.Feedbacks
{
public class MMShaker : MMMonoBehaviour
{
[MMInspectorGroup("Shaker Settings", true, 3)]
/// whether to listen on a channel defined by an int or by a MMChannel scriptable object. Ints are simple to setup but can get messy and make it harder to remember what int corresponds to what.
/// MMChannel scriptable objects require you to create them in advance, but come with a readable name and are more scalable
[Tooltip("whether to listen on a channel defined by an int or by a MMChannel scriptable object. Ints are simple to setup but can get messy and make it harder to remember what int corresponds to what. " +
"MMChannel scriptable objects require you to create them in advance, but come with a readable name and are more scalable")]
public MMChannelModes ChannelMode = MMChannelModes.Int;
/// the channel to listen to - has to match the one on the feedback
[Tooltip("the channel to listen to - has to match the one on the feedback")]
[MMEnumCondition("ChannelMode", (int)MMChannelModes.Int)]
public int Channel = 0;
/// the MMChannel definition asset to use to listen for events. The feedbacks targeting this shaker will have to reference that same MMChannel definition to receive events - to create a MMChannel,
/// right click anywhere in your project (usually in a Data folder) and go MoreMountains > MMChannel, then name it with some unique name
[Tooltip("the MMChannel definition asset to use to listen for events. The feedbacks targeting this shaker will have to reference that same MMChannel definition to receive events - to create a MMChannel, " +
"right click anywhere in your project (usually in a Data folder) and go MoreMountains > MMChannel, then name it with some unique name")]
[MMEnumCondition("ChannelMode", (int)MMChannelModes.MMChannel)]
public MMChannel MMChannelDefinition = null;
/// the duration of the shake, in seconds
[Tooltip("the duration of the shake, in seconds")]
public float ShakeDuration = 0.2f;
/// if this is true this shaker will play on awake
[Tooltip("if this is true this shaker will play on awake")]
public bool PlayOnAwake = false;
/// if this is true, the shaker will shake permanently as long as its game object is active
[Tooltip("if this is true, the shaker will shake permanently as long as its game object is active")]
public bool PermanentShake = false;
/// if this is true, a new shake can happen while shaking
[Tooltip("if this is true, a new shake can happen while shaking")]
public bool Interruptible = true;
/// if this is true, this shaker will always reset target values, regardless of how it was called
[Tooltip("if this is true, this shaker will always reset target values, regardless of how it was called")]
public bool AlwaysResetTargetValuesAfterShake = false;
/// if this is true, this shaker will ignore any value passed in an event that triggered it, and will instead use the values set on its inspector
[Tooltip("if this is true, this shaker will ignore any value passed in an event that triggered it, and will instead use the values set on its inspector")]
public bool OnlyUseShakerValues = false;
/// a cooldown, in seconds, after a shake, during which no other shake can start
[Tooltip("a cooldown, in seconds, after a shake, during which no other shake can start")]
public float CooldownBetweenShakes = 0f;
/// whether or not this shaker is shaking right now
[Tooltip("whether or not this shaker is shaking right now")]
[MMFReadOnly]
public bool Shaking = false;
[HideInInspector]
public bool ForwardDirection = true;
[HideInInspector]
public TimescaleModes TimescaleMode = TimescaleModes.Scaled;
public virtual float GetTime() { return (TimescaleMode == TimescaleModes.Scaled) ? Time.time : Time.unscaledTime; }
public virtual float GetDeltaTime() { return (TimescaleMode == TimescaleModes.Scaled) ? Time.deltaTime : Time.unscaledDeltaTime; }
public virtual MMChannelData ChannelData => new MMChannelData(ChannelMode, Channel, MMChannelDefinition);
public virtual bool ListeningToEvents => _listeningToEvents;
[HideInInspector]
internal bool _listeningToEvents = false;
protected float _shakeStartedTimestamp = -Single.MaxValue;
protected float _remappedTimeSinceStart;
protected bool _resetShakerValuesAfterShake;
protected bool _resetTargetValuesAfterShake;
protected float _journey;
/// <summary>
/// On Awake we grab our volume and profile
/// </summary>
protected virtual void Awake()
{
Initialization();
// in case someone else trigger StartListening before Awake
if (!_listeningToEvents)
{
StartListening();
}
Shaking = PlayOnAwake;
this.enabled = PlayOnAwake;
}
/// <summary>
/// Override this method to initialize your shaker
/// </summary>
protected virtual void Initialization()
{
}
/// <summary>
/// Call this externally if you need to force a new initialization
/// </summary>
public virtual void ForceInitialization()
{
Initialization();
}
/// <summary>
/// Starts shaking the values
/// </summary>
public virtual void StartShaking()
{
_journey = ForwardDirection ? 0f : ShakeDuration;
if (GetTime() - _shakeStartedTimestamp < CooldownBetweenShakes)
{
return;
}
if (Shaking)
{
return;
}
else
{
this.enabled = true;
_shakeStartedTimestamp = GetTime();
Shaking = true;
GrabInitialValues();
ShakeStarts();
}
}
/// <summary>
/// Describes what happens when a shake starts
/// </summary>
protected virtual void ShakeStarts()
{
}
/// <summary>
/// A method designed to collect initial values
/// </summary>
protected virtual void GrabInitialValues()
{
}
/// <summary>
/// On Update, we shake our values if needed, or reset if our shake has ended
/// </summary>
protected virtual void Update()
{
if (Shaking || PermanentShake)
{
Shake();
_journey += ForwardDirection ? GetDeltaTime() : -GetDeltaTime();
}
if (Shaking && !PermanentShake && ((_journey < 0) || (_journey > ShakeDuration)))
{
Shaking = false;
ShakeComplete();
}
if (PermanentShake)
{
if (_journey < 0)
{
_journey = ShakeDuration;
}
if (_journey > ShakeDuration)
{
_journey = 0;
}
}
}
/// <summary>
/// Override this method to implement shake over time
/// </summary>
protected virtual void Shake()
{
}
/// <summary>
/// A method used to "shake" a flot over time along a curve
/// </summary>
/// <param name="curve"></param>
/// <param name="remapMin"></param>
/// <param name="remapMax"></param>
/// <param name="relativeIntensity"></param>
/// <param name="initialValue"></param>
/// <returns></returns>
protected virtual float ShakeFloat(AnimationCurve curve, float remapMin, float remapMax, bool relativeIntensity, float initialValue)
{
float newValue = 0f;
float remappedTime = MMFeedbacksHelpers.Remap(_journey, 0f, ShakeDuration, 0f, 1f);
float curveValue = curve.Evaluate(remappedTime);
newValue = MMFeedbacksHelpers.Remap(curveValue, 0f, 1f, remapMin, remapMax);
if (relativeIntensity)
{
newValue += initialValue;
}
return newValue;
}
protected virtual Color ShakeGradient(Gradient gradient)
{
float remappedTime = MMFeedbacksHelpers.Remap(_journey, 0f, ShakeDuration, 0f, 1f);
return gradient.Evaluate(remappedTime);
}
/// <summary>
/// Resets the values on the target
/// </summary>
protected virtual void ResetTargetValues()
{
}
/// <summary>
/// Resets the values on the shaker
/// </summary>
protected virtual void ResetShakerValues()
{
}
/// <summary>
/// Describes what happens when the shake is complete
/// </summary>
protected virtual void ShakeComplete()
{
_journey = ForwardDirection ? ShakeDuration : 0f;
Shake();
if (_resetTargetValuesAfterShake || AlwaysResetTargetValuesAfterShake)
{
ResetTargetValues();
}
if (_resetShakerValuesAfterShake)
{
ResetShakerValues();
}
this.enabled = false;
}
/// <summary>
/// On enable we start shaking if needed
/// </summary>
protected virtual void OnEnable()
{
StartShaking();
}
/// <summary>
/// On destroy we stop listening for events
/// </summary>
protected virtual void OnDestroy()
{
StopListening();
}
/// <summary>
/// On disable we complete our shake if it was in progress
/// </summary>
protected virtual void OnDisable()
{
if (Shaking)
{
ShakeComplete();
}
}
/// <summary>
/// Starts this shaker
/// </summary>
public virtual void Play()
{
if (GetTime() - _shakeStartedTimestamp < CooldownBetweenShakes)
{
return;
}
this.enabled = true;
}
/// <summary>
/// Stops this shaker
/// </summary>
public virtual void Stop()
{
Shaking = false;
ShakeComplete();
}
/// <summary>
/// Starts listening for events
/// </summary>
public virtual void StartListening()
{
_listeningToEvents = true;
}
/// <summary>
/// Stops listening for events
/// </summary>
public virtual void StopListening()
{
_listeningToEvents = false;
}
/// <summary>
/// Returns true if this shaker should listen to events, false otherwise
/// </summary>
/// <param name="channel"></param>
/// <returns></returns>
protected virtual bool CheckEventAllowed(MMChannelData channelData, bool useRange = false, float range = 0f, Vector3 eventOriginPosition = default(Vector3))
{
if (!MMChannel.Match(channelData, ChannelMode, Channel, MMChannelDefinition))
{
return false;
}
if (!this.gameObject.activeInHierarchy)
{
return false;
}
else
{
if (useRange)
{
if (Vector3.Distance(this.transform.position, eventOriginPosition) > range)
{
return false;
}
}
return true;
}
}
public virtual float ComputeRangeIntensity(bool useRange, float rangeDistance, bool useRangeFalloff, AnimationCurve rangeFalloff, Vector2 remapRangeFalloff, Vector3 rangePosition)
{
if (!useRange)
{
return 1f;
}
float distanceToCenter = Vector3.Distance(rangePosition, this.transform.position);
if (distanceToCenter > rangeDistance)
{
return 0f;
}
if (!useRangeFalloff)
{
return 1f;
}
float normalizedDistance = MMMaths.Remap(distanceToCenter, 0f, rangeDistance, 0f, 1f);
float curveValue = rangeFalloff.Evaluate(normalizedDistance);
float newIntensity = MMMaths.Remap(curveValue, 0f, 1f, remapRangeFalloff.x, remapRangeFalloff.y);
return newIntensity;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 25d31bbce27b6524192fba29ccfe28df
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: dea619c1b9acaab45aaea2653a61c4af
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,236 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace MoreMountains.Feedbacks
{
public class MMMiniObjectPooler : MonoBehaviour
{
/// the game object we'll instantiate
public GameObject GameObjectToPool;
/// the number of objects we'll add to the pool
public int PoolSize = 20;
/// if true, the pool will automatically add objects to the itself if needed
public bool PoolCanExpand = true;
/// if this is true, the pool will try not to create a new waiting pool if it finds one with the same name.
public bool MutualizeWaitingPools = false;
/// if this is true, all waiting and active objects will be regrouped under an empty game object. Otherwise they'll just be at top level in the hierarchy
public bool NestWaitingPool = true;
/// this object is just used to group the pooled objects
protected GameObject _waitingPool = null;
protected MMMiniObjectPool _objectPool;
protected const int _initialPoolsListCapacity = 5;
static List<MMMiniObjectPool> _pools = new List<MMMiniObjectPool>(_initialPoolsListCapacity);
/// <summary>
/// Adds a pooler to the static list if needed
/// </summary>
/// <param name="pool"></param>
public static void AddPool(MMMiniObjectPool pool)
{
if (_pools == null)
{
_pools = new List<MMMiniObjectPool>(_initialPoolsListCapacity);
}
if (!_pools.Contains(pool))
{
_pools.Add(pool);
}
}
/// <summary>
/// Removes a pooler from the static list
/// </summary>
/// <param name="pool"></param>
public static void RemovePool(MMMiniObjectPool pool)
{
_pools?.Remove(pool);
}
/// <summary>
/// On awake we fill our object pool
/// </summary>
protected virtual void Awake()
{
FillObjectPool();
}
/// <summary>
/// On Destroy we remove ourselves from the list of poolers
/// </summary>
private void OnDestroy()
{
if (_objectPool != null)
{
RemovePool(_objectPool);
}
}
/// <summary>
/// Looks for an existing pooler for the same object, returns it if found, returns null otherwise
/// </summary>
/// <param name="objectToPool"></param>
/// <returns></returns>
public virtual MMMiniObjectPool ExistingPool(string poolName)
{
if (_pools == null)
{
_pools = new List<MMMiniObjectPool>(_initialPoolsListCapacity);
}
if (_pools.Count == 0)
{
var pools = FindObjectsOfType<MMMiniObjectPool>();
if (pools.Length > 0)
{
_pools.AddRange(pools);
}
}
foreach (MMMiniObjectPool pool in _pools)
{
if ((pool != null) && (pool.name == poolName)/* && (pool.gameObject.scene == this.gameObject.scene)*/)
{
return pool;
}
}
return null;
}
/// <summary>
/// Creates the waiting pool or tries to reuse one if there's already one available
/// </summary>
protected virtual void CreateWaitingPool()
{
if (!MutualizeWaitingPools)
{
// we create a container that will hold all the instances we create
_objectPool = this.gameObject.AddComponent<MMMiniObjectPool>();
_objectPool.PooledGameObjects = new List<GameObject>();
return;
}
else
{
MMMiniObjectPool waitingPool = ExistingPool(DetermineObjectPoolName(GameObjectToPool));
if (waitingPool != null)
{
_waitingPool = waitingPool.gameObject;
_objectPool = waitingPool;
}
else
{
GameObject newPool = new GameObject();
newPool.name = DetermineObjectPoolName(GameObjectToPool);
SceneManager.MoveGameObjectToScene(newPool, this.gameObject.scene);
_objectPool = newPool.AddComponent<MMMiniObjectPool>();
_objectPool.PooledGameObjects = new List<GameObject>();
AddPool(_objectPool);
}
}
}
/// <summary>
/// Determines the name of the object pool.
/// </summary>
/// <returns>The object pool name.</returns>
public static string DetermineObjectPoolName(GameObject gameObjectToPool)
{
return (gameObjectToPool.name + "_pool");
}
/// <summary>
/// Implement this method to fill the pool with objects
/// </summary>
public virtual void FillObjectPool()
{
if (GameObjectToPool == null)
{
return;
}
CreateWaitingPool();
int objectsToSpawn = PoolSize;
if (_objectPool != null)
{
objectsToSpawn -= _objectPool.PooledGameObjects.Count;
}
// we add to the pool the specified number of objects
for (int i = 0; i < objectsToSpawn; i++)
{
AddOneObjectToThePool();
}
}
/// <summary>
/// Implement this method to return a gameobject
/// </summary>
/// <returns>The pooled game object.</returns>
public virtual GameObject GetPooledGameObject()
{
// we go through the pool looking for an inactive object
for (int i = 0; i < _objectPool.PooledGameObjects.Count; i++)
{
if (!_objectPool.PooledGameObjects[i].gameObject.activeInHierarchy)
{
// if we find one, we return it
return _objectPool.PooledGameObjects[i];
}
}
// if we haven't found an inactive object (the pool is empty), and if we can extend it, we add one new object to the pool, and return it
if (PoolCanExpand)
{
return AddOneObjectToThePool();
}
// if the pool is empty and can't grow, we return nothing.
return null;
}
/// <summary>
/// Adds one object of the specified type (in the inspector) to the pool.
/// </summary>
/// <returns>The one object to the pool.</returns>
protected virtual GameObject AddOneObjectToThePool()
{
if (GameObjectToPool == null)
{
Debug.LogWarning("The " + gameObject.name + " ObjectPooler doesn't have any GameObjectToPool defined.", gameObject);
return null;
}
GameObjectToPool.gameObject.SetActive(false);
GameObject newGameObject = (GameObject)Instantiate(GameObjectToPool);
SceneManager.MoveGameObjectToScene(newGameObject, this.gameObject.scene);
if (NestWaitingPool)
{
newGameObject.transform.SetParent(_objectPool.transform);
}
newGameObject.name = GameObjectToPool.name + "-" + _objectPool.PooledGameObjects.Count;
_objectPool.PooledGameObjects.Add(newGameObject);
return newGameObject;
}
/// <summary>
/// Destroys the object pool
/// </summary>
public virtual void DestroyObjectPool()
{
if (_waitingPool != null)
{
Destroy(_waitingPool.gameObject);
}
}
}
public class MMMiniObjectPool : MonoBehaviour
{
[MMFReadOnly]
public List<GameObject> PooledGameObjects;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5a2e3aab8bd306249afbd26c52205f46
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,58 @@
using UnityEngine;
using System.Collections;
using MoreMountains.Feedbacks;
using System;
namespace MoreMountains.Feedbacks
{
/// <summary>
/// Add this class to an object that you expect to pool from an objectPooler.
/// Note that these objects can't be destroyed by calling Destroy(), they'll just be set inactive (that's the whole point).
/// </summary>
public class MMMiniPoolableObject : MonoBehaviour
{
public delegate void Events();
public event Events OnSpawnComplete;
/// The life time, in seconds, of the object. If set to 0 it'll live forever, if set to any positive value it'll be set inactive after that time.
public float LifeTime = 0f;
/// <summary>
/// Turns the instance inactive, in order to eventually reuse it.
/// </summary>
public virtual void Destroy()
{
gameObject.SetActive(false);
}
/// <summary>
/// When the objects get enabled (usually after having been pooled from an ObjectPooler, we initiate its death countdown.
/// </summary>
protected virtual void OnEnable()
{
if (LifeTime > 0)
{
Invoke("Destroy", LifeTime);
}
}
/// <summary>
/// When the object gets disabled (maybe it got out of bounds), we cancel its programmed death
/// </summary>
protected virtual void OnDisable()
{
CancelInvoke();
}
/// <summary>
/// Triggers the on spawn complete event
/// </summary>
public virtual void TriggerOnSpawnComplete()
{
if(OnSpawnComplete != null)
{
OnSpawnComplete();
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b0b3f00a75cc857439b13b8bafb37894
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: