Creating a new feedback
Creating a new feedback is very easy. Create a new class, have it inherit from MMF_Feedback, and override the methods you’re interested in (usually CustomInitialization, CustomPlayFeedback, CustomStopFeedback and CustomReset). You can look at each feedback for a reference of how it’s done, they’re all heavily commented. Here’s a template you can use as a starting point:
using UnityEngine;
using MoreMountains.Tools;
namespace MoreMountains.Feedbacks
{
[AddComponentMenu("")]
[System.Serializable]
[FeedbackHelp("You can add a description for your feedback here.")]
[FeedbackPath("ChosenPath/MyFeedbackNameGoesHere")]
public class MMF_MyFeedbackName : MMF_Feedback
{
/// a static bool used to disable all feedbacks of this type at once
public static bool FeedbackTypeAuthorized = true;
/// use this override to specify the duration of your feedback (don't hesitate to look at other feedbacks for reference)
public override float FeedbackDuration { get { return 0f; } }
/// pick a color here for your feedback's inspector
#if UNITY_EDITOR
public override Color FeedbackColor { get { return MMFeedbacksInspectorColors.DebugColor; } }
#endif
protected override void CustomInitialization(MMF_Player owner)
{
base.CustomInitialization(owner);
// your init code goes here
}
protected override void CustomPlayFeedback(Vector3 position, float feedbacksIntensity = 1.0f)
{
if (!Active || !FeedbackTypeAuthorized)
{
return;
}
// your play code goes here
}
protected override void CustomStopFeedback(Vector3 position, float feedbacksIntensity = 1)
{
if (!FeedbackTypeAuthorized)
{
return;
}
// your stop code goes here
}
}
}
Modifying an existing feedback
Sometimes you find a feedback that does almost what you’re after, but not exactly. In situations like that, you have a few options:
- create a new feedback entirely: this will work, but it may be a waste of time to redo everything another feedback already does, and it’ll make for a lot of duplicated code
- modify the existing feedback: this is also possible, but you may lose your changes the next time you update Feel
- inherit from the current feedback and change its behavior: that’s usually the recommended option, it’s the fastest, cleanest and safest way to do it
Inheriting from an existing feedback and extending it is done just like for any other class in C#. You can learn more about how it’s done in Unity’s documentation.
Let’s go over a simple example: let’s say we’re using the TextMeshPro Count To feedback to display our current in-game credits, maybe at the end of a level, to have that text display go from 0 to the collected amount. It’s all working fine, but we’d like to add a dollar sign at the end of it. Smarter people might say we could just add another text element next to it, with our dollar sign. But for the sake of the demonstration, what if we wanted to have the feedback itself add a suffix to the same text element?
That’s easily done with inheritance. The first thing to do is figure out what script to extend. The name of feedbacks in Feel always starts with the ‘MMF_’ prefix. You can usually find a feedback via the search bar in your editor’s Project panel, simply type its name. In our case, we want to extend MMF_TMPCountTo. Let’s do it:
using MoreMountains.Feedbacks;
using UnityEngine;
// here you specify how to access that feedback in the "Add Feedback" dropdown
[FeedbackPath("TextMesh Pro/TMP Count To With Suffix")]
// we declare the name of our class, and specify it inherits from MMF_TMPCountTo
public class MMF_TMPCountToSuffix : MMF_TMPCountTo
{
// we want this new feedback to display a suffix, so we create a new string we'll add to our text
public string Suffix = "$";
// we override the UpdateText method, which is the only part of the feedback we want to change,
// the rest will behave as it did before
protected override void UpdateText(float currentValue)
{
// we call the base method, as we want it to run unchanged, we just want to do an extra thing at the end
// of course that's not mandatory, and we could have changed entirely how that method behaves by simply not calling base
base.UpdateText(currentValue);
// we add our suffix to our TextMeshPro
TargetTMPText.text = TargetTMPText.text + Suffix;
}
}
Now we can just remove our old feedback, and start using this new one in its place. Extending feedbacks is a great way to use their existing power and adapt it to your exact project specs. If, while extending a feedback, you feel like the current feedback’s structure isn’t ideal, never hesitate to use the support form to request changes to it, Feel is designed to be extended upon, and is constantly improving on that aspect. All suggestions are welcome!
The MMF_FeedbackBase class
Feedbacks come with a base class you can extend if your use case applies. That abstract class is designed to handle cases where your feedback is meant to modify one or more values (float, vector, color, int, bool, etc) over time, or instantly. Good examples would be the alpha of a CanvasGroup, or the volume of an AudioSource.
If your use case matches, then using that class as a base will save you a lot of time. You can look at the MMF_CanvasGroup for a reference of how it’s done.
using MoreMountains.Tools;
using UnityEngine;
using UnityEngine.Scripting.APIUpdating;
namespace MoreMountains.Feedbacks
{
/// <summary>
/// This feedback lets you control the opacity of a canvas group over time
/// </summary>
[AddComponentMenu("")]
[FeedbackHelp("This feedback lets you control the opacity of a canvas group over time.")]
[MovedFrom(false, null, "MoreMountains.Feedbacks.MMTools")]
[FeedbackPath("UI/CanvasGroup")]
public class MMF_CanvasGroup : MMF_FeedbackBase
{
/// sets the inspector color for this feedback
#if UNITY_EDITOR
public override Color FeedbackColor { get { return MMFeedbacksInspectorColors.UIColor; } }
public override bool EvaluateRequiresSetup() { return (TargetCanvasGroup == null); }
public override string RequiredTargetText { get { return TargetCanvasGroup != null ? TargetCanvasGroup.name : ""; } }
public override string RequiresSetupText { get { return "This feedback requires that a TargetCanvasGroup be set to be able to work properly. You can set one below."; } }
#endif
public override bool HasAutomatedTargetAcquisition => true;
protected override void AutomateTargetAcquisition() => TargetCanvasGroup = FindAutomatedTarget<CanvasGroup>();
// below we declare a number of properties that are specific to our use case
// for each value you'll want to have a MMTweenType curve to move over,
// a target, remap zero and one values, and a value to apply on instant
[MMFInspectorGroup("Canvas Group", true, 12, true)]
/// the receiver to write the level to
[Tooltip("the receiver to write the level to")]
public CanvasGroup TargetCanvasGroup;
/// the curve to tween the opacity on
[Tooltip("the curve to tween the opacity on")]
[MMFEnumCondition("Mode", (int)MMFeedbackBase.Modes.OverTime)]
public MMTweenType AlphaCurve = new MMTweenType(new AnimationCurve(new Keyframe(0, 0), new Keyframe(0.3f, 1f), new Keyframe(1, 0)));
/// the value to remap the opacity curve's 0 to
[Tooltip("the value to remap the opacity curve's 0 to")]
[MMFEnumCondition("Mode", (int)MMFeedbackBase.Modes.OverTime)]
public float RemapZero = 0f;
/// the value to remap the opacity curve's 1 to
[Tooltip("the value to remap the opacity curve's 1 to")]
[MMFEnumCondition("Mode", (int)MMFeedbackBase.Modes.OverTime)]
public float RemapOne = 1f;
/// the value to move the opacity to in instant mode
[Tooltip("the value to move the opacity to in instant mode")]
[MMFEnumCondition("Mode", (int)MMFeedbackBase.Modes.Instant)]
public float InstantAlpha;
public override void OnAddFeedback()
{
base.OnAddFeedback();
RelativeValues = false;
}
// this method is where the magic happens.
// we create a base target and a receiver. We bind our receiver's object
// and specify what property we want to impact on it
// then we setup our MMF_FeedbackBaseTarget, and finally we add it to our list of targets. That's all!
protected override void FillTargets()
{
if (TargetCanvasGroup == null)
{
return;
}
MMF_FeedbackBaseTarget target = new MMF_FeedbackBaseTarget();
MMPropertyReceiver receiver = new MMPropertyReceiver();
receiver.TargetObject = TargetCanvasGroup.gameObject;
receiver.TargetComponent = TargetCanvasGroup;
receiver.TargetPropertyName = "alpha";
receiver.RelativeValue = RelativeValues;
target.Target = receiver;
target.LevelCurve = AlphaCurve;
target.RemapLevelZero = RemapZero;
target.RemapLevelOne = RemapOne;
target.InstantLevel = InstantAlpha;
_targets.Add(target);
}
}
}
Supporting automated target acquisition in your feedbacks
As shown in the example above, adding automated target acquisition support to a custom feedback only takes two lines: override HasAutomatedTargetAcquisition to return true, and implement AutomateTargetAcquisition(), usually by calling FindAutomatedTarget<T>() (or FindAutomatedTargetGameObject()) and storing the result in your target field.
If your feedback has a list of extra targets, you can also support multiple target acquisition the same way. FindAutomatedTargets<T>() (or FindAutomatedTargetGameObjects()) takes care of the selected mode, list mode, duplicates, and of excluding your main target:
public override bool HasAutomatedMultipleTargetAcquisition => true;
protected override void AutomateMultipleTargetAcquisition()
=> ExtraTargetCanvasGroups = FindAutomatedTargets<CanvasGroup>(ExtraTargetCanvasGroups, TargetCanvasGroup);
Both run automatically when the feedback initializes, and can be triggered again at runtime by calling ForceAutomateTargetAcquisition() on the feedback.
Creating a new Shaker
Some feedbacks act directly on a target (like the CanvasGroup one above), but many rely on shakers instead: the feedback broadcasts an event, and any shaker listening on the same channel catches it and does the work. This is what lets a single Vignette feedback drive every vignette in your scene, without any reference to them. You can use that same pattern for your own systems. It requires three parts:
- an event struct, with a static delegate, Register / Unregister methods, and a Trigger method
- a shaker, extending MMShaker, that registers to that event, and animates its target in its
Shake()method - a feedback that triggers the event when played
Here’s a complete example, a shaker that animates the range of a Light. You’d put it on the same object as your Light:
using MoreMountains.Feedbacks;
using MoreMountains.Tools;
using UnityEngine;
/// <summary>
/// Add this to a Light to shake its range when a MMLightRangeShakeEvent is caught
/// </summary>
[RequireComponent(typeof(Light))]
public class MMLightRangeShaker : MMShaker
{
[MMInspectorGroup("Light Range", true, 57)]
/// whether or not to add to the initial value
[Tooltip("whether or not to add to the initial value")]
public bool RelativeRange = false;
/// the curve used to animate the range on
[Tooltip("the curve used to animate the range on")]
public AnimationCurve ShakeRange = new AnimationCurve(new Keyframe(0, 0), new Keyframe(0.5f, 1), new Keyframe(1, 0));
/// the value to remap the curve's 0 to
[Tooltip("the value to remap the curve's 0 to")]
public float RemapRangeZero = 0f;
/// the value to remap the curve's 1 to
[Tooltip("the value to remap the curve's 1 to")]
public float RemapRangeOne = 10f;
protected Light _targetLight;
protected float _initialRange;
protected float _originalShakeDuration;
protected bool _originalRelativeRange;
protected AnimationCurve _originalShakeRange;
protected float _originalRemapRangeZero;
protected float _originalRemapRangeOne;
/// <summary>
/// On init we grab our target
/// </summary>
protected override void Initialization()
{
base.Initialization();
_targetLight = this.gameObject.GetComponent<Light>();
}
/// <summary>
/// Every frame while shaking, we evaluate our curve and apply the result
/// </summary>
protected override void Shake()
{
_targetLight.range = ShakeFloat(ShakeRange, RemapRangeZero, RemapRangeOne, RelativeRange, _initialRange);
}
/// <summary>
/// Collects initial values on the target
/// </summary>
protected override void GrabInitialValues()
{
_initialRange = _targetLight.range;
}
/// <summary>
/// When we catch an event, we store our values if needed, override them with the event's, and start shaking
/// </summary>
public virtual void OnMMLightRangeShakeEvent(AnimationCurve rangeCurve, float duration, float remapMin, float remapMax, bool relativeRange = false,
float feedbacksIntensity = 1.0f, MMChannelData channelData = null, bool resetShakerValuesAfterShake = true, bool resetTargetValuesAfterShake = true,
bool forwardDirection = true, TimescaleModes timescaleMode = TimescaleModes.Scaled, bool stop = false, bool restore = false)
{
if (!CheckEventAllowed(channelData) || (!Interruptible && Shaking))
{
return;
}
if (stop)
{
Stop();
return;
}
if (restore)
{
ResetTargetValues();
return;
}
_resetShakerValuesAfterShake = resetShakerValuesAfterShake;
_resetTargetValuesAfterShake = resetTargetValuesAfterShake;
if (resetShakerValuesAfterShake)
{
_originalShakeDuration = ShakeDuration;
_originalShakeRange = ShakeRange;
_originalRemapRangeZero = RemapRangeZero;
_originalRemapRangeOne = RemapRangeOne;
_originalRelativeRange = RelativeRange;
}
if (!OnlyUseShakerValues)
{
TimescaleMode = timescaleMode;
ShakeDuration = duration;
ShakeRange = rangeCurve;
RemapRangeZero = remapMin * feedbacksIntensity;
RemapRangeOne = remapMax * feedbacksIntensity;
RelativeRange = relativeRange;
ForwardDirection = forwardDirection;
}
Play();
}
/// <summary>
/// Puts the target back in its initial state
/// </summary>
protected override void ResetTargetValues()
{
base.ResetTargetValues();
_targetLight.range = _initialRange;
}
/// <summary>
/// Puts the shaker's values back to what they were before the event overrode them
/// </summary>
protected override void ResetShakerValues()
{
base.ResetShakerValues();
ShakeDuration = _originalShakeDuration;
ShakeRange = _originalShakeRange;
RemapRangeZero = _originalRemapRangeZero;
RemapRangeOne = _originalRemapRangeOne;
RelativeRange = _originalRelativeRange;
}
/// <summary>
/// Starts listening for events
/// </summary>
public override void StartListening()
{
base.StartListening();
MMLightRangeShakeEvent.Register(OnMMLightRangeShakeEvent);
}
/// <summary>
/// Stops listening for events
/// </summary>
public override void StopListening()
{
base.StopListening();
MMLightRangeShakeEvent.Unregister(OnMMLightRangeShakeEvent);
}
}
/// <summary>
/// The event used to trigger light range shakes
/// </summary>
public struct MMLightRangeShakeEvent
{
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(AnimationCurve rangeCurve, float duration, float remapMin, float remapMax, bool relativeRange = false,
float feedbacksIntensity = 1.0f, MMChannelData channelData = null, bool resetShakerValuesAfterShake = true, bool resetTargetValuesAfterShake = true,
bool forwardDirection = true, TimescaleModes timescaleMode = TimescaleModes.Scaled, bool stop = false, bool restore = false);
static public void Trigger(AnimationCurve rangeCurve, float duration, float remapMin, float remapMax, bool relativeRange = false,
float feedbacksIntensity = 1.0f, MMChannelData channelData = null, bool resetShakerValuesAfterShake = true, bool resetTargetValuesAfterShake = true,
bool forwardDirection = true, TimescaleModes timescaleMode = TimescaleModes.Scaled, bool stop = false, bool restore = false)
{
OnEvent?.Invoke(rangeCurve, duration, remapMin, remapMax, relativeRange,
feedbacksIntensity, channelData, resetShakerValuesAfterShake, resetTargetValuesAfterShake, forwardDirection, timescaleMode, stop, restore);
}
}
And the matching feedback, which simply triggers the event on play, stop and restore. Note the HasChannel override, which exposes the channel settings in the feedback’s inspector, so that it only reaches shakers on the same channel:
using MoreMountains.Feedbacks;
using UnityEngine;
[AddComponentMenu("")]
[System.Serializable]
[FeedbackPath("Lights/Light Range")]
[FeedbackHelp("This feedback lets you shake the range of any Light with a MMLightRangeShaker on the same channel.")]
public class MMF_LightRange : MMF_Feedback
{
/// a static bool used to disable all feedbacks of this type at once
public static bool FeedbackTypeAuthorized = true;
#if UNITY_EDITOR
public override Color FeedbackColor { get { return MMFeedbacksInspectorColors.LightColor; } }
public override string RequiredTargetText => RequiredChannelText;
#endif
public override float FeedbackDuration { get { return ApplyTimeMultiplier(Duration); } set { Duration = value; } }
public override bool HasChannel => true;
public override bool HasRandomness => true;
[MMFInspectorGroup("Light Range", true, 12)]
/// the duration of the shake, in seconds
[Tooltip("the duration of the shake, in seconds")]
public float Duration = 0.5f;
/// whether or not to reset shaker values after shake
[Tooltip("whether or not to reset shaker values after shake")]
public bool ResetShakerValuesAfterShake = true;
/// whether or not to reset the target's values after shake
[Tooltip("whether or not to reset the target's values after shake")]
public bool ResetTargetValuesAfterShake = true;
/// whether or not to add to the initial value
[Tooltip("whether or not to add to the initial value")]
public bool RelativeRange = false;
/// the curve used to animate the range on
[Tooltip("the curve used to animate the range on")]
public AnimationCurve RangeCurve = new AnimationCurve(new Keyframe(0, 0), new Keyframe(0.5f, 1), new Keyframe(1, 0));
/// the value to remap the curve's 0 to
[Tooltip("the value to remap the curve's 0 to")]
public float RemapRangeZero = 0f;
/// the value to remap the curve's 1 to
[Tooltip("the value to remap the curve's 1 to")]
public float RemapRangeOne = 10f;
protected override void CustomPlayFeedback(Vector3 position, float feedbacksIntensity = 1.0f)
{
if (!Active || !FeedbackTypeAuthorized)
{
return;
}
float intensityMultiplier = ComputeIntensity(feedbacksIntensity, position);
MMLightRangeShakeEvent.Trigger(RangeCurve, FeedbackDuration, RemapRangeZero, RemapRangeOne, RelativeRange,
intensityMultiplier, ChannelData, ResetShakerValuesAfterShake, ResetTargetValuesAfterShake, NormalPlayDirection, ComputedTimescaleMode);
}
protected override void CustomStopFeedback(Vector3 position, float feedbacksIntensity = 1)
{
if (!Active || !FeedbackTypeAuthorized)
{
return;
}
base.CustomStopFeedback(position, feedbacksIntensity);
MMLightRangeShakeEvent.Trigger(RangeCurve, FeedbackDuration, RemapRangeZero, RemapRangeOne, RelativeRange, stop:true);
}
protected override void CustomRestoreInitialValues()
{
if (!Active || !FeedbackTypeAuthorized)
{
return;
}
MMLightRangeShakeEvent.Trigger(RangeCurve, FeedbackDuration, RemapRangeZero, RemapRangeOne, RelativeRange, restore:true);
}
}
A few things worth knowing when writing your own shakers:
- Don’t register in your own Awake or OnEnable, override StartListening / StopListening (always calling base) as shown above. MMShaker already calls them at the right times, including after a domain reload in edit mode, which is what keeps feedback preview working after you recompile your scripts.
- Keep the
RuntimeInitializeOnLoadMethodline on your event struct. It clears the static delegate when entering play mode, which prevents destroyed shakers from lingering as listeners when domain reload is disabled. - Do your work in
Shake(), it’s called every frame while the shake is in progress, andShakeFloat()takes care of evaluating your curve (in either direction), remapping it, and adding it to the initial value in relative mode. For non float values,GetTime(),ShakeDurationandForwardDirectiongive you what you need to evaluate your own curves. - Respect
OnlyUseShakerValues, it lets users decide to ignore the values sent by feedbacks, and only use the ones set on the shaker itself. - The base shaker’s inspector already exposes channel settings, Play On Awake, Permanent Shake, Interruptible, cooldown and more, you get all of these for free.
Extensions Repository
If you create a new feedback and think others might benefit from it, or if you’re looking for more feedbacks or Feel tools, you can check the Feel Extensions Repository on Github. Contributions are of course always welcome!