10/30/2019
Over the past few years, I've developed an AppLog class that I use in all my Unity game development projects. The AppLog class started out merely as a means to allow me to write errors and exceptions to file to help with debugging software during development. Over time, it has evovled into a fully self contained class for handling all things log related. My AppLog class may not be the perfect solution, but it is perfect for how I use it. It works so well for me, that I am now releasing the class to the public under the MIT open source license. You'll find the full class code below, minus any namespace. Feel free to use it as you desire. Attrition is always nice, but not required here. By default AppLog logs your messages to the game screen, a Log.txt file in the persistent data path, and to the Unity console window. When the game is running in Release mode, only Errors and Exceptions will be logged. AppLog will only write to the text file while your game is running in the Unity Editor. Overall, the AppLog class is built as a copy-paste solution. Simply add the class to your Unity project, and start using it. You don't need to add anything to your scenes, nor do you need to worry about any prefabs. The AppLog class will handle the creation and disabling of its own assets as needed.using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
[RequireComponent(typeof(Canvas))]
public class AppLog : MonoBehaviour
{
private static AppLog _instance;
private static TextMeshProUGUI _text;
#region Static Methods
/// <summary>Delegate that declares the signature of any method that can handle a log added event.</summary>
/// <param name="message">The log message being added.</param>
public delegate void LogAddedHandler(string message);
private static List<LogData> _logs = new List<LogData>();
/// <summary>Read-Only list of logs collected by the AppLog</summary>
public static List<string> Logs
{
get
{
var lst = new List<string>();
foreach (var log in _logs)
{
lst.Add(log.LogMessage);
}
return lst;
}
}
/// <summary>
/// <para>Event that is triggered when a new log is added.</para>
/// <para>Only includes the message being logged.</para>
/// </summary>
public static event LogAddedHandler LogAdded;
/// <summary>Clears all entries from the AppLog</summary>
public static void Clear()
{
_logs.Clear();
_logs = new List<LogData>();
#if UNITY_EDITOR
RemoveLogs();
#endif
}
private static void Add(string type, string message, string methodName, params object[] args)
{
var msg = FormatOrFail(message, args);
var log = string.Format("{0}::{1}::{2}: {3}", DateTime.Now.ToString(Config.TimeStampFormat), type, methodName, msg);
_logs.Add(new LogData(Time.time, log));
TrimLogs();
if (_instance == null)
{
#if UNITY_EDITOR
RemoveLogs();
#endif
Create();
}
else
{
if (!_instance.gameObject.activeInHierarchy) _instance.gameObject.SetActive(true);
}
LogAdded?.Invoke(msg);
#if UNITY_EDITOR
Write(log);
#endif
}
private static void RemoveLogs()
{
#if UNITY_EDITOR
var path = string.Format("{0}/log.txt", Application.persistentDataPath);
if (File.Exists(path)) File.Delete(path);
#endif
}
private static void Write(string message)
{
#if UNITY_EDITOR
var path = string.Format("{0}/log.txt", Application.persistentDataPath);
using (var writer = File.AppendText(path))
{
writer.WriteLine(message);
}
#endif
}
private static string FormatOrFail(string message, params object[] args)
{
string msg;
try
{
msg = string.Format(message, args);
}
catch
{
msg = message.Replace('{', '[').Replace('}', ']');
}
return msg;
}
private static void TrimLogs()
{
while (_logs.Count > Config.Lines)
{
_logs.RemoveAt(0);
}
}
private static string GetMethodName()
{
var stackTrace = new StackTrace();
var frame = stackTrace.GetFrames()?[2];
if (frame == null) return "Unidentified method";
var method = frame.GetMethod();
var methodName = method.Name;
var methodClass = method.DeclaringType;
return methodClass == null
? "Undetermined method"
: string.Format("{0}.{1}()", methodClass.Name, methodName);
}
/// <summary>Logs a message to the AppLog</summary>
/// <param name="message">A formatted string to be logged</param>
/// <param name="args">Parameters used in the formatting of the message.</param>
public static void Log(string message, params object[] args)
{
#if DEBUG
var methodName = GetMethodName();
Add(Config.LogText, message, methodName, args);
#endif
}
/// <summary>Logs a debug message to the AppLog</summary>
/// <param name="message">A formatted string to be logged</param>
/// <param name="args">Parameters used in the formatting of the message.</param>
public static void Debug(string message, params object[] args)
{
#if DEBUG
var methodName = GetMethodName();
Add(Config.DebugText, message, methodName, args);
#endif
}
/// <summary>Logs a warning message to the AppLog</summary>
/// <param name="message">A formatted string to be logged</param>
/// <param name="args">Parameters used in the formatting of the message.</param>
public static void Warn(string message, params object[] args)
{
#if DEBUG
var methodName = GetMethodName();
Add(Config.WarningText, message, methodName, args);
#endif
}
/// <summary>Logs a error message to the AppLog</summary>
/// <param name="message">A formatted string to be logged</param>
/// <param name="args">Parameters used in the formatting of the message.</param>
public static void Error(string message, params object[] args)
{
var methodName = GetMethodName();
Add(Config.ErrorText, message, methodName, args);
}
/// <summary>Logs an exception to the AppLog</summary>
/// <param name="exception">A System.Exception object</param>
public static void Exception(Exception exception)
{
var methodName = GetMethodName();
var str = "";
var ex = exception;
while (ex != null)
{
str += ex.Message + Environment.NewLine;
str += ex.StackTrace + Environment.NewLine;
ex = ex.InnerException;
if (ex != null) str += "Inner Exception:" + Environment.NewLine;
}
Add(Config.ExceptionText, str, methodName);
}
/// <summary>Logs an exception to the AppLog.</summary>
/// <param name="exception">A System.Exception object.</param>
/// <param name="message">A formatted message to log with the exception.</param>
/// <param name="args">Formatting arguments for use with the message.</param>
public static void Exception(Exception exception, string message, params object[] args)
{
var methodName = GetMethodName();
var str = "";
var msg = FormatOrFail(message, args);
if (msg.Length > 0) str += msg + Environment.NewLine;
var ex = exception;
while (ex != null)
{
str += ex.Message + Environment.NewLine;
str += ex.StackTrace + Environment.NewLine;
ex = ex.InnerException;
if (ex != null) str += "Inner Exception:" + Environment.NewLine;
}
Add(Config.ExceptionText, str, methodName);
}
#endregion
#region MonoBehavior Methods
private static void Create()
{
var goAppLog = new GameObject("AppLog");
var canvas = goAppLog.AddComponent<Canvas>();
var scaler = goAppLog.AddComponent<CanvasScaler>();
_instance = goAppLog.AddComponent<AppLog>();
goAppLog.AddComponent<CanvasRenderer>();
var image = goAppLog.AddComponent<Image>();
image.color = new Color(0f, 0f, 0f, 0.5f);
var goText = new GameObject("AppLogText");
goText.AddComponent<CanvasRenderer>();
_text = goText.AddComponent<TextMeshProUGUI>();
goText.transform.SetParent(goAppLog.transform);
canvas.renderMode = RenderMode.ScreenSpaceCamera;
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920, 1080);
_text.fontSize = 20;
_text.rectTransform.anchorMin = Vector2.zero;
_text.rectTransform.anchorMax = Vector2.one;
_text.rectTransform.offsetMin = Vector2.zero;
_text.rectTransform.offsetMax = Vector2.zero;
_text.alignment = TextAlignmentOptions.TopLeft;
}
public void Start()
{
var canvas = GetComponent<Canvas>();
var cameras = FindObjectsOfType<Camera>();
if(cameras.Length > 0)
canvas.worldCamera = cameras[0];
DontDestroyOnLoad(this);
}
public void FixedUpdate()
{
RemoveOldLogs();
var str = LogsToString();
_text.text = str;
_instance.gameObject.SetActive(str.Length > 0);
}
private string LogsToString()
{
var str = "";
foreach (var log in _logs)
{
if (str.Length > 0) str += Environment.NewLine;
str += log.LogMessage;
}
return str;
}
private void RemoveOldLogs()
{
for (var i = _logs.Count - 1; i >= 0; i--)
{
if (_logs[i].TimeStamp + Config.LogDuration < Time.time)
{
_logs.RemoveAt(i);
}
}
}
#endregion
private class LogData
{
public readonly float TimeStamp;
public readonly string LogMessage;
public LogData(float time, string msg)
{
TimeStamp = time;
LogMessage = msg;
}
}
public static class Config
{
public static int Lines = 50;
public static string LogText = "LOG";
public static string DebugText = "DBG";
public static string WarningText = "WRN";
public static string ErrorText = "ERR";
public static string ExceptionText = "EXC";
public static string TimeStampFormat = "hh:mm tt";
public static float LogDuration = 5f;
}
}