Posted October 24, 2020 by caphhe
In our previous devlogs, we talked about different features and design decisions for Fireside. This time we're going to mix it up a bit with a tool that helps with the development of any game. Depending on the game, it can get quite difficult to debug certain problems, for example the quests and dialogues in Fireside can get very long and depend on complex conditions. You could just use Debug.Logs but this will either clutter your console output or result in not having the right log active at the moment your designer stumbles upon a hard to reproduce bug in your code. To avoid this problem, we have built our own logging tool after getting the idea from our professor Jochen Koubek at university. The logger generates a HTML file that contains all the important data you need for debugging. The file can then be uploaded to a server, which is especially useful when testing on mobile devices, since it eliminates the need to use a USB cable for debugging. You can check out a log output here, with the buttons on the top you can hide different kinds of nodes and focus on just the ones you need.
The Logger script can be broken up in two main parts: setup and runtime. At the start of the game a template is copied, and the buttons are added. During runtime, any script can call the Log function of the logger singleton and the given content is appended to the log file.
Setup
To copy the template you need to specify the path where you want to store the log, the complexity of this task depends on the platform of your game. The following code focuses on Windows for simplicity, but be aware that there are some specific quirks on other platforms when saving, loading & sending data (on Android, for example, you need to use a UnityWebRequest to get the template file from your own local filesystem, for example). You don’t need to copy the CSS file, it works without it and you just need it in the same folder as the log when you want to analyse it.
string templatePath = Application.dataPath + "/StreamingAssets/Logger/template.html"; string logFilePath = Application.dataPath + "/StreamingAssets/Logger/LogMenu.html"; Directory.CreateDirectory(Application.persistentDataPath + "/Logger"); File.Copy(templatePath, filePathMenu, true); Setup();
The next step is to create the buttons at the top by opening a StremWriter and continuing the file. All buttons should be created at once whether you will need them or not, because it would be difficult to go back to some point in the file and another button. To avoid inserting lines each log message has a type defined by an enum and each enum value is a button.
As you can see in the following code snippet the whole magic of the logger is to just paste predefined HTML code into an HTML file.
private void Setup() { stream= new StreamWriter(filePathMenu, true); colors = new Color[Enum.GetValues(typeof(LogMessageTypes)) .Length]; for (int i = 0; i < colors.Length; i++) { float h = (360f / colors.Length * i) / 360f; colors[i] = Color.HSVToRGB(h, 1, 1); CreateButton((LogMessageTypes)i); } stream.WriteLine("</div></p>"); stream.WriteLine("<h1>Fire log</h1>"); stream.WriteLine("<div id=\"logParent\">"); stream.Flush(); } private void CreateButton(LogMessageTypes type) { Color textColor = ContrastColor(colors[(int) type]); stream.Write("<button class=\"button active\"" + "style=\"background-color:#" + ColorUtility.ToHtmlStringRGB(colors[(int)type]) + ";"); stream.Write(" border-color:#" + ColorUtility.ToHtmlStringRGB(colors[(int)type]) + ";"); stream.WriteLine(" color:#" + ColorUtility.ToHtmlStringRGB(textColor) + " \"onclick=\"" + "hideLogType('" + type + "', this)\"/>" + type + ""); }
Logging Data
To log something, we have two different methods that both share functions. The simple version is to just log a short string.
public void LogString(string stringToLog, LogMessageTypes type) { StartLogTag(type); stream.WriteLine(stringToLog); CloseLogTag(type); }
To log longer strings, especially json dumps of some object we use the following code to set some CSS classes that create the grey box. It is important that the json is formatted in a human readable way (for example with Newtonsoft.Json.JsonConvert.SerializeObject(nodeData, Formatting.Indented)).
public void LogJson(string stringToLog, LogMessageTypes type, string json) { StartLogTag(type); stream.WriteLine(stringToLog); stream.WriteLine("<div>" + "<div class=\"json\">" + "<pre>"); stream.WriteLine(json); stream.WriteLine("</pre>" + "</div>" + "</div>"); CloseLogTag(type); }
The StartLogTags and CloseLogTags functions handle the HTML and CSS tags to generate the actual layout. The StreamWriter.Flush command at the end is important in case the game crashes shortly after this message was logged. Without it you gain some performance but all the lines you send to the StreamWriter are buffered and will only be added to the file at a later point in time.
private void StartLogTag(LogMessageTypes type) { stream.WriteLine("<div class=\"log-entry " + type + "\">"); stream.WriteLine("<div class=\"time-stamp\" " + "style=\"background-color:#" + ColorUtility.ToHtmlStringRGB(colors[(int)type]) + "; color: white\">" + DateTime.Now + "</div>"); stream.WriteLine("<div class=\"log-text\">"); } private void CloseLogTag(LogMessageTypes type) { stream.WriteLine("</div>"+ "</div>"); stream.Flush(); }
You should close all the open tags OnApplicationQuit and close the StreamWriter. But it is not necessary, web browsers will accept the file without the closing tags.
JS Functions
All the functionality of the HTML file is defined in the script tag that is copied from the template at the start. Each button calls a toggle function and passes the message type as a string and the button object. The function then toggles the active and inactive CSS classes for the button and searches with document.getElementsByClassName(type) all elements with the type as class. For each of those elements the style.display CSS property is set to none or to an empty string (which removes the property).
This kind of logger brings many advantages for the development of a complex game, especially if you have discrete states (for a fast-paced twin stick shooter it might be less useful):
But you have to be careful with the amount of data you log, because you can easily reach 100 mb or more when a tester is playing a longer session. At best, this is a problem for the file upload, but it could have consquences if you reach the maximum file size.
Do you have any similar tools like this or do you have any ideas to improve it? Tell us in the comments below or on the Fireside Discord Server.
If you want to follow the development of Fireside you should check out our last devlog from Paul about procedural level generation and check out our development streams every Thursday from 10:00 AM to 12:00 AM CET at twitch.tv/emergoentertainment.