Unity6 Game Development · Lesson 31

Cache a specific child component

Resolve a known hierarchy dependency and decide when a cached reference expires.

Unity 6 Editor walkthrough

The Unity script needs the Editor and the setup listed below. The separate C# companion practice runs in SharpRune without UnityEngine.

Resolve a role from a deliberately narrow subtree

A scene hierarchy can contain several renderers that serve different roles: body, weapon, selection highlight and an invisible effects mesh. Calling GetComponentInChildren on the whole actor and accepting the first result turns incidental structure into a hidden dependency. ChildRendererProbe instead accepts an explicit visualRoot, making the intended search scope visible in the Inspector. Even that subtree should have a clear selection rule when more than one renderer is present. This lesson therefore begins with exactly one intended renderer below the assigned root, then uses visual replacement to show when a cached dependency needs to be reconsidered.

Include inactive children and keep the replacement boundary explicit

Resolve assigns either the first matching Renderer under visualRoot or null, and passes true so inactive children are eligible. The API also searches the specified root itself, not just descendants. Awake establishes the initial cache; RefreshCache represents an explicit runtime replacement boundary. ToggleRenderer checks the retained Unity object against null before changing enabled, which handles an absent or destroyed component according to Unity’s object equality. That changes renderer visibility rather than GameObject activation. The code intentionally does not search every frame or mutate shared material assets. Those would be separate behaviors requiring their own identity and ownership rules.

    private void Resolve()
    {
        cachedRenderer = visualRoot != null ? visualRoot.GetComponentInChildren<Renderer>(true) : null;
        if (cachedRenderer == null) Debug.LogWarning("Assign a visualRoot containing the intended Renderer.", this);
    }

A copied reference does not track an owner’s replacement

The portable owner begins with one Part named arm-1, and cached receives that object reference. Replacing the list entry with a newly created arm-2 changes the owner’s current child description but does not rewrite cached. Printing cached again therefore still reports arm-1. Only an explicit new lookup produces arm-2. This is ordinary managed reference behavior; no Transform, engine object wrapper, search ordering or Unity destruction occurs. The three outputs make the lifetime issue visible without claiming a performance benchmark. A cache can be correct at acquisition time and stale after a structural change unless the owning API defines how replacement is communicated.

Search order is not a semantic identifier

GetComponentInChildren returns the first matching component, and multiple possible matches make that choice a weak role contract. Give important roles explicit references or filter a collected set by a documented criterion. Including inactive children is useful for initially hidden visuals, but a found Renderer can still sit on an inactive GameObject and remain invisible even when renderer.enabled becomes true. Reparenting a visual does not retarget an already cached reference either. Keep scene hierarchy changes, object destruction and component visibility as distinct operations. A missing mesh should not automatically trigger repeated broad searches that hide an invalid visualRoot assignment.

Practice: replace the visual without replacing the actor

Start with Actor and one assigned Visual child, then use Toggle Renderer to confirm only its mesh visibility changes. In a separate Play session, deactivate Visual and refresh the cache to inspect the includeInactive choice. Next replace Visual with another child, update the serialized reference and call Refresh Cache before toggling again. Record which object the controller now affects. If the actor owns a visual-swap method in production, make that method update the reference and refresh as one reviewed operation instead of asking unrelated callers to remember both steps. This exercise tests a concrete structural boundary rather than a general claim that caching is always faster.

  • Hint: component.enabled and GameObject.activeSelf are different values.
  • Hint: a stable role can deserve an explicit serialized Renderer reference instead of a search.

Practice: make cache invalidation part of the owner API

Wrap the plain child list in an owner with a ReplaceVisual operation and a monotonically increasing revision. Have a consumer re-resolve only when that revision changes, then test one replacement, no replacement and two consecutive replacements. Decide whether a consumer is allowed to keep the old Part as a historical snapshot or must always follow the newest one. Neither contract is automatically correct. Keep the pure example free of a pretend Transform class, and list the events a Unity adapter would need to supply when a visual is destroyed or swapped outside that owner’s method. The revision is supplied model data, not an engine discovery mechanism.

  • Hint: invalidation can be explicit even when the cached value is just one reference.
  • Hint: define whether old values are valid snapshots or forbidden stale dependencies.

Review search scope, visibility and ownership independently

The final setup should identify the assigned root, the renderer selected within it and the operation that invalidates that selection. A useful observation can then distinguish “lookup found nothing” from “renderer found but parent inactive” or “old visual retained after replacement.” The component follows Unity6000.0 API documentation and was syntax-checked as a real engine script; no actual hierarchy search was run locally in an Editor. The reference-replacement companion was executed on desktop .NET and establishes only its own managed-object outputs. Carry that precise scope into character skins, equipment and pooled visuals rather than claiming one successful cache lookup solves every dynamic hierarchy.

Runs in Unity 6 Editor

Unity Editor example

Set up in Unity 6

  1. Create an Actor empty root and one Visual child containing a Cube Renderer. Attach ChildRendererProbe.cs to Actor and assign Visual as visualRoot.
  2. Keep exactly one intended Renderer below that assigned root for this baseline. Other unrelated renderers can live elsewhere under Actor.
  3. Enter Play mode and choose Toggle Renderer from the script context menu. Confirm the mesh visibility changes while the Visual GameObject stays active.
  4. In a copied Play session, deactivate Visual and choose Refresh Cache: includeInactive true allows the lookup to retain that intended dependency.
  5. Replace Visual with another child, update visualRoot, and invoke Refresh Cache. Treat runtime visual replacement as an explicit API boundary; a cached reference is not a live query.

ChildRendererProbe.cs

Download Unity script ↗
using UnityEngine;
public sealed class ChildRendererProbe : MonoBehaviour
{
    [SerializeField] private Transform visualRoot;
    private Renderer cachedRenderer;
    private void Awake() { Resolve(); }
    private void Resolve()
    {
        cachedRenderer = visualRoot != null ? visualRoot.GetComponentInChildren<Renderer>(true) : null;
        if (cachedRenderer == null) Debug.LogWarning("Assign a visualRoot containing the intended Renderer.", this);
    }
    [ContextMenu("Refresh Cache (Play Mode)")]
    public void RefreshCache() { if (Application.isPlaying) Resolve(); }
    [ContextMenu("Toggle Renderer (Play Mode)")]
    public void ToggleRenderer()
    {
        if (!Application.isPlaying || !isActiveAndEnabled) return;
        if (cachedRenderer == null) { Debug.LogWarning("Renderer is missing; refresh after replacing the visual.", this); return; }
        cachedRenderer.enabled = !cachedRenderer.enabled;
    }
}

What to observe in Unity

The controller changes only its resolved Renderer.enabled flag. Replacing or destroying the visual requires a reviewed reference update and refresh rather than an assumed automatic retarget.

The console practice below isolates the C# logic. It does not run a Unity scene or emulate Unity engine callbacks.

C# companion practice

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;

var children = new List<Part> { new("arm-1") };
var cached = children.Single();
Console.WriteLine(cached.Name);
children[0] = new Part("arm-2");
Console.WriteLine(cached.Name);
cached = children.Single();
Console.WriteLine(cached.Name);
sealed class Part { public string Name { get; } public Part(string name) { Name = name; } }
Open C# practice

C# practice output

arm-1
arm-1
arm-2
Try this
Caching makes the replacement boundary your responsibility.

Further reading