using System.Collections.Generic; using System.Linq; using UnityEngine; namespace Unity.MLAgents { /// /// Monitor is used to display information about the Agent within the Unity /// scene. Use the log function to add information to your monitor. /// public class Monitor : MonoBehaviour { /// /// The type of monitor the information must be displayed in. /// corresponds to a single rectangle whose width is given /// by a float between -1 and 1. (green is positive, red is negative) /// /// corresponds to n vertical sliders. /// is a text field. /// is a rectangle of fixed length to represent the proportions /// of a list of floats. /// public enum DisplayType { Independent, Proportion } /// /// Represents how high above the target the monitors will be. /// public static float verticalOffset = 3f; static bool s_IsInstantiated; static GameObject s_Canvas; static Dictionary> s_DisplayTransformValues; /// /// Camera used to calculate GUI screen position relative to the target /// transform. /// static Dictionary s_TransformCamera; static Color[] s_BarColors; struct DisplayValue { public float time; public string stringValue; public float floatValue; public float[] floatArrayValues; public enum ValueType { Float, FloatarrayIndependent, FloatarrayProportion, String } public ValueType valueType; } static GUIStyle s_KeyStyle; static GUIStyle s_ValueStyle; static GUIStyle s_GreenStyle; static GUIStyle s_RedStyle; static GUIStyle[] s_ColorStyle; static bool s_Initialized; /// /// Use the Monitor.Log static function to attach information to a transform. /// /// The log. /// The name of the information you wish to Log. /// The string value you want to display. /// The transform you want to attach the information to. /// /// Camera used to calculate GUI position relative to /// the target. If null, `Camera.main` will be used. public static void Log( string key, string value, Transform target = null, Camera camera = null) { if (!s_IsInstantiated) { InstantiateCanvas(); s_IsInstantiated = true; } if (s_Canvas == null) { return; } if (target == null) { target = s_Canvas.transform; } s_TransformCamera[target] = camera; if (!s_DisplayTransformValues.Keys.Contains(target)) { s_DisplayTransformValues[target] = new Dictionary(); } var displayValues = s_DisplayTransformValues[target]; if (value == null) { RemoveValue(target, key); return; } if (!displayValues.ContainsKey(key)) { var dv = new DisplayValue(); dv.time = Time.timeSinceLevelLoad; dv.stringValue = value; dv.valueType = DisplayValue.ValueType.String; displayValues[key] = dv; while (displayValues.Count > 20) { var max = ( displayValues .Aggregate((l, r) => l.Value.time < r.Value.time ? l : r) .Key ); RemoveValue(target, max); } } else { var dv = displayValues[key]; dv.stringValue = value; dv.valueType = DisplayValue.ValueType.String; displayValues[key] = dv; } } /// /// Use the Monitor.Log static function to attach information to a transform. /// /// The log. /// The name of the information you wish to Log. /// The float value you want to display. /// The transform you want to attach the information to. /// /// Camera used to calculate GUI position relative to /// the target. If null, `Camera.main` will be used. public static void Log( string key, float value, Transform target = null, Camera camera = null) { if (!s_IsInstantiated) { InstantiateCanvas(); s_IsInstantiated = true; } if (target == null) { target = s_Canvas.transform; } s_TransformCamera[target] = camera; if (!s_DisplayTransformValues.Keys.Contains(target)) { s_DisplayTransformValues[target] = new Dictionary(); } var displayValues = s_DisplayTransformValues[target]; if (!displayValues.ContainsKey(key)) { var dv = new DisplayValue(); dv.time = Time.timeSinceLevelLoad; dv.floatValue = value; dv.valueType = DisplayValue.ValueType.Float; displayValues[key] = dv; while (displayValues.Count > 20) { var max = ( displayValues.Aggregate((l, r) => l.Value.time < r.Value.time ? l : r).Key); RemoveValue(target, max); } } else { var dv = displayValues[key]; dv.floatValue = value; dv.valueType = DisplayValue.ValueType.Float; displayValues[key] = dv; } } /// /// Use the Monitor.Log static function to attach information to a transform. /// /// The log. /// The name of the information you wish to Log. /// The array of float you want to display. /// The type of display. /// The transform you want to attach the information to. /// /// Camera used to calculate GUI position relative to /// the target. If null, `Camera.main` will be used. public static void Log( string key, float[] value, Transform target = null, DisplayType displayType = DisplayType.Independent, Camera camera = null ) { if (!s_IsInstantiated) { InstantiateCanvas(); s_IsInstantiated = true; } if (target == null) { target = s_Canvas.transform; } s_TransformCamera[target] = camera; if (!s_DisplayTransformValues.Keys.Contains(target)) { s_DisplayTransformValues[target] = new Dictionary(); } var displayValues = s_DisplayTransformValues[target]; if (!displayValues.ContainsKey(key)) { var dv = new DisplayValue(); dv.time = Time.timeSinceLevelLoad; dv.floatArrayValues = value; if (displayType == DisplayType.Independent) { dv.valueType = DisplayValue.ValueType.FloatarrayIndependent; } else { dv.valueType = DisplayValue.ValueType.FloatarrayProportion; } displayValues[key] = dv; while (displayValues.Count > 20) { var max = ( displayValues.Aggregate((l, r) => l.Value.time < r.Value.time ? l : r).Key); RemoveValue(target, max); } } else { var dv = displayValues[key]; dv.floatArrayValues = value; if (displayType == DisplayType.Independent) { dv.valueType = DisplayValue.ValueType.FloatarrayIndependent; } else { dv.valueType = DisplayValue.ValueType.FloatarrayProportion; } displayValues[key] = dv; } } /// /// Remove a value from a monitor. /// /// /// The transform to which the information is attached. /// /// The key of the information you want to remove. public static void RemoveValue(Transform target, string key) { if (target == null) { target = s_Canvas.transform; } if (s_DisplayTransformValues.Keys.Contains(target)) { if (s_DisplayTransformValues[target].ContainsKey(key)) { s_DisplayTransformValues[target].Remove(key); if (s_DisplayTransformValues[target].Keys.Count == 0) { s_DisplayTransformValues.Remove(target); } } } } /// /// Remove all information from a monitor. /// /// /// The transform to which the information is attached. /// public static void RemoveAllValues(Transform target) { if (target == null) { target = s_Canvas.transform; } if (s_DisplayTransformValues.Keys.Contains(target)) { s_DisplayTransformValues.Remove(target); } } /// /// Use SetActive to enable or disable the Monitor via script /// /// Value to set the Monitor's status to. public static void SetActive(bool active) { if (!s_IsInstantiated) { InstantiateCanvas(); s_IsInstantiated = true; } if (s_Canvas != null) { s_Canvas.SetActive(active); } } /// Initializes the canvas. static void InstantiateCanvas() { s_Canvas = GameObject.Find("AgentMonitorCanvas"); if (s_Canvas == null) { s_Canvas = new GameObject(); s_Canvas.name = "AgentMonitorCanvas"; s_Canvas.AddComponent(); } s_DisplayTransformValues = new Dictionary>(); s_TransformCamera = new Dictionary(); } void OnGUI() { if (!s_Initialized) { Initialize(); s_Initialized = true; } var toIterate = s_DisplayTransformValues.Keys.ToList(); foreach (var target in toIterate) { if (target == null) { s_DisplayTransformValues.Remove(target); continue; } // get camera var cam = s_TransformCamera[target]; if (cam == null) { cam = Camera.main; } var widthScaler = (Screen.width / 1000f); var keyPixelWidth = 100 * widthScaler; var keyPixelHeight = 20 * widthScaler; var paddingWidth = 10 * widthScaler; var scale = 1f; var origin = new Vector3( Screen.width / 2.0f - keyPixelWidth, Screen.height); if (!(target == s_Canvas.transform)) { var camTransform = cam.transform; var position = target.position; var cam2Obj = position - camTransform.position; scale = Mathf.Min( 1, 20f / (Vector3.Dot(cam2Obj, camTransform.forward))); var worldPosition = cam.WorldToScreenPoint( position + new Vector3(0, verticalOffset, 0)); origin = new Vector3( worldPosition.x - keyPixelWidth * scale, Screen.height - worldPosition.y); } keyPixelWidth *= scale; keyPixelHeight *= scale; paddingWidth *= scale; s_KeyStyle.fontSize = (int)(keyPixelHeight * 0.8f); if (s_KeyStyle.fontSize < 2) { continue; } var displayValues = s_DisplayTransformValues[target]; var index = 0; var orderedKeys = displayValues.Keys.OrderBy(x => - displayValues[x].time); foreach (var key in orderedKeys) { s_KeyStyle.alignment = TextAnchor.MiddleRight; GUI.Label( new Rect( origin.x, origin.y - (index + 1) * keyPixelHeight, keyPixelWidth, keyPixelHeight), key, s_KeyStyle); float[] vals; GUIStyle s; switch (displayValues[key].valueType) { case DisplayValue.ValueType.String: s_ValueStyle.alignment = TextAnchor.MiddleLeft; GUI.Label( new Rect( origin.x + paddingWidth + keyPixelWidth, origin.y - (index + 1) * keyPixelHeight, keyPixelWidth, keyPixelHeight), displayValues[key].stringValue, s_ValueStyle); break; case DisplayValue.ValueType.Float: var sliderValue = displayValues[key].floatValue; sliderValue = Mathf.Min(1f, sliderValue); s = s_GreenStyle; if (sliderValue < 0) { sliderValue = Mathf.Min(1f, -sliderValue); s = s_RedStyle; } GUI.Box( new Rect( origin.x + paddingWidth + keyPixelWidth, origin.y - (index + 0.9f) * keyPixelHeight, keyPixelWidth * sliderValue, keyPixelHeight * 0.8f), GUIContent.none, s); break; case DisplayValue.ValueType.FloatarrayIndependent: const float histWidth = 0.15f; vals = displayValues[key].floatArrayValues; for (var i = 0; i < vals.Length; i++) { var value = Mathf.Min(vals[i], 1); s = s_GreenStyle; if (value < 0) { value = Mathf.Min(1f, -value); s = s_RedStyle; } GUI.Box( new Rect( origin.x + paddingWidth + keyPixelWidth + (keyPixelWidth * histWidth + paddingWidth / 2) * i, origin.y - (index + 0.1f) * keyPixelHeight, keyPixelWidth * histWidth, -keyPixelHeight * value), GUIContent.none, s); } break; case DisplayValue.ValueType.FloatarrayProportion: var valsSum = 0f; var valsCum = 0f; vals = displayValues[key].floatArrayValues; foreach (var f in vals) { valsSum += Mathf.Max(f, 0); } if (valsSum < float.Epsilon) { Debug.LogError( $"The Monitor value for key {key} " + "must be a list or array of " + "positive values and cannot " + "be empty."); } else { for (var i = 0; i < vals.Length; i++) { var value = Mathf.Max(vals[i], 0) / valsSum; GUI.Box( new Rect( origin.x + paddingWidth + keyPixelWidth + keyPixelWidth * valsCum, origin.y - (index + 0.9f) * keyPixelHeight, keyPixelWidth * value, keyPixelHeight * 0.8f), GUIContent.none, s_ColorStyle[i % s_ColorStyle.Length]); valsCum += value; } } break; } index++; } } } /// Helper method used to initialize the GUI. Called once. void Initialize() { s_KeyStyle = GUI.skin.label; s_ValueStyle = GUI.skin.label; s_ValueStyle.clipping = TextClipping.Overflow; s_ValueStyle.wordWrap = false; s_BarColors = new[] { Color.magenta, Color.blue, Color.cyan, Color.green, Color.yellow, Color.red }; s_ColorStyle = new GUIStyle[s_BarColors.Length]; for (var i = 0; i < s_BarColors.Length; i++) { var texture = new Texture2D(1, 1, TextureFormat.ARGB32, false); texture.SetPixel(0, 0, s_BarColors[i]); texture.Apply(); var staticRectStyle = new GUIStyle(); staticRectStyle.normal.background = texture; s_ColorStyle[i] = staticRectStyle; } s_GreenStyle = s_ColorStyle[3]; s_RedStyle = s_ColorStyle[5]; } } }