LoginSignup
6

【Unity入門】プログラマーがとりあえずコーディングできるためのUnityプログラミング入門

Last updated at Posted at 2023-02-15

この記事は

プログラマーがUnityでそこそこゲームだったりシミュレーションだったりが作れるようになるための記事です。

前書きが長くなってしまったのですが、本体のみ興味ある方は次のh2タグの「環境構築」まで飛ばしてください。

「プログラマー」の定義

本記事の想定読者の技術力はそんなに高くありません。
C#じゃなくともPythonでもなんでもいいので、

  • for文
  • if文
  • クラス
  • (値渡しではなく)参照渡し
    が分かってれば困らないと思います。

前置き

東大エンジニアリング研究会というサークル(ぜひ入ってね!)でゲーム開発班が始動したり、学科では五月祭(学祭!ぜひ来てね!)にシミュレーションを展示をしたいという話になったりと、僕の周りにUnityを使い始めようという人が同時多発していて、その人たち向けの教材を模索していました。世の中のUnity教材はC#の文法からご丁寧に始まったり、(デザイナー向けに)Unityエディタ画面の操作に終始したりとプログラマーが手早く参戦できる教材があまりないように思います。僕のようなゲームエンジニア志望ならまだしも、みんなは本をやりこんでまでガッチリUnityをやりたいわけでもない。
なので、プログラマー手早くUnityプログラミングの要点を把握し、ググりながら開発できるようになる記事を目指します。

なぜUnityを使うか

ノーコードで色んなことができる

一番読みやすいコードとはノーコードであるという金言をどこかで見たことがある気がします(なかったら僕が言ったことにします)

エディタ画面を見ればすぐに分かることですが、わざわざコード内の数値を調整しなくとも、直観的な操作でオブジェクトの位置関係を調整できます。
sasa_20230214_224813.gif
こんな感じに、プログラマーがパラメーター調整欄を用意して、非プログラマーでも気軽に調整できるプラットフォームもできちゃいます。
image.png
また、プログラマーが書いたコードはこんな感じにドラッグ&ドロップするだけでオブジェクトに適用できます。(後述)
sasa_20230214_225757.gif
これにより、(ぶっちゃけプログラミングの知識がなくとも、)購入したり他人が書いてもらったコードを簡単に適用することも、外したりすることもできます。

簡単にまとめると、デザイナーなど非プログラマーでも参画でき、なおかつプログラマーもラクできるという利点ですね。

スーパー並列処理マシーン

Unityプログラミングはすごいオブジェクト指向です。

ゴミみたいな図を用意しました。
NPCに話しかけるプレイヤーと話しかけられるNPCの図です。
image.png
1からゲームやらを作るとなると、それぞれのクラスを同時に走らせるためのフレームワークを書かないといけません。
しかし、Unityによって、上図の青の部分だけ書けばいいんですね。「移動する」の処理だけ書いておいて、あとはUnityに任せれば自動的にそのコードを走らせてもらえます。

描画マシーン

描画の処理自分で書くのダルいので、その意味でもUnity使う価値は大いにありますよね。

「プログラミングできること」と「Unityプログラミングできる」ことの乖離

プログラミングができるからといって、すぐにノー勉でUnityプログラミングに参戦できるかというと、ちょっと難しいです。

Unityは大規模フレームワークであり、コンポーネント指向のやべー奴であり、スーパー並行処理マシーンであることを心構えておく必要があります。

例として、下記が単純に「指定されたスピードで進み続ける」プログラムです。

Advancer.cs
using UnityEngine;

public class Advancer : MonoBehaviour
{
    [SerializeField] private Vector3 speed;

    //毎フレーム呼ばれる
    void Update()
    {
        //進む
        transform.position += speed * Time.deltaTime;
    }
}

これでエディタ画面ではこのようになります。
image.png

それで、こうなります。

sasa_20230214_234436.gif

初見なら、コード見てこう思うと思います。

  • UnityEngineネームスペースってなんや
  • その継承してるMonoBehaviourクラスってなんや
  • SerializeFieldってなんや
  • へーVector3ってのがあるんやな
  • Update()関数定義しただけでなんで動くん?
  • transform未定義で急に出てきたやん
  • Time.deltaTimeってなんや
    発見がたくさん出てきますね。

上記のものは全てC#の文法ではなく、Unityオリジナルの機能になっています。すなわち、Unityプログラミングを不自由にこなすためには、Unity固有の機能、クラスを把握しておく必要があります。

この記事は入門ですので、ここでは覚えるべきもの覚えなくていいものを切り分け、覚えるべきものについてググりながら実装できるレベルを目指します。

