π Lesson 1.3: Accessing GameObjects and Components
A component that can't reach anything else can't do much. This lesson is about wiring: how one component gets a handle on another β on the same object, on a child, or across the scene β using GetComponent, direct references, and the [SerializeField] attribute that turns a field into an Inspector slot.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Use
GetComponent<T>()to reach another component on the same GameObject - Choose between
GetComponent,TryGetComponent, and a cached reference - Search relatives with
GetComponentInChildrenandGetComponentInParent - Expose a private field in the Inspector with
[SerializeField]β and know why that beatspublic - Wire references in the Editor and understand when to search in code instead
- Avoid the "missing component"
NullReferenceException
Estimated Time: 60 minutes
Project: Build a DamageOnContact component that finds and calls a Health component, wired three different ways.
In This Lesson
The Wiring Problem
In Lesson 1.1 we built a Health component and a Shooter, and said "the Shooter finds the target's Health and calls TakeDamage." That word β finds β is the whole subject of this lesson. Composition only pays off if components can locate each other.
From Lesson 1.1 you already have two references handed to you for free by the base classes:
| Property | Gives you | Inherited from |
|---|---|---|
gameObject | The GameObject this component is attached to | Component |
transform | That GameObject's Transform (position/rotation/scale + hierarchy) | Component |
Everything else you reach by one of three strategies, in rough order of preference:
β GetComponent"] Q --> B["On a child or parent?
β GetComponentInChildren / InParent"] Q --> C["A specific object elsewhere?
β a serialized reference (wire it in the Inspector)"] style A fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px style B fill:#eff6ff,stroke:#3b82f6,stroke-width:2px style C fill:#f3e8ff,stroke:#8b5cf6,stroke-width:2px
GetComponent on the Same Object
GetComponent<T>() asks the GameObject, "do you also have a component of type T?" and returns it β or null if there isn't one. It's the workhorse for components that live together on one object.
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public class Jumper : MonoBehaviour
{
private Rigidbody body; // cache the reference
void Awake()
{
// Grab the Rigidbody attached to THIS same GameObject.
body = GetComponent<Rigidbody>();
}
void Jump()
{
body.AddForce(Vector3.up * 5f, ForceMode.Impulse);
}
}
π Definition
[RequireComponent(typeof(Rigidbody))] is an attribute that tells Unity this script needs a Rigidbody. Unity will auto-add one when the script is attached and stop you from removing it β so GetComponent<Rigidbody>() can't come back null. Use it whenever your script depends on another component being present.
The safer variant: TryGetComponent
When a component might not be there, TryGetComponent<T>(out T) avoids both a null check and (in editor) a harmless-but-noisy allocation when nothing is found. It returns a bool and follows the classic C# Try... pattern you know from int.TryParse:
// Only heal if this object actually has a Health component.
if (TryGetComponent<Health>(out Health health))
{
health.Heal(10);
}
β οΈ Watch Out: GetComponent is not free
Each call does a lookup across the object's components. Calling it every frame in Update is a classic performance mistake. Call it once in Awake/Start and store the result in a field (we cover why in Lesson 5.1). The pattern above β cache in Awake β is the habit to build now.
Reaching Children & Parents
Complex objects are usually a hierarchy: a Player GameObject with child objects for the model, a weapon, and a health bar. Two variants search that hierarchy instead of just the one object.
| Method | Searches | Typical use |
|---|---|---|
GetComponent<T>() | This GameObject only | Sibling components on the same object |
GetComponentInChildren<T>() | This object and all descendants | Find an Animator or Collider on a child model |
GetComponentInParent<T>() | This object and its ancestors | A child part finding the root Player it belongs to |
GetComponentsInChildren<T>() | All descendants β returns an array | Disable every Renderer under an object |
PlayerController"] P --> M["Model
Animator"] P --> W["Weapon
Collider"] M --> Mesh["MeshRoot
Renderer(s)"] style P fill:#eff6ff,stroke:#3b82f6,stroke-width:2px style M fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px
public class PlayerController : MonoBehaviour
{
private Animator animator;
void Awake()
{
// The Animator lives on the child 'Model', not on the Player root.
animator = GetComponentInChildren<Animator>();
}
}
// A child weapon script finding the Player it belongs to:
public class Weapon : MonoBehaviour
{
private PlayerController owner;
void Awake()
{
owner = GetComponentInParent<PlayerController>();
}
}
π‘ Note: theInChildren/InParentversions include the object they're called on, not just its relatives. And they return the first match found in the hierarchy β use the pluralGetComponentsInChildrenwhen you want them all.
[SerializeField] & Inspector Wiring
Searching in code is great for relatives, but for a reference to a specific object elsewhere in the scene β "this door's key", "the camera to follow" β the cleanest approach is to expose a slot in the Inspector and drag the object in. No lookup code at all.
In Lesson 1.1 we made fields public so they'd show in the Inspector. That works, but it breaks encapsulation β now any other script can modify the field too. The idiomatic Unity solution is the [SerializeField] attribute: it shows a private field in the Inspector without making it public.
using UnityEngine;
public class CameraFollow : MonoBehaviour
{
// Shows in the Inspector as a slot, but stays private to other code.
[SerializeField] private Transform target;
[SerializeField] private float height = 5f; // also tunable in Inspector
void LateUpdate()
{
if (target != null)
transform.position = target.position + new Vector3(0f, height, -10f);
}
}
β public vs [SerializeField]
Rule of thumb: use [SerializeField] private for anything you want to set in the Inspector but keep other scripts out of. Reserve public for members that genuinely form your component's API for other code to use (like TakeDamage). "Visible in Inspector" and "accessible from other classes" are two different needs β [SerializeField] lets you ask for just the first.
| Shows in Inspector? | Other scripts can access? | |
|---|---|---|
public T field; | β Yes | β Yes (even by accident) |
[SerializeField] private T field; | β Yes | β No β encapsulated |
private T field; | β No | β No |
β οΈ Important: A serialized reference wired in the Inspector is set beforeAwakeruns β Unity deserializes it as the object loads. So you can safely use[SerializeField]references starting inAwake. (The counterpart[HideInInspector] publicdoes the opposite: public to code, hidden from the Inspector.)
Finding Objects Across the Scene
Sometimes you need an object you can't wire in the Inspector β perhaps it's spawned at runtime. Unity offers scene-wide search methods, but they come with a health warning.
// By name (searches the whole active scene β slow, brittle to renames):
GameObject player = GameObject.Find("Player");
// By tag (faster than Find, still a search):
GameObject respawn = GameObject.FindWithTag("Respawn");
// Find a component of a type anywhere in the scene (Unity 2023+ API name):
Camera cam = Object.FindFirstObjectByType<Camera>();
β οΈ Watch Out: prefer wiring over finding
GameObject.Find searches every object by string name β it's slow, and a typo or rename silently returns null. Never call these in Update. If you must use them, do it once in Awake/Start and cache the result. Better still: expose a [SerializeField] reference and drag the object in β it's faster, refactor-safe, and visible to anyone reading the Inspector.
π‘ The preference ladder
From best to last-resort: 1. A [SerializeField] reference wired in the Inspector β 2. GetComponent/InChildren/InParent for relatives β 3. a manager/singleton you register with (Lesson 4.1) β 4. Find/FindWithTag only when nothing else fits, cached once.
Caching & Avoiding Null
Two habits prevent the vast majority of "it compiled but crashed at runtime" bugs in Unity scripting.
1. Cache once, reuse forever
Look up a component once in Awake/Start, store it in a field, and use the field everywhere else. This is faster and makes your dependencies obvious at the top of the class.
2. Assume lookups can fail
Any lookup that isn't guaranteed (no [RequireComponent], no wired Inspector slot) can return null. Reading a member off null throws a NullReferenceException β the most common runtime error in Unity. Guard it:
public class DamageOnContact : MonoBehaviour
{
[SerializeField] private int damage = 10;
private void ApplyTo(GameObject other)
{
// TryGetComponent gives us a clean guarded call β no separate null check.
if (other.TryGetComponent<Health>(out Health health))
{
health.TakeDamage(damage);
}
// If there's no Health, we simply do nothing β no crash.
}
}
π‘ Unity's special null: Unity overloads==so that a destroyed object compares equal tonulleven though the C# reference technically still exists. That's whyif (target != null)is the right guard even after something wasDestroy-ed. It also means you should avoid the C# null-conditional?.on Unity objects in edge cases β a plain!= nullcheck is the safe idiom.
β Pro Tip
When a required reference is missing, fail loudly during development: if (target == null) Debug.LogError("CameraFollow: target not assigned", this);. Passing this as the second argument makes clicking the console message ping the offending object in the Hierarchy.
Exercise & Quiz
ποΈ Exercise: Wire Up Damage Three Ways
Objective: Reach a Health component using each of the three strategies, and feel when each one fits.
Instructions:
- Reuse the
Healthcomponent from Lesson 1.1 (with a publicTakeDamage(int)). Put it on a GameObject namedEnemy. - Same object: add a
Suicidalscript toEnemythat, onStart, callsGetComponent<Health>()and deals 5 damage to itself. - Inspector wiring: add a
Trapscript with a[SerializeField] private Health target;field; drag the Enemy's Health into that slot and damage it fromStart. - Guarded runtime lookup: add a
DamageZonescript whose method takes aGameObjectand usesTryGetComponent<Health>to damage it only if it has Health. - Answer in a comment: which of the three would keep working if you renamed the Enemy GameObject? (All three β none rely on the name. Now try it with
GameObject.Find("Enemy")and rename it to see it break.)
Starter Code:
using UnityEngine;
public class Trap : MonoBehaviour
{
[SerializeField] private Health target; // drag Enemy's Health here
void Start()
{
// TODO: if target is assigned, deal 5 damage to it.
}
}
π‘ Hint
For the same-object case, cache GetComponent<Health>() in a field in Awake, then call TakeDamage in Start. For the guarded lookup, remember TryGetComponent returns a bool and gives you the component via out, so no separate null check is needed.
β Solution
using UnityEngine;
// 1) Same object
public class Suicidal : MonoBehaviour
{
private Health health;
void Awake() => health = GetComponent<Health>();
void Start() => health.TakeDamage(5);
}
// 2) Inspector-wired reference
public class Trap : MonoBehaviour
{
[SerializeField] private Health target;
void Start()
{
if (target != null)
target.TakeDamage(5);
else
Debug.LogError("Trap: target not assigned", this);
}
}
// 3) Guarded runtime lookup
public class DamageZone : MonoBehaviour
{
[SerializeField] private int damage = 10;
public void ApplyTo(GameObject other)
{
if (other.TryGetComponent<Health>(out Health health))
health.TakeDamage(damage);
}
}
Notice none of the three mention the GameObject's name β renaming Enemy changes nothing. Only GameObject.Find("Enemy") would break on a rename, which is exactly why the wired reference is preferred.
π― Quick Quiz
Question 1: You want a private field to appear in the Inspector without letting other scripts modify it. What do you use?
Question 2: Why should you avoid calling GetComponent (or GameObject.Find) inside Update?
Question 3: The Animator is on a child of the object your script sits on. Which call retrieves it?
Summary
π Key Takeaways
- Every component gets
gameObjectandtransformfor free fromComponent. GetComponent<T>()reaches a sibling on the same object;GetComponentInChildren/InParentsearch the hierarchy (and include the object itself).TryGetComponent<T>(out T)is the clean, guarded way to handle a component that might not exist.[SerializeField] privateexposes a field in the Inspector without making it public β prefer it overpublicfor wiring; wired references are set beforeAwake.- Prefer wiring in the Inspector over
GameObject.Find; if you must search, do it once inAwake/Startand cache β never inUpdate. - Guard lookups against
null; remember Unity's overloaded==treats a destroyed object asnull.
π Additional Resources
- Scripting Reference β GetComponent
- Scripting Reference β TryGetComponent
- Scripting Reference β SerializeField
- Scripting Reference β RequireComponent
π What's Next?
That completes Module 1 β Unity's C# Model: you understand components, the lifecycle, and how to wire objects together. In Module 2 we put it to work β starting with Lesson 2.1, Input and Movement, where you'll read the player's input and move a Transform every frame.
π Module 1 complete!
You can now build objects out of components, control when they run, and connect them together. That's the entire foundation of Unity scripting β everything else builds on it.