Forum rules - please read before posting.

Component variables on Unity UI objects

edited January 2022 in Technical Q&A

I've just realised that having component variables attached to Unity UI components is complicated. It seems particular tricky for menus because they are not even instantiated in the scene before they are turned on for the first time.

There are two main issues:

(a) The remember components only work when the game is saved while the objects are enabled.

(b) The variables can only be manipulated via actions when the gameobject (not just the menu that contains it) has been enabled at least once during gameplay - i.e. if I turn a menu on, then enable a gameobject with a variables component, and then disable it again, actions will be able to set the variable even with the object disabled; otherwise the action will not work.

I was wondering how feasible it would be to have:

(1) A single remember script on a separate gameobject that looked for every variable component in a UI hierarchy (I have lots of them) and saved them automatically.

(2) A way to instantiate the entire menu on start without turning it on, so that the variables could be manipulated even when it was off. This would have to take (b) into account.

Comments

  • To give you a better idea of my use case:

    I have created the functionality of an in-game online forum that the player can navigate on a PC. This is made of a forum view as well as a thread view.

    The forum view looks like this:

    The "Replies 1" variable is linked to the number displayed by using a token in an AC menu. It represents the number of replies in the thread.

    The "Thread 1" variable represents the time the last post within the thread was made.

    Long story short, I have a script with an array for all the "thread" objects:

    [SerializeField] private GameObject[] Threads;

    First it reorders the array like this:

    Threads = Threads.OrderByDescending(x => x.GetComponent<AC.Variables>().GetVariable(0).IntegerValue).ToArray();

    Then a foreach loop goes though the Threads array and sets the sibling index for each individual thread:

    int x = 0;
    
    foreach (GameObject thread in Threads)
    {
        Threads[x].transform.SetSiblingIndex(x);
        x++;
    }
    

    (It's actually more complex than that [I also set the background colour for each one, hide older threads, etc], but you get the gist)

    And so I need those component variables to be able to be manipulated even when the objects are disabled, and I need them to be remembered too.

    The Thread view looks like this:

    The script to handle it is very similar to the one above, but posts are ordered in ascending order:

    Posts = Posts.OrderBy(x => x.GetComponent<AC.Variables>().GetVariable(0).IntegerValue).ToArray();
    

    Inside the foreach loop, I have this:

            Posts[x].transform.SetSiblingIndex(x);
    
            // Activate/deactivate posts by comparing their #0 component variable to the current game time. Excludes 800000 and 900000 because those numbers are assigned to the reply box and bottom UI elements (i.e. they are not posts and should always be visible)
    
                if (post.GetComponent<AC.Variables>().GetVariable(0).IntegerValue > AC.GlobalVariables.GetVariable(12).IntegerValue && post.GetComponent<AC.Variables>().GetVariable(0).IntegerValue != 800000 && post.GetComponent<AC.Variables>().GetVariable(0).IntegerValue != 900000)
                {
                    post.SetActive(false);
                }
                else
                {
                    post.SetActive(true);
                }
    

    Where AC.GlobalVariables.GetVariable(12) is the global variable that keeps track of the in-game time. So as time moves forward, most posts will be automatically posted (i.e. unhidden), EXCEPT the ones I named "(conditional)". The variables for those posts have very high numbers (way beyond the length of the game), and their variables will only be set to a lower number depending on the player's actions.

    So basically, I need to be able to use regular actionlists to change those variables even when the objects are disabled. Does that make sense?

  • (b) The variables can only be manipulated via actions when the gameobject (not just the menu that contains it) has been enabled at least once during gameplay - i.e. if I turn a menu on, then enable a gameobject with a variables component, and then disable it again, actions will be able to set the variable even with the object disabled; otherwise the action will not work.

    Upon further testing, this is not true. Component variables can be set via an actionlist if and only if their gameobject is enabled at the moment the menu is first instantiated in the scene. If the gameobject is disabled within the prefab, its variables will never be able to be set, no matter its current status. If the gameobject is enabled within the prefab, once the menu has been instantiated in the scene, an actionlist will always be able to set its value, even after it's been disabled.

  • edited January 2022

    Unity UI prefabs are spawned at the time that they are required - this is to prevent slow start-up times when multiple prefabs are loaded as the scene initialises.

    You can, however, spawn it in manually with:

    AC.PlayerMenus.GetMenuWithName ("MyMenu").LoadUnityUI ();
    

    As for saving multiple Variables in one Remember component, try this:

    using UnityEngine;
    using AC;
    
    public class RememberVariablesHierarchy : Remember
    {
    
        public GameObject rootObject;
        private bool loadedData = false;
    
    
        public override string SaveData ()
        {
            VariablesHierarchyData data = new VariablesHierarchyData ();
    
            Variables[] variables = rootObject.GetComponentsInChildren<Variables>();
    
            data.variablesDatas = new string[variables.Length];
            for (int i = 0; i < variables.Length; i++)
            {
                data.variablesDatas[i] = SaveSystem.CreateVariablesData (variables[i].vars, false, VariableLocation.Component);
            }
    
            return Serializer.SaveScriptData <VariablesHierarchyData> (data);
        }
    
    
        public override void LoadData (string stringData)
        {
            VariablesHierarchyData data = Serializer.LoadScriptData <VariablesHierarchyData> (stringData);
            if (data == null)
            {
                loadedData = false;
                return;
            }
            SavePrevented = data.savePrevented; if (savePrevented) return;
    
            Variables[] variables = rootObject.GetComponentsInChildren<Variables>();
            for (int i = 0; i < variables.Length; i++)
            {
                if (i < data.variablesDatas.Length)
                {
                    variables[i].vars = SaveSystem.UnloadVariablesData (data.variablesDatas[i], true, variables[i].vars);
                }
            }
    
            loadedData = true;
        }
    
    }
    
    [System.Serializable]
    public class VariablesHierarchyData : RememberData
    {
    
        public string[] variablesDatas;
    
    
        public VariablesHierarchyData ()
        {
            variablesDatas = new string[0];
        }
    
    }
    

    How many variables are you dealing with, exactly? These complications wouldn't be necessary if they were global - and you can simplify their display in Actions etc by prefixing their names with e.g. "Forum/".

  • edited January 2022

    Thank you! Sadly I'm getting a script error here:

    variables[i].vars = SaveSystem.UnloadVariablesData(data.variablesDatas[i], true, Variables.vars);

    It gives me a Compiler Error CS0120 (an object reference is required for the nonstatic field, method, or property 'Variables.vars').

    How many variables are you dealing with, exactly? These complications wouldn't be necessary if they were global - and you can simplify their display in Actions etc by prefixing their names with e.g. "Forum/".

    Considering the number of threads + posts together, that would easily be over 100 variables (closer to 200 if I'm honest). I could still use component variables, attaching them to any number of empty child objects in my Player prefab (no player switching in the game, so that basically makes them "global" but tidier), but the issue is that then I'm not sure how to sort the threads/posts based on them because they are not attached to their actual gameobjects. The simplicity of this system is that I can just sort the array like this:

    Threads = Threads.OrderByDescending(x => x.GetComponent<AC.Variables>().GetVariable(0).IntegerValue).ToArray();

    But if this data is not found in the gameobject itself, I'm not entirely sure what the most efficient way of doing this is.

    Note that this system is currently functional: my code is successful in sorting/displaying the threads and posts in the right order. It has no trouble updating their positions/visibility in the hierarchy based on game time, and if I change the AC variables manually in the inspector at runtime, it picks this up and updates them without issues.

    When your Remember script is working, and if I manage to keep the UI instantiated (shouldn't be an issue with the code snippet you provided), I believe the only problem left to fix will be the issue of actions not setting component variables whose gameobjects were disabled when the menu was first instantiated? Which I'm low-key hoping is due to the Remember scripts (and the object ID) not being initiated somehow - this is really just a wild guess, but in short, I want to see how your Remember script works before I attempt to troubleshoot this part of the issue.

  • Sorry - typo in my code. I've corrected it in an edit.

  • Thank you! One last issue (maybe?) re: the remember script. I see a public GameObject rootObject; which I'd have expected to be exposed in the inspector so that I could drag the menu prefab to it, but it's not showing.

    I tried your "Remember Enabled" script too, which I expected to work similarly, but you can't assign any objects to it either apparently.

  • Also, once this can be assigned, should I then remove the "remember variable" components of all objects in the hierarchy? Should I add a Constant ID component to each of them instead?

  • edited January 2022

    Edit: still working on this, will post my modifications soon.

  • Right, I had to modify your script to:

    • Include an Editor to assign an object to rootObject.
    • Add (true) to the GetComponentsInChildren method (this is the includeInactive parameter).

      using UnityEditor;
      using UnityEngine;
      using System.Collections;
      using AC;
      
      public class RememberVariablesHierarchy : Remember
      {
          public GameObject rootObject;
          private bool loadedData = false;
      
          public override string SaveData()
          {
              VariablesHierarchyData data = new VariablesHierarchyData();
      
              Variables[] variables = rootObject.GetComponentsInChildren<Variables>(true);
      
          data.variablesDatas = new string[variables.Length];
          for (int i = 0; i < variables.Length; i++)
          {
              data.variablesDatas[i] = SaveSystem.CreateVariablesData(variables[i].vars, false, VariableLocation.Component);
          }
      
          return Serializer.SaveScriptData<VariablesHierarchyData>(data);
      }
      
      
      public override void LoadData(string stringData)
      {
          VariablesHierarchyData data = Serializer.LoadScriptData<VariablesHierarchyData>(stringData);
          if (data == null)
          {
              loadedData = false;
              return;
          }
          SavePrevented = data.savePrevented; if (savePrevented) return;
      
          Variables[] variables = rootObject.GetComponentsInChildren<Variables>(true);
          for (int i = 0; i < variables.Length; i++)
          {
              if (i < data.variablesDatas.Length)
              {
                  variables[i].vars = SaveSystem.UnloadVariablesData(data.variablesDatas[i], true, variables[i].vars);
              }
          }
      
              loadedData = true;
          }
      
      
      #if UNITY_EDITOR
      
          public void ShowGUI()
          {
              CustomGUILayout.BeginVertical();
      
              rootObject = (GameObject)EditorGUILayout.ObjectField("Object to save:", rootObject, typeof(GameObject), true);        
      
              CustomGUILayout.EndVertical();
          }
      
      #endif
      
      
      
      }
      
      [System.Serializable]
      public class VariablesHierarchyData : RememberData
      {
      
          public string[] variablesDatas;
      
      
          public VariablesHierarchyData()
          {
              variablesDatas = new string[0];
          }
      
      
      
      
      }
      

    I also had to create an Editor script:

    #if UNITY_EDITOR
    
    using UnityEditor;
    
    namespace AC
    {
    
        [CustomEditor(typeof(RememberVariablesHierarchy), true)]
        public class RememberVariablesHierarchyEditor : ConstantIDEditor
        {
    
            public override void OnInspectorGUI()
            {
                RememberVariablesHierarchy _target = (RememberVariablesHierarchy)target;
                _target.ShowGUI();
                SharedGUI();
            }
    
        }
    
    }
    
    #endif
    
  • If I change the menu type to Unity UI in Scene, this works. I can't get it to work with a prefab, though. This might suffice for my purposes, BUT I'm thinking about having a second PC the player can use in a different location (I'm not sure about this yet), so it might be good to plan for that. Do you reckon it would be easy to adapt this to a prefab, or maybe I should consider simply not destroying those objects on scene change?

    But first things first: I can't seem to use an action to change a component variable when its object is turned off. I tested this behaviour again, now using the Unity UI in Scene, and the behaviour seems to be different from a prefab:

    • If the menu is off, it is not possible to set any component variables in its children.
    • If the menu is on, it is only possible to set component variables in the children that are also active.
    • There doesn't seem to be any weird behaviours where it was possible to set the variables in inactive objects as long as they were originally active in the prefab.

    This is really the main issue that needs to be tackled. Do you think it'd be possible to affect disabled objects with an action, or does the entire thing need to be redesigned?

  • edited January 2022

    Chris, I think I sorted the issue [Edit: no, I didn't lol]. I opened StateHandler.cs, and change this line:

    ConstantID[] allConstantIDs = Object.FindObjectsOfType <ConstantID>();

    To this:

    ConstantID[] allConstantIDs = Object.FindObjectsOfType <ConstantID>(true);

    Meaning inactive GameObjects are also included in initial record of ConstantID components in the Hierarchy.

    Is this something you foresee causing any issues?

    Edit: It definitely does cause lots of issues. One shot sounds via actions won't play anymore, linked menu elements won't actually link. It does allow me to change the variables in disabled GameObjects, but it breaks a lot of things, lol

  • When manipulating an in-scene UI's variables, how are you referring to the Variables component? Via a parameter/constant ID from an ActionList asset, or via a direct object/component assignment?

    A disabled object/component will not be visible to the Constant ID system. Changing this causes issues, as you've found, but it's arguable that this behaviour is aligned with Unity's best practices.

    If changing to Unity UI In Scene gives better results, I'd recommend switching to that and having this system occur in a dedicated scene to avoid the need for prefabs.

    If you later decide to have another scenario where the player can use a difference PC, you should be able to use the same scene, making any necessary cosmetic changes in the scene's OnStart cutscene to make it appear different.

  • edited January 2022

    When manipulating the in-scene UI's variables using an actionlist, when I assign the object/component directly, it still records its ConstantID, I believe. I don't think it's possible to assign the object directly, even if you drag it to the field?

    I think I'm very nearly there:

    • I decided to go with a UI prefab because it allows me to manipulate variables on disabled objects, as long as the object was originally enabled in the prefab.
    • I enabled all objects in the prefab.
    • I created a script with a function that disables the objects that need to be disabled once the menu has been instantiated.

    This means all component variables can now be manipulated, even when disabled.

    Now I believe my only issue is that the RememberVariablesHierarchy script can't be assigned a prefab, only a GameObject in the scene. I mean, as it is, it CAN be assigned a prefab, but at runtime it always points to the prefab rather than its instance in the scene, so it doesn't remember anything. I'm struggling a bit to make this work - could you show me how to achieve this, please?

  • edited January 2022

    I believe I found a solution using the script you provided here:

    https://www.adventurecreator.org/forum/discussion/10466/remember-variables-on-menu-prefabs

    Instead of trying to assign an instance of the prefab to the scene object's component, I added the RememberVariablesHierarchy component to a child in the prefab itself. Then I used the script above to detach the the object with the remember component from the menu hierarchy. It seems to work so far! I will report if I run into any issues.

    Thanks a lot for the help, this would have been hard without your scripts/pointers.

Sign In or Register to comment.

Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Welcome to the official forum for Adventure Creator.