Search code examples
c#unity-game-enginevuforia

Is there a way to start a method from button click without using Update() function in unity


Below is my C# script. I added a button to my project with a On Click event and called the Rotate() method. But for some reason it is not working

using System.Threading;
using UnityEngine;

public class Orbit : MonoBehaviour {

    public GameObject sun;
    public float speed;

    // Use this for initialization
    void Start () {

    }

    public void Update()
    {
        Rotate();
    }

    public void Rotate()
    {
        transform.RotateAround(sun.transform.position, Vector3.up, speed * 
        Time.deltaTime);
    }
}

I commented the Update() method when calling the Rotate() method. I also created a game object for the script.


Solution

  • The reason why it only works in Update currently is that

    public void Rotate()
    {
        transform.RotateAround(sun.transform.position, Vector3.up, speed * Time.deltaTime);
    }
    

    needs to be called repeatetly. Otherwise it will only rotate for exactly one frame and cause of Time.deltaTime only a very small amount. But the onClick event of the Button component is fired only once. It is similar to e.g. Input.GetKeyDown which is only called once when the key goes down. There is no implementation in the Button component itslef to handle a continued button press.


    What you want instead as far as I understand is rotating the object after the button click

    • start to rotate for ever
    • for a certain duration
    • until you press the button again
    • until it is released (-> implement a continuesly firing button see below)

    The Button component alone can only do the first three:

    Rotate for ever

    Either using a Coroutine

    private bool isRotating;
    
    public void Rotate()
    {
        // if aready rotating do nothing
        if(isRotating) return;
    
        // start the rotation
        StartCoroutine(RotateRoutine());
    
        isRotating = true;
    }
    
    private IEnumerator RotateRoutine()
    {
        // whuut?!
        // Don't worry coroutines work a bit different
        // the yield return handles that .. never forget it though ;)
        while(true)
        {
             // rotate a bit
             transform.RotateAround(sun.transform.position, Vector3.up, speed * Time.deltaTime);
    
            // leave here, render the frame and continue in the next frame
            yield return null;
        }
    }
    

    or still doing it in Update

    private bool isRotating = false;
    
    private void Update()
    {
        // if not rotating do nothing
        if(!isRotating) return;
    
        // rotate a bit
        transform.RotateAround(sun.transform.position, Vector3.up, speed * Time.deltaTime);
    }
    
    public void Rotate()
    {
        // enable the rotation
        isRotating = true;
    }
    

    Note that the Update solution is only for your understanding what is happening. It should not be used like that because it is not that efficient since Update is called continously and checks the bool also if not rotating yet. That produces unnecessary overhead. The same applies to all following examples: Prefere to use the Coroutines over Update (In this case! In other cases it is actuall better and more efficient to use one Update method instead of multiple concurrent Coroutines .. but that's another story.)

    Rotate for a certain duration

    As Coroutine

    // adjust in the inspector
    // how long should rotation carry on (in seconds)?
    public float duration = 1;
    
    private bool isAlreadyRotating;
    
    public void Rotate()
    {
        // if aready rotating do nothing
        if(isAlreadyRotating) return;
    
        // start a rottaion
        StartCoroutine(RotateRoutine());
    }
    
    private IEnumerator RotateRoutine()
    {
        // set the flag to prevent multiple callse
        isAlreadyRotating = true;
    
        float timePassed = 0.0f;
        while(timePassed < duration)
        {
             // rotate a small amount
             transform.RotateAround(sun.transform.position, Vector3.up, speed * Time.deltaTime);
    
             // add the time passed since last frame
             timePassed += Time.deltaTime;
    
             // leave here,  render the frame and continue in the next frame
             yield return null;
        }
    
        // reset the flag so another rotation might be started again
        isAlreadyRotating = false;
    }
    

    or in Update

    public float duration;
    
    private bool isRotating;
    private float timer;
    
    private void Update()
    {
        // if not rotating do nothing
        if(!isRotating) return;
    
        // reduce the timer by passed time since last frame
        timer -= Time.deltaTime;
    
        // rotate a small amount
        transform.RotateAround(sun.transform.position, Vector3.up, speed * Time.deltaTime);
    
        // if the timer is not 0 return
        if(timer > 0) return;
    
        // stop rottaing
        isRotating = false;
    }
    
    public void Rotate()
    {
        // if already rotating do nothing
        if(isRotating) return;
    
        // start rotating
        isRotating = true;
    
        // enable timer
        timer = duration;
    }
    

    Toggle rotation

    This is very similar to the one before but this time instead of the timer you stop the rotation by clicking again. (You even could combine the two but than be carefull to reset the isRotating flag correctly ;) )

    As Coroutine

    private bool isRotating;
    
    public void ToggleRotation()
    {
        // if rotating stop the routine otherwise start one
        if(isRotating)
        {
            StopCoroutine(RotateRoutine());
            isRotating = false;
        }
        else
        {
            StartCoroutine(RotateRoutine());
            isRotating = true;
        }
    }
    
    private IEnumerator RotateRoutine()
    {
        // whuut?!
        // Don't worry coroutines work a bit different
        // the yield return handles that .. never forget it though ;)
        while(true)
        {
            // rotate a bit
            transform.RotateAround(sun.transform.position, Vector3.up, speed * Time.deltaTime);
    
            // leave here, render the frame and continue in the next frame
            yield return null;
        }
    }
    

    or as Update

    private bool isRotating;
    
    private void Update()
    {
        // if not rotating do nothing
        if(!isRottaing) return;
    
        // rotate a bit
        transform.RotateAround(sun.transform.position, Vector3.up, speed * Time.deltaTime);
    }
    
    public void ToggleRotation()
    {
        // toggle the flag
        isRotating = !isRotating;
    }
    

    Rotate until released

    This is the most "complicated" part since the Button alone can not accomplish this (there is no "on Release"). But you can implement this using IPointerXHandler interfaces.

    The good news: You can keep your original script as you have it currently

    public void Rotate()
    {
        transform.RotateAround(sun.transform.position, Vector3.up, speed * 
        Time.deltaTime);
    }
    

    Now you need an extension for the button. It will call the whilePressed event repeatedly every frame like Update so you just have to reference your Rotate method in whilePressed instead of the onClick.

    Again there are two options either implementing it as a Coroutine:

    [RequireComponent(typeof(Button))]
    public class HoldableButton : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IPointerExitHandler
    {
        // reference the same way as in onClick
        public UnityEvent whilePressed;       
    
        private Button button;
        private bool isPressed;
    
        private void Awake()
        {
            button = GetComponent<Button>();
    
            if(!button)
            {
                Debug.LogError("Oh no no Button component on this object :O",this);
            }
        }
    
        // Handle pointer down
        public void OnPointerDown()
        {
            // skip if the button is not interactable
            if(!button.enabled || !button.interactable) return;
    
            // skip if already rotating
            if(isPressed) return;
    
            StartCoroutine(PressedRoutine());
            isPressed= true;
    
        }
    
        // Handle pointer up
        public void OnPointerUp()
        {
            isPressed= false;
        }
    
        // Handle pointer exit
        public void OnPointerExit()
        {
            isPressed= false;
        }
    
        private IEnumerator RotateRoutine()
        {
            // repeatedly call whilePressed until button isPressed turns false
            while(isPressed)
            {
                // break the routine if button was disabled meanwhile
                if(!button.enabled || !button.interactable)
                {
                    isPressed = false;
                    yield break;
                }
    
                // call whatever is referenced in whilePressed;
                whilePressed.Invoke();
    
                // leave here, render the frame and continue in the next frame
                yield return null;
            }
        }
    }
    

    or you could do the same in Update again as well

    [RequireComponent(typeof(Button))]
    public class HoldableButton : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IPointerExitHandler
    {
        public UnityEvent whilePressed;
    
        private bool isPressed;
        private Button button;
    
        private void Awake()
        {
            button = GetComponent<Button>();
    
            if(!button)
            {
                Debug.LogError("Oh no no Button component on this object :O",this);
            }
        }
    
    
        private void Update()
        {
            // if button is not interactable do nothing
            if(!button.enabled || !button.interactable) return;
    
            // if not rotating do nothing
            if(!isPressed) return;
    
            // call whatever is referenced in whilePressed;
            whilePressed.Invoke();
        }
    
        // Handle pointer down
        public void OnPointerDown()
        {
            // enable pressed
            isPressed= true;
        }
    
        // Handle pointer up
        public void OnPointerUp()
        {
            // disable pressed
            isPressed= false;
        }
    
        // Handle pointer exit
        public void OnPointerExit()
        {
            // disable pressed
            isPressed= false;
        }
    }
    

    Place this component next to a Button component. You don't have to reference anything in onClick just leave it empty. Instead reference something in onPressed. Keep the Button component though since it handles also the UI style for us (like hover changes the color/sprite etc.)


    Again: The Update solutions might look cleaner/simplier for now but are not as efficient (in this usecase) and easy to controll (this might be opinion based) as the Coroutine solutions.