0

I'm trying to make a tooltip pop up when I hover my mouse over a button. I had some issues calling the main thread from a separate thread, since the main thread is the only thread you can edit game objects from. So I decided to use a boolean and an update function to get the desired result. However, I ran into an issue I have never seen before. I am setting my boolean showToolTip to true, which is supposed to trigger the create pop-up function. However, by the time my update function runs, showTooltip is always false. I know when I set it to true, it becomes true, because I used debug statements in the requestShowTooltip function to see this. However, it is always false when the update function runs. I did some tests by changing showToolTip and removeToolTip to public booleans. I can change them in the editor while running the application, and when I manually change them through the editor they work 100% as expected, and the tooltip will appear and hide as I change the correlating booleans. However when they are changed from the script they are retained as false. I have also tried leaving them unitialized and initializing them to false in the start, and I was still having the same problem. I even tried taking "showTooltip = true;" out of the thread and just having the function do it immediately, and I was still having the same issue. showTooltip is always false when the update function is called. I have determined this by adding a debug statement in the update function that reports the value of the showTootip and removeTooltip booleans. Furthermore, no matter what, "Calling create tooltip" never appears in the console. Except for when I made the booleans public and changed them through the editor.

Some further explanation on how I set this up. I made a Tooltip Game Object that is a child of the canvas that contains the buttons I want the tooltip for. I added the Tooltips script to the tooltip object. I created event triggers in the button Game Objects. the Pointer Enter trigger calls the requestShowTooltip() function, and the Pointer Exit trigger calls the requestHideTooltip() function.

The Tooltip Background and Tooltip Text Game Objects are also children of this same canvas.

public class Tooltips : MonoBehaviour
{
     private GameObject toolTipBackground;
     private GameObject toolTipText;
     private GameObject inButtonObject;

     private bool showTooltip = false;
     private bool removeTooltip = false;

     void Start()
     {
          toolTipBackground = GameObject.Find("Tooltip background");
          toolTipText = GameObject.Find("Tooltip Text");
          //inButton = GameObject.Find("Aft Button");

          toolTipBackground.SetActive(false);
          toolTipText.SetActive(false);

          //Debug.Log("Tool Tip Start");
     }

     void Update()
     {
          // show the tooltip when the appropriate boolean has been set 
          // to true
          if(removeTooltip)
          {
               hideTooltip();
          }
          // this if statement is always false, because showTooltip is 
          // always false when the update function is called??
          if(showTooltip)
          {

               Debug.Log("Calling create tooltip");
               createTooltip();
          }
     }

     // A timed request for a tooltip to show.  The tooltip should 
     // currently show one second after the request has been made.  The 
     // text of the tooltip will be equal to the passed object's name
     public void requestShowTooltip(GameObject inButtonObject)
     {
          Thread thread = new Thread(delegate ()
          { 
               System.Threading.Thread.Sleep(1000);
               this.inButtonObject = inButtonObject;

               // I know this runs, but showTooltip is always returning 
               // to false when the update function is called??
               showTooltip = true;

               removeTooltip = false;
               Debug.Log("Request function completed");
          });

          thread.start();
     }

     public void createTooltip()
     {
          toolTipText.SetActive(true);
          toolTipBackground.SetActive(true);

          string labelString = inButtonObject.name;

          Button inButton = inButtonObject.GetComponent<Button>();

          int width = labelString.Length * 10;
          int height = 35;

          Vector2 size = new Vector2(width, height);
          Vector3 position = new Vector3(inButton.transform.x, 
                                     inButton.transform.position.y,
                                     inButton.transform.position.z + 1);

          toolTipBackground.GetComponent<RectTransform>().
          sizeDelta = size;

          toolTipText.GetComponent<RectTransform>().sizeDelta = size;

          toolTipBackground.transform.position = position;
          toolTipText.transform.position = position;

          toolTipText.GetComponent<Text>().text = labelString;

          showTooltip = false;
     }

     public void requestHideTooltip()
     {
          removeTooltip = true;
          showTooltip = false;
     }

