Notifications
Article
Building a Better Button with DOTween
Updated 20 days ago
445
2
Source Code Available [ if you make it to the end ]
I’m a big fan of DOTween by Demigiant (Daniele Giardini). If it moves or changes value over time, my contention is, DOTween it!
Conversely, I feel that Mechanim is a tad too cumbersome for UI animations... So, herein I present a brief rundown on how I leverage DOTween for my animated button needs.
Fist off, we’ll need to get DOTween from the Asset Store. Link is HERE.
There is a free version available that will do just about everything you need it to do, but I recommend buying the “Pro” version if you want to show the developer some love.
After you’ve downloaded, imported, and setup DOTween via the “DOTween Utility Panel”, we’re ready to get started.
Let’s create a Canvas. “Screen Space – Overlay” will do nicely. Right-Click on the Canvas and choose “Create Empty”. This will be the base of our button upon which we will build.
I use a naming convention that I like to refer to as “Screaming Hungarian Snake”. It allows me know at a glance what type of object I’m looking at. “Main Camera” is renamed to “CAM_Main”.“Directional Light” becomes “LGT_Directional_001”. And so forth...
Let’s call our empty GameObject “DBTN_Play”. Or not. Call it whatever you like.
The great thing about building our own button is that we decide what the composition will be. I’m going to add 3 child Image objects. The 3 images will be background, interior, and an icon.
I’ll use the same plain white square sprite for the background and interior images. You can create one easily enough in the “Projects” window by Right-Clicking and choosing “Create > Sprites > Square”. I’m not exactly sure when Unity added this feature of convenience, I’m using 2018.2. If you don’t have this option... I’m sure you can import your own white square without too much difficulty.
I want my button to be 120px wide and 80px in height, so let’s resize DBTN_Play to that size. Let’s make IMG_Background 120px by 80px as well. Set it’s color to #000000.
I want a 3px visible border around my IMG_Interior, so type 120-6 for IMG_Interior width and type 80-6 for IMG_Interior height. Hurray for simple math! One could probably do complicated math in the input fields as well... I dunno. I’ve never tried.
I chose #8D4167 for IMG_Interior color. For the IMG_Icon I used an icon from the Buuf icon set by Paul Davey. Here’s what my assembled button looks like:
The advantage of using several discreet images is that every element may be animated or altered independently which allows for greater creative latitude when implementing interactive effects.
Now let’s get to coding!
We’ll create a new script and call it DoButton. Drop the DoButton script onto DBTN_Play.
We’ll be using the Unity EventSystem that appears upon the creation of our Canvas, so the DoButton Monobehaviour must implement several interfaces:
IPointerDownHandler, IPointerUpHandler, IpointerClickHandler, IpointerEnterHandler, & IpointerExitHandler. If you’re designing for mobile, you can skip the Enter & Exit Handlers.
Our nascent script looks like this:
using UnityEngine; using UnityEngine.EventSystems; public class DoButton : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler { }
Let’s implement the missing members and move into a namespace of our choosing. Might as well add all the using statements that we’re going to need too.
using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; namespace FuguFirecracker.UI { public class DoButton : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler { public void OnPointerDown(PointerEventData eventData) { } public void OnPointerUp(PointerEventData eventData) { } public void OnPointerEnter(PointerEventData eventData) { } public void OnPointerExit(PointerEventData eventData) { } public void OnPointerClick(PointerEventData eventData) { } } }
We’ll also need some Interfaces of our own; IActionable.cs and IAnimatable.cs
namespace FuguFirecracker.UI { public interface IActionable { void Do(DoButton doButton); } }
namespace FuguFirecracker.UI { public interface IAnimatable { void OnDown(DoButton doButton); void OnUp(DoButton doButton); void OnOver(DoButton doButton); void OnOut(DoButton doButton); } }
Back in DoButton.cs, we need to add some fields and properties.
public Transform Transform; public Image[] Images; private Stack<IActionable> _actionStack = new Stack<IActionable>(); private IActionable _actionable; public IActionable Actionable { get { if (_actionable != null) return _actionable; _actionable = GetComponent<IActionable>(); return _actionable; } } private IAnimatable _animatable; public IAnimatable Animatable { get { if (_animatable != null) return _animatable; _animatable = GetComponent<IAnimatable>(); return _animatable; } }
Let’s now take this opportunity to wire up the serializable fields in the inspector.
Now we can complete the PointerEvent Methods.
public void OnPointerDown(PointerEventData eventData) { Animatable.OnDown(this); } public void OnPointerUp(PointerEventData eventData) { Animatable.OnUp(this); } public void OnPointerEnter(PointerEventData eventData) { Animatable.OnEnter(this); } public void OnPointerExit(PointerEventData eventData) { Animatable.OnExit(this); } public void OnPointerClick(PointerEventData eventData) { _actionStack.Push(Actionable); }
So what’s up with that Action Stack? Well... we want our animations to play unimpeded. The click event will store the intention to action, and the end of the animation will trigger that action to execute. To that end, let’s add another public method called Execute to DoButton.cs
public void Execute() { if (_actionStack.Count == 0) return; { _actionStack.Pop().Do(this); } }
Ok... So what’s the point of this? That’s a reference to our button. We pass a reference to our button object to IAnimatable and IActionable so we can directly affect the button as required.
Our DoButton.cs script is finished now. Here it is in it's completion:
using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; namespace FuguFirecracker.UI { public class DoButton : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IPointerClickHandler , IPointerEnterHandler, IPointerExitHandler { public Transform Transform; public Image[] Images; private Stack<IActionable> _actionStack = new Stack<IActionable>(); private IActionable _actionable; public IActionable Actionable { get { if (_actionable != null) return _actionable; _actionable = GetComponent<IActionable>(); return _actionable; } } private IAnimatable _animatable; public IAnimatable Animatable { get { if (_animatable != null) return _animatable; _animatable = GetComponent<IAnimatable>(); return _animatable; } } public void OnPointerDown(PointerEventData eventData) { Animatable.OnDown(this); } public void OnPointerUp(PointerEventData eventData) { Animatable.OnUp(this); } public void OnPointerEnter(PointerEventData eventData) { Animatable.OnEnter(this); } public void OnPointerExit(PointerEventData eventData) { Animatable.OnExit(this); } public void OnPointerClick(PointerEventData eventData) { _actionStack.Push(Actionable); } public void Execute() { if (_actionStack.Count == 0) return; { _actionStack.Pop().Do(this); } } } }
Now on to animation...
One of the great perks of this system is that you can very easily build up a library of animations that you can just drag and drop onto your DoButtons. I’ll code up 2 animation so you get the idea.
First, I’ll do a Hi-Lite Animation:
using DG.Tweening; using UnityEngine; namespace FuguFirecracker.UI { public class HiLite : MonoBehaviour, IAnimatable { //Set In Inspector public Color32 HiLiteColor; public Color32 HiLiteToo; [Range(0.5f, 1.5f)] public float Growth; // assign a variable to our Tweener so that we can Kill and reassign as required private Tween _tween; // I want to capture the color I've assigned to the background Image only once, // so I use an initial dummy color to check against such that I don't assign // a transitional color to the sprite while the color is changing. private static readonly Color32 DummyColor = new Color32(42, 42, 42, 42); private Color32 _originalcolor = DummyColor; private bool _isOverButton; public void OnEnter(DoButton doButton) { _isOverButton = true; if (_originalcolor.Equals(DummyColor)) _originalcolor = doButton.Images[0].color; // For any other Handler to fire, OnEnter HAS to have occured, // Therefore we only need check for null here if (_tween != null && _tween.IsActive()) _tween.Kill(); _tween = doButton.Images[0].DOColor(HiLiteColor, 0.42f); } public void OnExit(DoButton doButton) { _isOverButton = false; _tween.Kill(); _tween = doButton.Images[0].DOColor(_originalcolor, 0.42f); } public void OnDown(DoButton doButton) { var growthVector = new Vector3(1 + Growth * 0.1f, 1 + Growth * 0.1f, 1); _tween.Kill(); _tween = DOTween.Sequence() .Join(doButton.Images[0].transform.DOScale(growthVector, 0.42f)) .Join(doButton.Images[0].DOColor(HiLiteToo, 0.42f)); } // OnUp is special... We call back to doButton to check for a clickEvent // That is, if Up happened when the mouse (or finger) was over the button... That's a CLICK. // doButton will Pop() the Stack and Do() the Action public void OnUp(DoButton doButton) { _tween.Kill(); _tween = DOTween.Sequence() .Join(doButton.Images[0].transform.DOScale(Vector3.one, 0.42f)) .Join(doButton.Images[0].DOColor(DecideColor(), 0.42f)) .OnComplete(doButton.Execute); } private Color32 DecideColor() { return _isOverButton ? HiLiteColor : _originalcolor; } } }
Before I do another animation, we will need an Actionable so we don’t throw any errors.
Something super simple:
using UnityEngine; namespace FuguFirecracker.UI { public class PrintToConsole : MonoBehaviour, IActionable { [SerializeField] private string _message; public void Do(DoButton doButton) { Debug.Log(_message); } } }
This is how our DoButton looks in the inspector. You'll note that we have the DoButton base, 1 Animatable script and 1 Actionable script. The whole strength of this design is the ability to swap out the Actions and Animations as required.
using DG.Tweening; using UnityEngine; namespace FuguFirecracker.UI { public class Pulse : MonoBehaviour, IAnimatable { private Tween _tween; private bool _isOverButton; // We need to move our button up and down the SiblingIndex // such that it draws on top of any other elements private int _siblingIndex; private void Awake() { _siblingIndex = GetComponent<Transform>().GetSiblingIndex(); } public void OnEnter(DoButton doButton) { _isOverButton = true; if (_tween != null && _tween.IsActive()) _tween.Kill(); doButton.Transform.SetAsLastSibling(); Pulsate(doButton); } public void OnExit(DoButton doButton) { _isOverButton = false; _tween.Kill(); _tween = doButton.Transform.DOScale(1, 0.2f) .OnComplete(() => doButton.Transform.SetSiblingIndex(_siblingIndex)); } public void OnDown(DoButton doButton) { _tween.Kill(); _tween = doButton.Transform.DOLocalRotate(new Vector3(0, 0, 180), 0.2f); } public void OnUp(DoButton doButton) { _tween.Kill(); _tween = doButton.Transform.DOLocalRotate(Vector3.zero, 0.2f) .OnComplete(() => { if (_isOverButton) { _tween = doButton.Transform.DOScale(Vector3.one, 0.2f) .OnComplete(() => Pulsate(doButton)); } doButton.Execute(); }); } private void Pulsate(DoButton doButton) { _tween = doButton.Transform.DOScale(1.5f, 0.3f) .SetLoops(-1, LoopType.Yoyo) .SetEase(Ease.OutCubic); } } }
I know I said that this was going to be LESS cumbersome than Mechanim ;) Well... This is what works for my brain. I've posted the project to GitHub, should you like to check it out. I'll expand upon this basic theme and illustrate how to execute multiple Actions in separate scripts -either simultaneously or sequentially- should I receive enough interest. Thanks for reading.

Tags:
Robin
Pixel Wrangler - Designer
2
Comments
Robin
a month ago
Pixel Wrangler - Designer
Gilberto Bittencourtamazing, thank's i use the dotween a lot and this is amazing! thank's i wonder how it will work combined with the UniRX, reactive programming!
@Gilberto Bittencourt I wonder how it will work with UniRx too. I've been meaning to delve into UniRx. I understand that it is well loved by those whom use it. Let me know if you cobble something together :)
1
Game Developer - Programmer
amazing, thank's i use the dotween a lot and this is amazing! thank's i wonder how it will work combined with the UniRX, reactive programming!
1