この記事は
プログラマーがUnityでそこそこゲームだったりシミュレーションだったりが作れるようになるための記事です。
前書きが長くなってしまったのですが、本体のみ興味ある方は次のh2タグの「環境構築」まで飛ばしてください。
「プログラマー」の定義
本記事の想定読者の技術力はそんなに高くありません。
C#じゃなくともPythonでもなんでもいいので、
- 型
- for文
- if文
- クラス
- (値渡しではなく)参照渡し
が分かってれば困らないと思います。
前置き
学科では五月祭(学祭!ぜひ来てね!)にシミュレーションを展示をしたいという話になったりと、僕の周りにUnityを使い始めようという人が同時多発していて、その人たち向けの教材を模索していました。世の中のUnity教材はC#の文法からご丁寧に始まったり、(デザイナー向けに)Unityエディタ画面の操作に終始したりとプログラマーが手早く参戦できる教材があまりないように思います。僕のようなゲームエンジニア志望ならまだしも、みんなは本をやりこんでまでガッチリUnityをやりたいわけでもない。
なので、プログラマーが手早く、Unityプログラミングの要点を把握し、ググりながら開発できるようになる記事を目指します。
なぜUnityを使うか
ノーコードで色んなことができる
一番読みやすいコードとはノーコードであるという金言をどこかで見たことがある気がします(なかったら僕が言ったことにします)
エディタ画面を見ればすぐに分かることですが、わざわざコード内の数値を調整しなくとも、直観的な操作でオブジェクトの位置関係を調整できます。
こんな感じに、プログラマーがパラメーター調整欄を用意して、非プログラマーでも気軽に調整できるプラットフォームもできちゃいます。
また、プログラマーが書いたコードはこんな感じにドラッグ&ドロップするだけでオブジェクトに適用できます。(後述)
これにより、(ぶっちゃけプログラミングの知識がなくとも、)購入したり他人が書いてもらったコードを簡単に適用することも、外したりすることもできます。
簡単にまとめると、デザイナーなど非プログラマーでも参画でき、なおかつプログラマーもラクできるという利点ですね。
スーパー並列マシーン
厳密な「並列処理」ではなく、「別々に書いたコードがどうしてか並列して動いているように見える」という意味です。実際は直列です。
Unityプログラミングはすごいオブジェクト指向です。
ゴミみたいな図を用意しました。
NPCに話しかけるプレイヤーと話しかけられるNPCの図です。
1からゲームやらを作るとなると、それぞれのクラスを同時に走らせるためのフレームワークを書かないといけません。
しかし、Unityによって、上図の青の部分だけ書けばいいんですね。「移動する」の処理だけ書いておいて、あとはUnityに任せれば自動的にそのコードを走らせてもらえます。
描画マシーン
描画の処理自分で書くのダルいので、その意味でもUnity使う価値は大いにありますよね。
「プログラミングできること」と「Unityプログラミングできる」ことの乖離
プログラミングができるからといって、すぐにノー勉でUnityプログラミングに参戦できるかというと、ちょっと難しいです。
Unityは大規模フレームワークであり、コンポーネント指向のやべー奴であり、スーパー並行処理マシーンであることを心構えておく必要があります。
例として、下記が単純に「指定されたスピードで進み続ける」プログラムです。
using UnityEngine;
public class Advancer : MonoBehaviour
{
[SerializeField] private Vector3 speed;
//毎フレーム呼ばれる
void Update()
{
//進む
transform.position += speed * Time.deltaTime;
}
}
それで、こうなります。
初見なら、コード見てこう思うと思います。
- UnityEngineネームスペースってなんや
- その継承してるMonoBehaviourクラスってなんや
- SerializeFieldってなんや
- へーVector3ってのがあるんやな
- Update()関数定義しただけでなんで動くん?
- transform未定義で急に出てきたやん
- Time.deltaTimeってなんや
発見がたくさん出てきますね。
上記のものは全てC#の文法ではなく、Unityオリジナルの機能になっています。すなわち、Unityプログラミングを不自由にこなすためには、Unity固有の機能、クラスを把握しておく必要があります。
この記事は入門ですので、ここでは覚えるべきものと覚えなくていいものを切り分け、覚えるべきものについてググりながら実装できるレベルを目指します。
環境構築
解説されつくされているので省略します。
- Unity HubとUnityエディタ(バージョンはLTS=Long Term Supportだったらなんでもいい)
Visual Studio(VSCodeではない)
2024追記
VSCode
の発展によりVisual Studio
を使う必要がなくなりました。
導入手順: https://code.visualstudio.com/docs/other/unity
をインストールし、頑張ってUnityエディタとVisual Studioを連携させる設定をしてください
参考:https://qiita.com/engr_murao/items/b7bbb0eaa48f1c66fc2b
コードを適用するまで
そもそも、どうやって書いたコードを反映させるのでしょうか。剣を買っても装備しないと意味がないように、コードを書いただけでは
先述のコードをコピペして適用するから始めましょう(再掲)
using UnityEngine;
public class Advancer : MonoBehaviour
{
[SerializeField] private Vector3 speed;
//毎フレーム呼ばれる
void Update()
{
//進む
transform.position += speed * Time.deltaTime;
}
}
スクリプトファイルを作る
Project
(デフォルトでは下のファイルがあるところ)に右クリックして、Create → C# Scriptで空のスクリプトファイルを作れます。
ここで、絶対にファイル名をAdvancer
にしてください。 というのは、ファイル名とクラス名が一致しないとUnityが認識されません。
できたファイルをダブルクリックするとスクリプトエディタが開かれますので、上記スクリプトをコピペしてください。
オブジェクトを作る
とりあえずキューブを出現させてみましょう。
Hierarchy
(デフォルトなら左のオブジェクト一覧があるところ)に右クリックして、3D Object→Cubeをクリックすることで出現させることができます。
コードを適用する
このキューブが移動できるようにさせるには、このコードをキューブにアタッチ(=憑依)させます
アタッチのやり方は簡単。
AdvancerファイルをCubeにドラッグ&ドロップするだけです。
右下にチョロッと「Advancer」の項目が増えているのが分かりますね。
パラメーターを調整
Cubeを選択してから、Inspector
(デフォルトなら右)を見ると、下にAdvancerが追加されていますね。
そこでSpeedを調整できます(そういうコードなので)
好きな数字にでもしてください。
実行
エディタの操作はこれさえわかってれば大丈夫です。
コンポーネント指向
いよいよコーディングのお話です。
Unityはコンポーネント指向を採用しています。
さきほどのCubeを選択して、Inspector
を見てみてください。
この項目の一つ一つが、このCubeについているコンポーネントなのです。
コンポーネントとは、そのオブジェクトが持つ機能のことです。
このCubeのコンポーネントを図示すると、こんな感じです。
一つ一つのコンポーネントが、このCubeの処理を果たしているのですね。
Transform
は必須コンポーネントなので消せませんが、例えばMeshFilter
かMeshRenderer
を消してみる(右クリックでできます)と、キューブが透明になってしまいますね。
先ほどやったアタッチとは、すなわちコンポーネントを一つ付与したということになります。
つまるところ、Unityプログラミングとは、コンポーネントを作っていくということに集約できます。(嘘で、シェーダー書いたりすることもあるのですが、基本的にはこの認識で大丈夫です)
そして、コーディング面においても、他コンポーネントを参照することが頻出します。
例えば、オブジェクトの座標は先ほど出てきたTransform
コンポーネントを参照します。
把握すべきもの
もちろん全てのコンポーネントを覚える必要性は全くなく、下記の基本コンポーネントだけ把握しておけばコーディングには困らないです。
-
Transform
=位置・大きさ・回転をつかさどる 全てのオブジェクトに与えられている -
Collider
=当たり判定をつかさどる -
RigidBody
=物理演算をつかさどる
あとは、必要性に応じてググれば大丈夫です。
Transformコンポーネント
ここで、先ほどのスクリプトを見てみましょう。(再掲)
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
コンポーネントをアタッチしてみましょう。
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
だけは覚えておいてください。後述しますが頻繁に使います。
これらの存在だけを知っておくと、あとはググればなんとかなると思います。
これで理解がアンロックされますね。
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()
はとても重要ですね。
新規でスクリプトファイルを作成した際にデフォルトで書かれているぐらいには重要です。
先述のスクリプトの理解がもっとアンロックされました。
//毎フレーム呼ばれる
void Update()
{
//進む
transform.position += speed * Time.deltaTime;
}
の部分。
Update()
関数の中に「進む」処理を書いているので、この処理が毎フレーム発生しているということになりますね。簡単に言うと、常に進み続けるということです。
把握すべき機能
その他、コーディングにおいて知っておきたいUnity特有の機能が一つあります。
SerializeField
です。
メンバー変数の定義にあった
[SerializeField] private Vector3 speed;
の部分。
「SerializeField
」属性をこのようにspeed
変数に付与することによって、エディタ上から数値を調整できるようになります
実は、public
で変数を定義するとSerializeField
指定しなくとも同様のことができますが、一般的にpublic
変数を定義するとロクなことにならないと言われているので、SerializeField
を推奨します。
コーディングしよう!1
各概念の表面を撫でたので、実際のコーディングでこれまで学んだことの具体化を試みます。
回転する
using UnityEngine;
public class Rotater : MonoBehaviour
{
[SerializeField] private Vector3 angularSpeed;
void Update()
{
transform.rotation *= Quaternion.Euler(angularSpeed * Time.deltaTime);
}
}
transform.rotation
はオブジェクトの回転を保持しています。Quanternion
をQuanternion
で回転するのには、掛け算をします。
Quanternion.Euler
は、xyz軸の普通の角度を、Quanternion
に直す関数です。
これによって、エディタからxyzで入力した角速度で回転するようになります。
ちなみに、回転する部分はこんな書き方もできます。
transform.Rotate(angularSpeed * Time.deltaTime);
同様の処理をしています。
今まで覚えてきた関数以外にQuanternion.Euler
やらtransform.Rotate
やらいろいろ知らないものが出てくるやんけふざけんなとお思いかもしれませんが、これらは覚える必要は全くなく、「Unity 回転」とでもググればすぐ出てきます。また、Unity公式スクリプトリファレンスに各クラスがどんなメンバー変数・関数を持っているのかが明記されています。
ここで大事なのが、「回転の情報はTransform
コンポーネントが持っているからTransform
のスクリプトリファレンスを見ればいいや」と調べる検討がつくことかなと思っています。
キー入力に応じて移動
典型的なwで前進、adで回転(方向転換)するスクリプトを書いてみましょう。
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
は「前方向」のベクトルを返します。(前進に使用)
他コンポーネントを参照する
今度は他のコンポーネントから数値を寄越してきたり、他コンポーネントのメソッドを呼ぶプログラムを書きましょう。
まずは、呼ばれる側。
using UnityEngine;
public class ComponentA : MonoBehaviour
{
public string variable = "どりゃあああああ";
//呼ばれる関数
public void Log()
{
Debug.Log("でりゃああああああ");
}
}
次に、呼ぶ側。
using UnityEngine;
public class ComponentA : MonoBehaviour
{
public string variable = "どりゃあああああ";
//呼ばれる関数
public void Log()
{
Debug.Log("でりゃああああああ");
}
}
両方ともキューブにアタッチすると、コンソールにこう表示されます。
ここで大事なのはGetComponent()
です。
GetComponent<コンポーネントのクラス名>()
で、自分のオブジェクトの他コンポーネントを参照することができます。
他オブジェクトを参照する
今度は、上記のスクリプトを真似て、他オブジェクトのコンポーネントを読んでみましょう。
using UnityEngine;
public class ComponentA : MonoBehaviour
{
public string variable = "どりゃあああああ";
//呼ばれる関数
public void Log()
{
Debug.Log("でりゃああああああ");
}
}
上記スクリプトをCubeAにアタッチ。
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
にドラッグ&ドロップしてください。
すると先ほどと同じ結果を得られます。
前述のように、GameObject
型はオブジェクトそのものを指します。
これをSerializeField
することで、他オブジェクトを参照することができます。
先ほどのGetComponent
単体では自分のコンポーネントを返す一方、GameObject.GetComponent()
は参照しているオブジェクトのコンポーネントを返します。
ちなみに動的にオブジェクトを探して参照したい場合はオブジェクト名で検索するFind()
関数と、保持しているコンポーネントで探すFindObjectsOfType
関数などがありますが、Find()
は非推奨です。オブジェクト名が重複したり変更したりした瞬間にバグるので。
プレハブ
ここまでは、オブジェクトをエディタ上で設置してから動かすという静的配置でした。
銃オブジェクトが弾オブジェクトを生成して発射するといったゲーム中にオブジェクトを生成する動的配置を実現するには、プレハブの出番となります。
プレハブとは
いわば、オブジェクトの設計図です。
例えば銃が弾を発射する例でいくと、弾プレハブを作り、銃オブジェクトが弾プレハブを保持します。
そして、射撃するときは、銃オブジェクトが弾プレハブをInstantiate()
(インスタンス化)することで弾オブジェクトを生成します。
設計図の例えでいくと、弾の設計図を持っておいて、射撃する時はその設計図通りに弾を作って発射する、ということですね。
プレハブ化の操作
やり方は簡単。
まずは、キューブでも何でもいいので、普通に何かオブジェクトを生成してください。
そして、hierarchy
上のオブジェクトをProject
にドラッグ&ドロップ。なんか出来たフォルダが、プレハブファイルです。
一度プレハブファイルができたら、ワールド上にあるオブジェクトは削除しても大丈夫です。削除してください。
プレハブの生成
生成してみましょう。
ワールド上にあるキューブに、Rigidbody
をつけてから、下記をアタッチ。
using UnityEngine;
public class Instantiater : MonoBehaviour
{
[SerializeField] private GameObject prefab;
//毎フレーム呼ばれる
void Update()
{
//生成する
Instantiate(prefab);
}
}
まずは、プレハブをセットしなければなりません。
これは「他オブジェクトを参照」でやったように、ドラッグ&ドロップでできます。
これでプレイすると、ウミガメの卵みたいにキューブが生成されまくります。(重いですね)
Rigidbody
をつけた理由としては、「物理演算」がないと同じところに生成されまっても、一点に重複しながら生成されるだけなので生成されているのが確認できないからです。
コーディングしよう!2
当たり判定処理
簡単な当たり判定処理を書きます。
当たり屋
using UnityEngine;
//当たり屋
public class Hitter : MonoBehaviour
{
//当たった時の処理
private void OnCollisionEnter(Collision collision)
{
Debug.Log("Hitter当たった!");
}
}
被害者
using UnityEngine;
//当てられる被害者
public class Victim : MonoBehaviour
{
//当たった時の処理
private void OnCollisionEnter(Collision collision)
{
Debug.Log("Victim:当たった!");
}
}
上記ができたら「当たり屋」にRigidbody
をつけて、「被害者」を「当たり屋」の真下に置いてください。当たり屋が落下したところに被害者が来るように。
説明しきれていない重要概念
- 親オブジェクトと子オブジェクトの関係:オブジェクトには親子関係がありますが、親オブジェクトを動かすと子オブジェクトも動きます。
- Local座標とWorld座標:ワールド全体の絶対的な座標系と、オブジェクトから見た相対的な座標系があります。
-
Invoke()
で、指定時間遅延して関数を呼ぶことができます。 -
UnityEvent
でデリゲート処理ができます。 -
Scene
:ワールド(ないし画面)を複数作れます -
Animator
でアニメーションを制御できます
次回予告
追加執筆予定の記事では、実際にこのようなゲームを作るまでの工程を書いて、Unityプログラミングの具体例を掴めるようにしたいと思っています。