11
\$\begingroup\$

Developing a game in Unity, I'm making liberal use of [RequireComponent(typeof(%ComponentType%))] to ensure components have all dependencies met.

I'm now implementing a tutorial system which highlights various UI objects. To do the highlighting, I'm taking a reference to the GameObject in the scene, then I clone it with Instantiate() and then recursively strip away all components which aren't needed for display. The problem is that with the liberal use of RequireComponent in these components, many cannot be simply removed in any order, since they are depended on by other components, which have not been deleted yet.

My question is: Is there a way to determine whether a Component can be removed from the GameObject it is currently attached to.

The code I'm posting technically works, however it throws Unity errors (not caught in try-catch block, as seen in the image

enter image description here

Here's the code:

public void StripFunctionality(RectTransform rt)
{
    if (rt == null)
    {
        return;
    }

    int componentCount = rt.gameObject.GetComponents<Component>().Length;
    int safety = 10;
    int allowedComponents = 0;
    while (componentCount > allowedComponents && safety > 0)
    {
        Debug.Log("ITERATION ON "+rt.gameObject.name);
        safety--;
        allowedComponents = 0;
        foreach (Component c in rt.gameObject.GetComponents<Component>())
        {
            //Disable clicking on graphics
            if (c is Graphic)
            {
                ((Graphic)c).raycastTarget = false;
            }

            //Remove components we don't want
            if (
                !(c is Image) &&
                !(c is TextMeshProUGUI) &&
                !(c is RectTransform) &&
                !(c is CanvasRenderer)
                )
            {
                try
                {
                    DestroyImmediate(c);
                }
                catch
                {
                    //NoOp
                }
            }
            else
            {
                allowedComponents++;
            }
        }
        componentCount = rt.gameObject.GetComponents<Component>().Length;
    }

    //Recursive call to children
    foreach (RectTransform childRT in rt)
    {
        StripFunctionality(childRT);
    }
}

So the way this code generated the last three lines of the debug log above is the following: It took as input a GameObject with two components: Button and PopupOpener. PopupOpener requies that a Button component is present in the same GameObject with:

[RequireComponent(typeof(Button))]
public class PopupOpener : MonoBehaviour
{
    ...
}

The first iteration of the while loop (denoted by the text "ITERATION ON Button Invite(clone)") attempted to delete the Button first, but couldn't as it is depended on by the PopupOpener component. this threw an error. Then it attempted to delete the PopupOpener component, which it did, since it is not depended on by anything else. In the next iteration (Denoted by the second text "ITERATION ON Button Invite(clone)"), it attempted to delete the remaining Button component, which it now could do, since PopupOpener was deleted in the first iteration (after the error).

So my question is whether it is possible to check ahead of time if a specified component can be removed from its current GameObject, without invoking Destroy() or DestroyImmediate(). Thank you.

\$\endgroup\$
  • \$\begingroup\$ About the original problem - is the goal to make non-interactive copy(=visual) of GUI element(s)? \$\endgroup\$ – wondra May 7 '17 at 13:44
  • \$\begingroup\$ Yes. The goal is to make an identical, non interactable copy in the same position, scaled in the same way, showing the same text as the real gameobject underneath. The reason I went with this method is so the tutorial wouldn't need modifications if the UI was edited at a later point (which would need to happen if I showed screenshots). \$\endgroup\$ – Liam Lime May 7 '17 at 14:08
  • \$\begingroup\$ I don't have experience with Unity but I'm wondering if instead of cloning the whole thing and removing stuff could you only copy the parts you need? \$\endgroup\$ – Zan Lynx May 7 '17 at 18:52
  • \$\begingroup\$ I haven't tested it, but Destroy removes the component at the end of the frame (as opposed to Immediately). DestroyImmediate is considered bad style anyways, but I'm thinking switching to Destroy may actually eliminate these errors. \$\endgroup\$ – CAD97 May 8 '17 at 5:31
  • \$\begingroup\$ I tried both, starting with Destroy (since that is the proper way) and then switched to DestroyImmediate to see if it would work. Destroy still seems to go in the specified order, which causes the same exceptions, just at the end of the frame. \$\endgroup\$ – Liam Lime May 8 '17 at 8:49
9
\$\begingroup\$

It is definitely possible, but it requires quite a lot of legwork: you can check if there is a Component attached to the GameObject which has an Attribute of type RequireComponent which has one of its m_Type# fields assignable to the component type you are about to remove. All wrapped in an extension method:

static class GameObjectExtensions
{
    private static bool Requires(Type obj, Type requirement)
    {
        //also check for m_Type1 and m_Type2 if required
        return Attribute.IsDefined(obj, typeof(RequireComponent)) &&
               Attribute.GetCustomAttributes(obj, typeof(RequireComponent)).OfType<RequireComponent>()
               .Any(rc => rc.m_Type0.IsAssignableFrom(requirement));
    }

    internal static bool CanDestroy(this GameObject go, Type t)
    {
        return !go.GetComponents<Component>().Any(c => Requires(c.GetType(), t));
    }
}

after dropping GameObjectExtensions anywhere in the project (or alternatively make it a regular method on the script), you can check if a component can be removed, e.g.:

rt.gameObject.CanDestroy(c.getType());

However there can be other means to make un-interactive GUI object - for example, rendering it to texture, overlaying it with almost-transparent panel which will intercept clicks or disabling(instead of destroying) all Components deriving from MonoBehaviour or IPointerClickHandler.

|improve this answer|||||
\$\endgroup\$
  • \$\begingroup\$ Thanks! I was wondering if a ready-made solution ships with Unity, but I guess not :) Thank you for your code! \$\endgroup\$ – Liam Lime May 7 '17 at 15:46
  • \$\begingroup\$ @LiamLime Well, I cant really confirm there is none builtin but it is unlikely since the typical Unity paradigm is making code fault-tolerant(i.e. catch, log, continue) rather than preventing faults (i.e. if possible, then). That is, however, not much C# style (such as one in this answer). In the end your original code might have been closer to how things are typically done ...and best practice is matter of personal preference. \$\endgroup\$ – wondra May 7 '17 at 18:33
7
\$\begingroup\$

Instead of removing the components on the clone, you can just disable them by setting .enabled = false on them. This won't trigger the dependency checks, because a component doesn't need to be enabled to fulfill the dependency.

  • When a Behaviour is disabled, it will still be on the object and you can even change its properties and call its methods, but the engine will no longer call its Update method.
  • When a Renderer is disabled, the object turns invisible.
  • When a Collider is disabled, it won't cause any collision events to happen.
  • When an UI component is disabled, the player can't interact with it anymore, but it will still be rendered, unless you also deactivate its renderer.
|improve this answer|||||
\$\endgroup\$
  • \$\begingroup\$ Components do not have a notion of enabled or disabled. Behaviors, Colliders and some other types do. So this would require the programmer to make multiple calls to GetComponent for the various subclasses. \$\endgroup\$ – DubiousPusher Nov 2 '18 at 21:00

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.