Crossfade gallery, a simple and cool UI element

Last week I’ve released a new UI demo project with some effects made entirely in Unity3D’s UI and it had quite a good reception, in particular at some point I was asked to share.

untitled

Well, I’m not ready to put it on GitHub yet (some parts of the code need more polish before being released without shaming myself), but I do want to share. So I’ll give a downloadable link to the project in my newsletter and start a new tutorial series on the topic here.

Today’s topic is the first panel: the sequential crossfader.

What does crossfade mean?

You have probably seen this transition a gazillion times in your life. It’s when an image fades away and a new one takes its place.

How it’s done is quite simple: for each pixel you lerp its color between the color of the former image and the one of the next.

Luckily for us, we don’t have to even deal with that simple operation, UnityEngine.UI.Graphic has already all that we need.

Let’s wrap Graphic

wrap it tight
wrap it tight

From this class we only need one thing: the CrossFadeAlpha function. But we will need to do 3 things with it.

  1. reset the values to an initial state
  2. fade in
  3. fade out

To do this, we’ll just use a small MonoBehaviour component, but please notice that if you want to do this with whole panels instead of a single graphic, you’ll need to add a canvas group as target and change the functions to manually lerp its alpha value.

So, here’s the code:

using UnityEngine;
using UnityEngine.UI;

public class UICrossFader : MonoBehaviour
{
    [SerializeField]
    Graphic toCrossFade;
    [HideInInspector]
    public float inAlphaLevel = 1f;
    [HideInInspector]
    public float outAlphaLevel = 0f;
    [HideInInspector]
    public float duration = 1f;
    [HideInInspector]
    public bool ignoreTimeScale = false;

    public void FadeIn()
    {
        toCrossFade.CrossFadeAlpha(inAlphaLevel, duration, ignoreTimeScale);
    }

    public void FadeOut()
    {
        toCrossFade.CrossFadeAlpha(outAlphaLevel, duration, ignoreTimeScale);
    }
    
    public void Preset(bool isIn)
    {
        if (isIn)
            toCrossFade.CrossFadeAlpha(inAlphaLevel, 0f, true);
        else
            toCrossFade.CrossFadeAlpha(outAlphaLevel, 0f, true);
    }
}

Why are almost all the variables public but hidden? because if you want to control this behaviour you’ll want to do it by code and you don’t want anyone who doesn’t know what he’s doing to fiddle with it.

As you can see the Preset function can either set something to be visible or invisible (in or out) according to the isIn parameter and does so in just a frame, regardless of the timescale. This is because that function is meant to work behind the scenes, not while the show is going on.

The fade functions are just another wrap for the CrossFadeAlpha with pre-set values. This component is indeed very simple, but keep in mind that not everything in UI is a Graphic, and that also you may want to extend this to do more complex stuff one day.

Managing a sequence with a coroutine Start

flight-1179587_640

    public GameObject listParent;
    [SerializeField]
    float startDelay;
    [SerializeField]
    float interval = 4f;
    [SerializeField]
    bool doLoop = true;

