├── .gitignore ├── CHANGELOG.md ├── CHANGELOG.md.meta ├── Documentation~ └── images │ ├── example_awardmanager.png │ ├── example_helloworld.png │ ├── example_heroperframe.png │ ├── mainmenu.png │ └── window.png ├── Editor.meta ├── Editor ├── PlayModeInspector.cs ├── PlayModeInspector.cs.meta ├── Unity.PlayModeInspector.Editor.asmdef └── Unity.PlayModeInspector.Editor.asmdef.meta ├── LICENSE.md ├── LICENSE.md.meta ├── README.md ├── README.md.meta ├── Runtime.meta ├── Runtime ├── PlayModeInspector.cs ├── PlayModeInspector.cs.meta ├── Unity.PlayModeInspector.Runtime.asmdef └── Unity.PlayModeInspector.Runtime.asmdef.meta ├── package.json └── package.json.meta /.gitignore: -------------------------------------------------------------------------------- 1 | [Aa]rtifacts/ 2 | [Bb]uild/ 3 | [Ll]ibrary/ 4 | [Oo]bj/ 5 | [Tt]emp/ 6 | [Ll]og/ 7 | [Ll]ogs/ 8 | .vs 9 | .vscode 10 | .idea 11 | .DS_Store 12 | *.aspx 13 | *.browser 14 | *.csproj 15 | *.exe 16 | *.ini 17 | *.map 18 | *.mdb 19 | *.npmrc 20 | *.pyc 21 | *.resS 22 | *.sdf 23 | *.sln 24 | *.sublime-project 25 | *.sublime-workspace 26 | *.suo 27 | *.userprefs 28 | .npmrc 29 | *.leu 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this package are documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [1.6.0] - 2022-02-06 8 | ### Fixed 9 | - Fixed that overridden virtual methods, decorated with [PlayModeInspectorMethod], show up twice in the PlayMode Inspector window. 10 | 11 | ## [1.5.0] - 2022-01-12 12 | ### Fixed 13 | - Fixed that methods in base classes, decorated with [PlayModeInspectorMethod], don't show up in the PlayMode Inspector window. 14 | 15 | ## [1.4.0] - 2021-12-31 16 | ### Fixed 17 | - Fixed compile errors that occurred in Unity 2021.2 and newer. 18 | - Fixed that ExitGUIException caused the PlayMode Inspector window to display "An error occurred" until the GameObject selection was changed. 19 | 20 | ## [1.3.0] - 2021-05-14 21 | ### Changed 22 | - Improved message when the current selection doesn't contain a Component or ScriptableObject with a [PlayModeInspectorMethod] attribute. 23 | 24 | ### Fixed 25 | - Opening the PlayMode Inspector window, while an object is selected already, not correctly displays the selected object in PlayMode Inspector, without the need to deselect and then select the object again. 26 | 27 | ## [1.2.0] - 2020-08-13 28 | ### Changed 29 | - Removed "sealed" keyword from the PlayModeInspectorMethodAttribute class. This allows to derive from it and use your own attribute in your code. In case you want to get rid of the PlayMode Inspector package, you only need to change your own attribute and everything still compiles. 30 | 31 | ## [1.1.0] - 2020-07-18 32 | ### Added 33 | - Functionality to override the default display name shown in the PlayMode Inspector item header. Use the "displayName" property found in the PlayModeInspectorMethod attribute for this. 34 | - Functionality to expand/collapse an item by clicking anywhere in the header, rather than on the toggle only. 35 | 36 | ### Fixed 37 | - Window icon barely visible with Professional Editor Theme (Issue #1). 38 | - Clicking the "Add PlayMode Inspector" button to create a new window, displays the current selected object now, rather than an empty window. 39 | - Do not call PlayMode Inspector method on prefabs in the project. Call the method only, when the object is located in a scene. This is necessary, because all the Awake/Start/OnEnable/etc methods are only called on Components in a scene. And if we would call the PlayMode Inspector method on a prefab in the project, it most likely is in an undefined state. 40 | - Do not call PlayMode Inspector method on inactive components. This is necessary, because the Component perhaps hasn't initialized and thus is in an undefined state. 41 | - Do not call PlayMode Inspector method on Components in the prefab stage, because all the Awake/Start/OnEnable/etc methods are only called on Components in a scene. And if we would call the PlayMode Inspector method on a prefab in the project, it most likely is in an undefined state. 42 | 43 | ## [1.0.0] - 2020-05-31 44 | - First public release 45 | -------------------------------------------------------------------------------- /CHANGELOG.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5a45c25cb2601944da9aa1a823aef8c2 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Documentation~/images/example_awardmanager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschraut/UnityPlayModeInspector/19a2fa1fe17c53bafcce59914ba9f1edf79604ce/Documentation~/images/example_awardmanager.png -------------------------------------------------------------------------------- /Documentation~/images/example_helloworld.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschraut/UnityPlayModeInspector/19a2fa1fe17c53bafcce59914ba9f1edf79604ce/Documentation~/images/example_helloworld.png -------------------------------------------------------------------------------- /Documentation~/images/example_heroperframe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschraut/UnityPlayModeInspector/19a2fa1fe17c53bafcce59914ba9f1edf79604ce/Documentation~/images/example_heroperframe.png -------------------------------------------------------------------------------- /Documentation~/images/mainmenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschraut/UnityPlayModeInspector/19a2fa1fe17c53bafcce59914ba9f1edf79604ce/Documentation~/images/mainmenu.png -------------------------------------------------------------------------------- /Documentation~/images/window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschraut/UnityPlayModeInspector/19a2fa1fe17c53bafcce59914ba9f1edf79604ce/Documentation~/images/window.png -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 356445bb818c1b04fa7d4720b5290c90 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/PlayModeInspector.cs: -------------------------------------------------------------------------------- 1 | // 2 | // PlayMode Inspector for Unity. Copyright (c) 2015-2024 Peter Schraut (www.console-dev.de). See LICENSE.md 3 | // https://github.com/pschraut/UnityPlayModeInspector 4 | // 5 | #pragma warning disable IDE1006, IDE0017 6 | using UnityEngine; 7 | using UnityEditor; 8 | using System.Collections.Generic; 9 | using System.Reflection; 10 | using System; 11 | using Oddworm.Framework; 12 | 13 | #if UNITY_2021_2_OR_NEWER 14 | using UnityEditor.SceneManagement; 15 | #else 16 | using UnityEditor.Experimental.SceneManagement; 17 | #endif 18 | 19 | namespace Oddworm.EditorFramework 20 | { 21 | class PlayModeInspector : EditorWindow 22 | { 23 | List m_Entries; // A list of all methods to inspect. 24 | Vector2 m_ScrollPosition; // The scroll position in the playmode inspector. 25 | bool m_ExceptionOccurred; // Whether an exception occurred while drawing the playmode inspector. 26 | bool m_Locked; // Whether the object selected is locked. 27 | 28 | [MenuItem("Window/Analysis/PlayMode Inspector", priority = 500)] 29 | static void CreateMenuItem() 30 | { 31 | var wnd = EditorWindow.GetWindow(); 32 | if (wnd != null) 33 | wnd.Show(); 34 | } 35 | 36 | void OnEnable() 37 | { 38 | m_Locked = false; 39 | m_ExceptionOccurred = false; 40 | titleContent = new GUIContent("PlayMode Inspector"); 41 | 42 | var icon = EditorGUIUtility.IconContent("UnityEditor.InspectorWindow"); 43 | if (icon != null) 44 | titleContent.image = icon.image; 45 | 46 | EditorApplication.playModeStateChanged += OnPlayModeStateChanged; 47 | 48 | // If an object is selected already and the PlayMode Inspector window is opened 49 | // afterwards, make sure to display the selected object without having it to reselect. 50 | OnSelectionChange(); 51 | } 52 | 53 | void OnDisable() 54 | { 55 | EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; 56 | } 57 | 58 | void OnPlayModeStateChanged(PlayModeStateChange playMode) 59 | { 60 | m_Locked = false; 61 | m_ExceptionOccurred = false; 62 | 63 | OnSelectionChange(); 64 | } 65 | 66 | void OnSelectionChange() 67 | { 68 | if (!Application.isPlaying || m_Locked) 69 | return; 70 | 71 | m_Entries = new List(); 72 | m_ExceptionOccurred = false; 73 | 74 | // If a GameObject is selected, we want to check each of its Components 75 | // if one or multiple of them have a method with the [PlayModeInspectorMethod] attribute 76 | // Only call method on active gameobjects in a scene. Otherwise variables could not be initialized when we select a prefab. 77 | if (Selection.activeGameObject != null) 78 | { 79 | var show = Selection.activeGameObject.activeInHierarchy; 80 | 81 | if (!Selection.activeGameObject.scene.IsValid()) 82 | show = false; // prefab in the project window, not a gameobject in the scene 83 | 84 | if (PrefabStageUtility.GetPrefabStage(Selection.activeGameObject) != null) 85 | show = false; // don't run in prefab stage 86 | 87 | if (show) 88 | { 89 | foreach (var c in Selection.activeGameObject.GetComponents()) 90 | UnityEngineObjectEntry.TryCreate(c, m_Entries); 91 | } 92 | } 93 | 94 | // If a ScriptableObject is selected, there really is just that one object where 95 | // a method with the [PlayModeInspectorMethod] attribute can be found. 96 | if (Selection.activeObject is ScriptableObject) 97 | UnityEngineObjectEntry.TryCreate(Selection.activeObject, m_Entries); 98 | 99 | Repaint(); 100 | } 101 | 102 | void OnInspectorUpdate() 103 | { 104 | // OnInspectorUpdate is called more often than OnGUI if no repaint events are triggered. 105 | // In order for the play mode inspector to update its UI without user interaction, we use 106 | // OnInspectorUpdate to trigger repaint events. 107 | 108 | if (!Application.isPlaying) 109 | return; 110 | 111 | Repaint(); 112 | } 113 | 114 | void OnGUI() 115 | { 116 | EditorGUI.BeginDisabledGroup(!Application.isPlaying); 117 | DrawToolbar(); 118 | EditorGUI.EndDisabledGroup(); 119 | 120 | if (!Application.isPlaying) 121 | { 122 | EditorGUILayout.HelpBox("PlayMode Inspector is interactive during play mode only.", MessageType.Info); 123 | return; 124 | } 125 | 126 | if (m_Entries == null || m_Entries.Count == 0) 127 | { 128 | EditorGUILayout.HelpBox($"Current selection does not contain a Component or ScriptableObject with a [PlayModeInspectorMethod] attribute.", MessageType.Info); 129 | return; 130 | } 131 | 132 | // If an exception occurred, display a helpbox and early out. 133 | // This is to avoid causing an exception every frame. 134 | if (m_ExceptionOccurred) 135 | { 136 | EditorGUILayout.HelpBox("An error occurred, please see Console for details.", MessageType.Error); 137 | 138 | EditorGUILayout.BeginHorizontal(); 139 | GUILayout.FlexibleSpace(); 140 | if (GUILayout.Button("Ignore")) 141 | m_ExceptionOccurred = false; 142 | EditorGUILayout.EndHorizontal(); 143 | return; 144 | } 145 | 146 | m_ScrollPosition = EditorGUILayout.BeginScrollView(m_ScrollPosition); 147 | { 148 | for (var n = 0; n < m_Entries.Count; ++n) 149 | { 150 | var entry = m_Entries[n]; 151 | 152 | EditorGUIUtility.labelWidth = Mathf.Min(250, position.width * 0.3f); 153 | EditorGUIUtility.fieldWidth = 0; 154 | GUI.enabled = true; 155 | GUI.matrix = Matrix4x4.identity; 156 | 157 | EditorGUILayout.BeginVertical(); 158 | { 159 | var isExpanded = DrawTitlebar(entry); 160 | 161 | EditorGUILayout.BeginHorizontal(); 162 | { 163 | GUILayout.Space(18); 164 | 165 | EditorGUILayout.BeginVertical(); 166 | { 167 | GUILayout.Space(6); 168 | 169 | try 170 | { 171 | if (isExpanded) 172 | entry.Invoke(); // This will actually call the method to draw the GUI 173 | } 174 | catch (Exception e) 175 | { 176 | if (IsCrititalException(e)) 177 | { 178 | m_ExceptionOccurred = true; 179 | Debug.LogException(e); 180 | } 181 | } 182 | 183 | GUILayout.Space(8); 184 | } 185 | EditorGUILayout.EndVertical(); 186 | } 187 | EditorGUILayout.EndHorizontal(); 188 | } 189 | EditorGUILayout.EndVertical(); 190 | } 191 | 192 | GUILayout.Space(16); 193 | } 194 | EditorGUILayout.EndScrollView(); 195 | } 196 | 197 | bool IsCrititalException(Exception e) 198 | { 199 | if (e is ExitGUIException) 200 | return false; 201 | if (e != null && e.InnerException is ExitGUIException) 202 | return false; 203 | 204 | return true; 205 | } 206 | 207 | void DrawToolbar() 208 | { 209 | EditorGUILayout.BeginHorizontal(); 210 | { 211 | var newLock = GUILayout.Toggle(m_Locked, "Lock", EditorStyles.toolbarButton, GUILayout.Width(50)); 212 | if (newLock != m_Locked) 213 | { 214 | m_Locked = newLock; 215 | OnSelectionChange(); 216 | GUIUtility.ExitGUI(); 217 | return; 218 | } 219 | 220 | if (GUILayout.Button("Static...", EditorStyles.toolbarDropDown, GUILayout.Width(60))) 221 | { 222 | ShowStaticMethodPopup(); 223 | GUIUtility.ExitGUI(); 224 | return; 225 | } 226 | 227 | GUILayout.Space(1); 228 | GUILayout.FlexibleSpace(); 229 | 230 | // Draw button to add a new play mode inspector window 231 | if (GUILayout.Button(new GUIContent("+", "Add PlayMode Inspector"), EditorStyles.toolbarButton, GUILayout.Width(24))) 232 | { 233 | var wnd = EditorWindow.CreateInstance(); 234 | if (wnd != null) 235 | { 236 | wnd.Show(); 237 | wnd.OnSelectionChange(); 238 | } 239 | } 240 | GUILayout.Space(1); 241 | 242 | GUILayout.Box("", EditorStyles.toolbar, GUILayout.ExpandWidth(true)); 243 | } 244 | EditorGUILayout.EndHorizontal(); 245 | 246 | // Draws a separator line as found in all Unity toolbars. 247 | // TODO: There must be an existing style for it, right? 248 | var r = GUILayoutUtility.GetRect(0, 1, GUILayout.ExpandWidth(true)); 249 | r.y -= 1; 250 | var c = GUI.color; 251 | GUI.color = new Color(0, 0, 0, EditorGUIUtility.isProSkin ? 0.3f : 0.2f); 252 | GUI.DrawTexture(r, EditorGUIUtility.whiteTexture, ScaleMode.StretchToFill); 253 | GUI.color = c; 254 | } 255 | 256 | void ShowStaticMethodPopup() 257 | { 258 | var menu = new GenericMenu(); 259 | var entries = new List(); 260 | 261 | // Find all static methods that are decorated with the PlayModeInspectorMethod attribute. 262 | foreach (var method in TypeCache.GetMethodsWithAttribute()) 263 | { 264 | if (!method.IsStatic) 265 | continue; 266 | 267 | if (method.DeclaringType.IsGenericType) 268 | continue; 269 | 270 | StaticMethodEntry.TryCreate(method, entries); 271 | } 272 | 273 | // Sort methods, so they appear in a stable order in the menu. 274 | entries.Sort(delegate (AbstractEntry x, AbstractEntry y) 275 | { 276 | return x.title.text.CompareTo(y.title.text); 277 | }); 278 | 279 | // Add each method to the context-menu. 280 | foreach (var entry in entries) 281 | { 282 | var title = entry.title; 283 | 284 | menu.AddItem(title, false, 285 | delegate (object userData) 286 | { 287 | // If the item is selected, assign it to be inspected. 288 | m_Entries = new List(); 289 | m_Entries.Add(entry); 290 | }, 291 | entry); 292 | } 293 | 294 | menu.ShowAsContext(); 295 | } 296 | 297 | // Draws a titlebar with a particular method entry. 298 | // I tried to mimic the look&feel of the regular Unity Inspector titlebar. 299 | bool DrawTitlebar(AbstractEntry entry) 300 | { 301 | GUIStyle inspectorTitlebar = "IN Title"; 302 | 303 | var editorPrefsKey = $"PlayModeInspector.{entry.editorPrefKey}.expanded"; 304 | var isExpanded = EditorPrefs.GetBool(editorPrefsKey, true); 305 | 306 | var r = GUILayoutUtility.GetRect(0, 22, GUILayout.ExpandWidth(true)); 307 | r.y -= 1; 308 | 309 | var rtitlebar = r; 310 | GUI.Box(rtitlebar, "", inspectorTitlebar); 311 | 312 | var e = Event.current; 313 | if (e != null) 314 | { 315 | if (e.type == EventType.MouseDown && e.button == 0 && rtitlebar.Contains(e.mousePosition)) 316 | { 317 | EditorPrefs.SetBool(editorPrefsKey, !isExpanded); 318 | e.Use(); 319 | } 320 | } 321 | 322 | var rfoldout = r; 323 | rfoldout.x += 4; rfoldout.width = 16; 324 | GUI.Toggle(rfoldout, isExpanded, "", EditorStyles.foldout); 325 | 326 | var title = entry.title; 327 | 328 | var ricon = r; 329 | ricon.x += 20; ricon.y += 3; ricon.width = 16; ricon.height = 16; 330 | if (title.image != null) 331 | GUI.DrawTexture(ricon, AssetPreview.GetMiniThumbnail(title.image)); 332 | 333 | var rlabel = r; 334 | rlabel.x += 38; 335 | GUI.Button(rlabel, title.text, EditorStyles.boldLabel); 336 | 337 | return isExpanded; 338 | } 339 | 340 | abstract class AbstractEntry 341 | { 342 | public GUIContent title 343 | { 344 | get 345 | { 346 | string title; 347 | 348 | var attribute = m_Method.GetCustomAttribute(true); 349 | if (attribute != null && !string.IsNullOrEmpty(attribute.displayName)) 350 | title = attribute.displayName; 351 | else 352 | title = $"{m_Method.DeclaringType.Name}.{m_Method.Name}"; 353 | 354 | if (m_Object != null) 355 | title += $" ({m_Object.name})"; 356 | 357 | GUIContent content = new GUIContent(title); 358 | 359 | if (m_Object != null) 360 | content.image = AssetPreview.GetMiniThumbnail(m_Object); 361 | else 362 | content.image = AssetPreview.GetMiniTypeThumbnail(typeof(MonoScript)); 363 | 364 | return content; 365 | } 366 | } 367 | 368 | public string editorPrefKey 369 | { 370 | get 371 | { 372 | return $"{m_Method.DeclaringType.Name}.{m_Method.Name}"; 373 | } 374 | } 375 | 376 | protected UnityEngine.Object m_Object; 377 | protected MethodInfo m_Method; 378 | 379 | abstract public void Invoke(); 380 | 381 | static protected bool IsMethodValid(MethodInfo method) 382 | { 383 | if (method == null) 384 | return false; 385 | 386 | // Accept parameterless method only. 387 | var parameters = method.GetParameters(); 388 | if (parameters == null || parameters.Length != 0) 389 | return false; 390 | 391 | // Accept void return type only. 392 | if (method.ReturnType != typeof(void)) 393 | return false; 394 | 395 | return true; 396 | } 397 | } 398 | 399 | 400 | class UnityEngineObjectEntry : AbstractEntry 401 | { 402 | public static bool TryCreate(UnityEngine.Object o, List target) 403 | { 404 | if (o == null) 405 | return false; 406 | 407 | var ignore = new List(); 408 | var loopguard = 0; 409 | var type = o.GetType(); 410 | while (type != null) 411 | { 412 | foreach (var method in type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) 413 | { 414 | // ignore base methods that have been overridden 415 | if (ignore.Contains(method)) 416 | continue; 417 | var baseDef = method.GetBaseDefinition(); 418 | if (baseDef != null) 419 | ignore.Add(baseDef); 420 | 421 | var attr = method.GetCustomAttribute(); 422 | if (attr != null && IsMethodValid(method)) 423 | { 424 | target.Add(new UnityEngineObjectEntry(o, method)); 425 | } 426 | } 427 | 428 | type = type.BaseType; 429 | if (loopguard++ > 32) 430 | break; // 32 levels of inheritance? just give up... 431 | } 432 | 433 | return true; 434 | } 435 | 436 | UnityEngineObjectEntry(UnityEngine.Object o, MethodInfo method) 437 | { 438 | m_Object = o; 439 | m_Method = method; 440 | } 441 | 442 | public override void Invoke() 443 | { 444 | if (m_Method != null) 445 | m_Method.Invoke(m_Object, null); 446 | } 447 | } 448 | 449 | class StaticMethodEntry : AbstractEntry 450 | { 451 | public static bool TryCreate(MethodInfo method, List target) 452 | { 453 | if (method == null) 454 | return false; 455 | 456 | if (!method.IsStatic) 457 | return false; 458 | 459 | if (!IsMethodValid(method)) 460 | return false; 461 | 462 | target.Add(new StaticMethodEntry(method)); 463 | return true; 464 | } 465 | 466 | public StaticMethodEntry(MethodInfo o) 467 | { 468 | m_Method = o; 469 | } 470 | 471 | public override void Invoke() 472 | { 473 | m_Method.Invoke(null, null); 474 | } 475 | } 476 | } 477 | } 478 | -------------------------------------------------------------------------------- /Editor/PlayModeInspector.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b30d84974e0d2014aabcd688d7deeb35 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Unity.PlayModeInspector.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Unity.PlayModeInspector.Editor", 3 | "references": [ 4 | "GUID:e028a5ff6e66c3948862cb135cce531c" 5 | ], 6 | "includePlatforms": [ 7 | "Editor" 8 | ], 9 | "excludePlatforms": [], 10 | "allowUnsafeCode": false, 11 | "overrideReferences": false, 12 | "precompiledReferences": [], 13 | "autoReferenced": true, 14 | "defineConstraints": [], 15 | "versionDefines": [], 16 | "noEngineReferences": false 17 | } -------------------------------------------------------------------------------- /Editor/Unity.PlayModeInspector.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 05208aff00ed17e4aa42d5fd8bf27b3c 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015-2024 Peter Schraut (http://www.console-dev.de) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0b57b3a07f7ce044ca01557cf0f5335c 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PlayMode Inspector for Unity 2 | 3 | PlayMode Inspector allows you to draw EditorGUI from inside your MonoBehaviour to a dedicated editor window: The PlayMode Inspector window. 4 | 5 | When I write program code, such as new MonoBehaviour, I often want to visualize part of its internal state. 6 | 7 | This helps me to understand what it's really doing, thus allows me to iterate on features and fix bugs faster than not knowing. 8 | 9 | Such internal state is mostly interesting for developers. I don't want to expose it to the regular Unity Inspector, where everybody working on the project gets then annoyed and perhaps overwhelmed by it. 10 | 11 | I don't want to write a custom Editor for every MonoBehaviour. 12 | 13 | I don't want to create or use an overly complex Inspector system and specify how to visualize certain things using a gazillion interconnected attributes. 14 | 15 | For me, it's much simpler if I can just write EditorGUI code directly inside the MonoBehaviour with an `#if UNITY_EDITOR #endif` block around. 16 | 17 | Writing the EditorGUI code inside the MonoBehaviour allows me to access private and non-serialized variables. Internal state most of the time is just that. 18 | 19 | I came up with the PlayMode Inspector idea in 2015 and it proved to be a very useful tool all the time. I hope you find it equally useful. 20 | 21 | 22 | # Installation 23 | 24 | In Unity's Package Manager, choose "Add package from git URL" and insert one of the Package URL's you can find below. 25 | 26 | ## Package URL's 27 | I recommend to right-click the URL below and choose "Copy Link" rather than selecting the text and copying it, because sometimes it copies a space at the end and the Unity Package Manager can't handle it and spits out an error when you try to add the package. 28 | 29 | Please see the ```CHANGELOG.md``` file to see what's changed in each version. 30 | 31 | | Version | Link | 32 | |----------|---------------| 33 | | 1.6.0 | https://github.com/pschraut/UnityPlayModeInspector.git#1.6.0 | 34 | | 1.5.0 | https://github.com/pschraut/UnityPlayModeInspector.git#1.5.0 | 35 | | 1.4.0 | https://github.com/pschraut/UnityPlayModeInspector.git#1.4.0 | 36 | | 1.3.0 | https://github.com/pschraut/UnityPlayModeInspector.git#1.3.0 | 37 | | 1.2.0 | https://github.com/pschraut/UnityPlayModeInspector.git#1.2.0 | 38 | | 1.1.0 | https://github.com/pschraut/UnityPlayModeInspector.git#1.1.0 | 39 | | 1.0.0 | https://github.com/pschraut/UnityPlayModeInspector.git#1.0.0 | 40 | 41 | # Credits 42 | 43 | If you find this package useful, please mention my name in your credits screen. 44 | Something like "PlayMode Inspector by Peter Schraut" or "Thanks to Peter Schraut" would be very much appreciated. 45 | 46 | 47 | # How it works 48 | 49 | When you open the PlayMode Inspector from Unity's main menu "Window > Analysis > PlayMode Inspector", you are presented with a rather empty window that has no functionality yet. 50 | 51 | ![alt text](Documentation~/images/window.png "Screenshot") 52 | 53 | In order for it to show something, you need to implement a parameterless method either in your MonoBehaviour , ScriptableObject or a static method in any type. 54 | 55 | You then decorate this method with the `[PlayModeInspectorMethod]` attribute. 56 | ```csharp 57 | [Oddworm.Framework.PlayModeInspectorMethod] 58 | void MyPlayModeInspectorMethod() 59 | { 60 | } 61 | ``` 62 | You can have as many `[PlayModeInspectorMethod]` methods in the same class as you want. 63 | 64 | Since this is supposed to be used in the Unity editor only and actually uses editor-only functionality from the `UnityEditor` namespace, I put the code inside an `#if UNITY_EDITOR #endif` block as shown below. 65 | 66 | ```csharp 67 | [Oddworm.Framework.PlayModeInspectorMethod] 68 | void MyPlayModeInspectorMethod() 69 | { 70 | #if UNITY_EDITOR 71 | UnityEditor.EditorGUILayout.HelpBox("Hello World", UnityEditor.MessageType.Info); 72 | #endif 73 | } 74 | ``` 75 | 76 | If you then select the GameObject that contains a MonoBehaviour with the above method, it ends up in PlayMode Inspector as shown below. 77 | 78 | ![alt text](Documentation~/images/example_helloworld.png "Screenshot") 79 | 80 | `[PlayModeInspectorMethod]` methods, while they're shown in the PlayMode Inspector window, are called periodically. More precisely, they are called 10 times per second. 81 | 82 | # Examples 83 | 84 | Below you can find a few examples how I use PlayMode Inspector. 85 | 86 | Along with every example, I provide further information about the context and describe why I implemented it that way, to give you an idea how and where you could use it. 87 | 88 | ## Example - Hello World 89 | 90 | Let's start with a Hello World example first. 91 | 92 | If you decorate a method with the `[PlayModeInspectorMethod]`, it instructs the PlayMode Inspector window to call that method periodically (draw the EditorGUI), when you select the GameObject with that particular MonoBehaviour. 93 | 94 | ![alt text](Documentation~/images/example_helloworld.png "Screenshot") 95 | 96 | ```csharp 97 | using UnityEngine; 98 | 99 | public class TestScript : MonoBehaviour 100 | { 101 | [Oddworm.Framework.PlayModeInspectorMethod] 102 | void MyPlayModeInspectorMethod() 103 | { 104 | #if UNITY_EDITOR 105 | UnityEditor.EditorGUILayout.HelpBox("Hello World", UnityEditor.MessageType.Info); 106 | #endif 107 | } 108 | } 109 | ``` 110 | 111 | 112 | ## Example - Instance Method 113 | The following code snippet is a real-world application of PlayMode Inspector. It shows how to expose an instance method to PlayMode Inspector. 114 | 115 | It's part of an AwardManager class and displays the progress of each Award. It provides buttons to advance the progress of an Award, for testing purposes. This allows me to test whether an Award unlocks, without actually playing the game. It's a great way to test a first implementation and to cheat during development. 116 | 117 | ![alt text](Documentation~/images/example_awardmanager.png "Screenshot") 118 | 119 | ```csharp 120 | [Oddworm.Framework.PlayModeInspectorMethod] 121 | void PlayModeInspectorMethod() 122 | { 123 | #if UNITY_EDITOR 124 | // Draw a progressbar for each award and cheat buttons to advance the award progress. 125 | for (var n = 0; n < GetCount(); ++n) 126 | { 127 | UnityEditor.EditorGUILayout.BeginHorizontal(); 128 | 129 | // Get a reference to the award 130 | var award = GetAward(n); 131 | 132 | // Draw the reference to the Award 133 | UnityEditor.EditorGUILayout.ObjectField(award, typeof(Award), false); 134 | 135 | // Draw the award progress 136 | UnityEditor.EditorGUI.ProgressBar(GUILayoutUtility.GetRect(100, GUI.skin.button.CalcHeight(new GUIContent("Wg"), 100)), 137 | award.NormalizedProgress, 138 | string.Format("{0} / {1} ({2:F2}%)", award.Progress, award.TriggerValue, award.NormalizedProgress)); 139 | 140 | // Draw button to trigger award progression 1 time 141 | if (GUILayout.Button("+1", GUILayout.Width(30))) 142 | { 143 | Award.Editor.Trigger(award); 144 | } 145 | 146 | // Draw button to trigger award progression 5 times 147 | if (GUILayout.Button("+5", GUILayout.Width(30))) 148 | { 149 | for (var j = 0; j < 5; ++j) 150 | Award.Editor.Trigger(award); 151 | } 152 | 153 | // Draw button to trigger award progression 10 times 154 | if (GUILayout.Button("+10", GUILayout.Width(35))) 155 | { 156 | for (var j = 0; j < 10; ++j) 157 | Award.Editor.Trigger(award); 158 | } 159 | 160 | UnityEditor.EditorGUILayout.EndHorizontal(); 161 | GUILayout.Space(2); 162 | } 163 | #endif // UNITY_EDITOR 164 | } 165 | ``` 166 | 167 | 168 | 169 | ## Example - Static Method 170 | 171 | The following code snippet is a real-world application of PlayMode Inspector. It shows how to expose a static method to PlayMode Inspector. 172 | 173 | The Player (Hero) class in that particular game writes many of its attributes once per frame to a global static class named HeroPerFrame. The silent agreement with that HeroPerFrame class is that only the Hero code writes to it, all other code reads from it only. 174 | 175 | For example: The Hero writes its current Health values to this class, once per frame. Other code, such as the HUD, read from it. It's a very simple one-way data exchange approach. I like to keep things simple. 176 | 177 | The PlayMode Inspector method in HeroPerFrame then displays its variables, the actual Hero attributes. In this case, it not only displays them, but even allows to change them! 178 | 179 | This "pattern" allows me to test the HUD code, that drives the UI, without having to play the game. This works, because if the Hero class that is normally responsible to write to HeroPerFrame doesn't exist, only the PlayMode Inspector method is actually modifying the variables. 180 | 181 | Other code, such as the HUD, only read those values and don't care from where they come. 182 | 183 | This allows me to only load the HUD scene without all the other game around, press Play and modify via the PlayMode Inspector the HeroPerFrame variables and the HUD updates accordingly, like the game would normally run. 184 | 185 | I can test how the HUD code drives the UI, when I change the Health, Stamina, etc values without having to play. This made it really compfortable for me to implement and iterate on a lot of HUD code. 186 | 187 | ![alt text](Documentation~/images/example_heroperframe.png "Screenshot") 188 | 189 | ```csharp 190 | using UnityEngine; 191 | 192 | // Hero class writes to HeroPerFrame, all other classes must read from it only. 193 | // This is to keep data exchange very simple. 194 | public static class HeroPerFrame 195 | { 196 | public static int Health; 197 | public static int HealthMaximum; 198 | public static int OvercharedMaximumHealth; 199 | 200 | public static int Armor; 201 | public static int ArmorMaximum; 202 | public static int OvercharedMaximumArmor; 203 | 204 | public static int Stamina; 205 | public static int StaminaMaximum; 206 | public static bool IsExhausted; 207 | 208 | // ... and many many more attributes 209 | 210 | [Oddworm.Framework.PlayModeInspectorMethod] 211 | static void PlayModeInspectorMethod() 212 | { 213 | #if UNITY_EDITOR 214 | UnityEditor.EditorGUILayout.LabelField("Health", UnityEditor.EditorStyles.boldLabel); 215 | UnityEditor.EditorGUI.indentLevel++; 216 | Health = UnityEditor.EditorGUILayout.IntSlider("Health", Health, 0, OvercharedMaximumHealth); 217 | HealthMaximum = UnityEditor.EditorGUILayout.IntSlider("Maximum", HealthMaximum, 0, 100); 218 | OvercharedMaximumHealth = UnityEditor.EditorGUILayout.IntSlider("Overchared Maximum", OvercharedMaximumHealth, 0, 200); 219 | UnityEditor.EditorGUI.indentLevel--; 220 | 221 | GUILayout.Space(4); 222 | UnityEditor.EditorGUILayout.LabelField("Armor", UnityEditor.EditorStyles.boldLabel); 223 | UnityEditor.EditorGUI.indentLevel++; 224 | Armor = UnityEditor.EditorGUILayout.IntSlider("Armor", Armor, 0, OvercharedMaximumArmor); 225 | ArmorMaximum = UnityEditor.EditorGUILayout.IntSlider("Maximum", ArmorMaximum, 0, 100); 226 | OvercharedMaximumArmor = UnityEditor.EditorGUILayout.IntSlider("Overchared Maximum", OvercharedMaximumArmor, 0, 200); 227 | UnityEditor.EditorGUI.indentLevel--; 228 | 229 | GUILayout.Space(4); 230 | UnityEditor.EditorGUILayout.LabelField("Stamina", UnityEditor.EditorStyles.boldLabel); 231 | UnityEditor.EditorGUI.indentLevel++; 232 | Stamina = UnityEditor.EditorGUILayout.IntSlider("Stamina", Stamina, 0, StaminaMaximum); 233 | StaminaMaximum = UnityEditor.EditorGUILayout.IntSlider("Maximum", StaminaMaximum, 0, 100); 234 | IsExhausted = UnityEditor.EditorGUILayout.Toggle("IsExhausted", IsExhausted); 235 | UnityEditor.EditorGUI.indentLevel--; 236 | #endif 237 | } 238 | } 239 | ``` 240 | 241 | 242 | ## Example - Custom PlayModeInspectorMethod Attribute 243 | 244 | You can implement your own `PlayModeInspectorMethodAttribute` and use your implementation throughout your code. The advantage of doing it this way is if you remove PlayMode Inspector from the project, you need to do minimal code changes only for it to still compile. In this case you would only need to change your own attribute and the project would still compile without PlayMode Inspector in the project! 245 | 246 | 247 | ```csharp 248 | public class CustomPlayModeInspectorMethodAttribute : Oddworm.Framework.PlayModeInspectorMethodAttribute 249 | { 250 | } 251 | 252 | [CustomPlayModeInspectorMethod] 253 | void MyCustomPlayModeInspectorMethod() 254 | { 255 | #if UNITY_EDITOR 256 | UnityEditor.EditorGUILayout.HelpBox("Using a custom attribute.", UnityEditor.MessageType.Info); 257 | #endif 258 | } 259 | ``` 260 | 261 | 262 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d9ba5029f1993e24498455480cebd72a 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ead998f3655d4554e97c149c599b464b 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/PlayModeInspector.cs: -------------------------------------------------------------------------------- 1 | // 2 | // PlayMode Inspector for Unity. Copyright (c) 2015-2024 Peter Schraut (www.console-dev.de). See LICENSE.md 3 | // https://github.com/pschraut/UnityPlayModeInspector 4 | // 5 | using System; 6 | using UnityEngine; 7 | 8 | namespace Oddworm.Framework 9 | { 10 | /// 11 | /// Use this attribute to expose a method to the PlayMode Inspector window. 12 | /// 13 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] 14 | public class PlayModeInspectorMethodAttribute : Attribute 15 | { 16 | /// 17 | /// The display name in the PlayMode Inspector titlebar. 18 | /// It displays "TypeName.MethodName" by default, the displayName property allows you to override it. 19 | /// 20 | public string displayName 21 | { 22 | get; 23 | set; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Runtime/PlayModeInspector.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 035fb249e8f32bf4c86c16357f4287ba 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Unity.PlayModeInspector.Runtime.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Unity.PlayModeInspector.Runtime" 3 | } 4 | -------------------------------------------------------------------------------- /Runtime/Unity.PlayModeInspector.Runtime.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e028a5ff6e66c3948862cb135cce531c 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.oddworm.playmodeinspector", 3 | "version": "1.6.0", 4 | "displayName": "PlayMode Inspector", 5 | "description": "PlayMode Inspector allows you to write EditorGUI code inside your MonoBehaviour classes to display it inside the dedicated PlayMode Inspector window.\n\nOpen PlayMode Inspector from the Unity main menu under 'Window > Analysis > PlayMode Inspector.'", 6 | "unity": "2019.3", 7 | "documentationUrl": "https://github.com/pschraut/UnityPlayModeInspector", 8 | "dependencies": { 9 | }, 10 | "keywords": [ 11 | "unity", 12 | "debug", 13 | "analysis" 14 | ], 15 | "author": { 16 | "name": "Peter Schraut", 17 | "url": "http://console-dev.de" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/pschraut/UnityPlayModeInspector.git" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3617cc774a90bce41ab224479fa76072 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------