Cached component lookups, safe hierarchy walking, recursive activation and layer changes, and transform helpers that work in polar, cylindrical and spherical coordinates.
Summary
Four classes cover the work every Unity project does against GameObject, Component and Transform: finding a child by name or path, getting a component without writing the null check, activating a whole subtree, moving something to a layer along with everything under it, and resetting a transform without touching three properties by hand.
The headline is ComponentUtil, which caches component lookups. GetComponent is not free, and a system that calls it in Update pays for that repeatedly against an object whose components have not changed since the scene loaded. ComponentUtil.Get<T>() answers from a cache instead, and the cache prunes itself when a scene unloads so destroyed objects do not accumulate. That turns “cache your component references in Awake” from a discipline you have to remember into the default behavior of the call.
TransformExtensions is the one with a surprise in it. Alongside the expected reset, copy and per-axis setters, it works in polar, cylindrical and spherical coordinates. If you have ever written the sin and cos by hand to put an enemy on a ring around the player, or to orbit a camera at a fixed radius, those conversions are already here in both world and local space.
The classes overlap deliberately: several operations exist as both a static call on GameObjectUtil and an extension method on GameObjectExtensions, so you can write whichever reads better at the call site. They differ in null handling, though, and that’s worth checking before you pick one.
Use the Unity object utilities for:
- Getting components in per-frame code without paying for
GetComponentevery call. - Building a UI or effect hierarchy from code, where objects must exist whether or not a previous run created them.
- Turning a whole subtree on or off, or moving it to a different layer, in one call.
- Finding a child several levels down by name or by slash-separated path.
- Placing objects on rings, arcs and domes without writing the trigonometry.
Features
- Cached component lookups.
Get<T>,TryGet<T>andHas<T>answer from an internal cache rather than hitting Unity’sGetComponenteach time, with automatic pruning when a scene unloads. - Get-or-add in two flavors.
Require<T>is the cached, null-tolerant form;GetOrAddComponent<T>is the uncached form that throws on a null object. Both return an existing component or add one. - Idempotent object creation.
EnsureGameObjectandEnsureGameObjectWithComponentreturn the existing child of that name or create it, so setup code is safe to run twice. - Hierarchy search.
GetChildfinds a direct child,FindChildRecursivesearches any depth, andFindByPathfollows a slash-separated path from a known root. - Recursive activation and layers.
SetActiveRecursivelyandSetLayerRecursivelywalk the whole subtree, withincludeRootcontrol on the former. - Bounds from renderers.
GetBoundingBox,TryGetBoundingBoxandGetCentroidcompute the extents of an object and its children, optionally including inactive ones. - Visibility without deactivation.
SetVisibilitytoggles renderers andEnableColliderstoggles colliders, both leaving the object itself active so its scripts keep running. - Polar, cylindrical and spherical transforms.
TransformExtensionsgets and sets a transform’s position in any of the three, in world or local space, without the caller writing the conversion. All three put the radius first: polar is(r, theta), cylindrical(r, theta, y), spherical(rho, theta, phi). - Per-axis position setters.
SetPositionX,SetPositionY, andSetPositionZ, along with their local equivalents, replace the read-modify-write of a wholeVector3. - Transform reset and copy.
Resetclears position, rotation and scale together;CopyFromandCopyLocalFromtake all three from another transform. - Safe destruction.
Destroypicks the right call for edit or play mode, andDestroyChildrenclears a subtree with an immediate option.
The four classes
They divide by what they operate on and how you call them. The overlap between GameObjectUtil and GameObjectExtensions is intentional, and their null behavior is where they part company.
| Class | Style | Covers | Notes |
|---|---|---|---|
ComponentUtil | Extensions on Component and GameObject | Component get, add, require, and the cache | The only one with a cache. Get, TryGet, Has, Add and Require all read or write it, and every method is null-tolerant, returning null rather than throwing. Prune drops entries for destroyed objects and runs automatically on scene unload; ClearCache empties it wholesale or for one object. |
GameObjectUtil | Static calls | Creation, search, bounds, activation, layers, destruction | The broadest of the four. Its GetOrAddComponent throws ArgumentNullException on a null object, which is the opposite of ComponentUtil.Require and the main reason to read this row. Also holds EnsureGameObject, the clone helpers, and the recursive setters. |
GameObjectExtensions | Extensions on GameObject | A subset of GameObjectUtil, in extension form | Same operations, called as enemy.GetBoundingBox() rather than GameObjectUtil.GetBoundingBox(enemy). Pick whichever reads better; the behavior matches except that SetActiveSafe is the extension-form name for the guarded activation. |
TransformExtensions | Extensions on Transform | Coordinate systems, per-axis setters, reset, copy, child destruction | The coordinate helpers are the distinctive part, and all of them take the radius as the first component: polar (r, theta), cylindrical (r, theta, y), spherical (rho, theta, phi) where theta is the angle around Y and phi is measured down from the Y axis. Everything else is the boilerplate that otherwise gets rewritten: resetting a transform, setting one axis, clearing children, finding a descendant by name with FindDeep. |
Recipes
Each recipe stands on its own, though the first two are the ones you’ll reach for most often.
Stop paying for GetComponent every frame
Problem. Your damage system looks up a Health component on whatever it hits, and that happens many times a frame during a firefight. Caching every reference by hand means a field and an Awake assignment for each one, and it does not help when the target is discovered at runtime.
Solution. ComponentUtil.Get<T> answers from a cache keyed on the object, so the second and later lookups skip Unity’s search entirely.
/* First call does the real lookup and caches it; later calls read the cache. Safe to
call on a null object, in which case it returns null rather than throwing. */
var health = hitObject.Get<Health>();
if (health != null)
{
health.ApplyDamage(damage);
}
/* TryGet when the branch reads better as a bool, which is most of the time. */
if (hitObject.TryGet<Shield>(out var shield))
{
shield.Absorb(ref damage);
}
/* Has for a pure existence question, with no reference wanted. */
if (hitObject.Has<Invulnerable>())
{
return;
}
Build a UI hierarchy that survives being set up twice
Problem. Your HUD builds itself from code, and the setup runs again on a scene reload or when the player changes a display option. A second run should reuse what is already there rather than producing a duplicate of every panel.
Solution. The Ensure family returns the existing object if one matches, and creates it otherwise.
/* Returns the existing child named "HealthBar" under the HUD root, or creates it.
resetLocal defaults to true, so a freshly created object starts at identity. */
var healthBarRoot = GameObjectUtil.EnsureGameObject("HealthBar", hudRoot);
/* The component form does the same and guarantees the component is on it, which is
the usual shape for a widget that owns a script. */
var minimap = GameObjectUtil.EnsureGameObjectWithComponent<MinimapView>("Minimap", hudRoot);
/* Require is get-or-add on an object you already have: it returns the existing
component or adds one, and caches the result either way. */
var canvasGroup = healthBarRoot.Require<CanvasGroup>();
canvasGroup.alpha = 0f;
Turn a whole subtree off without deactivating it
Problem. You want a spawned enemy to vanish during a cutscene, but its scripts need to keep running so its AI state and timers stay live. Deactivating the object stops everything.
Solution. SetVisibility toggles the renderers and leaves the object active. EnableColliders does the same for physics.
/* Renderers off, scripts still ticking. The object stays active, so coroutines and
Update keep running and the AI does not lose its place. */
enemy.SetVisibility(false);
/* Colliders separately, for a phase-through or an invulnerability window where the
enemy should still be visible. */
enemy.EnableColliders(false);
/* When you really do want the whole subtree off, the recursive setter walks it.
includeRoot controls whether the object you passed is affected too. */
GameObjectUtil.SetActiveRecursively(bossArena, false, includeRoot: false);
/* And the layer equivalent, which is how a whole model moves onto a render layer
for an outline pass or a first-person view model. */
GameObjectUtil.SetLayerRecursively(viewModel, LayerMask.NameToLayer("ViewModel"));
Frame the camera on an object and its children
Problem. You want to fit the camera to a selected unit, a captured objective, or a spawned prop, and the object’s own transform tells you nothing about how big it actually is.
Solution. The bounds helpers combine the renderers in the subtree into one volume.
/* Combines every renderer under the object, including inactive ones by default,
which matters when part of a model is toggled off. */
if (selectedUnit.TryGetBoundingBox(out var bounds))
{
var framingDistance = bounds.size.magnitude * 1.5f;
cameraRig.FrameOn(bounds.center, framingDistance);
}
/* The non-Try form when you know there is at least one renderer. */
var wholeSquadBounds = squadRoot.GetBoundingBox();
/* Centroid when you want the middle without the extents: a marker position, an
effect origin, or the point a projectile should home toward. */
var aimPoint = selectedUnit.GetCentroid();
Place enemies on a ring without writing the trigonometry
Problem. You want adds to spawn in a circle around the boss, and a camera to orbit at a fixed radius and height. Both are the same conversion written by hand, twice, slightly differently.
Solution. The transform coordinate helpers do the conversion in both directions, in world or local space.
/* Polar is (r, theta): radius first, then the angle in radians around Y. The plain
overload puts the object on the y = 0 plane; the one taking a height keeps the Y
you pass instead. */
foreach (var add in adds)
{
var theta = i / (float) adds.Count * Mathf.PI * 2f;
add.transform.SetPolarPosition(new Vector2(ringRadius, theta), bossArena.position.y);
}
/* Cylindrical is (r, theta, y): the polar pair plus a height, as one value. Right for
a spiral of platforms or a column of orbiting shields. */
shield.transform.SetCylindricalPosition(new Vector3(2f, theta, heightOffset));
/* Spherical is (rho, theta, phi): distance, the angle around Y, and the angle down
from the Y axis. That is the natural way to express an orbit camera, with phi at
Mathf.PI / 2 sitting level with the target. */
cameraTransform.SetSphericalPosition(new Vector3(distance, yaw, pitch));
/* Every setter has a getter, so you can read the current orbit, nudge one component,
and write it back rather than reconstructing the whole position. Radius is x. */
var orbit = cameraTransform.GetSphericalPosition();
orbit.x = Mathf.Lerp(orbit.x, targetDistance, Time.deltaTime * zoomSpeed);
cameraTransform.SetSphericalPosition(orbit);
/* Each family has a local-space pair, for placement relative to a parent rather than
to the world. */
turret.transform.SetPolarLocalPosition(new Vector2(mountRadius, theta));
Nudge one axis without rebuilding the vector
Problem. You want to pin a character’s Y to the ground height while physics owns X and Z. The read-modify-write of a whole Vector3 is three lines for a one-value change.
Solution. The per-axis setters.
/* Sets Y and leaves X and Z alone, without the temporary vector. */
player.transform.SetPositionY(groundHeight);
/* The local equivalents, for a child whose parent is moving. */
weaponMuzzle.SetLocalPositionZ(barrelLength);
/* Reset clears position, rotation and scale in one call, which is what a pooled
object needs before it goes back into circulation. */
pooledEffect.transform.Reset();
/* Or one at a time when only part of the state is stale. */
recoiledWeapon.transform.ResetRotation();
/* CopyFrom takes all three from another transform, for snapping a preview ghost onto
a placement target or aligning a respawned player to a checkpoint. */
respawnedPlayer.transform.CopyFrom(checkpoint.transform);
Find something deep in a hierarchy you did not author
Problem. You need the muzzle transform on an imported weapon model, and it is four levels down under names you cannot change.
Solution. Three search shapes, depending on what you know about where it is.
/* Direct child only, by name. Fastest, and the right choice when the structure is
known and shallow. */
var barrel = weaponModel.GetChild("Barrel");
/* Any depth, by name. includeInactive defaults to true, so a disabled attachment
point is still found. */
var muzzle = GameObjectUtil.FindChildRecursive(weaponModel, "Muzzle_FX");
/* By path from a known root, when the name alone is ambiguous. This is the form to
prefer when the same name appears more than once in the hierarchy. */
var leftHandGrip = GameObjectUtil.FindByPath(characterRoot, "Armature/Spine/LeftArm/Hand/Grip");
/* The transform-side equivalent of the recursive search. */
var socket = characterRoot.transform.FindDeep("WeaponSocket");
Clear a hierarchy between rebuilds
Problem. Your inventory grid rebuilds when the contents change, and the old slot objects need to go first.
Solution. The child-destruction helpers, with the edit-mode variant where you need it.
/* Destroys every child, leaving the parent. The normal play-mode call. */
inventoryGrid.transform.DestroyChildren();
/* The immediate form for editor tooling, where deferred destruction would leave the
old children in place for the rest of the frame. */
inventoryGrid.transform.DestroyChildrenImmediate();
/* GameObjectUtil.Destroy picks the right call for the current mode, so shared code
that runs in both does not have to branch. */
GameObjectUtil.Destroy(temporaryPreview);
Clone a configured object rather than a prefab
Problem. You want another instance of an object exactly as it currently is, including runtime changes that a prefab does not know about.
Solution. The clone helpers wrap Instantiate with the parenting behavior spelled out.
/* worldPositionStays defaults to false, so the clone keeps its local transform
relative to the new parent rather than its world position. That is what you want
for UI and for anything being reparented into a rig. */
var duplicateSlot = GameObjectUtil.Clone(templateSlot, inventoryGrid.transform);
/* The typed form returns the component rather than the GameObject, which saves the
Get call that would otherwise follow immediately. */
var newTurret = GameObjectUtil.Clone(turretTemplate, baseTransform);
API Sketch
All four live in Scylla.Core.Util. ComponentUtil, GameObjectExtensions, and TransformExtensions are extension methods; GameObjectUtil alone is exposed as static calls.
/* ---------------------------------------------------------------- */
/* ComponentUtil: cached component access */
/* ---------------------------------------------------------------- */
T Get<T>(this Component component); // cached; null if absent
T Get<T>(this GameObject gameObject);
Component Get(this Component component, Type type);
Component Get(this GameObject gameObject, Type type);
bool TryGet<T>(this Component component, out T result);
bool TryGet<T>(this GameObject gameObject, out T result);
bool Has<T>(this Component component);
bool Has<T>(this GameObject gameObject);
T Add<T>(this Component component); // ALWAYS adds a new one
T Add<T>(this GameObject gameObject);
Component Add(this Component component, Type type);
Component Add(this GameObject gameObject, Type type);
T Require<T>(this Component component); // get-or-add, cached
T Require<T>(this GameObject gameObject);
Component Require(this Component component, Type type);
Component Require(this GameObject gameObject, Type type);
void Prune(); // drop entries for destroyed objects
void ClearCache();
void ClearCache(GameObject gameObject);
void ClearCache(Component component);
/* ---------------------------------------------------------------- */
/* GameObjectUtil: static, the broadest surface */
/* ---------------------------------------------------------------- */
GameObject EnsureGameObject(string name, Transform parent = null, bool resetLocal = true);
T EnsureGameObjectWithComponent<T>(string name, Transform parent = null, bool resetLocal = true);
T GetOrAddComponent<T>(GameObject obj); // throws on null obj
bool TryGetOrAddComponent<T>(GameObject obj, out T component);
T Clone<T>(T source);
GameObject Clone(GameObject source, Transform parent = null, bool worldPositionStays = false);
T Clone<T>(T source, Transform parent, bool worldPositionStays = false);
GameObject GetChild(GameObject gameObject, string childName);
GameObject FindChildRecursive(GameObject gameObject, string childName, bool includeInactive = true);
GameObject FindByPath(GameObject root, string path);
bool TryGetBoundingBox(GameObject obj, out Bounds bounds, bool includeInactive = true);
Bounds GetBoundingBox(GameObject obj, bool includeInactive = true);
Vector3 GetCentroid(GameObject obj, bool includeInactive = true);
bool IsActiveSelf(GameObject obj);
bool IsActiveInHierarchy(GameObject obj);
void SetActive(GameObject obj, bool status);
void SetActiveRecursively(GameObject obj, bool status, bool includeRoot = true);
void SetVisibility(GameObject obj, bool status, bool includeInactive = true);
void EnableColliders(GameObject obj, bool status, bool includeInactive = true);
void SetLayerRecursively(GameObject obj, int layer);
void Destroy(Object obj, bool allowDestroyingAssets = false);
void DestroyChildren(Transform parent, bool immediate = false);
/* ---------------------------------------------------------------- */
/* GameObjectExtensions: the same, in extension form */
/* ---------------------------------------------------------------- */
GameObject GetChild(this GameObject gameObject, string childName);
Bounds GetBoundingBox(this GameObject gameObject, bool includeInactive = true);
bool TryGetBoundingBox(this GameObject gameObject, out Bounds bounds, bool includeInactive = true);
Vector3 GetCentroid(this GameObject gameObject, bool includeInactive = true);
T GetOrAddComponent<T>(this GameObject gameObject);
void SetVisibility(this GameObject gameObject, bool status, bool includeInactive = true);
void EnableColliders(this GameObject gameObject, bool status, bool includeInactive = true);
void SetActiveSafe(this GameObject gameObject, bool status);
void SetActiveRecursively(this GameObject gameObject, bool status, bool includeRoot = true);
void SetLayerRecursively(this GameObject gameObject, int layer);
bool IsActiveSelf(this GameObject gameObject);
bool IsActiveInHierarchy(this GameObject gameObject);
/* ---------------------------------------------------------------- */
/* TransformExtensions */
/* ---------------------------------------------------------------- */
/* Coordinate systems, world and local, get and set. */
Vector3 GetCylindricalPosition(this Transform transform); // (r, theta, y)
void SetCylindricalPosition(this Transform transform, Vector3 cylindrical);
Vector3 GetCylindricalLocalPosition(this Transform transform);
void SetCylindricalLocalPosition(this Transform transform, Vector3 cylindrical);
Vector3 GetSphericalPosition(this Transform transform); // (rho, theta, phi)
void SetSphericalPosition(this Transform transform, Vector3 spherical);
Vector3 GetSphericalLocalPosition(this Transform transform);
void SetSphericalLocalPosition(this Transform transform, Vector3 spherical);
Vector2 GetPolarPosition(this Transform transform); // (r, theta)
void SetPolarPosition(this Transform transform, Vector2 polar);
void SetPolarPosition(this Transform transform, Vector2 polar, float height);
Vector2 GetPolarLocalPosition(this Transform transform);
void SetPolarLocalPosition(this Transform transform, Vector2 polar);
void SetPolarLocalPosition(this Transform transform, Vector2 polar, float height);
/* Reset, copy, per-axis, children. */
void Reset(this Transform transform);
void ResetPosition(this Transform transform);
void ResetRotation(this Transform transform);
void ResetScale(this Transform transform);
void CopyFrom(this Transform transform, Transform source);
void CopyLocalFrom(this Transform transform, Transform source);
void SetPositionX(this Transform transform, float x);
void SetPositionY(this Transform transform, float y);
void SetPositionZ(this Transform transform, float z);
void SetLocalPositionX(this Transform transform, float x);
void SetLocalPositionY(this Transform transform, float y);
void SetLocalPositionZ(this Transform transform, float z);
void DestroyChildren(this Transform transform);
void DestroyChildrenImmediate(this Transform transform);
Transform FindDeep(this Transform transform, string name);
RectTransform SetupRectTransform(this Transform transform, Vector2 anchor,
float x = 0f, float y = 0f, float width = 0f, float height = 0f);
See the API reference for full signatures, remarks, and exception contracts on every member.
Best Practices
- Prefer
Get<T>toGetComponent<T>in anything that runs per frame. The cache is the whole point, and it removes the need to hand-cache a reference inAwakefor every component you touch. - Use
Require, notAdd, when you mean get-or-add.Addalways adds, and on a type that allows more than one instance, that silently leaves duplicates behind. - Know which get-or-add you are calling.
ComponentUtil.Requirereturnsnullon a null object;GameObjectUtil.GetOrAddComponentthrowsArgumentNullException. Pick the one whose failure mode suits the call site rather than whichever comes to mind. - Call
Pruneafter bulk destruction, not every frame. It scans the whole cache, so it belongs between waves or after a large teardown. Scene unloads prune automatically. - Reach for
SetVisibilitywhen the object still needs to think. Deactivating stops coroutines andUpdate; hiding the renderers does not, which suits a cutscene or a stealth cloak that needs to keep working while invisible. - Remember
layeris not recursive in Unity.SetLayerRecursivelyexists because setting the property on a root leaves every child where it was, and the symptom is a render pass quietly missing half a model. - Use
TryGetBoundingBoxfor anything that might have no renderers. Pooled entries, logic-only objects and prefabs with streamed visuals all hit that case, and theTryform makes it a branch rather than a surprise. - Prefer
FindByPathover a recursive name search when names repeat. A recursive search returns the first match in traversal order, which is not stable against someone reordering the hierarchy. - Use the coordinate helpers instead of hand-rolled trigonometry. Ring spawns, orbit cameras and dome placements are all the same conversion, and having it in one place means the sign conventions match everywhere.
- Reset pooled objects on release, not on acquire. A pooled transform that is reset when it goes back is clean for whatever takes it next, and
Resetcovers all three properties in one call. See Object Pools. - Use
EnsureGameObjectfor setup that can run twice. Scene reloads, display-option changes and editor tooling all re-enter setup code, and the idempotent form stops a second run from duplicating the hierarchy. - Pick the static or extension form for readability, not behavior. They match, apart from the null handling noted above, so choose whichever makes the call site read better.
Pitfalls
Related
- 01 Scylla Core
- 33 Core Utilities – Geometry
- 22 Core Structures – Object Pools
- 08 Core Architecture – Bootstrap Lifecycle
- API reference:
Scylla.Core.Util.GameObjectUtil - API reference:
Scylla.Core.Util.ComponentUtil - API reference:
Scylla.Core.Util.TransformExtensions - API reference:
Scylla.Core.Util.GameObjectExtensions