We need just a few variables to decide:

  • what list of elements we’re going to fade (all the children of listParent who have a UICrossFader)
  • after how much time it will start
  • how much time each element should stay visible before we start the fading
  • if this sequence should be looped or not
    IEnumerator Start()
    {
    	bool executeLoop = true;
        UICrossFader[] crossfadeImagesList=null;

        yield return new WaitForSeconds(startDelay);

As you may know (or not, since as of now this seems to be an undocumented feature), the Start function of Monobehaviour can be used as a coroutine. All that you need to do is to change its return type to IEnumerator. I know, it’s crazy, but they did it this way and it IS very comfortable not to write a coroutine call every time you need it.

In the beginning we just declare a couple of variables we’ll need later on and let the execution to wait for the right time to start.

    if (listParent != null)
    {       
        crossfadeImagesList = listParent.GetComponentsInChildren<UICrossFader>();
        if (crossfadeImagesList.Length <= 0)
            Debug.LogError("SequenceFader error, list empity, check that children of listParent do have UICrossFader component");
        else
        {
             //everything else...
        }
    }
    else
    {
        Debug.LogError("SequenceFader error, listParent not assigned!");
    }

The first thing we do is to check that listParent has been set, otherwise an error is due. Then we need to see is if it has stuff to crossfade in it or another error must be raised. And at last, if we have something to crossfade we’ll start doing so.

Now let’s get into that “everything else”:

                foreach (var item in crossfadeImagesList)
                {
                    item.Preset(false);
                }

                System.Collections.IEnumerator crossfadeEnumerator = crossfadeImagesList.GetEnumerator();
                crossfadeEnumerator.MoveNext();
                UICrossFader currentFader = (UICrossFader)crossfadeEnumerator.Current;
                currentFader.Preset(true);
                UICrossFader last = currentFader;
                yield return new WaitForSeconds(interval);

Another bit of initializations is now due: we begin with nothing visible, then we get the first element of the list and set it as visible. We also record it as a last element to have been visible. Then we wait the moment we’ll show the next image.

                while (executeLoop)
                {
                    currentFader = (UICrossFader)crossfadeEnumerator.Current;
                    last.FadeOut();
                    currentFader.FadeIn();
                    last = currentFader;
                    if (!crossfadeEnumerator.MoveNext())
                    {
                        if (doLoop)
                        {
                            crossfadeEnumerator = crossfadeImagesList.GetEnumerator();
                            crossfadeEnumerator.MoveNext();
                        }
                        else executeLoop = false;
                    }
                    yield return new WaitForSeconds(interval);
                }

Usually one would just have the iterator MoveNext be called inside the while condition, but now we may want to stay inside the loop once that all the list is traversed and start from the beginning, so instead we’ll use the executeLoop flag. At each loop we get a new current element to fade in and start the crossfade by calling in the appropriate functions of the UICrossFader, then we update the last and check if this was the last element of the list while at the same time advancing the enumerator with its MoveNext.

to loop or not to loop? this is the question!
to loop or not to loop? this is the question!

If it was the last element, then we have 2 possibilities: if we should not loop, the executeLoop is set to false and we’re out for good; if we should loop then the enumerator is reset to its first element.

After this check we just wait one more interval before continuing the loop.

That’s all folks!

And we’re done, now your images will transition one after another with a nice crossfade. You can use this for all sorts of purposes and even extend it to a “video” fader in which a dialog is played one image after another (I did so for a then killed project back in the day). If you want to get updates on the next tutorials or just download the full project, get on my newsletter, I’ll give a download link for it next week too (maybe with some minor updates since I’m polishing the code to write this tutorial series). For any questions or comments don’t hesitate writing me here or hitting me up on twitter.

P.S.: I’m currently looking for a gamedev job, if you are interested give a look at my portfolio. Also, if you are a fellow Unity3D dev, let’s connect!

using UnityEngine;
using UnityEngine.UI;

public class UICrossFader : MonoBehaviour
{
    [SerializeField]
    Graphic toCrossFade;
    [HideInInspector]
    public float inAlphaLevel = 1f;
    [HideInInspector]
    public float outAlphaLevel = 0f;
    [HideInInspector]
    public float duration = 1f;
    [HideInInspector]
    public bool ignoreTimeScale = false;

    public void FadeIn()
    {
        toCrossFade.CrossFadeAlpha(inAlphaLevel, duration, ignoreTimeScale);
    }

    public void FadeOut()
    {
        toCrossFade.CrossFadeAlpha(outAlphaLevel, duration, ignoreTimeScale);
    }
    
    public void Preset(bool isIn)
    {
        if (isIn)
            toCrossFade.CrossFadeAlpha(inAlphaLevel, 0f, true);
        else
            toCrossFade.CrossFadeAlpha(outAlphaLevel, 0f, true);
    }
}
public class SequenceFader : MonoBehaviour
{
    public GameObject listParent;
    [SerializeField]
    float startDelay;
    [SerializeField]
    float interval = 4f;
    [SerializeField]
    bool doLoop = true;
    bool executeLoop = true;


    IEnumerator Start()
    {
    	bool executeLoop = true;
        UICrossFader[] crossfadeImagesList=null;

        yield return new WaitForSeconds(startDelay);

        if (listParent != null)
        {       
            crossfadeImagesList = listParent.GetComponentsInChildren<UICrossFader>();
            if (crossfadeImagesList.Length <= 0)
                Debug.LogError("SequenceFader error, list empity, check that children of listParent do have UICrossFader component");
            else
            {
                foreach (var item in crossfadeImagesList)
                {
                    item.Preset(false);
                }

                System.Collections.IEnumerator crossfadeEnumerator = crossfadeImagesList.GetEnumerator();
                crossfadeEnumerator.MoveNext();
                UICrossFader currentFader = (UICrossFader)crossfadeEnumerator.Current;
                currentFader.Preset(true);
                UICrossFader last = currentFader;

                yield return new WaitForSeconds(interval);
                while (executeLoop)
                {
                    currentFader = (UICrossFader)crossfadeEnumerator.Current;
                    last.FadeOut();
                    currentFader.FadeIn();
                    last = currentFader;
                    if (!crossfadeEnumerator.MoveNext())
                    {
                        if (doLoop)
                        {
                            crossfadeEnumerator = crossfadeImagesList.GetEnumerator();
                            crossfadeEnumerator.MoveNext();
                        }
                        else executeLoop = false;
                    }
                    yield return new WaitForSeconds(interval);
                }
            }
        }
        else
        {
            Debug.LogError("SequenceFader error, listParent not assigned!");
        }
    }
}

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  

Custom drawer in unity3d to pick an enum – event tutorial followup

  • Part 1: basics event system
  • Part 2: event picker drawer for in-editor usage [you are here]
  • Part 3: debugging with platform dependant compilation
  • Part 4: multiple dispatchers
  • Part 5: optimization

For today’s tutorial we’re going to make something that’s really comfortable to use: a dropdown menu to select an event. We’ll do that by creating anEventPickerclass and a custom drawer by estending thePropertyDrawerclass thatUnityEnginekindly gives us.

First, well’need aEventPickerclass. This class will actually hold the data inside the game, and will not in any way define the editor UI. It’s the data for which we’re going to define a Custom Drawer in Unity3d, not the drawer itself.

custom drawer in unity3d
let’s make a custom drawer

The EventPicker class

To function with the Event system we defined in last tutorial we’ll need for it the ability to read which event channel and which specific event have been selected. It also needs to allow us to identify the specific type of enum to which each channel will be associated with, so that later the custom drawer can use this information to draw the appropriate dropdown menu. Therefore we’ll need at least :

  • one field to store the selected channel
  • one field to store the selected event for each channel
  • one property to give access to the actual currently selected event to any class that will actually use the event picker.
using UnityEngine;

[System.Serializable]
public class EventPicker
{
    public const string channelVarName = "channel";
    public eventChannels channel;

    public inGameChannelEvents inGame;
    public menuChannelEvents menu;

    public System.Enum Selected
    {
        get
        {
            switch (channel)
            {
                case eventChannels.menu: return menu;
                case eventChannels.inGame: return inGame;
                default:
                    Debug.LogError("EventPicker was passed an unimplemented channel!!! " + channel.ToString());
                    break;
            }
            return channel;
        }
    }


}

Sadly, this means that each time a channel is added inChannelEnumsclass, it will need to be added here too. Also, since we’re going to need a Drawer, we don’t have the freedom to choose whatever variable names we want to pick since it relies on getting that name as a string.

String-run-fools
A string! Run, you fools!

We could solve this problem with C# reflection but that’s a deep topic that I’d rather address another time. For now, let’s just use a strong naming convention and have the the event-storage variables use the same name as the channel, while we addchannelVarNameto keep thechannelvariable name as a constant string field near the variable whose name it stores, so that whenever we may need to rename it we can just as easily copy-paste that name inside the string.

TheSelectedproperty is just a switch that returns the value of the field described by the content of thechannelvariable. So that we have a fast way to get the selected event from theEventPicker.

The actual Custom Drawer

The Custom Drawer must have this declaration:

[CustomPropertyDrawer(typeof(EventPicker))]
public class EventPickerDrawer : PropertyDrawer
{

So that unity can assign this drawer to the class that it’s intended to draw. It’s also important to understand that we won’t be writing something from scratch here. We’ll actually be extending aPropertyDrawerclass that already exists in unity. I won’t explain in depth what this class is or does, for that there is the documentation, but please notice we’re going to override two methods:OnGUIandGetPropertyHeight.

Let’s begin with the easy part:

public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        return base.GetPropertyHeight(property, label) * 3f + 5f;
    }

Now, what we’re doing here is basically telling unity to reserve a height equal to three times a normal variable plus an extra 5 pixels. This is because we’re going to put one header and then two dropdowns, and each one is going to use the height of one variable.

public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.PrefixLabel(position, label);
        Rect tempPos = position;
        tempPos.height = ROWHEIGHT;
        tempPos.y += ROWHEIGHT;
        tempPos.x += position.width * .4f;
        tempPos.width *= .6f;

TheOnGUIfunction actually tells unity what to draw in the inspector when it encounters anEventPickervariable. Here we start by telling it to draw a label which indicates the kind of objetc to which the data belongs and then declaring aRectwith the appropriate size and coordinates to store the rest of the UI elements.

        SerializedProperty channel = property.FindPropertyRelative(EventPicker.channelVarName);
        EditorGUI.PropertyField(tempPos, channel, GUIContent.none);

This bit of code recovers the data about thechannelvariable in theEventPickervia the serialization information (which means you can’t use this on anything that can’t be serialized), then draws new field with that information. Thechannelvariable is identified by its name, that we recover with the static string insideEventPicker.

        tempPos.y += ROWHEIGHT;
        eventChannels chosen = (eventChannels)channel.enumValueIndex;
        SerializedProperty selection = property.FindPropertyRelative(chosen.ToString());
        EditorGUI.PropertyField(tempPos, selection, GUIContent.none);

Here we increment the starting point of theRectso that Unity won’t draw the fields one over the other.

Then we proceed to get the value of the selected channel converted in itsenumform, so that we can then recover the serialization information for the appropriate variable by using theToStringmethod and benefitting from the strong naming convention used in theEventPickerclass.

That’s all folks

So, let’s recap: first you need to create the data container, but in a way that allows you to access all the names of the fields, then we can write another class that must be linked through aCustomPropertyDrawertag to the data class and that overrides thePropertyDrawerfunctions to draw our class the way we want it to.

Now you have a nice UI element that you can use to pick events for your Event System, without having to write it as a horrible string and then converting it. As usual down here there’s all the code in a copy-paste-friendly format. More on this event system is still coming next week. If you want to be sure not losing it, subscribe to my newsletter. For any feedback comments are below or you can just add me on twitter.

using UnityEngine;

[System.Serializable]
public class EventPicker
{
    public const string channelVarName = "channel";
    public eventChannels channel;

    public inGameChannelEvents inGame;
    public menuChannelEvents menu;

    public System.Enum Selected
    {
        get
        {
            switch (channel)
            {
                case eventChannels.menu: return menu;
                case eventChannels.inGame: return inGame;
                default:
                    Debug.LogError("EventPicker was passed an unimplemented channel!!! " + channel.ToString());
                    break;
            }
            return channel;
        }
    }
}

 

using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer(typeof(EventPicker))]
public class EventPickerDrawer : PropertyDrawer
{
    protected virtual float ROWHEIGHT
    {
        get
        { return 18f; }
    }
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.PrefixLabel(position, label);
        Rect tempPos = position;
        tempPos.height = ROWHEIGHT;
        tempPos.y += ROWHEIGHT;
        tempPos.x += position.width * .4f;
        tempPos.width *= .6f;

