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. 最小サンプルの作成手順
- Hierarchy で Create > Create Empty → 名前を Player。
- 同様に空オブジェクトを作成 → 名前を Enemy。
- 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();
}
何をしている?
- Keyboard.current != null
- 新Input Systemの“いま使えるキーボード”デバイスへの参照が 存在するか を確認しています。
- ない環境(例:物理キーボードが無い端末)や入力が無効なら null になり得るので、ぬるぽ防止です。
- Keyboard.current.spaceKey
- そのキーボード上の Spaceキーを表す KeyControl です。
- 旧APIでいう KeyCode.Space に相当する“キーそのもの”のオブジェクト。
- .wasPressedThisFrame
- このフレーム中に「離れていた→押された」状態遷移が起きたか(立ち上がり)を返す真偽値。
- つまり 押した瞬間の1フレームだけ true。押しっぱなしでも次フレーム以降は false に戻ります。
- 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で安全に再現できています。
- Player に PlayerController を、Enemy に EnemyController を Add Component。
- 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. 演習(手を動かして理解を固定)
- 自分のコンポーネント取得:PlayerController から自分の Transform を GetComponent<Transform>() で取得して position をログに出す。
- 0以下で撃破:EnemyController に IsDead(bool)を追加し、HP が 0 になったら gameObject.SetActive(false) にする。
- 敵が複数: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 メソッド へリファクタリングして、設計の“責任の所在”を明確にしていきましょう。
ディスカッション
コメント一覧
まだ、コメントがありません