環境構築

解説されつくされているので省略します。

  • Unity HubとUnityエディタ(バージョンはLTS=Long Term Supportだったらなんでもいい)
  • Visual Studio(VSCodeではない)

をインストールし、頑張ってUnityエディタとVisual Studioを例系させる設定をしてください 
参考:https://qiita.com/engr_murao/items/b7bbb0eaa48f1c66fc2b

コードを適用するまで

そもそも、どうやって書いたコードを反映させるのでしょうか。剣を買っても装備しないと意味がないように、コードを書いただけでは

先述のコードをコピペして適用するから始めましょう(再掲)

Advance.cs
using UnityEngine;

public class Advancer : MonoBehaviour
{
    [SerializeField] private Vector3 speed;

    //毎フレーム呼ばれる
    void Update()
    {
        //進む
        transform.position += speed * Time.deltaTime;
    }
}

スクリプトファイルを作る

Project(デフォルトでは下のファイルがあるところ)に右クリックして、Create → C# Scriptで空のスクリプトファイルを作れます。
ここで、絶対にファイル名をAdvancerにしてください。 というのは、ファイル名とクラス名が一致しないとUnityが認識されません。
sasa_20230215_000751.gif

できたファイルをダブルクリックするとスクリプトエディタが開かれますので、上記スクリプトをコピペしてください。

オブジェクトを作る

とりあえずキューブを出現させてみましょう。

Hierarchy(デフォルトなら左のオブジェクト一覧があるところ)に右クリックして、3D Object→Cubeをクリックすることで出現させることができます。
sasa_20230215_001944.gif

コードを適用する

このキューブが移動できるようにさせるには、このコードをキューブにアタッチ(=憑依)させます

アタッチのやり方は簡単。
AdvancerファイルをCubeにドラッグ&ドロップするだけです。
sasa_20230215_002644.gif
右下にチョロッと「Advancer」の項目が増えているのが分かりますね。

パラメーターを調整

Cubeを選択してから、Inspector(デフォルトなら右)を見ると、下にAdvancerが追加されていますね。
そこでSpeedを調整できます(そういうコードなので)
image.png
好きな数字にでもしてください。

実行

上の再生ボタンで動かすことができます。
image.png

エディタの操作はこれさえわかってれば大丈夫です。

コンポーネント指向

いよいよコーディングのお話です。

Unityはコンポーネント指向を採用しています。
さきほどのCubeを選択して、Inspectorを見てみてください。
image.png
image.png
この項目の一つ一つが、このCubeについているコンポーネントなのです。

コンポーネントとは、そのオブジェクトが持つ機能のことです。
このCubeのコンポーネントを図示すると、こんな感じです。
image.png
一つ一つのコンポーネントが、このCubeの処理を果たしているのですね。

Transformは必須コンポーネントなので消せませんが、例えばMeshFilterMeshRendererを消してみる(右クリックでできます)と、キューブが透明になってしまいますね。

先ほどやったアタッチとは、すなわちコンポーネントを一つ付与したということになります。

つまるところ、Unityプログラミングとは、コンポーネントを作っていくということに集約できます。(嘘で、シェーダー書いたりすることもあるのですが、基本的にはこの認識で大丈夫です)

そして、コーディング面においても、他コンポーネントを参照することが頻出します。
例えば、オブジェクトの座標は先ほど出てきたTransformコンポーネントを参照します。

把握すべきもの

もちろん全てのコンポーネントを覚える必要性は全くなく、下記の基本コンポーネントだけ把握しておけばコーディングには困らないです。

  • Transform
    =位置・大きさ・回転をつかさどる 全てのオブジェクトに与えられている
  • Collider
    =当たり判定をつかさどる
  • RigidBody
    =物理演算をつかさどる

あとは、必要性に応じてググれば大丈夫です。

Transformコンポーネント

ここで、先ほどのスクリプトを見てみましょう。(再掲)

Advance.cs
using UnityEngine;

public class Advancer : MonoBehaviour
{
    [SerializeField] private Vector3 speed;

    //毎フレーム呼ばれる
    void Update()
    {
        //進む
        transform.position += speed * Time.deltaTime;
    }
}

進む部分がこれでちょっとわかりますね。

transform.position += speed * Time.deltaTime;

の部分。transformとは、'Transformコンポーネントのことです。 そのTransformクラスのposition`変数、すなわち座標に、スピード(ベクトル)*時間=道のりを足し算してる、ということなんですね。(右辺については次のh2タグで)

