Wibble
Wibble

Reputation: 89

Wait for Response before continuing Main Routine

I am trying to make a simple Message box that helps guide the readability of my output. I am using C# in Unity.

The reason for this is that the Message is often long, convoluted or occasionally needs a response.

I am trying to call a DisplayMessage(string _message) Method with a simple bool flag to allow it to close up and continue. But no matter what i do it either freezes (thank you while infinite loop) or passes through to the main thread

IEnumerables are not one of my strong points and unless I am missing something basic and obvious (a possibility I am more than willing to admit) then the Coroutine is still not going to hold up DoDisplay() properly until I get the Button input

I have spent some hours looking but my eyes are glazing over at options from the Systems.Threading solutions

public Class MainTurn: MonoBehavior {
    public GameControl gCon;

    public void OnPhaseOne(GameControl gCon) {
        // *** EVENT CARDS ***

        DisplayManager dMan = GameObject.FindGameObjectWithTag("Display Manager").GetComponent<DisplayManager>();
        dMan.DoDisplay("Take 2 Cards, update Red Count");

        // Check for 6+ Event Cards
        if (gCon.Cards >= 6) {
            dMan.DoDisplay("Discard a Card\n Discard Order:\n1. 2. 3. 4.");
        }

        // Allow for Exchange
        if (gCon.Cards == 2) {
            dMan.DoDisplay("Exchange Card as Required");
        }

    }
}

    public class DisplayManager : MonoBehaviour {
        public GameObject DisplayPanel;

        public Text Message_Text;
        public Button Complete_Button;

        private bool _isComplete;

        public void DoDisplay(string _displayMessage) 
        {
            // Activate Panel
            DisplayPanel.SetActive(true);

            // Set flag and display message
            _isComplete = false;
            Message_Text.text = _displayMessage;

            // Wait until Complete
            while (_isComplete == false) 
            {
                Debug.Log("Waiting");
            }

            //Clear and close
            Message_Text.text = "";
            DisplayPanel.SetActive(false);
        }

        // A simple Button press to clear the flag
        public void OnComplete() 
        {
            _isComplete = true;
        }
    }

Upvotes: 0

Views: 229

Answers (2)

derHugo
derHugo

Reputation: 90779

First of all: You do not want to use Threads here! Use them for heavy operations like file IO and serialization, yes, etc but not for just waiting until something happens in your App. The only thing achieving by that would be having to additionally take care that the according callback is passed back to the main thread etc.

No routine/loop required

If it is only about one-time actions that don't require any continuous update or smooth movements etc then how about making it event-based without using a bool flag and a routine or loop at all:

public void DoDisplay(string _displayMessage) 
{
    // Activate Panel
    DisplayPanel.SetActive(true);

    // display message
    Message_Text.text = _displayMessage;

    // log only once
    Debug.Log("Waiting");
}

// A simple Button press to clear the flag
public void OnComplete() 
{
    //Clear and close
    Message_Text.text = "";
    DisplayPanel.SetActive(false);
}

In general you should avoid a frame-wise usage of Debug.Log anyway since it slows down your app even if you don't see the console output anymore in a build.


Coroutine

Since you asked - Alternative could be using a Coroutine and either WaitWhile or WaitUntil or if you really want again a while loop with a simple yield return null.

Note that coroutines are still executed in the main thread so any heavy operation here might still freeze your app - like for example a closed while loop if you do not yield return anywhere within it ;)

See them as temporary Update methods since actually (using the default yield returns) it is executed frame-wise after the actual Update method. There are exceptions like e.g. WaitForFixedUpdate or WaitForEndOfFrame but maybe this would go to far for now.

In very simple words for Unity's coroutines yield return means something like "Pause this routine, continue executing the main thread. Later continue this routine from where you left."

public void DoDisplay(string _displayMessage)
{
    // Be careful now with concurrent routines in case this gets called twice
    // You probably would want to stop the last routine and start a new one
    StopAllCoroutines();
    StartCoroutine(DoDisplayRoutine(_displayMessage));
}

