1
\$\begingroup\$

When structuring the script to control a game object with several generations of children underneath, I gather from one of Jason Weimann's videos that it's a bad idea to use the string based methods to get your references like transform.Find("objectName") because those names could change and break things.

So what's the best way to do it? I'd prefer not to have to wire everything up via the inspector. I guess you can use GetChild() but then you run the risk of breaking it a different way if that index ever changes.

As an example, I currently have a custom UI slider prefab that is a game object containing several child game objects, which in turn have components or subchildren with components such as text and shapes (from the Shapes plugin), which move dynamically during interaction and therefore need their own local transforms. There is also a raw image to catch interactions.

The whole thing is controlled by a script on the root game object, so it needs references to all the children to set their position / values / colors etc.

The dependencies are all in the master script, none of the sub components know about the master script, their parameters get set by it.

Currently I have it set up to reference via things like transform.Find("child1").Find("child2").gameObject.GetComponent<>....

Is that the best way in this case? Would it be better to refactor so each component has its own script and just referenced the master script?

One thing I don't think I made clear:

This isn't a regular slider that sets the parameters of shapes, it is its self a new stylable UI element I've created that replaces Unity's standard slider, and is built out of Shape and TextMeshPro assets.

There aren't any testable rules about which object should be referenced, the variable for the "Outline thickness" will always need to link to that parameter on a particular Shape component a few levels down the hierarchy.

Maybe in this case just using names and making sure they don't get changed is fine?

\$\endgroup\$
5
  • \$\begingroup\$ To know which object we should find, we need to know something about the criteria that distinguish a "correct" object selection from an "incorrect" selection. So, you'll need to add some concrete detail here. Can you give us an example of a particular instance where you need to look up child objects at runtime? In that instance, what characterizes/distinguishes the correct objects to be returned? \$\endgroup\$
    – DMGregory
    Dec 6, 2021 at 22:12
  • \$\begingroup\$ @DMGregory Hi, added more detail in original post. \$\endgroup\$ Dec 7, 2021 at 16:59
  • \$\begingroup\$ We're still missing a clear statement of a rule of the form "You will know you have the correct object when it has the trait [X]". Before we can teach the computer to follow that rule in code, we need to have a clear picture of what that rule is. So, walk us through the specific shapes and text that you want this slider to set. What sets each shape/text item apart from the others? What are the different operations that the master script has to perform on each? (I know this may sound overly specific, but in solving the specific problem we can show techniques that generalize to similar cases) \$\endgroup\$
    – DMGregory
    Dec 7, 2021 at 18:12
  • 2
    \$\begingroup\$ Your exact situation is unclear to me but maybe look for a ways to make the whole thing more "modular" where each parent is only responsible for its immediate children and deals only with them. Then the next "level" have its own component and so on. This way the number of inspector references won't get overwhelmingly many and the relations of who does what will be more clear. Anyway it seems to me that maybe inspector references are the right way even if it is left as one MonoBehaviour. \$\endgroup\$
    – Nikaas
    Dec 8, 2021 at 7:49
  • \$\begingroup\$ Related: docs.unity3d.com/Packages/[email protected]/manual/… \$\endgroup\$
    – Evorlor
    Jan 7, 2022 at 22:21

3 Answers 3

1
\$\begingroup\$

There aren't any testable rules about which object should be referenced

If you can't state a rule for the computer to follow, then there's no way to automate this. You will have to manually specify which child object/component to use for each reference.

That means your options are:

  1. Give distinct names to each child you need to access, and look them up with transform.Find("name here") as you describe.

    This has the problem you mentioned that changing the names or making a typo anywhere could cause the script to break. That makes this approach very brittle - especially if you ever need to spawn objects dynamically or have multiple copies of certain items. For this reason, I strongly recommend avoiding name-based lookups whenever you can.

  2. Navigate to children by order in hierarchy, using transform.GetChild(index_here). As you note, this has similar problems, where now just re-ordering children could cause the script to break, and it will be even less clear why because the script is full of magic number index values that don't give good clues which object they were supposed to find.

  3. Use Inspector references. I know you've said:

    I'd prefer not to have to wire everything up via the inspector

    But it really is worth doing just that. Some advantages you get:

    • References are robust against name or hierarchy changes.

    • They're deserialized efficiently on the loading thread, so they don't contribute to an initial hitch when loading a scene (which you can get if lots of scripts are running search routines to find their references on the first frame).

    • They require less code, and less work overall - you would literally spend longer typing up an equivalent code snippet to assign the reference in Start/Awake than it would take to just drag the right reference in via the UI.

So if you want a "best practice" for assigning references that don't have a stable identifying characteristic you can just search for, wiring up those references in the inspector at edit time is far and away the best way to go.


There is one halfway approach you could take, if you really want to avoid the UI:

[SerializeField, HideInInspector] TextMeshPro _someText;

