Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Picking data from Json file in custom editor?

Discussion in 'Immediate Mode GUI (IMGUI)' started by Olipool, Dec 4, 2017.

  1. Olipool

    Olipool

    Joined:
    Feb 8, 2015
    Posts:
    320
    Hi all,

    I am fairly new to custom editors and stuff so maybe I am lacking the right terms for a successful search. That's why I'm reaching out to you :)
    I want to store game data for a football manager game in Json objects for easy modification later on.

    What I have:
    I have a class called CoachData which holds a List of Coach objects.
    I have a class called ClubData which holds a List of Club objects.
    Every Club has a field "coachName" which is a reference to the corresponding coach.name field in the Coach object. So it basically works like a foreign key in a database.

    I have custom editor windows for loading, editing and saving the CoachData and ClubData in serialized Json format. I want to have a greater number of coaches than clubs, so integrating the coach into the club as a struct or so won't work.

    The problem:
    When editing the Club I have to write the coachName carefully and be sure there is a Coach by that name so the Club can find the Coach object later on.

    What I need:
    I really would like the text field where I enter coachName to be some kind of object picker, which shows me a list of available coaches by parsing the Json for the CoachData so I can only assign an existing Coach to a Club.

    Is there any way to do that? The object picker could signal to the editor which Coach has been selected and I would then store the name in the Club.
    I read about custom editors, drawers, windows and stuff but need a pointer where to look because the terminology is a bit overwhelming atm :)

    Thanks for your time and advice!
     
  2. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,670
    Hi,

    Declare a List<string>, scan through your CoachData adding coachNames to the list, and then assign it to a string[] array. Use this array in EditorGUI.Popup (or GUILayout.Popup if you're using auto layout).

    You can do this once in OnEnable, and then update the list whenever the user edits CoachData.
     
  3. Olipool

    Olipool

    Joined:
    Feb 8, 2015
    Posts:
    320
    Hi, this sounds super cool, thanks for your help!

    If I understand it correctly, I would mark the field "coachName" as [HideInInspector] and instead I would put this Popup in my editor window and internally I would fill the field of my Club? Like:
    club.coachName=coaches[EditorGui.Popup(...)]

    Only thing I can't imagine right now, since my ClubData is a List of clubs, if I can generate that Popup for every single club in that list. But I will play around a bit like I said, Editors and OnGui and stuff is really new for me :)

    Thanks again!
     
  4. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,670
    Happy to help! Custom editors are great, but they take a while to get the hang of. Fortunately there are good tutorials out there. I recommend just getting it working, ugly code or not. Then rewrite it, this time with the benefit of experience.
     
  5. Olipool

    Olipool

    Joined:
    Feb 8, 2015
    Posts:
    320
    Will do, fascinating stuff and worth the effort I think. Thanks again! :) I checked you AssetStore tools, very nice! I really dig Love/Hate, maybe I will need it someday ;)
     
  6. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,670
    Thanks!
     
    Abouttt likes this.
  7. Olipool

    Olipool

    Joined:
    Feb 8, 2015
    Posts:
    320
    So I played around a bit and I think the editor does what it supposed to do. I go through my list and for every item/club I render a popup for picking a Coach which then sets the coachName property of the club. I used an array of indices, not sure if this is the way to go but as you said, make it work in the first place ;)
    After that I render the whole club, I should customize this a bit further so maybe the coachName field does not get shown etc. I also tried to implement custom drawers/editors for one Club but that did not work at all.
    This is my solution and the result so far:

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. using System.Collections;
    4. using System.IO;
    5. using System.Linq;
    6.  
    7. public class ClubDataEditor :  EditorWindow
    8. {
    9.  
    10.     public ClubData clubData;
    11.     public CoachData coachData;
    12.     public string[] coachNames;
    13.     private int[] index;
    14.  
    15.     private string clubDataProjectFilePath = "/StreamingAssets/clubs.json";
    16.     private string coachDataProjectFilePath = "/StreamingAssets/coaches.json";
    17.  
    18.     [MenuItem ("Window/Club Data Editor")]
    19.     static void Init()
    20.     {
    21.         EditorWindow.GetWindow (typeof(ClubDataEditor)).Show ();
    22.     }
    23.  
    24.     void OnGUI()
    25.     {
    26.         if (clubData != null)
    27.         {
    28.             SerializedObject serializedObject = new SerializedObject (this);
    29.             SerializedProperty serializedProperty = serializedObject.FindProperty ("clubData");
    30.             int idx = 0;
    31.             foreach (SerializedProperty clubProp in serializedProperty.FindPropertyRelative("clubs")) {
    32.                 index[idx] = EditorGUILayout.Popup (
    33.                     "Coach:",
    34.                     index[idx],
    35.                     coachNames);
    36.                 clubProp.FindPropertyRelative ("coachName").stringValue = coachNames [index[idx]];//EditorGUILayout.IntField (clubProp.FindPropertyRelative("reputation").intValue);
    37.                 EditorGUILayout.PropertyField( clubProp, true);
    38.                 idx++;
    39.             }
    40.  
    41.             serializedObject.ApplyModifiedProperties ();
    42.  
    43.             if (GUILayout.Button ("Save data"))
    44.             {
    45.                 SaveGameData();
    46.             }
    47.         }
    48.  
    49.         if (GUILayout.Button ("Load data"))
    50.         {
    51.             LoadGameData();
    52.         }
    53.     }
    54.  
    55.     private void LoadGameData()
    56.     {
    57.         string filePath = Application.dataPath + clubDataProjectFilePath;
    58.  
    59.         if (File.Exists (filePath)) {
    60.             string dataAsJson = File.ReadAllText (filePath);
    61.             clubData = JsonUtility.FromJson<ClubData> (dataAsJson);
    62.         } else
    63.         {
    64.             clubData = new ClubData();
    65.         }
    66.         index = new int[clubData.clubs.Count];
    67.  
    68.         filePath = Application.dataPath + coachDataProjectFilePath;
    69.  
    70.         if (File.Exists (filePath)) {
    71.             string dataAsJson = File.ReadAllText (filePath);
    72.             coachData = JsonUtility.FromJson<CoachData> (dataAsJson);
    73.         } else
    74.         {
    75.             coachData = new CoachData();
    76.         }
    77.         coachNames=coachData.coaches.Select(c=>c.name).ToArray();
    78.     }
    79.  
    80.     private void SaveGameData()
    81.     {
    82.  
    83.         string dataAsJson = JsonUtility.ToJson (clubData,true);
    84.  
    85.         string filePath = Application.dataPath + clubDataProjectFilePath;
    86.         File.WriteAllText (filePath, dataAsJson);
    87.  
    88.     }
    89. }
    coachDataEditor.png
     
  8. CDF

    CDF

    Joined:
    Sep 14, 2013
    Posts:
    1,306
    Is there a reason you need to use json? You could solve this by using ScriptableObjects then maybe add some import/export json buttons to their custom inspectors.

    e.g

    A ScriptableObject that defines a Coach
    A ScriptableObject that defines a Club

    "Club" object contains a "Coach" object field. Which you can assign in the editor. And because you're creating object references, changing coach names/data won't leave you will missing references. String based references are never a good thing.

    Some code:

    Code (CSharp):
    1. [CreateAssetMenu(fileName = "New Coach", menuName = "MyGame/Coach")]
    2. public class Coach : ScriptableObject {
    3.  
    4.     //add any properties to coach
    5. }
    6.  
    7. [CreateAssetMenu(fileName = "New Club", menuName = "MyGame/Club")]
    8. public class Club : ScriptableObject {
    9.  
    10.     public Coach coach;
    11.  
    12.     //any other club properties
    13. }
     
    Last edited: Dec 8, 2017
  9. Olipool

    Olipool

    Joined:
    Feb 8, 2015
    Posts:
    320
    Thanks for the input! At first, I was using SOs, not for one Club but for a class ClubData which contains essentially List<Coach>. By that, I had every Club in one file.

    Then I wanted to do object references so I would have to create one asset per Club or Coach in order to drag one Coach into one Club, which seemed a bit much work. Also, I would have to load all that data at startup so I would have to assign every Club to some List in the inspector (true? Or is it possible to load all SOs of one type into a List?)
    Then I also thought about readability and modding stuff so Json seemed the better choice, but yes I could load the data from Json into SOs...at the moment I'm a bit unsure about those serialized object references, Unity stores the instanceID for the object, but those ID may change (don't know exactly when), so the reference is also broken.

    And you are right about changing names of Coaches (bad thing to do!). Since I have a bit of database background, string references are not such a bad thing per se, normally one would use GUIDs and they are strings, too. Maybe choosing the name as a "primary key" is not the best choice and I will add a fixed ID field, but I figured, renaming a Coach would not be that common (yeah, maybe it is...:) )

    Thanks again!
     
  10. CDF

    CDF

    Joined:
    Sep 14, 2013
    Posts:
    1,306
    I guess it's a personal preference thing. I prefer ScriptableObjects rather than dealing with any old text coming in ;)
    You could always load the SO's from Resources. But anyway... One thing you might want to change in your Editor window is caching the SerializedObject, creating that every GUI event is probably eating up some resources as Unity needs to construct the layout of all that data.
     
  11. Olipool

    Olipool

    Joined:
    Feb 8, 2015
    Posts:
    320
    You are absolutely right regarding caching. Would it be possible to also cache
    every SerializableProperty (should I decide to redo the GUI completely) or is this overkill since they are only references? Would that break serilization? I don't know how Unity/C# handles this.
    Regarding the Json, my train of thought was: should I release the game I would not be able to use real club or coach names (due to silly license stuff). So it would be nice to have one single file with all clubs that users can edit in a text editor. Of course, I could provide an editor...but with SO I would have hundreds of .asset files. Sharing and installing those between users would be more difficult (hmm...ok not that much...). With Json I also could load the game data from the web and provide a web editor or something like that. Well, let's see if I finish that game first ;)

    Thanks!

    p.s. where would be the best way to cache? Init is static so that won't work, how about that:
    Code (CSharp):
    1. void OnGUI()
    2.     {
    3.         if (clubData != null)
    4.         {  
    5.             if (serializedObject == null) {
    6.                 serializedObject = new SerializedObject (this);
    7.             }
    And one last(!) question:
    If I select a Coach that is already assigned to another Club, I want to show a warning. What I tried in OnGui after detecting the issue:
    EditorUtility.DisplayDialog ("Warning", "Coach already in use", "Ok");

    This works fine...except I got no chance of closing that dialog again because OnGui fires too fast. Where would I put such Dialog calls?
     
    Last edited: Dec 9, 2017
  12. lavila

    lavila

    Joined:
    Aug 22, 2009
    Posts:
    7
    EditorUtility.DisplayDialog returns a bool. So you would want something like
    Code (CSharp):
    1. If(EditorUtility.DisplayDialog("warning", "in use", "ok", "cancel"){
    2. //ok code here
    3. }else{
    4. //cancel code here
    5. }