0
\$\begingroup\$

I have a menu that gets populated at runtime with buttons whose text components are based on objects that the game manager has in its databases. At the start, the menu is empty:

enter image description here

See that "A" graphic? It's a button called Button_Magic_DropDown that activates/deactivates the panel called Panel_MagicMenu. At runtime, regardless of whether the panel is active, its child panel Empty_Inset_MagicMenu gets 6 children, which are all instances of the button prefab, Button_BattleMenu. When one of these buttons are clicked, I want the panel called Panel_MagicMenu to get deactivated. See image below:

enter image description here

What I am struggling with is deactivating [the parent panel of a ] panel that a button sits in when it is clicked. I am trying to do this by getting the second parent programmatically and deactivating it. Here is the script containing that function OnClickDeactivatePanel(), which I am assigning in the button prefab:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BattleUI : MonoBehaviour
{
    public void ToggleActive(GameObject panel)
    {
        panel.SetActive(!panel.activeInHierarchy);
    }

    public void OnClickDeactivatePanel()
    {
        //On click, deactivates the panel that the button sits in, two parents up
        GameObject secondParent = gameObject.transform.parent.parent.gameObject;
        //Debug.Log("Second parent is " + secondParent);
        secondParent.SetActive(false);
    }
}

As you can guess, this didn't work. Unity is unable to identify the parents of an instanced object this way. I get a null-reference error. What can I do?

Here is the button prefab, Button_BattleMenu, if it helps: enter image description here

This is obviously an encapsulation issue. The buttons are cloned into the menu during an Awake() function in another script.

EDIT: Changing Awake() to OnEnable() and OnStart() didn't work, but I appreciate the help. Here is the script that loads the buttons (you can see I have an impending decoupling problem on my hands:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;

public class _CharStatManager : MonoBehaviour
{
    public float scaleFactor = 0.75f;
    public float resolution = 100f;
    [SerializeField] GameObject entry;
    private TMP_Text tmpText;

    // MAGIC
    public List<SpecialAttack> magicList = new List<SpecialAttack>();
    private uint magicList_size;
    [SerializeField] GameObject magicContainer;
    [SerializeField] GameObject magicContainerParent;

    // ITEM
    public List<Item> itemList = new List<Item>();
    private uint itemList_size;
    [SerializeField] GameObject itemContainer;
    [SerializeField] GameObject itemContainerParent;

    // ATTACK
    [SerializeField] GameObject attackContainer;
    [SerializeField] GameObject attackContainerParent;

    //DEFENSE
    [SerializeField] GameObject defenseContainer;
    [SerializeField] GameObject defenseContainerParent;

    void Awake()
    {
        //Don't Destroy:
        DontDestroyOnLoad(this.gameObject);
        float multiplier = Mathf.Pow(resolution * scaleFactor,-1);
        //=================================================================
        //=================================================================

        //+==================================+
        //|$$$$$$$$$$$ MAGIC MENU $$$$$$$$$$$|
        //+==================================+

        //Populate list of all Special Attacks (magic)
        magicList.Add(new SpecialAttack("Jump", -15, -2,SpecialAttack.Types.PhysDamage));
        magicList.Add(new SpecialAttack("Fireball", -15, -2,SpecialAttack.Types.Fire));
        magicList.Add(new SpecialAttack("Super Jump", -20, -5, SpecialAttack.Types.PhysDamage));
        magicList.Add(new SpecialAttack("Super Flame", -20, -5, SpecialAttack.Types.Fire));
        magicList.Add(new SpecialAttack("Ultra Jump", -25, -8, SpecialAttack.Types.PhysDamage));
        magicList.Add(new SpecialAttack("Ultra Flame", -25, -8, SpecialAttack.Types.Fire));

        magicList_size = (uint)magicList.Count;
        float buttonHeight = entry.GetComponent<RectTransform>().sizeDelta.y;

        /*This is where it gets tricky: Need to expand the height of the
         * magic menu "container" before we instantiate more buttons.
         * Those buttons will be offset automatically thanks to Vertical Layout Group component.*/

        // Resize Container:
        float magicContainerHeight = magicContainerParent.GetComponent<RectTransform>().sizeDelta.y;
        float magicTargetHeight = magicContainerHeight + buttonHeight * (magicList_size - 1);
        magicContainerParent.GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, magicTargetHeight);
        // Instantiate children of Container (Vertical Layout Group component will take care of alignment for me)
        for (int i = 0; i < magicList_size; i++)
        {
            GameObject newMagicButton = Instantiate(entry, magicContainer.transform, false);
            newMagicButton.GetComponentInChildren<TMP_Text>().text = magicList[i].menuName;
        }

        //=================================================================
        //=================================================================

        //+==================================+
        //|$$$$$$$$$$ DEFENSE MENU $$$$$$$$$$|
        //+==================================+

        GameObject newDefenseButton = Instantiate(entry, defenseContainer.transform, false);
        newDefenseButton.GetComponentInChildren<TMP_Text>().text = "Defend";

        //=================================================================
        //=================================================================

        //+===================================+
        //|$$$$$$$$$$$ ATTACK MENU $$$$$$$$$$$|
        //+===================================+

        PhysicalAttack onlyAttack = new PhysicalAttack("Punch Glove", -10);
        GameObject newAttackButton = Instantiate(entry, attackContainer.transform, false);
        newAttackButton.GetComponentInChildren<TMP_Text>().text = onlyAttack.menuName;

        //=================================================================
        //=================================================================

        //+=================================+
        //|$$$$$$$$$$$ ITEM MENU $$$$$$$$$$$|
        //+=================================+