        SerializedProperty channel = property.FindPropertyRelative(EventPicker.channelVarName);
        EditorGUI.PropertyField(tempPos, channel, GUIContent.none);
        tempPos.y += ROWHEIGHT;

        eventChannels chosen = (eventChannels)channel.enumValueIndex;
        SerializedProperty selection = property.FindPropertyRelative(chosen.ToString());
        EditorGUI.PropertyField(tempPos, selection, GUIContent.none);

    }
    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        return base.GetPropertyHeight(property, label) * 3f + 5f;
    }
}

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  

Scroll to select: image gallery tutorial

 

 

Every now and then in a UI you need the user to select an option among many, there are several ways to do so, one method could be the good’ole command line, another could be the use of toggles in a group (or radio button in Android/HTML environment), but that’s not nearly as cool as just looking at the thing you want and it being already selected. So today we’ll implement a scroll to select UI interface in unity3d.

We’ll do this in the image gallery that we’ve been building here for quite a while, here I’ll also give a unified version of the code with all of the features in it.

The idea is to define an area where the selection happens, so that whatever image happens to be in the area gets selected automatically. Of course the snapping behaviour works well with this but it’s not essential, since we are defining an area, not a single position.

Scroll to select area: let’s build it

It will go something like this:

select a ninja by scrolling a gallery of images
Don’t move and select, but select by moving

Let’s get into the code. First we’ll need a new animation curve and a place to store the selected image information:

    public AnimationCurve selectionArea;
    public RectTransform selected;

We’ll use a very narrow spike function, it’s supposed to only be over .75 in the selection area (with respect to the alpha we used all along).

Narrow spike function
Make it larger to have a larger area of selection. Caos will ensue if two images can be both in the area at a time

And to complete the opera at the very end of our refresh image foreach loop we’ll insert this

        if (selectionArea.Evaluate(transformedPosition) >= 0.75f)
        {
            selected = img;
        }

This way only the image that’s been scrolled to the center is in the range for which the selectionArea curve evaluates to more than .75, which means only that image gets selected. It’s important to use a very narrow area so that only one image at time can get selected.

And that’s all for the scroll to select part of this tutorial!

This one was very easy, wasn’t it? So, at long last we are over this whole guide and you now have a fully functional scroll-to-select image gallery that looks fucking awesome! Of course there’s a new tutorial coming soon, don’t lose anything, join my newsletter.