そう、1コンポーネントに1クラスが書かれています。実際にこのAdvancerクラスもそうですね。
TransformコンポーネントにTransformクラスがある、ということですね。

ちなみにこのスクリプトでtransform変数が未定義で使えたのは継承元のMonoBehaviourが定義してくれているだけで、他のコンポーネントはこのようにはいきません。
後述のGetComponent()関数でもって他コンポーネントを参照します。

Collider、Rigidbodyコンポーネント

試しに、キューブにRigidbodyコンポーネントをアタッチしてみましょう。
sasa_20230215_010532.gif

Rigidbodyは物理演算を担うので、再生すると重力で落ちます。

Colliderコンポーネントは当たり判定ですが、ちょっとめんどくさいことに Collider
コンポーネントをつけただけではすり抜けちゃいます

「オブジェクト同士がぶつかったときの挙動」はRigidbodyの仕事なので、Rigidbodyをつけないとダメ、ってわけですね。

把握すべきクラス

上記では把握すべきコンポーネントについて書きました。
各コンポーネントには1クラスあるので、特にTransformクラスは把握していないといけないというのは言うまでもありません。

それに加え、下記クラスを把握しておくと自由度が爆上がりします。

  • GameObjectクラス
     =オブジェクトそのもの。各インスタンスが各オブジェクト、あるいはプレハブ(後述)を参照している。超重要
  • Timeクラス
    =時間に関する情報が保持されている。まあdeltaTime変数だけ覚えとけば大丈夫かもしれない
  • Vector3クラス
    =ベクトルを表現するクラス。スピードなどの有効線分として使われ方もあれば、位置座標のように位置ベクトルとして使われるケースもあるし、単純に大きさや回転など「x,y,zの3つ数字が並んだセット」という使われ方もされている。近頃数学Bから数学Cに移行するようなので、ベクトル自体の説明もいずれ必要になるだろうか。
  • Quanternionクラス
     =ベクトルに関する回転計算。難しい。こちらの記事がオススメ:https://qiita.com/drken/items/0639cf34cce14e8d58a5
  • Mathfクラス
    =各種数学の関数(sin, cos, absなど)を提供。
  • Debugクラス
     =デバッグ用。特にコンソールデバッグができるDebug.Log()が多用されるが、ブレークポイントを貼ってゲームを止めてデバッグする方がかっこいいとされている
  • Inputクラス
     =キー入力の取得用。Input.GetKey("a")は「A」キーが押されたらtrue、押されていない間はfalseを返す関数である。

覚えることがたくさんあって大変ですね。最悪全部忘れてもいいのでGameObjectだけは覚えておいてください。後述しますが頻繁に使います。

これらの存在だけを知っておくと、あとはググればなんとかなると思います。

これで理解がアンロックされますね。

Advancer.csの一部
transform.position += speed * Time.deltaTime;

の部分。speedの型はVector3クラス。そしてTime.deltaTimeは「1フレーム(後述)にかかった時間」
そうやって「速度(ベクトル)」*「時間」=「移動量(ベクトル)」を、Transform.positionに足し算している、というわけです。

把握すべき関数

自作するコンポーネントは全てMonoBehaviourクラスを継承します。(継承しないとエラー発生します)
MonoBehaviourクラス自体は深く知らなくとも大丈夫なのですが、MonoBehaviourクラスが持っている関数をオーバーライドすることが頻出です。(というか動かすためには基本的にそうします)

そのMonoBehaviourクラスの把握しておくべき関数には下記があります。

  • Start()
     =最初のフレームの一回だけ呼ばれる。初期化などに使う
  • Update()
     =毎フレーム呼ばれる。
  • GetComponent()
    =他コンポーネントを返す。使い方は後述参照。
  • OnCollisionEnter()
     =Colliderコンポーネント持ちとRigidbodyコンポーネント持ちのオブジェクトが衝突したら呼ばれる。当たり判定の処理に使う。

他の関数はあまり使いませんが、気になる場合はこちらを参照。

フレームとは:https://ekulabo.com/frame-meaning
簡単に言うと、ゲームの処理は各処理のループなのですが、1ループのことを1フレームと呼んでいることですね。

Start()Update()はとても重要ですね。
新規でスクリプトファイルを作成した際にデフォルトで書かれているぐらいには重要です。

先述のスクリプトの理解がもっとアンロックされました。

Advancer.csの一部
//毎フレーム呼ばれる
void Update()
{
    //進む
    transform.position += speed * Time.deltaTime;
}

の部分。
Update()関数の中に「進む」処理を書いているので、この処理が毎フレーム発生しているということになりますね。簡単に言うと、常に進み続けるということです。

