Text Message Box

Hi all,

I’m sure there are tutorials for what I’m looking for, but I don’t really know what the name is for the type of “message box” I’m wanting to make.

I just need a text field (it’s height could be the entire height of the screen, but a fraction of the width, and off to the side) that can display messages sent from various objects that will be useful for debugging, but also potentially useful to give tips to new players of my game. For example, the spaceships in my game can only pick up ammo that matches their weapon type. New players may not understand why they can’t collect the pickup, so a message like, “Fighter Ship cannot use plasma ammo” popping up might be handy.

I’d want the messages to sort of “stack”. As in, each new message pushes the previous ones down the screen. All of this ought to be straight-forward enough, but I’m not sure how to handle the 'time to delete". I would usually use coroutines for (almost) anything that needs a timer, but I don’t know how I’d do that in this case. Like, I wouldn’t want each object to be a new item that turns off when the timer expires-- they should all be just lines of text in one text field on one game object.

Any suggestions?

Hi, i will go with coroutine, you can do something like this :
1)This is my code so you need to do few adjustments in order to use it e.g. ScriptName etc.

You will call like this :

                  string infoString = "Test Message";   
                  StartCoroutine(Static.ShowMessage(infoString, 3));

My static method: - method is in my static class called “Static”

      public static IEnumerator ShowMessage(string message, float delay)
        {
            RuntimeManager rm = UnityEngine.Object.FindObjectOfType<RuntimeManager>();
            rm.ConsoleMessage.GetComponent<Animator>().enabled = true;
            rm.ConsoleMessage.text += message+"\n";
            rm.ConsoleMessage.enabled = true;
            yield return new WaitForSeconds(delay);
            rm.ConsoleMessage.enabled = false;
            rm.ConsoleMessage.GetComponent<Animator>().enabled = false;

        }

You probably dont have any script called “RuntimeManager” in your project so just adjust it to your needs, this is what i am using.Only thing you need is in your script to have some gameobject with animator component.

If you do it like this then you can use it from anywhere in your namespace you only must have RuntimeManager in your scene with valid gameobject with text and animator component.

2)Or you can make some gameobject what will hold all messages, each message will be one child and when you will have more message then you want you just destroy some.You can do it easy with VerticalLayoutGroup component.

As parent i used scrollview because i can have many objects in que
it looks like this :
https://gifyu.com/image/G9oR

1 Like

Thank you for the suggestions. I’m probably going to avoid using the Animator, so your second suggestion is likely what I’ll go with. My only concern there is creating and destroying objects so frequently. I’m sure I can figure out how to recycle the child objects, so it’s not a big deal.

Still, I’m thinking there ought to be a way that I can simplify this even more (no vertical layout group, no instantiating child objects, no object pool manager).

I’m thinking of something where every message passed into the box starts with the new line character “/n”, or possibly some other special character like an asterisk (this character would be stuck onto the start of it by the coroutine function). After the timer expires in a message’s coroutine, it would run a loop that removes characters from the end of the message box’s string until (and including) it reaches my delimiter character. This this strike anyone as particularly slow/inefficient? To me, it’s a simpler approach, but my knowledge of optimization is not the greatest.

This seems to be doing the job. The most-recent message is always displayed at the top, and older messages are pushed down. I’m still not sure if this is bad practice (going through each character of the string), but it works, and I don’t have to use any fancy components outside of the TextMeshPro.

using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;

public class MessageBoxController : MonoBehaviour
{
    public TextMeshProUGUI textMesh;
    private char delimiter = '*';   // added to message, used to parse messages from text string
    const float _timeToLive = 3;   // the amount of time a message remains on screen

    public void Awake()
    {
        textMesh = GetComponent<TextMeshProUGUI>();
    }

    public void Update()
    {
        if (Input.GetKeyDown(KeyCode.V))
            DisplayMessage("This is a message, yo!");

        if (Input.GetKeyDown(KeyCode.P))
            DisplayMessage("Totally different message.");
    }

    /// <summary>
    /// The string, msg, will be appended to the textfield and removed after _timeToLive seconds
    /// </summary>
    /// <param name="s"></param>
    /// <param name="ttl"></param>
    public void DisplayMessage(string msg)
    {
        msg = delimiter + " " + msg + "\n";        // add the delimiter and a space to the start of the message string, and a new line character to the end
        textMesh.text = msg + textMesh.text;               // add the message to the start of the TextMesh component
        StartCoroutine(DisplayMessageRoutine());
    }