A recap to put EVERYTHING in one place for you to copy-paste:

    public AnimationCurve orderFactor;
    public AnimationCurve positionCurve;
    public AnimationCurve zoomCurve;
    public AnimationCurve selectionArea;
    public RectTransform selected;

    public RectTransform[] imgset;
    public float rotationSensibility = 1f; //how sensible is to swipe input
    protected float alphaStep = 1f; //will host the image-to-image distance.
    public float swipeFieldWidth = 300f;//Total distance from leftmost position to rightmost one
    [SerializeField]    protected float alpha; //the parameter around wich all things revolve
    protected float lastFrameAlpha; //last frame's alpha value
    public CatchSwipe inputSource; //A module to handle input that gives us the net x-axis swipe value
    Dictionary<RectTransform, float> alphaoff = new Dictionary<RectTransform, float>();//a dictionary to store all image's offsets

    [Header("inertia Parameters")]
    public bool useInertia;
    protected float inertia;
    public float inertialDampening = 0.9f; 
    public float inertiaPersistenceFactor = 10f;

    [Header("Magnetizaiton Parameters")]
    public bool doMagnetize = true;
    public float magnetizationMaxForceFactor = 0.09f;
    public float magnetizationRangeAlphaStepFraction = 0.5f;
    protected float magneticForce;

    public virtual void Awake()
    {
        if (useInertia)
            doMagnetize = false;
    }

    public virtual void Start()
    {
        alphaInitialization();
        refreshImages();
    }

    protected virtual void alphaInitialization()
    {
        alpha = 1;
        if (imgset.Length > 1)
            alphaStep = 1f / (imgset.Length - 1f);
        float minAlpha = -(imgset.Length - 1f) * alphaStep / 2f;
        alphaoff.Clear();
        foreach (var item in imgset)
        {
            alphaoff.Add(item, minAlpha);
            minAlpha += alphaStep;
        }
        lastFrameAlpha = alpha;
    }

    protected virtual void Update()
    {
        updateAlpha();
        updateInertia();
        updateMagnetization();
        if (Mathf.Abs(lastFrameAlpha - alpha) > 0.001f)
        {
            refreshImages();
        }
        lastFrameAlpha = alpha;
    }
    protected virtual void updateAlpha()
    {
        alpha = Mathf.Clamp(
                alpha + (Mathf.Abs(inputSource.NetSwipe.x) > 0 ?
                    inputSource.NetSwipe.x * rotationSensibility
                    :
                    inertia + magneticForce), 
                0,
                1
                );
    }
    protected virtual void updateInertia()
    {
        if (useInertia)
        {
            inertia = Mathf.Abs(inputSource.NetSwipe.x) > 0 ? alpha - lastFrameAlpha : inertialDampening * inertia;
            if (Mathf.Abs(inertia) < rotationSensibility / inertiaPersistenceFactor)
                inertia = 0;
        }
    }


    protected virtual void updateMagnetization()
    {
        if ((Mathf.Abs(inputSource.NetSwipe.x) <= 0.001) && (doMagnetize))
        {
            float rest = alpha % alphaStep;
            if (rest < magnetizationRangeAlphaStepFraction * alphaStep)
            {
                if (rest < rotationSensibility)
                    magneticForce = -rest;
                else
                    magneticForce = -rest * magnetizationMaxForceFactor;
            }
            else if (rest > alphaStep * (1 - magnetizationRangeAlphaStepFraction))
            {
                if (rest < rotationSensibility)
                    magneticForce = rest;
                else
                    magneticForce = (alphaStep - rest) * magnetizationMaxForceFactor;
            }
        }
    }

    protected virtual void refreshImages()
    {
        List<KeyValuePair<RectTransform, float>> orderingLayerList = new List<KeyValuePair<RectTransform, float>>();
        foreach (var img in imgset)
        {
            float transformedPosition = positionCurve.Evaluate(alpha + alphaoff[img]);
            img.localPosition = Vector3.right * (transformedPosition * swipeFieldWidth - (swipeFieldWidth / 2f));
            img.localScale = Vector3.one * (zoomCurve.Evaluate(transformedPosition));
            orderingLayerList.Add(new KeyValuePair<RectTransform, float>(img, orderFactor.Evaluate(alpha + alphaoff[img])));
            if (selectionArea.Evaluate(transformedPosition) >= 0.75f)
            {
                selected = img;
            }
        }
        var orderByVal = orderingLayerList.OrderBy(kvp => kvp.Value);
        foreach (var item in orderByVal)
        {
            item.Key.SetAsLastSibling();
        }
    }

And this finally ends this lenghty tutorial. I hope you will find it useful 🙂
Tell me what you think about it and follow me on twitter for more useful stuff.

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  

Of men & scroll galleries: in-depth tutorial

In my last project I had to build a skin selection menu. I could have gone away by simply using a scrollrect, but who likes cool when you can have awesome?

I didn’t want just a scrollrect. I wanted Icons to change size depending on screen position. And to cover each other when too far away from the currently selected one. All with inertia and snap-to-option movement. Of course, no tap-to-select, whatever’s centered should be selected. In other words I wanted a scrolling gallery of images that looked and felt awesome where I could pick my ninja.

I also wanted a Lamborghini, but you can’t get THAT just by coding it.

Yet.

Let’s begin with some high-level concepts

So what to do? First I started with some math.
I had to figure out how to mathematecally handle all the movements, which functions to encode in unity so that at every frame I had exactly what I needed. Of course using math does not mean using calculus, if you remember this trick of mine.

So, mathematically speaking I wanted a bellshaped curve (AKA Gaussian) of zoom, and some easing for position on x axis.
All that must be controlled with a swipe, which means with a single parameter. What we have is a linear swipe, what we want is an eased movement and zoom.

An example of easing in animation
The upper picture represents a linear movement, the lower one an eased movement

The trick is not to think on what kind of formulas can give the result I need, but to just revert the functions that would do the trick graphically, which means a bell shaped curve for zoom and an s-shaped one for x position.
The mechanics will be very similar to spinning an invisible wheel, with our images are glued on its border and then watching from a small distance. You just increase linearly the rotation angle at its center and then you’ll see exactly the movement we need: big and fast picture in the middle, many others small and slow on the sides. We’ll call the rotation angle of the imaginary wheel our “alpha” angle.

Brace yourselves: code is coming

Let’s review the variables we’ll need. AnimationCurves first.

 public AnimationCurve positionCurve;
 public AnimationCurve zoomCurve;

These will be our “math” functions. Zoomcurve needs a bell-shape with peak in .5f, positioncurve it’s a regular ease-in ease-out.

public RectTransform[] imgset;

Here we’ll store the images to move. Actually their RectTransforms.

And then there’s some “constants”:

public float rotationSensibility = 1f; //how sensible is to swipe input
protected float alphaStep = 1f; //will host the image-to-image distance.
public float swipeFieldWidth = 300f;//Total distance from leftmost position to rightmost one

And finally some internal variables

[SerializeField] protected float alpha; //the parameter around wich all things revolve
protected float lastFrameAlpha; //last frame's alpha value
public CatchSwipe inputSource; //A module to handle input that gives us the net x-axis swipe value
Dictionary<RectTransform, float> alphaoff = new Dictionary<RectTransform, float>();//a dictionary to store all image's offsets

The eagle-eye view

So, our high-level code would be something like this:

    public virtual void Start()
    {
        alphaInitialization();
        refreshImages();
    }

    protected virtual void Update()
    {
        updateAlpha();
        if (Mathf.Abs(lastFrameAlpha - alpha) > 0.001f)
        {
            refreshImages();
        }
        lastFrameAlpha = alpha;
    }

Which basically reads: first, initialize my shit and show it, then update my alpha and if it’s different than last frame, show the update .
It’s a simple behaviour.

Warning: this check will save a lot of cpu but may get some added effects not too smooth with extra slow movements, it’s up to you to decide your tradeoff.

Let’s see what those functions actually do.

 protected virtual void alphaInitialization()
    {
        alpha = 1;

We just start with the leftmost image at center.

        if (imgset.Length > 1)
            alphaStep = 1f / (imgset.Length - 1f);

Here we calculate the alphaStep if it needs to be less than one. We want the images to be equally spaced on the imaginary wheel border, so that we can calculate easily how they move.

        float minAlpha = -(imgset.Length - 1f) * alphaStep / 2f;
        alphaoff.Clear();
        foreach (var item in imgset)
        {
            alphaoff.Add(item, minAlpha);
            minAlpha += alphaStep;
        }

Then we calculate what the offset values are for our images. This will enable us to rule them all with just one ring… err… parameter, the “alpha” rotation value of the imaginary wheel. The middle image uses the base alpha value and the others will shift it with the offset. Simple huh?

        lastFrameAlpha = alpha;
    }

And lastly, we also initialize the lastFrameAlpha value.

protected virtual void updateAlpha()
    {
        alpha = Mathf.Clamp(alpha + inputSource.NetSwipe.x * rotationSensibility, 0, 1 );
    }

Now, this is what one would expect to be the complicated part… but actually it’s the simplest. When the user swipes he’s just spinning the wheel, nothing more. So we increase the alpha by exactly the amount he swipes times the sensibility.

And now let’s see where the magic actually happens!

    protected virtual void refreshImages()
    {
        foreach (var img in imgset)
        {
            float transformedPosition = positionCurve.Evaluate(alpha + alphaoff[img]);

HERE! did you see that? Here I changed the alpha value to a phisical position. Before we only knew how much the wheel would spin (alpha), then we got how exactly it was spinning for the image we’re checking (alpha + alphaoff[img]). The actual transformation would have been some good old math if we didn’t use an AnimationCurve to do it in our place.
Fuck that. I’m lazy and want the UI designer to be able to change how the slider behaves without touching MY code with his filthy fingers.
Our result is still normalized in a 0-1 scale. To get a position in the proper scale we need :

            img.localPosition = Vector3.right * (transformedPosition * swipeFieldWidth - (swipeFieldWidth / 2f));

So, basically, by subtracting half swipeFieldWidth we have the results centered in 0, but then we’ll have that with transformedPosition==0, a position of (- swipeFieldWidth/2,0,0) will be assigned, and with transformedPosition==1,the result will of course be (swipeFieldWidth/2,0,0). The result is that the width of it all it’s exactly swipeFieldWidth.

            img.localScale = Vector3.one * (zoomCurve.Evaluate(transformedPosition));
        }
    }

And this is what gives the “zoom” effect: we just make the scale bigger the closer the image is to the center (which means transformedPosition==.5f )
This too of course will be done with an AnimationCurve so that we can thinker with results without having to change the code.

What we did so far should look something like this:

[note to self: never delete a gif again or you’ll have to go through hundreds of tweets to get it back]

I hope this will be enough for now. I also hope you would like to read how to add the other stuff I mentioned before like inertia and snapping. If so, join my newsletter to know when the next part of this guide is ready! [edit: next part is here]

And yes, of course I know you want to copy-paste this, so here’s all in one place:

    public AnimationCurve positionCurve;
    public AnimationCurve zoomCurve;
    public RectTransform[] imgset;
    public float rotationSensibility = 1f; //how sensible is to swipe input
    protected float alphaStep = 1f; //will host the image-to-image distance.
    public float swipeFieldWidth = 300f;//Total distance from leftmost position to rightmost one
    [SerializeField]    protected float alpha; //the parameter around wich all things revolve
    protected float lastFrameAlpha; //last frame's alpha value
    public CatchSwipe inputSource; //A module to handle input that gives us the net x-axis swipe value
    Dictionary<RectTransform, float> alphaoff = new Dictionary<RectTransform, float>();//a dictionary to store all image's offsets

    public virtual void Start()
    {
        alphaInitialization();
        refreshImages();
    }

    protected virtual void Update()
    {
        updateAlpha();
        if (Mathf.Abs(lastFrameAlpha - alpha) > 0.001f)
        {
            refreshImages();
        }
        lastFrameAlpha = alpha;

    }

 protected virtual void alphaInitialization()
    {
        alpha = 1;

        if (imgset.Length > 1)
            alphaStep = 1f / (imgset.Length - 1f);

        float minAlpha = -(imgset.Length - 1f) * alphaStep / 2f;
        alphaoff.Clear();
        foreach (var item in imgset)
        {
            alphaoff.Add(item, minAlpha);
            minAlpha += alphaStep;
        }

        lastFrameAlpha = alpha;
    }


protected virtual void updateAlpha()
    {
        alpha = Mathf.Clamp(alpha + inputSource.NetSwipe.x * rotationSensibility, 0, 1 );
    }

    protected virtual void refreshImages()
    {
        foreach (var img in imgset)
        {
            float transformedPosition = positionCurve.Evaluate(alpha + alphaoff[img]);

            img.localPosition = Vector3.right * (transformedPosition * swipeFieldWidth - (swipeFieldWidth / 2f));

            img.localScale = Vector3.one * (zoomCurve.Evaluate(transformedPosition));
        }
    }

And here are the position curve:

position-curve

And zoom curve:

zoom-curve

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  

Catching Swipes – a touch screen tutorial

One of the hurdles of dealing with touch screens is to handle finger tracking and boiling it down to a clean cut movement so that one can process gesture recognition.

Let’s look how to make a component that:

  1. only gets swipes in a specific screen area
  2. can detect multiple touches and store their individual details
  3. measures movement since last frame

Touch interface input – variables needed

For this we’ll need a couple of things in our data:

    public GameObject toCheck;
    Dictionary<int, Vector2> lastTouchPositionByFinger = new Dictionary<int, Vector2>();
    Vector2 lastClickPositionByMouse;
    Vector2 currentMousePos;
    [SerializeField]
    Vector2 netSwipe;
    public Vector2 NetSwipe
    {
        get { return netSwipe; }
    }

