itch.io is community of indie game creators and players

Devlogs

Our HTML Logging Tool

Fireside
A downloadable Fireside

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 output of out Logger

Implementation

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 + "");
}


The HSV color space: h = Hue, s = Saturation, V = Value(Lightness)

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). 

Conclusion

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):

  • The logs are in an interactive and human readable from
  • You get the full logs even if the game crashes
  • You can extract the log via ftp or over a REST API for easy access
  • You can create different log files for different parts of your game (e.g. menu, dialog, inventory, etc)
  • You can refresh the browser any time while the game is running to inspect the current state
  • You can create data objects to log just the properties you need to avoid cluttering the log with uninteresting data
  • You could organize the logs in some kind of database on a server to better handle logs from big test sessions

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.

Read comments (2)