Skip to main content

πŸ“ 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 GetComponentInChildren and GetComponentInParent
  • Expose a private field in the Inspector with [SerializeField] β€” and know why that beats public
  • 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:

PropertyGives youInherited from
gameObjectThe GameObject this component is attached toComponent
transformThat GameObject's Transform (position/rotation/scale + hierarchy)Component

Everything else you reach by one of three strategies, in rough order of preference:

graph TD Q["I need another component…"] --> A["On the SAME GameObject?
➜ 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.

MethodSearchesTypical use
GetComponent<T>()This GameObject onlySibling components on the same object
GetComponentInChildren<T>()This object and all descendantsFind an Animator or Collider on a child model
GetComponentInParent<T>()This object and its ancestorsA child part finding the root Player it belongs to
GetComponentsInChildren<T>()All descendants β€” returns an arrayDisable every Renderer under an object
graph TD P["Player (root)
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: the InChildren/InParent versions include the object they're called on, not just its relatives. And they return the first match found in the hierarchy β€” use the plural GetComponentsInChildren when 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 before Awake runs β€” Unity deserializes it as the object loads. So you can safely use [SerializeField] references starting in Awake. (The counterpart [HideInInspector] public does 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 to null even though the C# reference technically still exists. That's why if (target != null) is the right guard even after something was Destroy-ed. It also means you should avoid the C# null-conditional ?. on Unity objects in edge cases β€” a plain != null check 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:

  1. Reuse the Health component from Lesson 1.1 (with a public TakeDamage(int)). Put it on a GameObject named Enemy.
  2. Same object: add a Suicidal script to Enemy that, on Start, calls GetComponent<Health>() and deals 5 damage to itself.
  3. Inspector wiring: add a Trap script with a [SerializeField] private Health target; field; drag the Enemy's Health into that slot and damage it from Start.
  4. Guarded runtime lookup: add a DamageZone script whose method takes a GameObject and uses TryGetComponent<Health> to damage it only if it has Health.
  5. 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 gameObject and transform for free from Component.
  • GetComponent<T>() reaches a sibling on the same object; GetComponentInChildren/InParent search 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] private exposes a field in the Inspector without making it public β€” prefer it over public for wiring; wired references are set before Awake.
  • Prefer wiring in the Inspector over GameObject.Find; if you must search, do it once in Awake/Start and cache β€” never in Update.
  • Guard lookups against null; remember Unity's overloaded == treats a destroyed object as null.

πŸ“š Additional Resources

πŸš€ 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.