First of all we get a GameObject we’ll use to define the area in which the swipes shall be catched, it must be a UI element that can be Raycasted.
The next item will be a dictionary in which to store the current position of every single tracked finger. Also, since on Windows 10 computer touch screens are treated as mouse input we’ll need an extra place for mouse input position. And at last, of course, we have a property which will contain our movement since last frame inside the area.

Notice: there are up to 10 fingers and just one net movement. What we will do will be to have the total movement of the frame in this netSwipe variable, if you need to track the fingers independently, you will need to change this approach.

How to check if touch is over a UI element

So, how do we implement our first requisite? We just ask the event system if a finger is over the image (which of course can be made invisible by setting a 0 in its alpha channel).

    private static List<RaycastResult> tempRaycastResults = new List<RaycastResult>();

    public bool PointIsOverUI(Vector2 position)
    {

        var eventDataCurrentPosition = new PointerEventData(EventSystem.current);

        eventDataCurrentPosition.position = position;

        tempRaycastResults.Clear();

        EventSystem.current.RaycastAll(eventDataCurrentPosition, tempRaycastResults);
        foreach (var item in tempRaycastResults)
        {
            if (item.gameObject.GetInstanceID() == toCheck.GetInstanceID() && item.index == 0)
                return true;
        }
        return false;
    }

We’ll need an extra list for this function, to store all the results of the Raycast. At first we poll the EventSystem for a RaycastAll , then we go through every hit object and only if the object we hit is the one we’re looking to hit and only if there is no other object over it we give true as an answer. Otherwise we’re not hitting the right item and the touch should not be contributing to net swipe.

How to process a touch input in Unity3d

Now we can check if the touch should be counted or not, but that’s not enough. We also need to check for the touch phase in which we are and put tracking in place, let’s start with the Update function’s structure:

 void Update()
    {
        if (Input.touchCount > 0)
        {
            for (int i = 0; i < Input.touchCount; i++)
            {
                var touch = Input.GetTouch(i);
                switch (touch.phase)
                {
                    case TouchPhase.Began:
                        //stuff
                        break;
                    case TouchPhase.Moved:
                        //moar stuff
                        break;
                    case TouchPhase.Ended:
                        //stuff again
                        break;
                    case TouchPhase.Canceled:
                        //last stuff
                        break;
                    default:
                        break;
                }
            }
        }

    }

The first thing to check, of course, is if there are any touches at all. If there aren’t, then we don’t want to waste any resources. Then we cycle through each touch and do stuff appropriately.

Let’s get to the details. Before even starting our check we want to ensure that if no touches are active our netSwipe is reset to zero, so:

void Update()
    {
        netSwipe = Vector2.zero;
        if (Input.touchCount > 0)
        {

Getting to the actual stuff, when the touch begins and ends or gets cancelled we just want to add or remove it from our tracking accordingly, so we have:

                   case TouchPhase.Began:
                         lastTouchPositionByFinger.Add(touch.fingerId, touch.position);
                        break;

and:

                    case TouchPhase.Ended:
                        lastTouchPositionByFinger.Remove(touch.fingerId);
                        break;
                    case TouchPhase.Canceled:
                        lastTouchPositionByFinger.Remove(touch.fingerId);
                        break;

When the finger is moving, instead, is where the fun part happens:

                    case TouchPhase.Moved:
                        if (PointIsOverUI(touch.position))
                        {
                            netSwipe += touch.position - lastTouchPositionByFinger[touch.fingerId];
                            lastTouchPositionByFinger[touch.fingerId] = touch.position;
                        }
                        break;

First we check if the finger is over the area of interest, then we add its movement since last frame to our  netSwipe variable and update the finger’s position in our dictionary and that’s it! You now have also the tracking and net movement over the image.

Dealing with Windows 10

        if (Input.GetMouseButton(0))
        {
            currentMousePos = Input.mousePosition;
            if (PointIsOverUI(currentMousePos))
                netSwipe += (currentMousePos - lastClickPositionByMouse);

            lastClickPositionByMouse = currentMousePos;
        }

As I said before, on Windows 10 laptops touchscreens touches are read as mouse input. In that case we’re going to make an exception ad use a special treatment. The logic is still the same, but without the tracking of a finger touch. Simply put, if the button is down, then we check if it’s over the UI element we’re tracking and if so we use current position versus last frame’s one to contribute to netswipe.

That’s all folks!

Now go out there and do something awesome with that. I’ll post soon enough a tutorial that makes use of this component to control a gallery of sliding images. Join my newsletter to never miss my stuff and hit me up on Twitter for any remarks!

And of course here’s the script in a copypaste-friendly format:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class CatchSwipe : MonoBehaviour
{

    public GameObject toCheck;
    Dictionary<int, Vector2> lastTouchPositionByFinger = new Dictionary<int, Vector2>();
    Vector2 lastClickPositionByMouse;
    Vector2 currentMousePos;
    [SerializeField]
    Vector2 netSwipe;
    public Vector2 NetSwipe
    {
        get { return netSwipe; }
    }

    void Update()
    {
        netSwipe = Vector2.zero;
        if (Input.GetMouseButton(0))
        {
            currentMousePos = Input.mousePosition;
            if (PointIsOverUI(currentMousePos))
                netSwipe += (currentMousePos - lastClickPositionByMouse);

            lastClickPositionByMouse = currentMousePos;
        }
        if (Input.touchCount > 0)
        {
            for (int i = 0; i < Input.touchCount; i++)
            {
                var touch = Input.GetTouch(i);
                switch (touch.phase)
                {
                    case TouchPhase.Began:
                        lastTouchPositionByFinger.Add(touch.fingerId, touch.position);
                        break;
                    case TouchPhase.Moved:
                        if (PointIsOverUI(touch.position))
                        {
                            netSwipe += touch.position - lastTouchPositionByFinger[touch.fingerId];
                            lastTouchPositionByFinger[touch.fingerId] = touch.position;
                        }
                        break;
                    case TouchPhase.Ended:
                        lastTouchPositionByFinger.Remove(touch.fingerId);
                        break;
                    case TouchPhase.Canceled:
                        lastTouchPositionByFinger.Remove(touch.fingerId);
                        break;
                    default:
                        break;
                }
            }
        }
    }


    private static List<RaycastResult> tempRaycastResults = new List<RaycastResult>();
    public bool PointIsOverUI(Vector2 position)
    {

        var eventDataCurrentPosition = new PointerEventData(EventSystem.current);

        eventDataCurrentPosition.position = position;

        tempRaycastResults.Clear();

        EventSystem.current.RaycastAll(eventDataCurrentPosition, tempRaycastResults);
        foreach (var item in tempRaycastResults)
        {
            if (item.gameObject.GetInstanceID() == toCheck.GetInstanceID() && item.index == 0)
                return true;
        }
        return false;
    }
}

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  

Validating email in C#

Input sanitation is a must when dealing with web services, but it’s also smart to avoid to waste precious seconds in registration procedures due to a wrong email address. That’s true in web development, that’s true in mobile apps, that’s just true on any digital platform.
So it’s best to validate email in unity3d too before any web service is called.

The variables

First we get the references via unity editor to catch both theInputFieldandButton.

    [SerializeField]
    InputField mail;
    [SerializeField]
    Button sendButton;

Then we create aRegexusing the mail validation pattern from .Net framework (be aware, it’s not perfect, but it’s good enough).

    System.Text.RegularExpressions.Regex mailValidator = new System.Text.RegularExpressions.Regex(@"^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$");

TheRegexclass will provide us with a pattern-matching function that we can easily use to check if the pattern on which is built does or not match any input. The pattern will follow the regex syntax. Which by the way I found out not to be a regular expression language any more due to backreferences.

Validate email

Then, on eachUpdatewe first reset the button’s state, then we make it notinteractableagain only if any of the validation criteria is false. So only if the mail validation check DOES NOT recognize our string as a mail address it should make it notinteractable, but in the converse case it shouldn’t make itinteractablesince another validation criteria may fail independently.

    void Update()
    {
        sendButton.interactable = true;

        //other sanitation for other stuff

        if (!mailValidator.IsMatch(mail.text))
                sendButton.interactable = false;
    }

That’s all folks!

Really? really.
It’s not more than just 15 lines of code but it’s a big deal in reducing friction during your registration process for any service or game. If you like this kind of tutorial or just want to hear from me in the future, join my newsletter.

And here’s everything ready for copy-paste :

    [SerializeField]
    InputField mail;
    [SerializeField]
    Button sendButton;
    System.Text.RegularExpressions.Regex mailValidator= new System.Text.RegularExpressions.Regex(@"^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$");
    void Update()
    {
        sendButton.interactable = true;

        //other sanitation for other stuff

        if (!mailValidator.IsMatch(mail.text))
                sendButton.interactable = false;
    }
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  

Snap effect in scroll area: in-depth tutorial

This tutorial is part of a series on how to construct an awesome-looking scroll gallery, with inertia, snap effect, zoom and other stuff. Now we will add a snap effect to it, or as I call it, a magnetic behaviour. When the scrolling gallery is left somewhere between two images, the behaviour will move the one closest to the center towards the middle by scrolling all the sliding gallery. Easier done than said, actually:

snap to center effect
The snap effect will look like this. Magnetic, isn’t it?

Let’s get on the snap effect parameters!

Let’s start with the parameters we’ll need to add to our script this time:

    [Header("Magnetizaiton Parameters")]
    public bool doMagnetize = true;
    public float magnetizationMaxForceFactor = 0.09f;
    public float magnetizationRangeAlphaStepFraction = 0.5f;
    protected float magneticForce;

The first one is just to be able to disable the whole behaviour in one click. The variable magneticForce, instead, will be used to pass the actual per-frame movement to perform, as caused by the snap behaviour, to our updateAlpha function (that will change accordingly).
As for magnetizationMaxForceFactor it will be a factor that defines the maximum movement that can be done in a single frame. And last but not least we have magnetizationRangeAlphaStepFraction, a variable that represents which range of the intermediate positions between two images should be a valid starting point for our snapping behaviour (0.5 being just everywhere and less will leave some resting areas in the middle of two images).

To the bat-functions!

We won’t need to change the start function, but our Update will need a fix:

    protected virtual void Update()
    {
        updateAlpha();
        updateMagnetization();
        if (Mathf.Abs(lastFrameAlpha - alpha) > 0.001f)
        {
            refreshImages();
        }
        lastFrameAlpha = alpha;
    }

We’ll have to make a call to update the magneticForce parameter we introduced before and it must to be done at every frame.

As for our UpdateAlpha we’ll check for input and use the magneticForce if and only if there is no ongoing action

    protected virtual void updateAlpha()
    {
        alpha = Mathf.Clamp(
                alpha + (Mathf.Abs(inputSource.NetSwipe.x) > 0 ?
                    inputSource.NetSwipe.x * rotationSensibility
                    :
                    magneticForce),
                0,
                1
                );
    }

See? Only when the absolute value of the horizontal swipe is exactly zero we’ll add our magneticForce instead of the input-driven value. And now let’s go for the main dish:

    protected virtual void updateMagnetization()
    {
        if ((Mathf.Abs(inputSource.NetSwipe.x) <= 0.001) && (doMagnetize))
        {
            float rest = alpha % alphaStep;
            if (rest < magnetizationRangeAlphaStepFraction * alphaStep)
            {
                if (rest < rotationSensibility)
                    magneticForce = -rest;
                else
                    magneticForce = -rest * magnetizationMaxForceFactor;

            }
            else if (rest > alphaStep * (1 - magnetizationRangeAlphaStepFraction))
            {
                if (rest < rotationSensibility)
                    magneticForce = rest;
                else
                    magneticForce = (alphaStep - rest) * magnetizationMaxForceFactor;
            }
        }
    }

First we check that there’s no input and that the magnetization flag is set to true, otherwise we’d only be wasting our CPU cycles. Then we get the difference between the current position and the nearest image thanks to the module operator and the alphaStep variable that gives us the distance between images.

Implementing the actual snap effect

Now we can distinguish two cases: either we need to snap to left, or we need to snap to right. But we also could be in the case that magnetizationRangeAlphaStepFraction is small enough to allow for a resting position between images and that shall be checked too. Once we decided this we’ll see if we’re so close that it’s better to just skip to the final destination (by using the whole rest value as our next magneticForce ) or if we should go move of a fraction of it (by multiplying for magnetizationMaxForceFactor ).

Well, now the slider looks way nicer but it still lacks an inertia option and the abilty to select stuff. You’ll have to wait a bit for that, until next part is ready. Join my newsletter, I’ll link it there! [edit: next part is here]

Here’s a copy-paste friendly recap of all the changes we made to the base script in last article

    [Header("Magnetizaiton Parameters")]
    public bool doMagnetize = true;
    public float magnetizationMaxForceFactor = 0.09f;
    public float magnetizationRangeAlphaStepFraction = 0.5f;
    protected float magneticForce;

    protected virtual void Update()
    {
        updateAlpha();
        updateMagnetization();
        if (Mathf.Abs(lastFrameAlpha - alpha) > 0.001f)
        {
            refreshImages();
        }
        lastFrameAlpha = alpha;
    }

    protected virtual void updateAlpha()
    {
        alpha = Mathf.Clamp(
                alpha + (Mathf.Abs(inputSource.NetSwipe.x) > 0 ?
                    inputSource.NetSwipe.x * rotationSensibility
                    :
                    magneticForce),
                0,
                1
                );
    }

    protected virtual void updateMagnetization()
    {
        if ((Mathf.Abs(inputSource.NetSwipe.x) <= 0.001) && (doMagnetize))
        {
            float rest = alpha % alphaStep;
            if (rest < magnetizationRangeAlphaStepFraction * alphaStep)
            {
                if (rest < rotationSensibility)
                    magneticForce = -rest;
                else
                    magneticForce = -rest * magnetizationMaxForceFactor;

            }
            else if (rest > alphaStep * (1 - magnetizationRangeAlphaStepFraction))
            {
                if (rest < rotationSensibility)
                    magneticForce = rest;
                else
                    magneticForce = (alphaStep - rest) * magnetizationMaxForceFactor;
            }
        }
    }

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  

Scroll inertia: in-depth tutorial

This tutorial series on how to build a cool scrollable selection area  in unity is nearing its end, here we’ll mod the previous script to add a scroll inertia effect, so that the scroll doesn’t stop immediately when the user lifts the finger.

In last edit we added a snapping behaviour to our scroll area, and if you have not so many items to scroll that can be enough. But what if there are a lot of them? Let’s be clear, if you need to stash more than 30, I’d advice to use at least a scroll slider bar too, makes easier to navigate and keep track of what-is-where. But already with 15 or so elements it would be nice not to have to scroll through every single one of them with your finger. Let’s have one powerful slide do the job of a dozen weak ones!

Scrolling through an image gallery with inertia on
A couple scrolls can have very different effects with inertia…

Let’s set the parameters

To achieve this we’ll add an inertia “factor” to our movement, pretty much in the same way we added a magnetic force.
So, here’s how:

    [Header("inertia Parameters")]
    public bool useInertia;
    protected float inertia;
    public float inertialDampening = 0.9f; 
    public float inertiaPersistenceFactor = 10f;
  • useInertiawill be used as a control flag to switch the behaviour on and off
  • inertiais the variable where we store the movement due to inertia for the frame
  • inertialDampeningis our attrition factor, we’ll just mutiply this to last frame’s inertia to dampen it
  • inertiaPersistenceFactorinstead gives us control on how much the inertia effect should persist before being set to exactly 0

Changing the functions for scroll inertia

This time we’ll also have to change the Awake function because as of now magnetic behaviour and inertia are quite conflicting, they could produce a jerky or oscillating motion. It’s easy to merge them elegantly but that’s something I won’t do here, sorry 😛

    public virtual void Awake()
    {
        if (useInertia)
            doMagnetize = false;
    }

As for our Update function, that very predictably becomes:

    protected virtual void Update()
    {
        updateAlpha();
        updateInertia();
        updateMagnetization();
        if (Mathf.Abs(lastFrameAlpha - alpha) > 0.001f)
        {
            refreshImages();
        }
        lastFrameAlpha = alpha;
    }

Where updateInertia is:

    protected virtual void updateInertia()
    {
        if (useInertia)
        {
            inertia = Mathf.Abs(inputSource.NetSwipe.x) > 0 ? alpha - lastFrameAlpha : inertialDampening * inertia;
            if (Mathf.Abs(inertia) < rotationSensibility / inertiaPersistenceFactor)
                inertia = 0;
        }
    }

What happens here is: first we check our flag, then we evaluate if there is any input. If there is we update inertia to exactly that frame’s movement, so that we may use it later. If there is none, inertia gets its own value, dampened.

When it’s just too low to care about it, we set it to 0;

Lastly we need to insert the inertia we calculated into our updateAlpha function like this:

    protected virtual void updateAlpha()
    {
        alpha = Mathf.Clamp(
                alpha + (Mathf.Abs(inputSource.NetSwipe.x) > 0 ?
                    inputSource.NetSwipe.x * rotationSensibility
                    :
                    inertia + magneticForce), 
                0,
                1
                );
    }

See where we just sum inertia and magnetic force? there is where you want to intervene if you want to use them both without strange behaviours

And that’s it!

Now you can add inertia to your scrolling behaviour. Isn’t it cool? Last part of this tutorial series will introduce selection, which can be key to using this thing in your UIs without having the player tap on the image/button too. Don’t miss it, join my newsletter and I’ll tell you when it’s out. [edit: next part is here]

Here’s the usual copy-paste friendly version:

    [Header("inertia Parameters")]
    public bool useInertia;
    protected float inertia;
    public float inertialDampening = 0.9f; 
    public float inertiaPersistenceFactor = 10f;

    public virtual void Awake()
    {
        if (useInertia)
            doMagnetize = false;
    }
    protected virtual void Update()
    {
        updateAlpha();
        updateInertia();
        updateMagnetization();
        if (Mathf.Abs(lastFrameAlpha - alpha) > 0.001f)
        {
            refreshImages();
        }
        lastFrameAlpha = alpha;
    }

    protected virtual void updateAlpha()
    {
        alpha = Mathf.Clamp(
                alpha + (Mathf.Abs(inputSource.NetSwipe.x) > 0 ?
                    inputSource.NetSwipe.x * rotationSensibility
                    :
                    inertia + magneticForce), 
                0,
                1
                );
    }
    protected virtual void updateInertia()
    {
        if (useInertia)
        {
            inertia = Mathf.Abs(inputSource.NetSwipe.x) > 0 ? alpha - lastFrameAlpha : inertialDampening * inertia;
            if (Mathf.Abs(inertia) < rotationSensibility / inertiaPersistenceFactor)
                inertia = 0;
        }
    }
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •