TryGetComponentとは?

TryGetComponentは、GameObjectにアタッチされたコンポーネントを安全に取得するためのメソッドです。

従来のGetComponentでは「取得してからnullチェックする」という書き方が定番でしたが、TryGetComponentを使うと取得の成否をboolで返してくれるので、if文と組み合わせて自然な流れで書けます。

Unity 2019.2から追加されたメソッドで、現行のUnity 6でも標準的なAPIとして利用できます。

基本的な使い方

TryGetComponentout変数と組み合わせて使うのが基本です。

コンポーネントが見つかればtrueを返し、out引数に取得結果が入ります。見つからなければfalseを返し、out引数はnullになります。

using UnityEngine;

public class Sample : MonoBehaviour
{
    private void Start()
    {
        // コンポーネントの取得を試みる
        if (TryGetComponent(out Rigidbody body))
        {
            // 取得できた場合のみ処理を行う
            // linearVelocityはUnity 6以降の書き方。それ以前のバージョンではvelocity
            body.linearVelocity = Vector3.up;
        }
    }
}

out変数の宣言をif文の中に書けるので、変数のスコープを必要な範囲に絞れるのがポイントです。

他のGameObjectを対象に呼び出すこともできます。

private void OnCollisionEnter(Collision other)
{
    // 衝突相手からRigidbodyを取得
    if (other.gameObject.TryGetComponent(out Rigidbody body))
    {
        body.AddForce(Vector3.up * 10f, ForceMode.Impulse);
    }
}

GetComponentとの違い

機能面では、GetComponentの結果をnullチェックするのとほぼ同じ結果が得られます。

ただし内部実装に違いがあり、書き方の見た目も変わってきます。

同じ「Rigidbodyが付いていれば速度をリセットする」という処理を書いてみると違いがよく分かります。

// GetComponent + nullチェック
var body = GetComponent<Rigidbody>();
if (body != null)
{
    body.linearVelocity = Vector3.zero;
}

// TryGetComponent
if (TryGetComponent(out Rigidbody body))
{
    body.linearVelocity = Vector3.zero;
}

行数自体はほとんど変わりませんが、TryGetComponentの方が「取得を試みる」という意図がそのままコードに表れます。

Editor上のGC Allocが発生しない

GetComponentでは、対象のコンポーネントが見つからなかった場合にEditor上でGC Allocが発生することがあります。

これは、対象のコンポーネントが存在しないケースでも、内部でC#側のmanaged wrapperオブジェクトを生成してしまうのが原因です。

TryGetComponentは、このラッパー生成自体を避けるように設計されています。

取得に失敗した場合でも余計なメモリ確保が発生しないため、Editor上でのアロケーションを抑えられます。

ちなみに、ビルド後のアプリでは元々GC Allocが発生しないので、この差は主に開発中のEditor上で意味を持ちます。

実用例:当たり判定で安全にRigidbodyを操作する

衝突時に相手のRigidbodyを操作したいケースは多いと思いますが、相手が必ずしもRigidbodyを持っているとは限りません。

こういう場面でTryGetComponentが活躍します。

using UnityEngine;

public class Bumper : MonoBehaviour
{
    [SerializeField] private float _power = 10f;

    private void OnCollisionEnter(Collision collision)
    {
        // 相手にRigidbodyが付いていれば力を加える
        if (collision.gameObject.TryGetComponent(out Rigidbody body))
        {
            var direction = collision.transform.position - transform.position;
            body.AddForce(direction.normalized * _power, ForceMode.Impulse);
        }
    }
}

if文の中だけでbodyを扱えるので、「nullじゃないことを確認したあと使う」というガード処理を一行で書けます。

インターフェイスを取得する用途でも使えます。

GetComponent<T>()はジェネリック型引数にインターフェイスを指定できますが、TryGetComponentでも同じことができます。

using UnityEngine;

public interface IDamageable
{
    void TakeDamage(int amount);
}

public class DamageArea : MonoBehaviour
{
    [SerializeField] private int _damage = 10;

    private void OnTriggerEnter(Collider other)
    {
        // 相手がIDamageableを実装していればダメージを与える
        if (other.TryGetComponent(out IDamageable damageable))
        {
            damageable.TakeDamage(_damage);
        }
    }
}

注意点

TryGetComponentは便利ですが、過信は禁物です。

実行時の検索コストは同じ

TryGetComponentは内部でGetComponentと同じ仕組みで検索を行います。

書き方が変わっただけで、コンポーネントを探す処理自体が軽くなるわけではありません。

Updateの中で毎フレーム呼ぶような使い方をすると、GetComponentと同様にパフォーマンスへの影響が出ます。

頻繁に使う参照はAwakeStartでキャッシュするか、SerializeFieldで事前に設定しておくのが基本方針です。

public class Player : MonoBehaviour
{
    private Rigidbody _body;

    private void Awake()
    {
        // 起動時に一度だけ取得してキャッシュする
        // 戻り値を確認し、見つからない場合は警告を出しておく
        if (!TryGetComponent(out _body))
        {
            Debug.LogWarning($"{name}にRigidbodyが付いていません", this);
        }
    }

    private void FixedUpdate()
    {
        // キャッシュ済みの参照を使う
        _body.AddForce(Vector3.forward);
    }
}

「必ず存在することが保証されている」前提なら、RequireComponentを付けたうえでGetComponentを使う方が意図が明快です。

TryGetComponentは「存在しない可能性がある」ケースに向いています。

最適化の主役はEditor上の挙動

前述の通り、TryGetComponentのメリットはEditor上でGC Allocが発生しない点にあります。

ビルド後のランタイム性能を大きく改善するわけではないので、「TryGetComponentに置き換えるだけで速くなる」というのは誤解です。

「読みやすく、開発中も余計なメモリ確保が起きない書き方」として採用するのが現実的かなと思います。

まとめ

今回はTryGetComponentについて紹介しました。

GetComponentnullチェックの組み合わせを、out変数で一行にまとめられる書き方です。

コードの意図がはっきりするうえ、Editor上でのGC Allocも抑えられるので、衝突判定など「取得できないこともある」場面では積極的に使っていきたいメソッドかなと思います。

ただし実行時の検索コスト自体はGetComponentと変わらないので、頻繁に呼ぶ場合のキャッシュやSerializeFieldでの事前設定といった基本的な対策は引き続き有効です。場面に応じて使い分けていきましょう。