把握すべき機能

その他、コーディングにおいて知っておきたいUnity特有の機能が一つあります。
SerializeFieldです。

メンバー変数の定義にあった

Advancer.csの一部
[SerializeField] private Vector3 speed;

の部分。
SerializeField」属性をこのようにspeed変数に付与することによって、エディタ上から数値を調整できるようになります
image.png

実は、publicで変数を定義するとSerializeField指定しなくとも同様のことができますが、一般的にpublic変数を定義するとロクなことにならないと言われているので、SerializeFieldを推奨します。

コーディングしよう!1

各概念の表面を撫でたので、実際のコーディングでこれまで学んだことの具体化を試みます。

回転する

ezgif-5-1993f6f430.gif

Rotater.cs
using UnityEngine;

public class Rotater : MonoBehaviour
{
    [SerializeField] private Vector3 angularSpeed;

    void Update()
    {
        transform.rotation *= Quaternion.Euler(angularSpeed * Time.deltaTime);
    }
}

transform.rotationはオブジェクトの回転を保持しています。QuanternionQuanternionで回転するのには、掛け算をします。
Quanternion.Eulerは、xyz軸の普通の角度を、Quanternionに直す関数です。
これによって、エディタからxyzで入力した角速度で回転するようになります。
ちなみに、回転する部分はこんな書き方もできます。

transform.Rotate(angularSpeed * Time.deltaTime);

同様の処理をしています。

今まで覚えてきた関数以外にQuanternion.Eulerやらtransform.Rotateやらいろいろ知らないものが出てくるやんけふざけんなとお思いかもしれませんが、これらは覚える必要は全くなく、「Unity 回転」とでもググればすぐ出てきます。また、Unity公式スクリプトリファレンスに各クラスがどんなメンバー変数・関数を持っているのかが明記されています。
ここで大事なのが、「回転の情報はTransformコンポーネントが持っているからTransformのスクリプトリファレンスを見ればいいや」と調べる検討がつくことかなと思っています。

キー入力に応じて移動

典型的なwで前進、adで回転(方向転換)するスクリプトを書いてみましょう。
sasa_20230215_164258.gif

Controller.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Controller : MonoBehaviour
{
    [SerializeField] private float speed;
    [SerializeField] private float angularSpeed;

    void Update()
    {
        //wが押されたら前進
        if (Input.GetKey("w"))
        {
            //前進
            transform.position += transform.forward * speed * Time.deltaTime;
        }

        //aが押されたら左回転
        if (Input.GetKey("a"))
        {
            //左回転
            transform.rotation *= Quaternion.Euler(transform.up * (-angularSpeed) * Time.deltaTime);
        }

        //dが押されたら右回転
        if (Input.GetKey("d"))
        {
            //左回転
            transform.rotation *= Quaternion.Euler(transform.up * angularSpeed * Time.deltaTime);
        }
    }
}

transform.upは「オブジェクトから見て上方向」のベクトル(回転の回転軸に使用)、transform.forwardは「前方向」のベクトルを返します。(前進に使用)

他コンポーネントを参照する

今度は他のコンポーネントから数値を寄越してきたり、他コンポーネントのメソッドを呼ぶプログラムを書きましょう。

まずは、呼ばれる側。

ComponentA.cs
using UnityEngine;

public class ComponentA : MonoBehaviour
{
    public string variable = "どりゃあああああ";

    //呼ばれる関数
    public void Log()
    {
        Debug.Log("でりゃああああああ");
    }
}

次に、呼ぶ側。

ComponentB.cs
using UnityEngine;

public class ComponentA : MonoBehaviour
{
    public string variable = "どりゃあああああ";

    //呼ばれる関数
    public void Log()
    {
        Debug.Log("でりゃああああああ");
    }
}

両方ともキューブにアタッチすると、コンソールにこう表示されます。
image.png

ここで大事なのはGetComponent()です。
GetComponent<コンポーネントのクラス名>()
で、自分のオブジェクトの他コンポーネントを参照することができます。

他オブジェクトを参照する

今度は、上記のスクリプトを真似て、他オブジェクトのコンポーネントを読んでみましょう。

ComponentA.cs
using UnityEngine;

public class ComponentA : MonoBehaviour
{
    public string variable = "どりゃあああああ";

    //呼ばれる関数
    public void Log()
    {
        Debug.Log("でりゃああああああ");
    }
}

上記スクリプトをCubeAにアタッチ。

ComponentB.cs
using UnityEngine;

public class ComponentB : MonoBehaviour
{
    [SerializeField] private GameObject targetObject;

    // 最初のフレームだけ
    void Start()
    {
        //ComponentAを参照
        ComponentA componentA = targetObject.GetComponent<ComponentA>();

        //componentA.variableをコンソールに表示
        Debug.Log(componentA.variable);

        //関数を呼ぶ
        componentA.Log();
    }
}

上記スクリプトをCubeB(生成してください)にアタッチ。

ここで、CubeAをCubeBのtargetObjectにドラッグ&ドロップしてください。
sasa_20230215_165833.gif
すると先ほどと同じ結果を得られます。

前述のように、GameObject型はオブジェクトそのものを指します。
これをSerializeFieldすることで、他オブジェクトを参照することができます。
先ほどのGetComponent単体では自分のコンポーネントを返す一方、GameObject.GetComponent()は参照しているオブジェクトのコンポーネントを返します。

ちなみに動的にオブジェクトを探して参照したい場合はオブジェクト名で検索するFind()関数と、保持しているコンポーネントで探すFindObjectsOfType関数などがありますが、Find()は非推奨です。オブジェクト名が重複したり変更したりした瞬間にバグるので。

プレハブ

ここまでは、オブジェクトをエディタ上で設置してから動かすという静的配置でした。
銃オブジェクトが弾オブジェクトを生成して発射するといったゲーム中にオブジェクトを生成する動的配置を実現するには、プレハブの出番となります。

プレハブとは

いわば、オブジェクトの設計図です。

例えば銃が弾を発射する例でいくと、弾プレハブを作り、銃オブジェクトが弾プレハブを保持します。
そして、射撃するときは、銃オブジェクトが弾プレハブをInstantiate()(インスタンス化)することで弾オブジェクトを生成します。

設計図の例えでいくと、弾の設計図を持っておいて、射撃する時はその設計図通りに弾を作って発射する、ということですね。

プレハブ化の操作

やり方は簡単。
まずは、キューブでも何でもいいので、普通に何かオブジェクトを生成してください。

そして、hierarchy上のオブジェクトをProjectにドラッグ&ドロップ。なんか出来たフォルダが、プレハブファイルです。

sasa_20230215_200302.gif

一度プレハブファイルができたら、ワールド上にあるオブジェクトは削除しても大丈夫です。削除してください。

プレハブの生成

生成してみましょう。

ワールド上にあるキューブに、Rigidbodyをつけてから、下記をアタッチ。

Instatiater.cs
using UnityEngine;

public class Instantiater : MonoBehaviour
{
    [SerializeField] private GameObject prefab;

    //毎フレーム呼ばれる
    void Update()
    {
        //生成する
        Instantiate(prefab);
    }
}

まずは、プレハブをセットしなければなりません。
これは「他オブジェクトを参照」でやったように、ドラッグ&ドロップでできます。

これでプレイすると、ウミガメの卵みたいにキューブが生成されまくります。(重いですね)

sasa_20230215_201131.gif

Rigidbodyをつけた理由としては、「物理演算」がないと同じところに生成されまっても、一点に重複しながら生成されるだけなので生成されているのが確認できないからです。

コーディングしよう!2

当たり判定処理

簡単な当たり判定処理を書きます。

当たり屋

Hitter.cs
using UnityEngine;

//当たり屋
public class Hitter : MonoBehaviour
{
    //当たった時の処理
    private void OnCollisionEnter(Collision collision)
    {
        Debug.Log("Hitter当たった!");
    }
}

被害者

Victim.cs
using UnityEngine;

//当てられる被害者
public class Victim : MonoBehaviour
{
    //当たった時の処理
    private void OnCollisionEnter(Collision collision)
    {
        Debug.Log("Victim:当たった!");
    }
}

上記ができたら「当たり屋」にRigidbodyをつけて、「被害者」を「当たり屋」の真下に置いてください。当たり屋が落下したところに被害者が来るように。

sasa_20230215_202503.gif

説明しきれていない重要概念

  • 親オブジェクトと子オブジェクトの関係:オブジェクトには親子関係がありますが、親オブジェクトを動かすと子オブジェクトも動きます。
  • Local座標とWorld座標:ワールド全体の絶対的な座標系と、オブジェクトから見た相対的な座標系があります。
  • Invoke()で、指定時間遅延して関数を呼ぶことができます。
  • UnityEventでデリゲート処理ができます。
  • Scene:ワールド(ないし画面)を複数作れます
  • Animatorでアニメーションを制御できます

次回予告

追加執筆予定の記事では、実際にこのようなゲームを作るまでの工程を書いて、Unityプログラミングの具体例を掴めるようにしたいと思っています。
image.png

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
6