    private IEnumerator DisplayMessageRoutine()
    {
        yield return new WaitForSecondsRealtime(_timeToLive);

        string tmp = textMesh.text;         // this may be unnecessary...
        bool delimiterReached = false;
        int escape = 200;                   // just in case I screwed up

        // remove characters from the end of the string until (and including) the delimiter is reached
        while (!delimiterReached && escape > 0)
        {
            if (tmp[tmp.Length - 1] == delimiter)
                delimiterReached = true;

            tmp = tmp.Substring(0, tmp.Length - 1);

            escape--;
            if (escape <= 0)
                Debug.LogError("loop exited by escape case");
        }

        textMesh.text = tmp;
    }
}

If it is work then go for it but i think goal should be make it as generic and independent as possible. So in your code create object from prefab, populate it with values you want and then destroy it when no need is better IMO.

If i was you i use “my solution” and extend it by your character removing script , so you still will have parent who hold all gameobjects in vertical layout group.

Because when you want for example put some image next to each message then you will need to reweork it anyway and probably end up with some “container” for each message. One string for more messages is not good imo.

instead of manually searching for the delimiter you can use IndexOf
for example

var test = "some message on the left side|some message on the right side";
var index = test.IndexOf('|');
if(index >= 0) test = test.Substring(index + 1);
Debug.Log(test);

if you intend to use both parts, you can use Split, which is a bit um clumsy, but basically bundles IndexOf

var test = "some message on the left side|some message on the right side";
string[] delimited = test.Split('|');
if(delimited.Length > 1) {
  Debug.Log($"messages are {delimited[0]} and {delimited[1]}");
} else {
  Debug.Log(test);
}
1 Like

also when you are concatenating long-ish (or longer) strings, and by that I really mean anything greater than 32 characters or so, a good practice to consider is StringBuilder because it allocates optimally, works directly on the strings, and doesn’t kill the garbage collector with the amount of garbage strings can leave hanging around.

for example

var post = "a relatively long sentence intended to demonstrate a prepend";
var pre = "what would happen if this was a really really dumb concatenation with ";
var sb = new StringBuilder(post);
sb.Insert(0, pre);
var final = sb.ToString();

Try to benchmark this in a loop against the + operator (and string.Concat).
You will be amazed. (also: normally you just do Append, for the most obvious effect.)

1 Like

Thanks for the feedback and the suggetions orionsyndrome. I don’t think IndexOf would be best in my case as the text field would have multiple instances of my delimiter if there are multiple messages. Granted, I could find all instances, load their indices into an array, and use the one closest to the end… but that feels maybe unnecessary.

I had no idea that strings created an especially large amount of garbage, but that’s just the kind of thing I was hoping to get filled in on, so thank you.

Have you seen other overloads of IndexOf?
And there is also LastIndexOf. Plenty of ways to do this thoroughly.

Here’s one of them

using StringSplittingExtensions;

var test = "some message|348934|left side|sdiuter|message 2|right side";

foreach(var piece in test.SplitForward('|')) {
  Debug.Log(piece);
}
Debug.Log("---");
foreach(var piece in test.SplitBackward('|')) {
  Debug.Log(piece);
}

// {another file}

using System.Collections.Generic;

static public class StringSplittingExtensions {
  static IEnumerable<string> SplitForward(this string s, char c) {
    int l, i = -1;
    do {
      l = i;
      i = s.IndexOf(c, i+1);
      if(i >= 0) yield return s.Substring(l+1, (i-l)-1);
    } while(i >= 0);
    yield return s.Substring(l-i);
  }

  static IEnumerable<string> SplitBackward(this string s, char c) {
    int l, i = s.Length;
    do {
      l = i;
      if(i-1 < 0) break;
      i = s.LastIndexOf(c, i-1);
      if(i >= 0) yield return s.Substring(i+1, -(i-l)-1);
    } while(i >= 0);
    yield return s.Substring(0, System.Math.Max(0, l-i-1));
  }
}

You’re welcome.

edit:
fixed a bug with an awkward case of SplitBackward (where string would begin with delimiter).

1 Like