        //Populate list of items (magic)
        itemList.Add(new Item("HP Shroom","Recover 30hp."));
        itemList.Add(new Item("Power Star","Strength + 3"));
        itemList.Add(new Item("Chocolate Mousse","A treat you shouldn't keep to yourself."));
        itemList_size = (uint)itemList.Count;

        // Resize Container:
        float itemContainerHeight = itemContainerParent.GetComponent<RectTransform>().sizeDelta.y;
        Debug.Log(itemContainerHeight);
        float itemTargetHeight = itemContainerHeight + buttonHeight * (itemList_size - 1);
        Debug.Log(itemTargetHeight);
        itemContainerParent.GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, itemTargetHeight);
        // Instantiate children of Container (Vertical Layout Group component will take care of alignment for me)
        for (int i = 0; i < itemList_size; i++)
        {
            GameObject newItemButton = Instantiate(entry, itemContainer.transform, false);
            newItemButton.GetComponentInChildren<TMP_Text>().text = itemList[i]._menuName;
        }
    }
}
\$\endgroup\$
1
\$\begingroup\$

Swap the Awake() to either OnEnable() or Start() just to be sure you are not referencing the panels before they are fully loaded, sized and able to cache.

If it still shows a null then you can use transform.root to get the top transform then perform tansform.GetChild(index) with index being the panel you want to disable.

If neither help but you must have dynamically I would suggest to get the root then load those children into a array to loop through and change as you want.

A better solution may be to just cache the windows and set Active to false on Start or the simplest solution would be to make public so they are always ready and available.

\$\endgroup\$
  • \$\begingroup\$ or transform.parent to get the direct parent. \$\endgroup\$ – Draco18s no longer trusts SE Oct 20 '19 at 18:38
  • \$\begingroup\$ yea that too, he may have lost reference where he does transform.parent.parent if in fact no parent of the parent was available. Hope that gets the issue corrected though. \$\endgroup\$ – Justin Markwell Oct 20 '19 at 19:27
0
\$\begingroup\$

Well, it works folks. In fact, it works exactly as it was supposed to. Why didn't it before? Because the OBJECT I was using in the Editor that had the same script on it was the WRONG OBJECT! I was using the Canvas, not the button prefab, both which have the same script. This makes PERFECT SENSE because the canvas is the root, and would not have a parent! XD

enter image description here

using System.Collections; using System.Collections.Generic; using UnityEngine;

public class BattleUI : MonoBehaviour
{

    public void ToggleActive(GameObject panel)
    {
        panel.SetActive(!panel.activeInHierarchy);
    }

    public void CloseOnClick()
    {
        //On click, deactivates the panel that the button sits in, two parents up

        GameObject secondParent = gameObject.transform.parent.transform.parent.gameObject;
        //Debug.Log("Second parent is " + secondParent);
        secondParent.SetActive(false);
    }
}

Thank you so much @Draco18s and @Justin Markwell for helping me

enter image description here

EDIT: @JUSTIN MARKWELL WAS RIGHT! So I just realized on demonstration to a colleague that it is possible to open multiple menus at the same time with this script. That must not be. So I realized that @JustinMarkwell's answer was the best option. It goes to the root (Canvas) and deactivates every panel under it:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BattleUI : MonoBehaviour
{

    private void ToggleActive(GameObject panel)
    {
        panel.SetActive(!panel.activeInHierarchy);
    }

    private void CloseOnClick()
    {
        Transform root = transform.root;
        GameObject magicPanel = root.GetChild(0).transform.GetChild(0).transform.GetChild(2).gameObject;
        GameObject attackPanel = root.GetChild(0).transform.GetChild(1).transform.GetChild(2).gameObject;
        GameObject defensePanel = root.GetChild(0).transform.GetChild(2).transform.GetChild(2).gameObject;
        GameObject itemPanel = root.GetChild(0).transform.GetChild(3).transform.GetChild(2).gameObject;
        magicPanel.SetActive(false);
        attackPanel.SetActive(false);
        defensePanel.SetActive(false);
        itemPanel.SetActive(false);
    }
}

This takes us (halfway) there because it closes every open panel. Of course, multiple panels should not be open in the first place--UGH! So what I really want to do then is stick to my original script and use Justin's idea to make sure all other buttons' panels are deactivated when clicked. This post will be updated again.

EDIT 2: ARMAGEDDON

Ok, now we have it! I took the BattleUI script off all UI elements (except the button prefab) and stuck them only to the dropdown menu buttons. Then I updated the ToggleActive() script (renaming it TogglePanel(), for clarity). The OnClick Editor will only list functions that have a maximum of 1 input (I guess) so all the panels I want to turn off must be accessed from the parent of the dropdown button the script is attached to:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BattleUI : MonoBehaviour
{
    public void TogglePanel(GameObject thisPanel)
    {
        int thisButtonIndex = gameObject.transform.GetSiblingIndex();
        thisPanel.SetActive(!thisPanel.activeInHierarchy);
        Transform firstParent = gameObject.transform.parent;
        for (int i = 0; i < 4; i++)
        {
            if (i != thisButtonIndex)
            {
                firstParent.GetChild(i).transform.GetChild(2).gameObject.SetActive(false);
            }
        }
    }

    public void CloseOnClick()
    {
        //On click, deactivates the panel that the button sits in, two parents up
        GameObject secondParent = gameObject.transform.parent.transform.parent.gameObject;
        secondParent.SetActive(false);
    }
}

This allows makes sure that no other menu is open when I click a dropdown button, and I have only 2 lines of code to close a menu once an instantiated button (option) is clicked:

enter image description here

So now the actual solution has been found! Thanks to Justin Markwell for pointing me in the right direction, he gets all the credit

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.