Search Unity

smooth slider movement

Discussion in 'Scripting' started by davejones1, Mar 20, 2018.

  1. davejones1

    davejones1

    Joined:
    Jan 19, 2018
    Posts:
    183
    I am using a custom made UI slider that is used to limit the speed at which the slider can be moved. The issue I am having is that when I move the slider the movement isn't smooth. The slider is used to scrub through an animation and it needs to moved smoothly and slowly. So far the speed at which the slider can be moved has been limited, however when the slider is moved it isn't smooth. How do I smooth the movement of the slider? The script used is attached.
    Code (CSharp):
    1. public class Test13 : Slider
    2. {
    3.     float nextTime = 0;
    4.     float currentValue = 0;
    5.     bool changed = false;
    6.     public void SliderValueChanged(float val)
    7.     {
    8.         changed = true;
    9.     }
    10.     void LateUpdate()
    11.     {
    12.         if (!changed) return;
    13.         if (Time.time < nextTime)
    14.         {
    15.             value = currentValue;
    16.             changed = false;
    17.             return;
    18.         }
    19.         if (value < currentValue) currentValue--;
    20.         else if (value > currentValue) currentValue++;
    21.         value = currentValue;
    22.         nextTime = Time.time + .4f;
    23.         changed = false;
    24.     }
    25. }
    26.  
     
  2. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Well right now you're updating currentValue by a whole 1 unit, but doing this only every 0.4 seconds. So it would jump in value a few times per second. I assume that's what you mean by not being smooth.

    To fix that, delete nextTime; you should update currentValue on every frame. Just don't update it as much. In fact how much you update should depend on Time.deltaTime, something like:

    Code (csharp):
    1. currentValue = Mathf.MoveTowards(currentValue, value, changeSpeed * Time.deltaTime);
    where changeSpeed is how fast (in units/sec) you want your value to change.
     
  3. davejones1

    davejones1

    Joined:
    Jan 19, 2018
    Posts:
    183
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.UI;
    5.  
    6. public class Test13 : Slider
    7. {
    8.     float nextTime = 0;
    9.     float currentValue = 0f;
    10.     bool changed = false;
    11.  
    12.     public void SliderValueChanged(float val)
    13.     {
    14.         changed = true;
    15.     }
    16.     void LateUpdate()
    17.     {
    18.         if (!changed) return;
    19.         if (Time.time < currentValue)
    20.         {
    21.             value = currentValue;
    22.             changed = false;
    23.             return;
    24.         }
    25.         if (value < currentValue) currentValue--;
    26.         else if (value > currentValue) currentValue++;
    27.         value = currentValue;
    28.         currentValue = Mathf.MoveTowards(currentValue, value, -1f * Time.deltaTime);
    29.         changed = false;
    30.     }
    31. }
     
  4. davejones1

    davejones1

    Joined:
    Jan 19, 2018
    Posts:
    183
    The issue now is that although the speed of the slider has been slowed down and smoothed out, the animation speed isn't in sync with the slider and the maxvalue reached 'if statement' doesn't seem to take effect. The maxvaluereached() function should only be excecuted when the slider is at its end position..








    Code (CSharp):
    1. private void OnValueChanged()
    2.     {
    3.        
    4.  
    5.         animator.enabled = true;
    6.         animator.speed = 0;
    7.         animator.Play("removeretainingplate", -1, slider.normalizedValue);
    8.    
    9.     }
    10.  
    11.     public void maxvaluereached()
    12.     {
    13.         if (slider.value >= 1     ) {
    14.  
    15.             step1p1complete.SetActive (true);
    16.             AudioSource.PlayClipAtPoint (pingsound, transform.position);
    17.             StartCoroutine ("nextstepp2");
    18.         }
    19.  
    20.     }    
     
  5. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    This seems like a very Rube Goldberg-ish structure. What is it exactly you're trying to do?!
     
  6. davejones1

    davejones1

    Joined:
    Jan 19, 2018
    Posts:
    183
    I am trying to scrub through an animation using a UI slider. The speed at which the slider can be moved has to be limited so that the animation is played smoothly. If the slider speed isn't limited the animation can be played rapidly, which isn't desired. The animation should be played from start to finish as the slider has a minimum and maximum value.

    This means that the animation plays in sync with the position of the slider. From the minimum to maximum value of the slider, the animation should be played.
     
  7. hamoodrex

    hamoodrex

    Joined:
    Mar 21, 2018
    Posts:
    5
    Increase the maximum value by a lot and move it faster, and get the value by multiplying (old maximum value / new maximum value)
     
    davejones1 likes this.
  8. davejones1

    davejones1

    Joined:
    Jan 19, 2018
    Posts:
    183
    I am not sure I entirely understand your recommendation. I have increased the max value of the slider. What do you mean by move it faster?
     
  9. hamoodrex

    hamoodrex

    Joined:
    Mar 21, 2018
    Posts:
    5
    By reducing the time and since you are using negative value I apologize for saying max value you are supposed to increase it on the negative side, like instead of -175 make it -1750 and increase the speed by changing the delay to a 10th of the time, like 0.04f.
     
  10. davejones1

    davejones1

    Joined:
    Jan 19, 2018
    Posts:
    183
    I will try this and let you know if it solved the issue.
     
  11. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    No, don't do that. hamoodrex is suggesting that it's a scaling problem, but that's not it. But your current script is not on the right track, either. It's time to start over.

    First, define exactly how you want it to behave. Normal "scrub" behavior does not limit the speed of the slider; if the user grabs the slider from position A and drags it directly to position B, the thing it's controlling (video/music/animation) should jump directly to position B. Are you sure you don't want it to work that way in your app?

    If so, then how exactly do you want it to work? The user tries to drag it to position B but they can't? They drag it to position B, but the animation position lags behind until it gets there at the normal speed? Or something else?
     
  12. davejones1

    davejones1

    Joined:
    Jan 19, 2018
    Posts:
    183
    Your recommendation didn't solve the issue. It just made the slider move even slower.
    I only want the code I have highlighted to be executed, when the slider is at the max value. At the moment when I move the slider, the maxvaluereached() function is executed before the slider is at its end position.


    Code (CSharp):
    1. private void OnValueChanged()
    2.     {
    3.      
    4.         animator.enabled = true;
    5.         animator.speed = 0;
    6.         animator.Play("removeretainingplate", -1, slider.normalizedValue);
    7.  
    8.     }
    9.     public void maxvaluereached()
    10.     {
    11.         if (slider.value >= 1     ) {
    12.           [COLOR=#ff0000]  step1p1complete.SetActive (true);
    13.             AudioSource.PlayClipAtPoint (pingsound, transform.position);
    14.             StartCoroutine ("nextstepp2");[/COLOR]
    15.         }
    16.     }  
    17.  
     
  13. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Yes. Your current approach subclasses Slider (which is not a great idea to begin with) and attempts to override the changing of the value, through a convoluted bit of plumbing involving responding to its own events. This will not work.

    I can help you, but only if you're willing. Right now you are on a path to wailing and gnashing of teeth. You need to back up and choose the path of happiness. That begins by defining the goal very clearly, in words. Your goal is not to get your maxvaluereached() method invoked at the right time. You've said your goal is to make an animation slider, but you've also described desired behavior that is inconsistent with how such sliders normally work (and is not complete enough to choose an approach in any case).

    So, start by thinking and describing more clearly the end goal... and assume you will be throwing out this Slider subclass and starting over.
     
    davejones1 likes this.
  14. davejones1

    davejones1

    Joined:
    Jan 19, 2018
    Posts:
    183
    Starting from scratch - I don't want the user to just grab the slider from position A to position B, so no it isn't normal scrub behaviour. The UI slider is used to control an animation. At the moment, when the standard slider is used by a user it can be moved from A to B rapidly. I want to reduce the speed at which the slider can be moved from A to B so that the user can't just quickly slide through the animation. The animation speed needs to be in sync with the speed at which the slider is moved. The range from the slider minimum value to the maximum value should be the range in which the animation played.
     
  15. methos5k

    methos5k

    Joined:
    Aug 3, 2015
    Posts:
    8,712
    I came up with the code for the slider, but later I couldn't quite understand what the OP was after, so I just let it go.
     
  16. davejones1

    davejones1

    Joined:
    Jan 19, 2018
    Posts:
    183
    The slider is used to control an animation. The animation is of a screw being removed from a wall. The speed at which the slider can be moved by the user has to be limited so that the animation can be seen clearly. When the maxvalue of the slider has been reached, a message will appear telling the user they have completed the step.
     
  17. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Why not just have a "Play" button then? I don't understand why you're sort-of letting the user scrub, but not really.

    If I understand what you've described correctly, the user experience would go like this:
    1. User sees a stopped animation, and a stopped slider at position A.
    2. User grabs the slider with their mouse, and drags their mouse over to position B. (Presumably because they want to jump to position B, but we're not going to let them...)
    3. Slider does not track with the mouse; instead it starts playing at the standard animation rate towards position B.
    4. When the slider and animation finally catch up to where the mouse is (or was at mouse-up), they stop.
    5. If the user drags the slider backwards, we... I dunno... start playing at the standard rate but backwards, I guess, until again the slider and animation have reached the current or mouse-up position.
    Is this all correct? And if so... wouldn't a "Play" (and perhaps a separate "Play Reverse") button both easier for you, and less frustrating for the user?
     
    davejones1 likes this.
  18. davejones1

    davejones1

    Joined:
    Jan 19, 2018
    Posts:
    183
    The way you have described the user experience is correct.
    The reason why I am using a slider rather than a "play" button is because the slider gives the user more control over the animation. By having a slider the user can reverse the animation much easier. Remember, this is used to show a screw coming out of a wall. The user sees a slider which is used to control an animation.It is just an animation that is being controlled by a slider which relies on input from a users finger.
     
    Last edited: Mar 23, 2018
  19. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    OK, it's your app. :)

    So to do this, you need to do several things:
    • Block direct access to the slider, say with an Image overlay that is invisible but nonetheless blocks raycasts.
    • Detect clicks and drags on that overlay, and use them to set a target for the animation.
    • Have a script that watches this target, and compares it to the current animation position. If the target is ahead, then play the animation (if not playing already). If the target is behind, then play in reverse (if not already). When the target is reached (or close enough — within 1 frame or so), stop.
    • Have another script that watches the animation, and updates the visible slider position to match.
    Notice that the slider the user sees is only a display element; in fact you should uncheck "raycastTarget" for it to be sure. Its job is to reflect the current position of the animation, not to control it. Maybe do this script first, as it's pretty easy.

    The other steps are fairly easy too if you take them one at a time.

    Note that we haven't defined what should happen if the user clicks on the slider outside the scroll thumb. If you want to ignore that, then that script (the second bullet point above) will need to look at the slider or animation just to figure out where the thumb is, and bail out (don't process any further) if it's not hit. An easier behavior would be to not worry about it; let a tap anywhere on the overlay cause the animation to start playing/rewinding to that point.
     
    davejones1 likes this.
  20. davejones1

    davejones1

    Joined:
    Jan 19, 2018
    Posts:
    183
    I will have a go at this and get back to you in a day or two. Thank you for your advice.
     
    JoeStrout likes this.
  21. davejones1

    davejones1

    Joined:
    Jan 19, 2018
    Posts:
    183
    Can 2d physics be used to influence UI sliders?
     
  22. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    What?!? The slider here should be doing nothing other than displaying the current animation position on every frame.

    (To answer your question, no, not directly, though of course with code you can do anything you want.)
     
  23. davejones1

    davejones1

    Joined:
    Jan 19, 2018
    Posts:
    183
    Ok no problem, thanks for your response.
     
  24. davejones1

    davejones1

    Joined:
    Jan 19, 2018
    Posts:
    183

    I have added an image overlay that is invisible. I have also unchecked raycastTarget on the slider gameobject. I am unable to find a way to detect clicks and drags on the overlay image that are used as a target for the animation. I have seen multiple ways to detect drags and clicks such as ray-casting but can't seem to implement a solution. .
     
  25. BlackPete

    BlackPete

    Joined:
    Nov 16, 2016
    Posts:
    970
    I kinda skimmed the thread, so apologies if the following points have already been made, but I thought it'd be good to repeat if necessary:

    1) Overriding Slider is a bad idea. Keep a layer of separation between the UI and your logic code. In fact, keep your slider scale to 0 to 1. Think of it as a lerp value to use in your logic.

    2) Your code has min and max values. If it's your animation scrubber, then I assume you have start and end times. Just use the slider to figure out the current time between start and end times. In fact, you can just use Lerp for this.

    3) Be very very very very very very very very very very....
    ....very.....
    .....very.....
    careful in changing the slider value from code, especially if the slider has an OnSliderValueChanged callback. You can get stuck in a circular update, which often creates jittery/stuttery/non-smooth movement in your slider. The callback itself doesn't know why the value changed -- was it from the UI? Or from code? You can usually address this by doing something like this:

    Code (csharp):
    1.  
    2. bool ignoreEvents = false; // add this to the class
    3. public Slider slider;
    4.  
    5. void OnSliderValueChanged()
    6. {
    7.   if (ignoreEvents) return;
    8.  
    9.   scrub.time = scrub.start + (scrub.end - scrub.start) * slider.Value;
    10. }
    11.  
    12. void Update()
    13. {
    14.   // ... blah blah blah... code that changed scrub.time, and now we want to update the slider...
    15.   ignoreEvents = true;
    16.   slider.Value = (scrub.time - scrub.start) / (scrub.end - scrub.start);
    17.   ignoreEvents = false;
    18. }
    19.  
    An alternative way would be to remove the callback from the slider, change the slider value, then re-add the callback, but I prefer a simple bool.
     
    davejones1 likes this.
  26. davejones1

    davejones1

    Joined:
    Jan 19, 2018
    Posts:
    183
    Thanks for your reply, I have a number of options lined up as to how I am going to solve the issue. Just confused as to the best way to implement them.
     
  27. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    OK, you're off to a good start (this new approach avoids all the pitfalls @BlackPete points out).

    To detect clicks and drags on the image, there are indeed several valid ways to do it. I think maybe best is to add an EventTrigger component to your image. Then you can use the Inspector to tell it what methods on your script you want to call on PointerDown or on Drag.
     
    davejones1 likes this.
  28. davejones1

    davejones1

    Joined:
    Jan 19, 2018
    Posts:
    183
    I have added an EventTrigger component to the overlay image. In the inspector for the EventTrigger I have added two event types. Those two types would be "Pointer Down" and "Drag". I have then created a script which is used to drag the overlay image from the start point of the slider to the end point of the slider. I have attached that script to this reply. I haven't added any methods to the "Pointer Down" event type but I have added a method from the "draggableimage" class to the "Drag" event type.

    I am able to move the overlay Image from the start to end position, however there are a few issues. The issues are listed below.
    1. I can't move the image overlay back to it's start position. I can only move it to the end position.
    2. The speed that the overlay image moves at seems to slow down towards the end. It doesn't seem to move at a constant rate.

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.UI;
    5.  
    6. public class draggableimage : MonoBehaviour {
    7.  
    8.     public Transform Startposition;
    9.     public Transform Endposition;
    10.     public float speed = 1;
    11.  
    12.  
    13.  
    14.     public void dragimage () {
    15.  
    16.         transform.position = Vector3.Lerp (Startposition.position, Endposition.position, Time.deltaTime * speed);
    17.  
    18.  
    19.     }
    20.     }
    21.  
     

    Attached Files:

  29. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    I don't understand what you're trying to do here. If we're still working on what you described before, you don't need any draggable image.

    Instead, drags on the invisible overlay image (i.e. code that responds to the Drag event of the EventTrigger) should, first, just log the pointer position with Debug.Log. Test that and make sure it works.

    Then, you want to add more to that code to control the playing of the animation, as I described before: if the position indicated by the drag is ahead of the current animation point, play forward; if it's behind, play in reverse.

    Do that. Then get back to us. (You don't need the draggableimage script.)
     
    davejones1 likes this.
  30. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    I assume this is the kind of code you're looking for. just attach it to the gameobject that has the slider and make sure the slider itself has interactible turned off. and it should work out of the box. added easing feature as a bonus but its not needed.

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.EventSystems;
    3. using UnityEngine.UI;
    4.  
    5. public class FollowDragger : MonoBehaviour, IDragHandler, IInitializePotentialDragHandler, IEndDragHandler
    6. {
    7.  
    8.     public float normalizedSliderSpeed = 0.7f;
    9.     public bool easing = false;
    10.     public float smoothing = 0.2f;
    11.  
    12.     private Vector2 m_currentPosition;
    13.     private float currentVelocity;
    14.  
    15.     [SerializeField]private Slider m_slider;
    16.     public Slider Slider
    17.     {
    18.         get
    19.         {
    20.             if (!this.m_slider)
    21.                 this.m_slider = this.GetComponent<Slider>();
    22.  
    23.             if (!this.m_slider) return null;
    24.  
    25.             return this.m_slider;
    26.         }
    27.         set { this.m_slider = value; }
    28.     }
    29.  
    30.     private RectTransform m_sliderTransform;
    31.     private RectTransform SliderTransform
    32.     {
    33.         get
    34.         {
    35.             if (!this.Slider)
    36.                 return null;
    37.  
    38.             if (!this.m_sliderTransform)
    39.                 this.m_sliderTransform = this.Slider.GetComponent<RectTransform>();
    40.  
    41.             return this.m_sliderTransform;
    42.         }
    43.     }
    44.  
    45.     private void Start()
    46.     {
    47.         SetTargetToCurrentValue();
    48.     }
    49.  
    50.  
    51.     public void OnInitializePotentialDrag(PointerEventData eventData)
    52.     {
    53.         if (!this.SliderTransform) return;
    54.  
    55.         Vector2 localPoint;
    56.  
    57.         if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(this.SliderTransform, eventData.position, eventData.pressEventCamera, out localPoint))
    58.             return;
    59.  
    60.         this.m_currentPosition = localPoint;
    61.     }
    62.  
    63.     public void OnDrag(PointerEventData eventData)
    64.     {
    65.         if (!this.SliderTransform) return;
    66.  
    67.         Vector2 localPoint;
    68.  
    69.         if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(this.SliderTransform, eventData.position, eventData.pressEventCamera, out localPoint))
    70.             return;
    71.  
    72.         this.m_currentPosition = localPoint;
    73.     }
    74.  
    75.     public void OnEndDrag(PointerEventData eventData)
    76.     {
    77.         SetTargetToCurrentValue();
    78.     }
    79.  
    80.     private Vector2 NormalizeLocalPosition(Vector2 value)
    81.     {
    82.         if (!this.SliderTransform) return Vector2.zero;
    83.  
    84.         var size = this.SliderTransform.rect.size;
    85.         var pivot = this.SliderTransform.pivot;
    86.  
    87.         value = new Vector2(Mathf.Clamp01(value.x / size.x + pivot.x), Mathf.Clamp01(value.y / size.y + pivot.y));
    88.         return value;
    89.     }
    90.  
    91.  
    92.  
    93.     private void SetTargetToCurrentValue()
    94.     {
    95.         if ((bool)this.SliderTransform)
    96.         {
    97.             m_currentPosition = new Vector2((this.Slider.normalizedValue - this.SliderTransform.pivot.x) * this.SliderTransform.rect.size.x, 0);
    98.         }
    99.     }
    100.  
    101.     private void OnValidate()
    102.     {
    103.         currentVelocity = 0;
    104.     }
    105.  
    106.     private void Update()
    107.     {
    108.         if (!this.Slider) return;
    109.  
    110.         var current = this.Slider.normalizedValue;
    111.         var target = NormalizeLocalPosition(this.m_currentPosition);
    112.  
    113.         if (Mathf.Abs(target.x - current) <= float.Epsilon)
    114.             return;
    115.  
    116.         if (easing)
    117.         {
    118.             this.Slider.normalizedValue = Mathf.SmoothDamp(current, target.x, ref currentVelocity, smoothing, this.normalizedSliderSpeed);
    119.         }
    120.         else
    121.         {
    122.             this.Slider.normalizedValue = Mathf.MoveTowards(current, target.x, Time.deltaTime * this.normalizedSliderSpeed);
    123.         }
    124.  
    125.     }
    126.  
    127. }
    128.  
     
  31. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    It's really not. It's what he originally said he wanted, but it leads down the wrong path. He needs the dragging on the image to control the animation, and the slider to merely reflect the current animation position on every frame.
     
  32. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    is it really that far off? it all seems pretty straightforward to me.

    it didn't take me five minutes to hook up an animationclip and targetGameobject with this script and get that exact functionality. of course normalizedSpeed is a field exposed to the inspector, but nothing is preventing you from setting it so that speed is equal to 1/ clip.length. now add a listener to the slider.OnValueChange to call clip.SampleAnimation(targetGameobject, clip.Length * value), and you're mostly done (i.e. dragging to control animation).

    include the Animation component instead and you can use it to manage when said OnValueChange handler samples while not playing and to update the normalized value in the slider during Update while the animation is playing (i.e. update slider while animation is automatically playing).
     
    davejones1 likes this.
  33. davejones1

    davejones1

    Joined:
    Jan 19, 2018
    Posts:
    183
    Hi Joshua, the script you have added is close to what I am trying to achieve. The only issue now is that when the slider has been moved and than disabled and than enabled again the slider moves on its own. How do I prevent the slider from moving on its own? Would this have something to do with setting the target to the current value?
     
    Last edited: Mar 25, 2018
  34. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    SetTargetToCurrentValue is only being called inside Start and OnEndDrag. calling it in OnEnable will solve that.
     
    davejones1 likes this.
  35. davejones1

    davejones1

    Joined:
    Jan 19, 2018
    Posts:
    183
    Hi Joshua, thanks for the response. The issue has now been solved, great job!!!!
     
  36. Hemanthchinnu

    Hemanthchinnu

    Joined:
    Apr 25, 2019
    Posts:
    1
    I know a another way
    Just download DoTweenModuleUI and you are good to go
    https://github.com/fisheraf/Stroids.../Demigiant/DOTween/Modules/DOTweenModuleUI.cs
    just save this script in any external plugins folder and use
    yourimageName.DoFillAmout(TargetValue,Duration); That's it
    Something like this imageSlider.DOFillAmount(perSecElixir / 10, 0.5f);
    gif would be in low quality but it's super smooth anyway try this
    Untitled video - Made with Clipchamp.gif Untitled video - Made with Clipchamp.gif