     public void hideTooltip()
     {
          Vector2 hide = new Vector2(0, 0);
          toolTipBackground.GetComponent<RectTransform>().
          sizeDelta = hide;
          toolTipText.GetComponent<RectTransform>().sizeDelta = hide;

          toolTipText.setActive(false);
          toolTopBackground.SetActive(false);

          removeTooltip = false;
          showTooltip = false;
     }

}
JMC0352
  • 25
  • 7
  • 1
    Pretty sure you're going to need to mark a field [volatile](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/volatile) if you want to modify it from another thread. I'm suspicious that Unity won't support this. – Jonathon Chase Jul 11 '19 at 22:23
  • I changed them to volatile, and I still got the same result. I also did testing where I took the boolean out of the thread, and changed it on the main unity thread (while still keeping it in the requestShowTooltip() function), and it still was having the same problem. – JMC0352 Jul 11 '19 at 22:35
  • Is there a particular reason you're using a separate thread? It seems like delaying the appearance of a tooltip could be just as easily handled by a coroutine without the headache of thread management – Ruzihm Jul 11 '19 at 22:40
  • Also, and this is a longshot, try adding [System.Nonserialized] before the boolean fields, i.e.: `[System.Nonserialized] private bool showTooltip = false; [System.Nonserialized]private bool removeTooltip = false;` – Ruzihm Jul 11 '19 at 22:48
  • Initially a thread seemed quicker, but I'll try a coroutine. I'll follow up on Monday since I won't be back to work until then. However, I'm still curious as to why that variable keeps changing. I fear I'll run into this issue again. I know it's not the thread, I believe it has something to do with Unity. – JMC0352 Jul 11 '19 at 22:55
  • I tried the [System.Nonserialized], I'm still getting the same result. – JMC0352 Jul 11 '19 at 23:00
  • After looking at this again I think what might be causing this is that the button is configured to call `requestShowTooltip` on a different `Tooltips` than the one enabled and in the scene, and thus changing the state of different instances of `showTooltip` and `removeTooltip` . It might be running it on a disabled `Tooltips` or on a `Tooltips` on a prefab. Make sure that is not the case. [This question](https://stackoverflow.com/questions/56570358/unity3d-navmeshagent-isonnavmesh-becomes-false-in-specific-function/56585888) had a similar cause. – Ruzihm Jul 12 '19 at 01:29
  • You can only modify unity values from the main unity thread. You could google tricks to get execution of your code back on the main thread, but to be honest it would be easier to use a coroutine. – Leo Bartkus Jul 12 '19 at 13:13
  • Are you sure it runs because e.g. `Debug.Log` is also not allowed from a thread only on the main thread. Basically everything from the Unity API needs to be called on the main thread. – derHugo Jul 12 '19 at 14:48

1 Answers1

1

Since basically everything in Unity needs to be called in the main Thread (with very few exceptions) I would suggest to change your code using a ConcurrentQueue<Action> and TryDequeue to let the main thread work through all the responses from any thread.

// a queue for storing actions that shall be handled by the main thread
// a ConcurrentQueue in specific is thread-save
private ConcurrentQueue<Action> actions = new ConcurrentQueue<Action>();

private void Update()
{
    // invoke all actions from the threads in the main thread
    while(!actions.IsEmpty)
    {
        // TryDequeue writes the first entry to currentAction
        // and at the same time removes it from the queue
        // if it was successfull invoke the action otherwise do nothing
        if(actions.TryDequeue(out var currentAction))
        {
            currentAction?.Invoke();
        } 
    }

    if(removeTooltip)
    {
         hideTooltip();
    }

    if(showTooltip)
    {
         Debug.Log("Calling create tooltip");
         createTooltip();
    }
}

Then in your Thread instead of making the thread directly execute the stuff itself rather pass it back to the main thread using the actions queue and add a new Action to the end using Enqueue

public void requestShowTooltip(GameObject inButtonObject)
{
     Thread thread = new Thread(delegate()
     { 
          System.Threading.Thread.Sleep(1000);

          // add an Action to the end of the queue
          // e.g. as lambda-expression
          actions.Enqueue(()=>
          {
              this.inButtonObject = inButtonObject;

              // I know this runs, but showTooltip is always returning 
              // to false when the update function is called??
              showTooltip = true;

              removeTooltip = false;
              Debug.Log("Request function completed");
          });
     });

     thread.start();
}

You might want to consider btw to make inButtonObject of type Button - first in order to make sure the passed object always has a/is a Button and second you can get rid of the performance intense GetComponent call.

The same way I would rather store

RectTransform toolTipBackgroundRectTransform;
Text toolTipText;
RectTransform toolTipTextrectTransform;

since those are the components you can get already once in Start and then you can re-use always the same references and get rid of all further GetComponent calls.

derHugo
  • 83,094
  • 9
  • 75
  • 115
  • OK. Thank you for the reply. I will try this when I get into work on Monday. – JMC0352 Jul 13 '19 at 15:22
  • Ok, it turns out my Unity was still using to .Net 3.5 (which was preventing me from using System.Collections.Concurrent), but when I switched to .Net 4.0 all my links in my GameObjects got deleted. So I will need to manually restore those, before I can test this change. – JMC0352 Jul 15 '19 at 21:11
  • OK, that is working. Thanks. It's aloso good that you helped me find that I was on the wrong version of .Net. – JMC0352 Jul 15 '19 at 21:38