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, “FighterShip 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.
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 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
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);
}
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.)
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.