public IEnumerator DoDisplayRoutine(string _displayMessage) 
{
    // Activate Panel
    DisplayPanel.SetActive(true);

    // Set flag and display message
    _isComplete = false;
    Message_Text.text = _displayMessage;

    Debug.Log("Waiting");
    yield return new WaitUntil(() => _isComplete);

    // OR if you really want to display the log every frame meanwhile
    while(!_isComplete)
    {
        Debug.Log("Waiting");
        // render this frame and continue from here in the next one
        // If you missed to yield return somewhere in a while loop
        // then yes, this would also freeze the main thread
        yield return null;
    }

    //Clear and close
    Message_Text.text = "";
    DisplayPanel.SetActive(false);
}

// A simple Button press to clear the flag
public void OnComplete() 
{
    _isComplete = true;
}

I don't know what your main code does as you didn't add it ... What you could do would be making it a routine as well like e.g.

IEnumerator YourMainCode()
{
    ... do something

    // show message and wait until it gets closed (using the later coroutine solution)
    yield return DoDisplayRoutine(_displayMessage);

    ... continue after message was closed
}

Now we have your code ;)

as said you could do it like

public void OnPhaseOne(GameControl gCon) 
{
    StartCoroutine(PhaseOneRoutine());
}

private IEnumerator PhaseOneRoutine()
    // *** EVENT CARDS ***

    // if there is only one in the sene you can simply use
    DisplayManager dMan = FindObjectOfType<DisplayManager>();
    // in general you should rather do it in Awake and only once

    // this now "blocks" until the message is completed
    yield return dMan.DoDisplayRoutine("Take 2 Cards, update Red Count");

    // Check for 6+ Event Cards
    if (gCon.Cards >= 6) {
        yield return dMan.DoDisplayRoutine("Discard a Card\n Discard Order:\n1. 2. 3. 4.");
    }

    // Allow for Exchange
    if (gCon.Cards == 2) {
        yield return dMan.DoDisplayRoutine("Exchange Card as Required");
    }
}

Upvotes: 2

Jack Mariani
Jack Mariani

Reputation: 2408

SITUATION

From what I see you want the following:

  • Show a message on the screen when no other message is shown
  • If another message is shown wait before showing the next

This does not require complex elements such as threading, coroutines or other task managent. This is the work of a Queue.

You can implement this algorithm:

ALGORITHM

  1. Try Display a message
  2. NO PREVIOUS MESSAGE => Confirm Display the message
  3. PREVIOUS MESSAGE ON SCREEN => Enqueue the message
  4. On Complete is called
  5. QUEUE HAS NO MESSAGES => Close the menu
  6. QUEUE HAS MESSAGES => Show the next message

MESSAGE QUEUE

An implementation, using your code, would be the following.

public class DisplayManager : MonoBehaviour
{
    public GameObject DisplayPanel;

    public Text Message_Text;
    public Button Complete_Button;

    private Queue<string> _messages;

    public void DoDisplay(string displayMessage)
    {
        //if no messages are displayed, you display the message 
        if (string.IsNullOrEmpty(Message_Text.text)) ShowMessage(displayMessage);
        //if other messages are displayed you enqueue the message
        else _messages.Enqueue(displayMessage);
    }

    private void ShowMessage(string displayMessage)
    {
        // Activate Panel, if not already active
        if(!DisplayPanel.activeSelf) DisplayPanel.SetActive(true);

        // set the message on the text
        Message_Text.text = displayMessage;
    }

    // A simple Button press to clear the flag
    public void OnComplete()
    {
        //if we have other messages we show the next
        if (_messages.Count > 0) ShowMessage(_messages.Dequeue());
        //otherwise we close the menu
        else CloseMessage();
    }

    private void CloseMessage()
    {
        Message_Text.text = "";
        DisplayPanel.SetActive(false);
    }
}

FURTHER LOGIC

If you want to add further logic on the message displayer you may just do that on ShowMessage method.

There you may add anything such as

if(displayMessage == "Add Card") 
    //DO SOMETHING

Upvotes: 1

Related Questions