GameObjectとComponent入門:Player→Enemyに攻撃してHPを減らす(Input System対応・初学者向け)

ねらい:Unityで最も大事な「GameObject と Component の関係」と、「別オブジェクトのコンポーネントへアクセスする方法」を、超最小サンプル(Player が Space で Enemy の HP を減らす)で理解します。Unity 6.2(Input System有効)を前提にします。


TL;DR(最短まとめ)

  • GameObject は“入れ物”、Component は“機能(振る舞い)”。
  • スクリプトは MonoBehaviour として Component になる(Transform は必ず1つ付く)。
  • コンポーネント取得の基本:
    • 自分のオブジェクト … GetComponent<T>()
    • 子/親 … GetComponentInChildren<T>() / GetComponentInParent<T>()
    • 別オブジェクト … 参照を保持(SerializeField) が基本。応急処置で GameObject.Find も可だが常用しない。
  • 入力は Input System:Keyboard.current.spaceKey.wasPressedThisFrame で Space を検出。

ゴール

  • Player が Space を押すと、Enemy の EnemyController.Hp が 10 減る
  • その過程で GameObject ⇄ Component の関係と、アクセスの方法 を理解する。

前提・環境

  • Unity 6.2(新規テンプレートは Input System が入っている想定)
  • 添付スクリーンショットのような空の 2D プロジェクトでOK
  • 確認ポイント:Edit > Project Settings > Player > Active Input Handling が Input System もしくは Both

用語の整理(最短理解)

  • GameObject:シーン上の“物体”。見た目/当たり判定/スクリプト等は Component が担当。
  • Component:GameObject に“差す”機能。例:Transform、SpriteRenderer、Collider、(自作)PlayerController
  • MonoBehaviour:自作スクリプトが継承する基底クラス。Add Component で GameObject に付けて使う。
Scene
├─ Player  (GameObject)
│   └─ PlayerController (Component: MonoBehaviour)
└─ Enemy   (GameObject)
    └─ EnemyController  (Component: MonoBehaviour)

1. 最小サンプルの作成手順

  1. Hierarchy で Create > Create Empty → 名前を Player
  2. 同様に空オブジェクトを作成 → 名前を Enemy
  3. Project で Scripts フォルダを作り、以下の 2 スクリプトを置く:

EnemyController.cs

using UnityEngine;

public class EnemyController : MonoBehaviour
{
    public int Hp = 100;
}

PlayerController.cs(Input System を使用)

using UnityEngine;
using UnityEngine.InputSystem; // Input System

public class PlayerController : MonoBehaviour
{
    public int Hp = 100;

    void Update()
    {
        // Spaceキーがこのフレームで押されたか?
        if (Keyboard.current != null && Keyboard.current.spaceKey.wasPressedThisFrame)
        {
            Attack();
        }
    }

    public void Attack()
    {
        // 超入門:名前で相手を探して、そのコンポーネントに触る
        var enemyGo = GameObject.Find("Enemy");
        if (enemyGo == null)
        {
            Debug.LogError("Enemy がシーンに見つかりません");
            return;
        }
        enemyGo.GetComponent<EnemyController>().Hp -= 10;
        Debug.Log($"Enemy HP を10減らした → {enemyGo.GetComponent<EnemyController>().Hp}");
    }
}
// Spaceキーがこのフレームで押されたか?
if (Keyboard.current != null && Keyboard.current.spaceKey.wasPressedThisFrame)
{
    Attack();
}

何をしている?

  1. Keyboard.current != null
    • 新Input Systemの“いま使えるキーボード”デバイスへの参照が 存在するか を確認しています。
    • ない環境(例:物理キーボードが無い端末)や入力が無効なら null になり得るので、ぬるぽ防止です。
  2. Keyboard.current.spaceKey
    • そのキーボード上の Spaceキーを表す KeyControl です。
    • 旧APIでいう KeyCode.Space に相当する“キーそのもの”のオブジェクト。
  3. .wasPressedThisFrame
    • このフレーム中に「離れていた→押された」状態遷移が起きたか(立ち上がり)を返す真偽値。
    • つまり 押した瞬間の1フレームだけ true。押しっぱなしでも次フレーム以降は false に戻ります。
  4. Attack();
    • 上記が true のその瞬間だけ呼ばれるので、1回のキー押下につき1回だけ攻撃が実行されます。

旧Inputとの対応表(覚えやすい)

  • Input.GetKeyDown(KeyCode.Space) ⇔ Keyboard.current.spaceKey.wasPressedThisFrame(押した瞬間)
  • Input.GetKey(KeyCode.Space) ⇔ Keyboard.current.spaceKey.isPressed(押している間ずっと)
  • Input.GetKeyUp(KeyCode.Space) ⇔ Keyboard.current.spaceKey.wasReleasedThisFrame(離した瞬間)

「フレーム」って?

  • 通常は Update() が1回呼ばれる間を1フレームと捉えます。
  • wasPressedThisFrame は そのフレームの間にだけ true になるため、Update() 内で1回だけ実行されます。

よくある疑問・注意

  • なぜ null チェックが必要?一部のプラットフォームや設定でキーボードデバイスが存在しないと Keyboard.current は null。このままアクセスすると NullReferenceException になります。
  • 長押しで連続攻撃したいisPressed を使って「押されている間タイマーで連射」などのロジックを足します。例:一定間隔で Attack() を呼ぶ。
  • ゲームパッドにも対応したい直接キーを見る方式はキーボード限定です。クロスデバイス対応は InputAction / PlayerInput を使い、<Keyboard>/space と <Gamepad>/south を同じアクションにバインドするのが定石です。
  • FixedUpdate との関係入力は通常 Update() で扱います(デフォルト設定では入力処理は Dynamic Update)。物理演算は FixedUpdate()。

この1行(+nullチェック)で、旧 GetKeyDown と同じ“押した瞬間だけ”の挙動を、新Input Systemで安全に再現できています。

  1. Player に PlayerController を、Enemy に EnemyController を Add Component
  2. Play を押し、Space を押す → Console に HP が減るログが出れば成功。

まずは“動く実感”を得るのが目的。ここから“より良い書き方”に発展させます。


