Gui work
some Gui stuff to make things easier for Lift
overview
Most of my GUI work is for Lift: The Last Days of the Westwind. As the lead VR developer, there have been many moments where I am sick of making certain changes based on Input.Keycode; it makes no sense for scalability if I have to go into Visual Studio every time I want to change what memory we want to update to test our narrative branching. So! I made some fun lil tools to help myself and our other devs/designers.
Loading/Test tools
We have a couple of developers who don’t have headsets at the ready, or are too lazy to whip them out and use them (guilty). To help ease guilt while keeping productivity up, I wanted to make an easy to use EditorWindow to manually load in conversations at specific points so that testing is a ~~breeze.
This tool is definitely a work in progress, as I want to make it automatically validate the input and to make sure it is correct.
Below, I show off my initial thoughts when realizing I could make some bomb GUI tools to help us. Obviously dirty, I went a little nuts thinking of all the possibilities. Some of the initial ideas stayed, but some changed (aka using toggles for Nod/Shake events).
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; /// <summary> /// Editor Window that will help us test our systems. Emphasis on non-VR tests and memory updating /// /// Goal is to be able to go to any conversation, and a specific node within that convo, with less than a couple of clicks. /// /// By: Alisia Martinez, July 2020 /// </summary> public class LoadingTestTools : EditorWindow { // Variables for string/int fields in window string conversationLoad = ""; string memoryVar = ""; int memoryVal; string itemToLoad = ""; string itemPosition = ""; string FSMNodeLocation = ""; // To help with scrolling Vector2 scrollPos; // TODO: I want to get a reference to the window to use when scaling w width/height, // not sure how to do that LoadingTestTools window;// = new LoadingTestTools(); [MenuItem("TESTING/TESTING:Loading")] static void Init() { GetWindow(typeof(LoadingTestTools), false, "TESTING:Loading"); } void OnGUI() { /////////////////////// Start Vertical with correct scrolling functionality ///// NOTE: Do not use window.postion.width, since it thinks you want to focus hard on the window /// // TODO: The goal here is to ideally center all items in this vertical in the middle EditorGUILayout.BeginVertical(GUILayout.Width(position.width), GUILayout.Height(position.height)); scrollPos = EditorGUILayout.BeginScrollView(scrollPos,true, true); EditorGUILayout.TextField("I made these so life will be easier, and to learn, xoxo", EditorStyles.helpBox, GUILayout.MaxWidth(300)); ///////////// Patron Conversation Loading /// // TODO: Ideally won't allow button press unless string field is active // TODO: Default Conversation Node text is based off default node from Patron Manager (first node in loadData) EditorGUILayout.LabelField("Manual Conversation Load", EditorStyles.boldLabel); conversationLoad = EditorGUILayout.TextField("Conversation Node: ", conversationLoad, GUILayout.MaxWidth(300)); if (GUILayout.Button("Load Conversation",GUILayout.MaxWidth(300))) { PatronManager.Instance.nodeToLoad = conversationLoad; PatronManager.Instance.ReadyToSpawn = true; } EditorGUILayout.Separator(); ///////////// Memory Setting /// EditorGUILayout.LabelField("Manual Memory Set", EditorStyles.boldLabel); memoryVar = EditorGUILayout.TextField("Memory Key ", memoryVar, GUILayout.MaxWidth(300)); memoryVal = EditorGUILayout.IntField("Memory Value ", memoryVal, GUILayout.MinWidth(50), GUILayout.MaxWidth(200)); if (GUILayout.Button("Set Memory", GUILayout.MaxWidth(300))) { PatronMemory.updatePatronMemory(memoryVar, memoryVal); // maybe we can set these strings as enterables Debug.Log("Updated memory variable " + memoryVar + " to value of "+ PatronMemory.fetchPatronMemory(memoryVar)); } EditorGUILayout.Separator(); ///////////// FSM TOOLS!! Now this is a big one, still under construction /// EditorGUILayout.LabelField("FSM Tools", EditorStyles.boldLabel); //////// Broadcasted Events for PlayMakerFSM Testing /// EditorGUILayout.LabelField("Broadcasted Events", EditorStyles.miniBoldLabel); /// DOOR STATE /// if door open var = true, then set to false, vice versa // TODO: Honestly, this is dirty, I know it is, let's do it a different way // Toggles aren't the right method I don't think... if (GUILayout.Button("Door open/closed", GUILayout.MaxWidth(300))) { if (GameManager.Instance.doorOpen) { Debug.Log("TOOLS: Setting door to close"); GameManager.Instance.doorOpen = false; } else { Debug.Log("TOOLS: Setting door to open"); GameManager.Instance.doorOpen = true; } } /// YES/NO gesture broadcast if (GUILayout.Button("Yes/Nod Broadcast", GUILayout.MaxWidth(300))) { Debug.Log("Sending Yes/Nod event"); PlayMakerFSM.BroadcastEvent("Interaction(nod)"); } if (GUILayout.Button("No/Shake Broadcast", GUILayout.MaxWidth(300))) { Debug.Log("Sending No/Shake event"); PlayMakerFSM.BroadcastEvent("Interaction(shakeHead)"); } EditorGUILayout.Separator(); //////// FSM Jump to Node Functionality /// Still under construction, but this ish is gonna be dope /// // TODO: Get active patron, look at which FSM is enabled, then set start node to input EditorGUILayout.LabelField("FSM Node Jump", EditorStyles.miniBoldLabel); FSMNodeLocation = EditorGUILayout.TextField("FSM Node Name ", FSMNodeLocation, GUILayout.MaxWidth(300)); if (GUILayout.Button("Set Start Node", GUILayout.MaxWidth(300))) { string currentFSM = PatronManager.Instance.currentPatron.gameObject.GetComponent<PatronFunctions>().returnActivatedFSM(); Debug.Log("Tried to set FSM start to " + FSMNodeLocation); } EditorGUILayout.Separator(); EditorGUILayout.Separator(); EditorGUILayout.Separator(); EditorGUILayout.Separator(); ///////////// Item loading, still under construction, would ideally be next to patron stuff /// TODO: Honestly, let's put this in Foldout, it doesnt need to be on constant display /// EditorGUILayout.LabelField("Manual Item Load - IN PROGRESS", EditorStyles.boldLabel); itemToLoad = EditorGUILayout.TextField("Item to Load ", itemToLoad, GUILayout.MaxWidth(300)); itemPosition = EditorGUILayout.TextField("Position to Load ", itemPosition, GUILayout.MaxWidth(300)); if (GUILayout.Button("Load Item at Postion- NOT WORKING", GUILayout.MaxWidth(300))) { Debug.Log("Item loading not implemented yet"); } EditorGUILayout.Separator(); EditorGUILayout.EndScrollView(); EditorGUILayout.EndVertical(); } }
Conversation Node / Patron editor visualization
Since we have a lot of custom classes with a lot of useful information, it would benefit us to see them in inspector! This is firstly important to check that the information is being correctly parsed, but also a good way for us to see the path of the nodes, without having to delve into our dirty JSON file that has all of our node data. Anything to stop me from sifting from that is good.
There are a couple of custom classes that need to be serialized so that we can see them:
Conversation Node
List<Patrons>
Patron
Items
List<NextNodes>
NextNodes
While this is a lot already, each of these items has useful information within. For example, in my NextNodes, there can be conditionals that need to be True/False in order for the Loader to know that is the next conversation we want to go to. (EX: Boss1 can go to Boss2 or Boss3, but can go to Boss3 if and only if VAR-PLAYER-HAS-HAT = 1. If not true, then goes to default path of Boss2).
This will get more and more complex the more content we have, so here is my attempt at cleaning it up!
Some more initial thoughts on how I want this information displayed. So many drop down menus! We love hiding information until it is relevant for the user.
Default view in editor - Not in Play mode
View on Play when load data is parsed into Conversation Nodes
View on Play when Conversation Node is loaded.
- Next node is calculated based on memory values
- Patrons/NextNode lists are populated
Conversation Node Drawer.cs
using UnityEditor; using UnityEngine; using UnityEditorInternal; /// <summary> /// Using code inspired from 25games Custom Property Drawer Tutorial (http://25games.net/custom-property-drawers/) /// /// Otherwise, all by Alisia Martinez, July 2020 /// </summary> [CustomPropertyDrawer(typeof(ConversationNode))] public class ConversationNodeDrawer : PropertyDrawer { float lineHeight; float lineHeightSpace; public bool showPosition = true; // Draw the property inside the given rect public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { // Using BeginProperty / EndProperty on the parent property means that // prefab override logic works on the entire property. EditorGUI.BeginProperty(position, label, property); // Precalculate some height values float heightHalf = (position.height - 20) * 0.5f; float heightOneFourth = (position.height - 20) * 0.25f; // Draw the label and calculate the new position // This time the label should be placed a bit lower. // That's why you need to adjust the shift again after drawing the label. Rect labelRect = new Rect(position.x, position.y + heightOneFourth, position.width, position.height); position = EditorGUI.PrefixLabel(labelRect, GUIUtility.GetControlID(FocusType.Passive), label); position.y -= heightOneFourth; // Don't make child fields be indented var indent = EditorGUI.indentLevel; EditorGUI.indentLevel = 0; // Calculate positions and dimensions for the GUI elements Rect IDrect = new Rect(position.x, position.y, position.width*.5f, heightHalf); Rect spawnRect = new Rect(position.x + position.width * .5f, position.y, position.width * .25f, heightHalf - 2.5f); Rect destRect = new Rect(position.x + position.width * .5f + position.width * .25f, position.y, position.width * .25f, heightHalf - 2.5f); Rect patronListRect = new Rect(position.x + 40, position.y + heightHalf + 2.5f, position.width - 25, heightHalf); Rect nodeListRect = new Rect(position.x + 40, position.y + heightHalf*2f + 2.5f, position.width - 25, heightHalf); // Get properties by exactly passing the names of the interval's attributes SerializedProperty nodeID = property.FindPropertyRelative("uniqueNodeID"); SerializedProperty patronList = property.FindPropertyRelative("patrons"); SerializedProperty nextNodeList = property.FindPropertyRelative("nextNodes"); SerializedProperty spawn = property.FindPropertyRelative("floorSpawn"); SerializedProperty destination = property.FindPropertyRelative("floorDestination"); EditorGUI.PropertyField(IDrect, nodeID, GUIContent.none); EditorGUI.PropertyField(spawnRect, spawn, GUIContent.none); EditorGUI.PropertyField(destRect, destination, GUIContent.none); // TODO: The values inside this List for a custom class do not show up // Do I repaint? EditorGUI.PropertyField(patronListRect, patronList, includeChildren:true); EditorGUI.PropertyField(nodeListRect, nextNodeList, includeChildren:true); // Set indent back to what it was EditorGUI.indentLevel = indent; EditorGUI.EndProperty(); } public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { // Height is two times the standard height plus 20 pixels return base.GetPropertyHeight(property, label) * 2 + 20; } }
Patron DRAWER.CS
using UnityEditor; using UnityEngine; /// <summary> /// Using code inspired from 25games Custom Property Drawer Tutorial (http://25games.net/custom-property-drawers/) /// /// Otherwise, all by Alisia Martinez, July 2020 /// </summary> [CustomPropertyDrawer(typeof(Patron))] public class PatronDrawer : PropertyDrawer { // Draw the property inside the given rect public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { // Using BeginProperty / EndProperty on the parent property means that // prefab override logic works on the entire property. EditorGUI.BeginProperty(position, label, property); // Precalculate some height values float heightHalf = (position.height - 20) * 0.5f; float heightOneFourth = (position.height - 20) * 0.25f; // Draw the label and calculate the new position // This time the label should be placed a bit lower. // That's why you need to adjust the shift again after drawing the label. Rect labelRect = new Rect(position.x, position.y + heightOneFourth, position.width, position.height); position = EditorGUI.PrefixLabel(labelRect, GUIUtility.GetControlID(FocusType.Passive), label); position.y -= heightOneFourth; // Don't make child fields be indented var indent = EditorGUI.indentLevel; EditorGUI.indentLevel = 0; // Calculate positions and dimensions for the GUI elements Rect nameRect = new Rect(position.x, position.y, position.width*.5f, heightHalf); Rect FSMrect = new Rect(position.x + position.width * .5f, position.y, position.width * .5f, heightHalf); Rect itemsRect = new Rect(position.x, position.y + heightHalf + 2.5f, position.width, heightHalf); // Get properties by exactly passing the names of the interval's attributes SerializedProperty patronName = property.FindPropertyRelative("name"); SerializedProperty patronFSM = property.FindPropertyRelative("FSM"); SerializedProperty itemsWorn = property.FindPropertyRelative("itemsWorn"); // TODO: This information isn't displaying properly, need to work out what it does on expand // EditorGUILayout.PropertyField(patronName,GUIContent.none,includeChildren:true, GUILayout.); // EditorGUI.PropertyField(nameRect, property.FindPropertyRelative("name"), GUIContent.none); // EditorGUI.PropertyField(FSMrect, property.FindPropertyRelative("FSM")); // EditorGUI.PropertyField(itemsRect, property.FindPropertyRelative("itemsWorn"), includeChildren:true); // Set indent back to what it was EditorGUI.indentLevel = indent; EditorGUI.EndProperty(); } }