Unityで使うC#の特徴 ― 普通のC#と何が違うのか

広告

目次

はじめに

この記事は、WinFormsなどでC#の基本文法を学び、

  • 変数
  • if
  • for / while
  • メソッド
  • クラス
  • 継承
  • イベント
  • 配列や List

といった基本をある程度理解した人を対象にしています。

Unityでも使う言語はC#です。
Unity専用の別のC#を覚えるわけではありません。

一方でUnityでは、C#を単独で動かすのではなく、Unityというフレームワークの中でC#を動かすという大きな特徴があります。

Unityはゲームエンジンですが、「自分が書いたコードをUnityが呼び出して動かす」という点で、フレームワークの性質を持っています。

そのため、

C#の文法は分かるのに、Unityのコードを見ると急に分からなくなる

ということも珍しくありません。

この記事では、通常のC#やWinFormsとの違いを意識しながら、UnityでC#を書くときの特徴を見ていきます。

UnityのC#も基本は普通のC#

まず押さえておきたいのは、Unityで使うC#も基本的には普通のC#だということです。

たとえば、通常のC#では次のようにクラスを定義できます。

public class PlayerData
{
    public string Name { get; set; }
    public int Hp { get; set; }
}

そして、

PlayerData player = new PlayerData();
player.Name = "Player1";
player.Hp = 100;

のようにオブジェクトを作ることができます。

このようなC#の基本的な考え方はUnityでも変わりません。

Unityでは、ここに

  • GameObject
  • MonoBehaviour
  • Transform
  • Vector2
  • Vector3
  • Rigidbody
  • Time

などのUnityが提供する機能が加わります。

つまり、最初の見取り図としては、

C#の文法 + Unity API

と考えると分かりやすいでしょう。

ただし、Unityにはもう1つ大事な要素があります。
それは、この記事の後半で見ていきます。

MonoBehaviour を継承するクラスが多い

Unityでスクリプトを作成すると、よく次のようなクラスが登場します。

using UnityEngine;

public class PlayerController : MonoBehaviour
{
}

注目したいのは、

: MonoBehaviour

という部分です。

これはC#の「継承」です。

MonoBehaviour はUnityが用意しているクラスで、これを継承することで、作成したクラスをGameObjectのComponentとして利用できるようになります。

つまり、

public class PlayerController : MonoBehaviour

は、

PlayerController は MonoBehaviour の機能を引き継いだクラス

という意味になります。

ここは、C#で学んだ「継承」が実際に使われている例でもあります。

スクリプトはGameObjectの「部品」になる

Unityでは、プレイヤー、カメラ、ライトなど、シーン上に存在するものをGameObjectとして扱います。

そしてGameObjectには、複数のComponentを追加できます。

たとえばプレイヤーなら、イメージとしては次のようになります。

Player(GameObject)
├─ Transform
├─ Rigidbody
├─ Collider
└─ PlayerController

Transform は位置・回転・大きさ、
Rigidbody は物理演算、
Collider は衝突判定、
PlayerController は自分で作成したC#スクリプトです。

Unityでは、

GameObjectに必要なComponentを組み合わせて機能を作る

という考え方が非常に重要です。

UnityのC#とGameObject・Componentの関係の図。UnityEngine.Objectからの継承関係、GameObjectとComponentのコンポジション、シーン上のインスタンスの3つに分けて示したイラスト
C#の継承とGameObject・Componentの関係(実際の画面をもとに描き起こしたイラスト、クリックで拡大)

Vector2 と Vector3 をよく使う

ゲームでは位置や方向を扱うことが多いため、Unityでは Vector2 や Vector3 が頻繁に登場します。

2次元なら、

Vector2 position;

3次元なら、

Vector3 position;

のように使います。

たとえば、

Vector3 position = new Vector3(1f, 2f, 3f);

なら、

  • X = 1
  • Y = 2
  • Z = 3

という値を表します。

Unityには、

Vector3.up
Vector3.down
Vector3.left
Vector3.right
Vector3.forward
Vector3.back

なども用意されています。

Transform で位置や回転を扱う

すべてのGameObjectには Transform Componentがあります。

Transform には主に、

  • 位置
  • 回転
  • 大きさ

の情報があります。

C#スクリプトからは、次のように位置にアクセスできます。

transform.position

たとえば、

transform.position = new Vector3(3f, 0f, 0f);

とすると、GameObjectの位置を変更できます。

また、

transform.position += Vector3.right;

とすると、現在位置から右方向へ移動します。

transform はどこから来たのか

ここで疑問が出てきます。

transform は、自分で宣言していません。

Transform transform;   // こんな宣言は書いていない

それなのに、なぜ transform.position と書けるのでしょうか。

答えは継承です。

PlayerController は MonoBehaviour を継承しています。
MonoBehaviour の継承関係をたどると、次のようになっています。

PlayerController
↓ 継承
MonoBehaviour
↓ 継承
Behaviour
↓ 継承
Component

transform と gameObject は、一番上の Component が持っているプロパティです。

PlayerController 自身には transform を宣言していなくても使えるのは、継承によって Component の機能を受け継いでいるためです。

つまり、transform.position と書けること自体が、C#で学んだ継承が実際に働いている例なのです。

MonoBehaviour は普通のクラスのように new しない

ここはUnityを学び始めたときに、特に重要な違いです。

先ほどの通常のC#クラスなら、

PlayerData player = new PlayerData();

と書けました。

しかし、MonoBehaviour を継承したクラスに対して、

// × MonoBehaviourを通常のC#クラスのようにnewしない
PlayerController player = new PlayerController();

とはしません。

MonoBehaviour は、UnityがGameObjectのComponentとして生成・管理するものです。
new で生成するのはUnityでは正しい方法ではなく、警告の原因になります。

では、どうやって作るのか

代表的な方法は3つあります。

エディターからGameObjectへ追加する

Unity EditorのInspectorで、

Add Component

からスクリプトを追加できます。

また、スクリプトをGameObjectへドラッグして追加することもできます。

これが最も基本的な方法です。

C#からComponentを追加する

実行中に追加したい場合は、AddComponent<T>() を使用できます。

PlayerController player =
    gameObject.AddComponent<PlayerController>();

これは、

このGameObjectに PlayerController というComponentを追加する

という意味です。

Prefabなどを複製する

あらかじめ作っておいたPrefabを実行中に複製するときは、Instantiate() を使います。

Prefabについては、別の記事で詳しく扱います。

ここでは、次の使い分けを覚えておいてください。

普通のC#クラス
→ new

Component
→ AddComponent

PrefabなどのUnityオブジェクト
→ Instantiate

もう1つの大事な要素:Unityから自動的に呼ばれるメソッド

ここまでは、「C#の文法」と「Unity API」の話でした。

ここからは、Unityにあるもう1つ大事な要素、Unityが自分のコードを呼び出す仕組みを見ていきます。

Unityでは、特定の名前のメソッドをUnity側が呼び出してくれます。
このようなメソッドを、Unityではイベント関数と呼びます。

代表的なのが、

void Start()
{
}

void Update()
{
}

です。

Start() は、そのスクリプトが有効になったあと、最初の Update() が実行される前に一度呼び出されます。

Update() は、スクリプトが有効な間、基本的に毎フレーム呼び出されます。

重要なのは、自分のコードに、

Start();

や、

Update();

と書いていないことです。

Unityが呼び出しています。

Awake() や OnEnable() もある

初期化に関係するイベント関数は Start() だけではありません。

Unityには、

Awake()
OnEnable()
Start()

などもあります。

大まかなイメージとしては、

Awake
↓
OnEnable
↓
Start
↓
Update
↓
Update
↓
Update
...

となります。

それぞれには役割や細かな実行条件があります。

最初の段階では、

Start() は最初の Update() より前に一度呼ばれる

と覚えておけばよいでしょう。

Awake() や OnEnable() の詳しい使い分けは、別の記事で扱います。

Awake と Start の違いなど、イベント関数の詳しい使い分けは、【学習】MonoBehaviour クラス(スクリプトの基底クラス)で解説しています。

Unityを理解する鍵は「誰が呼んでいるか」

Unityを理解するうえで、とても重要な視点があります。

このメソッドを誰が呼んでいるのか

という視点です。

通常のC#で、

Move();

と書けば、自分のコードが Move() を呼び出しています。

一方、

void Update()
{
}

は自分では呼び出していません。

Unityが呼び出します。

つまり、

自分のコード
↓
ライブラリの機能を呼ぶ

だけではなく、

Unity
↓
自分が書いたメソッドを呼ぶ

という流れがあります。

これは「制御の反転」と呼ばれる考え方につながります。

簡単に言えば、

ライブラリは自分から呼ぶことが多い
フレームワークでは自分のコードが呼ばれることがある

という違いです。

Unityの Start() や Update() は、その分かりやすい例です。

なぜ Start() や Update() に override がないのか

C#の継承を学んだあとでは、次のコードを見て疑問に思うかもしれません。

public class PlayerController : MonoBehaviour
{
    void Start()
    {
    }

    void Update()
    {
    }
}

PlayerController は MonoBehaviour を継承しているのに、なぜ override が付いていないのでしょうか。

これも、「誰が呼んでいるか」に関係する話です。

通常のC#の override

通常のC#では、基底クラスに virtual メソッドがあり、それを派生クラスで上書きするときに override を使います。

public class Base
{
    public virtual void Show()
    {
    }
}

public class Derived : Base
{
    public override void Show()
    {
    }
}

ここでは、基底クラス Base に virtual な Show() が定義されています。
Derived は、それを override して上書きしています。

MonoBehaviour には Update() が宣言されていない

では、MonoBehaviour はどうでしょうか。

MonoBehaviour には、Start() や Update() というメソッド自体が宣言されていません。
そのため、上書きする対象がなく、override を付けるとコンパイルエラーになります。

// × 上書きするメソッドがないため、コンパイルエラーになる
public override void Update()
{
}

では、なぜ次のコードは動くのでしょうか。

void Update()
{
}

Unityが、Update という決められた名前のメソッドを探して呼び出しているからです。

つまり、

基底クラスの Update() を上書きしている

のではなく、

Unityから呼び出される、Update() というイベント関数を定義している

ということです。

ここが、通常のC#の virtual / override と見た目が違う理由です。

private でも呼ばれる

アクセス修飾子を書かなければ、クラスのメソッドは private になります。

つまり、

void Update()
{
}

は、

private void Update()
{
}

と同じ意味です。

C#では、private のメソッドを他のクラスから呼び出すことはできません。

しかしUnityは、C#のコードとしてメソッドを呼ぶのではなく、Update という名前のメソッドを探して呼び出します。
そのため、private でも呼ばれます。

「Update() に書いたのに動かない」ときは名前を疑う

Update() の中に Debug.Log を書いたのに、何も表示されない。

そんなときは、まずメソッド名を確認してください。

たとえば、update() や Updata() と書いてしまったとします。

これらは、C#としては Update とは別の名前の普通のメソッドです。
そのため、コンパイルエラーにはなりません。

しかしUnityは Update という名前だけを探すため、これらは呼ばれないまま残ります。

通常の override であれば、存在しないメソッドを上書きしようとした時点で、コンパイラが間違いを教えてくれます。
イベント関数にはそれがないため、メソッド名を正確に書くことが重要です。

動かないときの確認ポイント

Start() や Update() に処理を書いたのに動かない場合は、次の点を確認しましょう。

  • 大文字と小文字は合っているか
  • スペルは合っているか
  • Consoleにコンパイルエラーが残っていないか
  • スクリプトがGameObjectに追加されているか
  • GameObjectが有効(アクティブ)になっているか
  • スクリプトのComponentが有効(チェックが入っている)か

Consoleにコンパイルエラーが残っていると、直したコードが反映されず、「動かない」原因になります。

Update() とゲームループ

ゲームでは画面を何度も更新しながら処理を進めます。

たとえば、

入力を確認する
↓
プレイヤーを動かす
↓
敵を動かす
↓
衝突を確認する
↓
画面を描画する
↓
次のフレームへ

という処理を繰り返します。

このような考え方をゲームループと呼びます。

Unityでは、この流れの中で Update() が基本的に毎フレーム呼ばれます。

移動では Time.deltaTime が重要

たとえば、右方向へ移動させたいとします。

次のように書くこともできます。

void Update()
{
    transform.position += Vector3.right * 0.01f;
}

しかし、この書き方では、

1フレームにつき0.01移動する

ことになります。

1秒間に実行されるフレーム数が違えば、1秒間に移動する距離も変わってしまいます。

そこで、時間を基準に移動させます。

private float speed = 5f;

void Update()
{
    transform.position +=
        Vector3.right * speed * Time.deltaTime;
}

Time.deltaTime には、前のフレームから現在のフレームまでにかかった時間が秒単位で入っています。

そのため、

speed * Time.deltaTime

とすることで、

1フレームにどれだけ進むか

ではなく、

1秒間にどれだけ進むか

という考え方で移動速度を設定できます。

Time.deltaTime はUnityで移動処理を書くうえで非常によく使われます。

WinFormsとの違い

WinFormsではイベントを中心に処理を書くことが多くあります。

たとえばボタンなら、

ボタンをクリック
↓
Clickイベント発生
↓
イベントハンドラ実行

となります。

private void button1_Click(object sender, EventArgs e)
{
    MessageBox.Show("クリックされました");
}

Unityにもイベントやコールバックがあります。

それに加えて、

毎フレーム
↓
Update()
↓
状態を確認
↓
ゲームの状態を更新

というゲームループの考え方が重要になります。

WinFormsとUnityの実行モデルの違いは、【学習】実行モデルで見る WinForms と Unity(つまずきの正体)でも詳しく解説しています。

WinFormsとUnityを対応させて考える

完全に同じものではありませんが、学習上は次のように対応させると理解しやすくなります。

WinFormsUnity補足
FormSceneアプリの画面とゲームのシーンという違いはある
ButtonなどのControlGameObject + ComponentUnityでは複数Componentの組み合わせになることが多い
プロパティウィンドウInspectorオブジェクトの設定を編集する
Click / KeyDownInput SystemやUnityEventなど入力の仕組みは異なる
TimerUpdate() / Coroutineなど用途によって使い分ける
PointVector2Pointは整数、Vector2はfloatを使う
イベントハンドラOnCollisionEnter() などUnityから呼ばれるメソッドも多い

ただし、この表は「同じもの」を示しているわけではありません。

あくまで、

WinFormsで知っている考え方をUnityではどこに結びつければよいか

を見るための対応です。

UnityにはInspectorがある

Unityでは、C#で定義したフィールドをInspectorから設定できます。

たとえば、

[SerializeField]
private float speed = 5f;

とします。

speed は private ですが、[SerializeField] を付けることでInspectorから編集できます。

これにより、プログラムを書き換えなくても、

Speed  3
Speed 5
Speed 10

などとUnity Editor上で値を調整できます。

ゲームでは、

実際に動かしながら数値を調整する

ことが多いため、非常に便利な仕組みです。

コードを書き換えてもInspectorの値が変わらないことがある

ここは最初につまずきやすいところです。

最初に、

[SerializeField]
private float speed = 5f;

と書いたとします。

一度Componentとして保存され、Inspector側に値が保持されている状態で、

[SerializeField]
private float speed = 10f;

とコードを書き換えても、既存のGameObjectのInspectorが自動的に 10 になるとは限りません。

Unityでは、シリアライズされた値が保存されるためです。

そのため、

コードでは10にしたのに、Unityでは5のまま

ということがあります。

この場合はInspectorに保存されている値も確認しましょう。

一方、新しくComponentを追加した場合には、新しい初期値が使われます。
また、InspectorのResetで、値を初期値に戻すこともできます。

Inspectorに出したいだけなら public にしなくてもよい

次のように書いてもInspectorには表示されます。

public float speed = 5f;

しかし public は、

他のクラスからアクセスできる

というC#のアクセス制御にも関係します。

Inspectorから設定したいだけなら、

[SerializeField]
private float speed = 5f;

とすることで、

  • 他のクラスからは直接変更させない
  • Inspectorからは設定できる

という形にできます。

Componentを取得する GetComponent

Unityでは、同じGameObjectについている別のComponentをC#から利用することがあります。

そのときによく使うのが GetComponent<T>() です。

たとえば3Dの物理演算で使う Rigidbody を取得するなら、

private Rigidbody rb;

void Start()
{
    rb = GetComponent<Rigidbody>();
}

と書けます。

イメージとしては、

Player
├─ Transform
├─ Rigidbody
└─ PlayerController
↓
Rigidbodyを取得

となります。

2Dゲームなら、

Rigidbody2D

を使うこともあります。

なぜフィールドに保存するのか

たとえば毎フレーム、

void Update()
{
    Rigidbody rb = GetComponent<Rigidbody>();
}

と書くと、同じComponentを毎フレーム探し直すことになります。

多くの場合、Componentは変わらないため、これは無駄です。

そこで、

private Rigidbody rb;

void Start()
{
    rb = GetComponent<Rigidbody>();
}

のように、Start() や Awake() で一度だけ取得し、フィールドに参照を保存して、あとで使い回します。

void Update()
{
    // 保存しておいたrbを使った処理
}

このように、

必要なComponentを一度取得し、参照を保持して利用する

という書き方は、Unityでよく登場します。

GameObject.Find との使い分けや、GetComponent を安全に使う方法は、次の記事で詳しく解説しています。

UnityのC#は「3つの要素」でできている

ここまで見てくると、Unityで必要なのは単にUnityのクラス名を覚えることだけではないと分かります。

整理すると、次の3つの要素です。

C#の文法
+
Unity API
+
Unityの実行モデル(Unityが自分のコードを呼び出す仕組み)

C#としては、

  • クラス
  • 継承
  • フィールド
  • メソッド
  • private / public
  • 配列
  • List
  • イベント

などを使います。

そこに、

  • MonoBehaviour
  • GameObject
  • Transform
  • Vector2
  • Vector3
  • Rigidbody
  • Time

などのUnity APIが加わります。

さらに、

  • Start()
  • Update()
  • OnCollisionEnter()

など、Unityから自分のコードが呼び出される仕組みがあります。

この3つの要素を分けて考えると、Unityのコードがかなり理解しやすくなります。

WinFormsで学んだC#はUnityでも使える

WinFormsとUnityはアプリケーションの作り方こそ違いますが、C#そのものは共通しています。

たとえばWinFormsで、

private bool isLeftPressed;

というフィールドを用意してキーの状態を保存したとします。

Unityでも、

入力を受け取る
↓
状態を保存する
↓
保存した状態を使って処理する

という考え方は同じです。

違うのは、

どの仕組みから入力を受け取るか

という部分です。

そのため、WinFormsで学んだ

  • 変数
  • フィールド
  • クラス
  • 継承
  • イベント
  • 状態管理

といった知識は、Unityでもそのまま役立ちます。

まとめ

Unityで使うC#は、別のプログラミング言語ではありません。

基本は通常のC#です。

ただし、UnityではC#をUnityというフレームワークの中で使用します。

特に重要なのは次の点です。

  • MonoBehaviour を継承するクラスが多い
  • スクリプトはGameObjectのComponentとして動く
  • transform などが使えるのは、継承によって Component の機能を受け継いでいるから
  • MonoBehaviour は通常のクラスのように new しない
  • Componentの追加には AddComponent<T>() が使える
  • Prefabなどの複製には Instantiate() を使う
  • Start() や Update() はUnityから呼び出されるイベント関数
  • イベント関数には override を付けず、名前を正確に書く必要がある
  • Update() はゲームループと深く関係している
  • 時間に応じた移動では Time.deltaTime が重要になる
  • InspectorとC#のフィールドを連携できる
  • Vector2、Vector3、Transform を頻繁に使う
  • GetComponent<T>() で他のComponentを取得し、フィールドに保存して使い回す
  • C#の継承など、これまで学んだ文法が実際に使われている

最初に考えてほしい3つの疑問は、次のとおりです。

  1. なぜ Start() や Update() を自分で呼んでいないのに動くのか
  2. なぜ MonoBehaviour を new しないのか
  3. なぜC#スクリプトをGameObjectへ追加するのか

この3つが分かってくると、Unityのコードは単なる「特殊なC#」ではなく、

これまで学んだC#を、Unityというフレームワークの仕組みに乗せて使っている

ことが見えてきます。

訪問数 32 回, 今日の訪問数 1回

広告

C#,Unity

Posted by hidepon