2. なぜそのコードで動くのか(関係の見える化)

  • GameObject.Find(“Enemy") は、シーン内で名前が Enemy の GameObject を1つ返す。
  • GetComponent<EnemyController>() は、その GameObject に 差さっている EnemyController コンポーネント を返す。
  • 返ってきた EnemyController の フィールド Hp を直接書き換えている(-= で 10 減算)。

つまり、**「物体(Enemy)を見つける」→「機能(EnemyController)を取り出す」→「そのデータ(Hp)を変更」**という流れです。


3. アクセスの基本パターン(覚えておくと一生得)

3-1. 同じ GameObject の別コンポーネントに触る

var rb = GetComponent<Rigidbody>(); // 自分に付いている Rigidbody
  • 最速・最短。同一オブジェクトなら常にこれ。

3-2. 子や親に触る

var colInChild  = GetComponentInChildren<Collider2D>();
var rbInParent  = GetComponentInParent<Rigidbody>();

結論:どちらも「自分自身」を含みます。

  • GetComponentInChildren()
    • まず 自分のGameObject を調べ、見つからなければ 子階層をたどって最初に見つかった T を返します。
    • 既定では 非アクティブの子は無視(includeInactive:false 相当)。非アクティブも対象にしたいときは 配列版の GetComponentsInChildren<T>(true) を使います。
  • GetComponentInParent()
    • まず 自分のGameObject を調べ、見つからなければ 親→親の親… と上にたどって最初に見つかった T を返します。
    • 非アクティブの親も含めたい/制御したい場合は 配列版の GetComponentsInParent<T>(includeInactive:true) を使います。

「自分は除外して子(or 親)だけ欲しい」場合の書き方例

// 子階層から“自分以外”の最初の Collider2D を取りたい
var colOnlyChild = GetComponentsInChildren<Collider2D>(false)
                    .FirstOrDefault(c => c.gameObject != gameObject);

// 親階層から“自分以外”の最初の Rigidbody を取りたい
var rbOnlyParent = GetComponentsInParent<Rigidbody>(false)
                    .FirstOrDefault(r => r.gameObject != gameObject);

補足

  • 単数系(GetComponentInChildren, GetComponentInParent)は 「最初に見つかった1個」 を返します。複数の候補がある設計なら配列版で明示的に選別しましょう。
  • 非アクティブを含める/除外する制御は 配列版の includeInactive 引数で行うのが確実です。
  • 親子関係が確定しているときに有効。

3-3. 別オブジェクトに触る(推奨:参照を保持)

[SerializeField] private EnemyController enemy; // Inspectorでアサイン

void Awake()
{
    // 万一未設定なら、最後の手段で探す(1回だけ)
    if (!enemy) enemy = GameObject.Find("Enemy")?.GetComponent<EnemyController>();
}

public void Attack()
{
    if (!enemy) { Debug.LogError("enemy 未設定"); return; }
    enemy.Hp -= 10;
}
  • 実運用は“事前に参照を持つ”のが正解。Find は高コスト・脆い(名前変更で壊れる)。
  • 探すなら Awake/Start で1回だけUpdate 内で探さない(毎フレーム重い)。

3-4. 型やタグで探す(シーンに1体なら)

var enemy = FindObjectOfType<EnemyController>(); // 型で探索(1体想定)
var target = GameObject.FindWithTag("Enemy");    // タグで探索
  • 複数体になると 曖昧。将来拡張を考えると参照保持がベター。

4. Input System(最小)

  • 旧 Input.GetKeyDown の置き換えとして、ポーリングで次の1行が使えます。
if (Keyboard.current.spaceKey.wasPressedThisFrame) { Attack(); }
  • アクションアセットや PlayerInput を使う方式はイベント駆動で拡張性が高いですが、初学者はまず キーボードAPIで“押された瞬間”を取るところから入るのが分かりやすいです。

5. より良い設計への一歩(安全・拡張に強く)

5-1. Null に強いガード

public void Attack()
{
    var enemyGo = GameObject.Find("Enemy");
    if (!enemyGo) { Debug.LogError("Enemy が見つからない"); return; }

    var ec = enemyGo.GetComponent<EnemyController>();
    if (!ec) { Debug.LogError("EnemyController が付いていない"); return; }

    ec.Hp = Mathf.Max(0, ec.Hp - 10);
    Debug.Log($"Enemy HP: {ec.Hp}");
}

5-2. データをメソッドで守る(カプセル化)

public class EnemyController : MonoBehaviour
{
    [SerializeField] int hp = 100;
    public int Hp => hp; // 読み取り専用公開

    public void Damage(int value)
    {
        hp = Mathf.Max(0, hp - Mathf.Abs(value));
        Debug.Log($"Enemy HP: {hp}");
    }
}

// Player 側
public void Attack()
{
    enemy.Damage(10); // 値の持ち主に任せる
}
  • データの持ち主が自分の整合性を守る設計(“誰が責任者か”を決める)。

5-3. 参照の自動配線(OnValidate)

#if UNITY_EDITOR
void OnValidate()
{
    if (!enemy)
        enemy = GameObject.Find("Enemy")?.GetComponent<EnemyController>();
}
#endif
  • エディタ上でスクリプトを保存するたびに未設定を自動補完(学習~試作で便利)。

6. ありがちなエラーと対処

  • NullReferenceException:相手が存在しない/コンポーネントが付いていない。
    • 対策:if (!go) return; / if (!comp) return; などの Null チェック を徹底。
  • Find で名前が変わって壊れる:ヒエラルキー名変更で参照不能。
    • 対策:SerializeField 参照に切替。タグアセット・アクションで安定参照。
  • Update で探し続けて重い:毎フレーム Find は地雷。
    • 対策:Awake/Start で1回だけ取得してキャッシュする。

7. 確認用チェックリスト

  • Player と Enemy は シーン上に1つずつある
  • Enemy には EnemyController がアタッチ済み
  • Player には PlayerController がアタッチ済み
  • Active Input Handling が Input System/Both
  • Play 中に Space で HP が 10 減る

8. 演習(手を動かして理解を固定)

  1. 自分のコンポーネント取得:PlayerController から自分の Transform を GetComponent<Transform>() で取得して position をログに出す。
  2. 0以下で撃破:EnemyController に IsDead(bool)を追加し、HP が 0 になったら gameObject.SetActive(false) にする。
  3. 敵が複数:Enemy を3体に増やし、タグ Enemy で配列取得して 一番近い敵にだけダメージを与える(距離計算:Vector3.Distance)。

9. まとめ

  • GameObject は“箱”、Component は“機能”。
  • アクセスの基本は GetComponent 系、参照保持、そして Null ガード
  • 初学者はまず 1対1のやり取り(Player→Enemy)で“つながる感覚”を掴む → 次に 複数体や イベント駆動へ拡張していきましょう。

付録:最小完成コード(そのままコピペ可)

EnemyController.cs

using UnityEngine;

public class EnemyController : MonoBehaviour
{
    public int Hp = 100;
}

PlayerController.cs

using UnityEngine;
using UnityEngine.InputSystem; // Input System

public class PlayerController : MonoBehaviour
{
    public int Hp = 100;

    void Update()
    {
        if (Keyboard.current != null && Keyboard.current.spaceKey.wasPressedThisFrame)
        {
            Attack();
        }
    }

    public void Attack()
    {
        var enemyGo = GameObject.Find("Enemy");
        if (!enemyGo) { Debug.LogError("Enemy が見つかりません"); return; }

        var ec = enemyGo.GetComponent<EnemyController>();
        if (!ec) { Debug.LogError("EnemyController が付いていません"); return; }

        ec.Hp = Mathf.Max(0, ec.Hp - 10);
        Debug.Log($"Enemy HP: {ec.Hp}");
    }
}

ここからは SerializeField 参照方式 や Damage メソッド へリファクタリングして、設計の“責任の所在”を明確にしていきましょう。

訪問数 3 回, 今日の訪問数 3回

Unity,Unity6

Posted by hidepon