// This could go in a static utility class if you like.
bool TryGetChildComponent<T>(Transform child, out T reference) where T:Component {
    if (child != null)
        return child.TryGetComponent(out reference);
        
    reference = null;
    return false;
}

#if UNITY_EDITOR
// OnValidate is called in Editor only, 
// after the component is added or changed, or on assembly reload.
void OnValidate() {
    if (_someText == null)
        if (!TryGetChildComponent(transform.Find("Container")?.Find("Label"), out _someText))
            Debug.LogWarning($"{name} could not find child path 'Container -> Label' - make sure you've named your child elements correctly.");

    // Repeat for each reference you need to wire up.
}

// Adds context menu option to un-wire and re-wire references.
[ContextMenu("Reset References")]
void ResetReferences() {
    // Ensure changes are tracked, saved, serialized.
    UnityEditor.Undo.RecordObject(this, "Reset References");

    _someText = null;
    // Repeat for each reference you need to clear.

    OnValidate();
}
#endif

This gives you a compromise between options 1 and 3:

  • Yes, the references are stringly typed, but the name is used only once at edit time to assign the reference. If the child later changes its name, the reference is not broken.

  • The references are assigned automatically, without cluttering the Inspector view for the component.

  • If a reference is missing, you get a warning at edit time, rather than waiting until runtime to find out.

  • At runtime, the reference is deserialized cheaply on the loading thread, just like in option 3.

\$\endgroup\$
1
\$\begingroup\$

In addition to the methods in the answer by DMGregory I would like to offer a 4th alternative: GetComponentInChildren<SomeDistinctComponent>().

In most cases, the reason why you want a specific child object is because it has a specific component you want to interact with. So by using GetComponentInChildren you can find that component on the first child that has it. In case you actually need the game object that component is on and not just the component itself, you can obtain that through the .gameObject property you have on every component.

Sometimes you might have multiple child-objects with the component in question. For that case there is also GetComponentsInChildren (plural) which gets you an array of all the components in the children of that object. You can then examine those components and the rest of their gameObjects to find out which one you want to interact with. Or interact with all of them, if that's what you want to do:

foreach(var thingDoer in GetComponentsInChildren<ThingDoer>()) {
    thingDoer.DoTheThing();
}

Oh, and by the way, the whole GetComponent* family of methods also works with interfaces. When you have multiple components that implement the same interface, then you can use this method to get all of them.

\$\endgroup\$
0
\$\begingroup\$

There are a few ways you can do this, with different impact on performance, and different ease of use. You haven't specified why you want to avoid setting them in the inspector, so it's hard to say if the solution offered will be better for you. Still, here are a couple that may work.

Option 1: Create a component that specifies what type of child this is (ChildTagger in my example below), add it to each of the children you want to reference to, then use GetComponentsInChildren to iterate these children and assign them each according to their tag. Changing hierarchy and object names will not break this as long as you keep this component and tags.

public class ChildTagger : MonoBehaviour {
    public string myTag;
}

public class MyClass : MonoBehaviour {
    private Transform circle;
    private Transform triangle;

    private void Awake() {
        foreach (ChildTagger child in gameObject.GetComponentsInChildren<ChildTagger>()) {
            switch (child.myTag) {
                case "circle":
                    this.circle = child.transform;
                    break;
                case "triangle":
                    this.triangle = child.transform;
                    break;
                default:
                    Debug.LogError("Unhandled child tag: " + child.myTag);
                    break;
            }
        }
    }
}

Option 2: Iterate the full child hierarchy and search for object by name. This is similar to the transform.find that you used in your example above, but it will not be dependant on the child's exact position in the hierarchy, whether it is a direct child or grandchild. It will, however, take longer to run, so don't use this if you plan to run this code many times per frame.

public class MyClass : MonoBehaviour {
    private Transform circle;
    private Transform triangle;

    private void Awake() {
        void IterateTransform(Transform t) {
            // Check if this object is one that we need to assign
            switch (t.name) {
                case "circle":
                    this.circle = t;
                    break;
                case "triangle":
                    this.triangle = t;
                    break;
            }               

            // Iterate child objects
            for (int i = 0; i < t.childCount; i++) {
                IterateTransform(t.GetChild(i));
            }
        }
    }
}

Last note, if you use this search methods for convenience, so you don't have to manually assign in the inspector, I would still recommend allowing for the options to assign in the inspector, and letting your component know that it should skip the search in such cases. That would allow much better performance in cases where it matters (by using the inspector), but still allow for convenience in cases where performance is not an issue.

\$\endgroup\$
1
  • \$\begingroup\$ I'd recommend making your child tags into enum values, so they don't suffer from the same stringly-typed problems OP is trying to avoid with name-based lookups. \$\endgroup\$
    – DMGregory
    Nov 9, 2022 at 13:50

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .