Unity で組んでみる


ゲームエンジン Unity の使い方の説明です (2011/10/3 新規作成) (2013/9/4 最新版にあわせて修正)。

Unity / インストール / 基本 / バージョン管理 / GameObject と Component / JavaScript / 配列 / 2次元配列 / Collider と Rigidbody / 戻る / トップページ


Unity

Unity は おそらく近年では最も有名なゲームエンジンの一つで、 ワンソースから iOS, Android, PC, Web プレイヤー、 PS3、Wii、Xbox360 へと出力でき、物理演算や 3D グラフィック機能、 エディタも包括しています。 始めは iOS 向けがメインでしたが、使いやすさや 商用のゲームエンジンでは破格の安さなどで、 2010年あたりからゲーム機向けも含めて急速にシェアを拡大してきました。
 
Unity は基本機能は無料で使うことができ、誰でも簡単に短時間で 相応のクオリティのゲームを作ることができます。 無料版は制限がきつすぎて実質あまり使えないと言うこともありません。 また、Assert Store を介して、グラフィックデータや プログラムモジュールを得られることも便利です。
 
なお、有償版は Unity Pro と呼ばれ、 Unity に機能がプラスされたものになります。 Pro 版はプラグインを書くことで、特定のプラットフォーム向けに Unity にはない機能を補ったり、リアルな影描画ができるようになったりします。
 
Unity で開発したゲームは、PC やスマホ向けの出力アドオンを使って、 簡単にそれぞれのプラットフォームで動作させることができます。 2013年5月21日から iOS と Android 用のアドオンライセンスが無料化されたので、 今ではスマホ向けゲームを自由に作れます。 iOS 向けアプリは Objective-C、Android 向けアプリは Java で開発する必要が ありますが、Unity で書けばそれらのコードを書く必要はありません。 また、逆にプラットフォーム依存のプラグインを書くことで、ない機能を 補うこともできます。
 
ただし、ビルドのためにそれぞれの SDK (Android SDK や Xcode) は インストールが必要です。iOS 向けは Xcode のインストールが必要なために 結果的に Windows では出力できません。 また発売までしたい場合は、それぞれのストアの登録料が必要になります。 AppStore への登録には iOS Developer Program への登録 (年会費8400円) が、 Google Play への登録には Google Playデベロッパー登録 (初回登録のみ25$) が必要となります。
 
まだまだ国内の Unity 情報が Web 上には少ないようなので、 自分が知ったことや調べたことを書き留めていこうと思います。

インストール

Unity の公式サイトは こちら です。 以前は日本語版のページは更新があまり追いついていないこともありましたが、 現在は大丈夫なようです。
 
開発は Windows XP SP2 か Mac OS X Snow Leopard 10.6 (Intel CPU) 以降で 総合環境 (エディタ) である Unity Editor を使って行いますが、 スクリプトは通常のテキストエディタを使うこともできます。 スクリプトは .NET のオープンソースによる再実装である「Mono」を ベースにしており、標準では JavaScript か C#、Boo でコードを記述します ( 共通中間言語に変換して実行するので、iOS 上で動くアプリであっても)。
 
Unity の最新版は 4.2 系で、 こちら から ダウンロードしたインストールプログラムを実行します。

使い方の基本

Unity でゲームを作るには、 専用のエディタでキャラクターや地形などの 「ゲームオブジェクト」を配置して、 ゲームオブジェクト毎に衝突時などイベントが発生した時に 処理をするコールバックのスクリプトを実装するのが基本となります。
 
ゲームオブジェクト初期化時や毎ループ呼び出されるコールバック、 見た目のないスクリプトを載せることを目的とした特殊な ゲームオブジェクトもあるため、それらを活用すると言えば ゲームプログラムをしたことのある方なら 概要は分かって貰えるのではないかと思います (プログラム言語が先にあり、それを補助するようなミドルウェアの存在を 期待する方にとってはちょっと違う)。
 
敵キャラクターなど、同じ処理をするけど数は状況によって 変化するようなものは、テンプレートとなるゲームオブジェクトを 「プレハブ」として登録し、スクリプトで動的にそれを実際に使う ゲームオブジェクトとして生成していきます。
 
また、2D ゲームの場合も 3D 空間上にゲームオブジェクトを配置して、 2次元的に扱うことで実現します。それをしやすくするような設定や機能は ありますが (2013年秋にリリースされる Unity 4.3 でさらに重点的に強化されます)、 あくまで 3D の概念上にあります。 逆にその統一された仕様のおかげで、3D と 2D の混在等がしやすくなっています。

バージョン管理

Unity が生成する各種ファイルは、一時ファイルが多いため、そのまま丸ごと バージョン管理ツールで扱おうとすると無駄が多く生じます。
 
プロジェクトの設定を変更することでバージョン管理しやすいファイル形式に 変更することができるので、プロジェクトを作ったらまずその設定をすることを お勧めします。
 
まず、Unity エディタでプロジェクトを開いた状態で、 メニューの Edit→Project Settings→Editor で "Editor Settings" を開きます。
 
Editor Settings の "Version Control" の "Mode" という項目を探し、 "Metafiles" を選択しましょう。 これで、Library ディレクトリが 重要なメタデータを管理するディレクトリではなくなり、 ただのキャッシュディレクトリになります (バージョン管理をしなくてよくなる)。 同時に、新たに Assets ディレクトリ以下の すべてのファイルとディレクトリについて .meta ファイルが生成され、 新たに ProjectSettings ディレクトリが生成されます。
 
もう一つ、 Editor Settings の "Asset Serialization" の "Mode" という項目を探し、 "Force Text" を選択しましょう。 これで、ほとんどの Asset ファイルがバイナリではなくテキスト形式で 保存されるようになり、diff や merge が行えるだけではなく、 差分だけをコミットするようになるため バージョン管理システムが扱うデータ量が減ります。
 
あとは、Library ディレクトリを除外リストに入れつつ、それ以外を バージョン管理ツールに登録すれば完了です。

GameObject と Component

ここからの説明は Unity の仕組みを見通しよくしますが、一度も実装を せずに読むと退屈なものになるので、一度チュートリアル等をなぞって 一回簡単なコードを組み立てる体験をしてから読むことをお勧めします。
 
Unity の仕組みの特徴として、"GameObject" と "Component" の概念があり、 これを把握しておくとスクリプトを実装する際に混乱しないですみます。
 
まず、キャラクターや地形などのゲームオブジェクトが "GameObject" として 存在し、これらが "Scene" に配置されます。 全ての処理は GameObject に結びつき、これらに関わるイベントをきっかけとして 状態が更新されていくことでゲームが進むのが基本となります。 オブジェクト指向のオブジェクトに相当し、ゲームプログラムをする方々は 普通これらをクラスとして定義するでしょうから、 この考えは分かりやすいと思います。
 
その GameObject は "Component" と呼ばれる機能部品が集まってできており、 Component は GameObject に自由に足すことができます。 どこにあるかを表す "Transform" は 全ての GameObject が持っている Component、 物理計算をさせるなら "Rigidbody" Component を付けます。
 
スクリプトさえも Component の一種で "Script" Component として GameObject に付けます。 つまり、Script Component が付与された GameObject がスクリプトで 動作するわけです。 Script Component をつけない GameObject や、 複数付与した GameObject も作れます。
 
この考え方は Unity の特徴でもあり、高い汎用性を誇っている点でも あるので忘れないようにしましょう。
 
ちなみに、見た目情報のない空っぽの GameObject もあるので、 スクリプトだけ置く場合はその Empty GameObject を作って、 スクリプト Component を付けます。

JavaScript での実装

C# でも実装できるのですが、私は JavaScript を使ったので、 ここでは JavaScript で説明を書いていきます。
 
Unity で使われる JavaScript 言語は JavaScript と呼ばれてはいますが、 実際は ECMAScript の独自拡張といったおもむきで、 Flash の開発言語である ActionScript に実は近いようです。 型宣言があり、標準関数は似てはいるものの違う書き方を要し ( Math.sin ではなく Mathf.Sin、Math.random はなく Random.Range を使うなど)、 多次元配列の書き方が違い、動的な処理は弱めです。 また、GameObject などがインスタンスとなる、 オブジェクト指向的な処理になります。
 
スクリプトも GameObject と Component を基本に考えます。 GameObject も Component もクラスで定義されており、 様々な関数やフィールドがあらかじめ定義されています (クラスのドキュメントは こちら)。 Component の種類にスクリプトを自由に書けるものがあり、 1 つの .js ファイルがそのような 1 つのクラスとなります。 これを定義して GameObject に Component として足すことで、 各 GameObject にスクリプトを加えていきます。
 
GameObject インスタンスが複数の Component インスタンスを保持しており、 GameObject インスタンスから、GetComponent メソッドで Component の インスタンスを取得してそれに指示を与えることになります。 ですが、毎回インスタンスの取得をするのは非効率で面倒なので、 基本的な Component に関しては、GameObject インスタンスの フィールド値で参照できるようになっており、そちらを使うのが一般的です。
	function Start() {
		// GameObject の参照
		print(gameObject);
		
		// その GameObject が持っている Transform の参照 (2つは同じ結果)
		print("gameObject.GetComponent(Transform)="+gameObject.GetComponent(Transform));
		print("gameObject.transform              ="+gameObject.transform);
	}
スクリプト自身も Component なので、スクリプトで宣言したり、 インスタンスを明示せずに (this. を書かずに) 参照する対象は、 そのスクリプトの Component インスタンスです。 GameObject インスタンスではない点に注意です。
	var field=3;
	
	function Start() {
		// フィールド値の参照 (2つは同じ結果)
		print("field     ="+field);
		print("this.field="+this.field);
		
		// メソッドの呼び出し (2つは同じ結果)
		MyFunction();
		this.MyFunction();
		
		// スクリプト Component (=this) が属している GameObject を取得する定義済みフィールド値 (2つは同じ結果)
		print("gameObject     ="+gameObject);
		print("this.gameObject="+this.gameObject);
	}
	
	function MyFunction() {
		print("function called");
	}
GameObject インスタンスからその保持する Component インスタンスを 取得する方法が Component クラスにも全く同様に用意されており、 スクリプト (Script Component インスタンス) から、その GameObject の 保持する他の Component インスタンスを直接引っ張ってこれます。 そのため便利なのですが、this が GameObject インスタンスと間違って 理解してしまうことが多いので、ご注意を。
	function Start() {
		// Transform の参照 (4つは同じ結果)
		print("this.gameObject.transform="+this.gameObject.transform);
		print("gameObject.transform     ="+gameObject.transform);
		print("this.transform           ="+this.transform);
		print("transform                ="+transform);
	}
スクリプトの Component クラスは、MonoBehaviour クラスを継承しており、 その定義済み関数をオーバーライドすることで、各機能を実現していきます。 オーバーライドはスクリプト上では単純にその名称で定義するだけで良いです。 インスタンス生成時に呼ばれる Awake()、毎フレーム呼ばれる Update()、 衝突発生時に呼ばれる OnCollisionEnter() などが良くオーバーライドされます。
	function Update() {
		if (Input.GetButtonDown ("Jump"))			// ジャンプボタンを押したら
		{
			rigidbody.AddForce (0.0, 300.0, 0.0);	// 高さ方向に力をかける
		}
	}

配列の扱い

Unity の配列の扱いは結構ややこしいので、正しく理解してから使い始めた方が 良いでしょう。
 
Unity で扱える配列には、 JavaScript が元々もっている配列と、 Unity の組み込み機能の (.NET が持っている) 配列の 2 種類があります。 ここでは便宜上、前者を「JavaScript 配列」、 後者を「組み込み配列」と呼びます。
 
まず、JavaScript 配列は通常の JavaScript の配列と同じように扱えます。
	// 空で初期化
	var array1=new Array();
	array1[0]=3;
	print(array1[0]);			// 3
	
	// 要素を列挙して初期化
	var array2=new Array("foo","bar");
	print(array2[0]);			// foo
	print(array2.Length);		// 2 (Unity の命名規則からメンバは大文字始まり)
	print(array2.length);		// 2 (でも、互換性のために小文字始まりもサポート)
	
	// new を省いた書き方
	var array3=Array("foo","bar");
	
	// 要素数を指定した初期化
	var array4=new Array(2);
	array4[0]="foo";
	array4[1]="bar";
	array4[2]="hoge";			// 後から拡張もできる
	
	// 注意!これは違う
	var array5=[1,2,3,4,5];
最後の JSON 的な表記は通常の JavaScript 言語では配列を表しますが、 Unity ではそれは組み込み配列の表記として扱われるので注意が必要です。 いつもの感覚で使ってしまうと、JavaScript 配列と組み込み配列の 違いで混乱してしまいます。
 
組み込み配列は型と要素数を限定した配列で、 その制約のために 性能面ではこちらの方が有利 です。
	// 要素数を指定した初期化
	var array4 :String[] =new String[2];
	array4[0]="foo";
	array4[1]="bar";
	print(array4.Length);		// メンバのアクセス方法は JavaScript 配列と同じ
	array4[2]="hoge";			// 拡張しようとするとエラー
	array4[1]=5;				// 型が一致しないとエラー
	
	// 要素を列挙して初期化
	var array5: int[] =[1,2,3,4,5];
JavaScript 配列と組み込み配列は簡単に相互変換できます。 状況に応じてうまく使い分けるなり、どちらかに統一しておくなりすると 良いでしょう。
	// 組み込み配列→JavaScript 配列
	var builtinArray1 :int[] =new int[2];
	builtinArray1[0]=11;
	builtinArray1[1]=22;
	var javaScriptArray1=new Array(builtinArray1);
	
	// JavaScript 配列→組み込み配列
	var javaScriptArray2=new Array("foo","bar");
	var builtinArray2 :String[] =javaScriptArray2.ToBuiltin(String) as String[];

2次元配列の扱い

2 次元配列はさらにややこしいです。
 
まず、JavaScript 配列の 2 次元配列は使えないようです。 明示されているドキュメントを見付けることができませんでしたが、 テストコードを書いて調べてみると、 1 次元配列に自動で変換されてしまうことが確認できます。
	// 要素を列挙して初期化する方法 (失敗)
	var array2=new Array(7,1,new Array(3,4,5));		// new Array(7,1,3,4,5) と同じになってしまう
	print(array2[0]);
	print(array2[2][1]);		// エラーになる
	print(array2[2]);			// 3
	print(array2[3]);			// 4
	print(array2[4]);			// 5
	
	// 要素数を指定した初期化して、代入する方法 (失敗)
	var array4=new Array(3);
	array4[0]=1;
	array4[1]=2;
	array4[2]=new Array(2);
	array4[2][0]=5;				// エラーになる
次に組み込み配列の 2 次元配列ですが、これはさらに 2 種類に分かれます。 「配列の配列」と「四角い多次元配列」です。 この考え方は .NET (C#) によるもののようですが、私はそれらを 扱ったことがないので、同じように知らない人向けに説明を書きます。
 
まず、配列の配列は JavaScript や他の言語でも よく使われている、配列の要素にさらに配列が入れ子になっていることで 多次元配列を実現する方法です。
	// 要素を列挙して初期化
	var array5 =[ [ 1, 2, 3 ], [ 4, 5 ] ];
	print(array5[0][1]);		// 2
この要素を列挙する書き方でのみ、組み込み配列の配列の配列を利用できます。 ただし、この場合の型宣言が JavaScript では現状用意されておらず、 中途半端な実装となっています。
 
次に、四角い多次元配列は、2 次元の要素を持つことを前提とした専用の 実装で 2 次元配列を実現する方法です。 配列の添え字内をカンマ区切りで複数指定する書き方をします。 配列の配列では 2 次元目の各要素数をばらばらにできますが、 四角い多次元配列では 2 次元目の要素数が一律になります。
	// 要素数を指定した初期化
	var array4 :int[,] =new int[2,3];
	array4[0,0]=9;
	array4[0,1]=8;
	array4[0,2]=7;
	array4[1,0]=6;
	array4[1,1]=5;
	array4[1,2]=4;
	print(array4[0,2]);				// 7
	print(array4.length);			// 6=2*3
	print(array4.GetLength(0));		// 2
	print(array4.GetLength(1));		// 3
ここまで読めばお気づきと思いますが、2 次元配列をまともに使おうと思うなら 組み込み配列の四角い多次元配列を使うのが良いでしょう。 そして、それとの対応やパフォーマンスを考えると、通常の配列も 組み込み配列を使うのが適切と思われます。

Collider と Rigidbody

衝突周りの処理も各要素を正しく把握していないと、思い通りに GameObject が 動いてくれなかったり、コールバックが得られなかったりしますので、 詳しくまとめます。
 
まず、衝突関係の Component として、 Collider と Rigidbody、CharacterController があります。
 
Collider は衝突判定をする形状を決める Component で、 これがないとあらゆる衝突判定は行われません。 Collider は形状を決めるだけで、 その GameObject が他と衝突した時にどうなるのかは 互いの GameObject に付加されている他の Component (Rigidbody や CharacterController) によります。
 
Rigidbody は物理計算を有効にする Component で、力や回転を考慮するように なるので、他の GameObject や関数で力が加わるとスクリプトで何もしなくても 動き続けることがあるようになります。 物理計算は衝突をした後の処理なので、衝突とは別の概念ですが、 衝突判定をしないと関数でしか力がかからなくなるので Collider と セットで使われることがほとんどです。
 
CharacterController は Collider の一種ですが、 かなり特殊な扱いがされている、 ゲームのキャラクター操作用の Component です。 Rigidbody のように他からの作用で勝手に動いたり回転されると ゲームのキャラクターとしては扱いにくいため、 専用の Move 関数を呼んだ時以外は勝手に動かず、 また Move 関数中は押し返される処理が働くようになっています。 Collider とは考えず、別ものと捉えておくと理解しやすいです。
 
そして、Collider には isTrigger フラグ (Inspector 上の Is Trigger) があり、 この有無によって扱いが大きく変わります。 壁のようなキャラクターがそれに重なりそうになると押し返されるものが 標準の Collider で、アイテムのようなキャラクターが重なった時に処理は するものの押し返したりしないものは Trigger と呼ばれる、 isTrigger フラグを有効にした Collider となります。 この isTrigger フラグの有無での Collider も別ものと捉えておくと 理解しやすいです。 同様に、Rigidbody にも isKinematic フラグ (Inspector 上の Is Kinematic) が あり、これが有効になると自分自身は物理計算されない (他の GameObject へは 影響を与える) 特殊なものになります。利用頻度は低いですが、 isKinematic フラグの有無での Rigidbody も別ものと捉えましょう。
 
 
この理解を前提として、押し出し処理が効く条件について見ていきます。
 
CharacterController を持っている GameObject は、 専用の Move/SimpleMove 関数で動かした時、 移動先に Trigger 以外の Collider (isTrigger==false) を持つ GameObject が あるなら押し返されます。 移動先に CharacterController を持っている 別の GameObject がある場合も同じです (Collider の一種だから)。
 
Rigidbody を持っている GameObject は、 Kinematic ではなく (isKinematic==false)、 移動先に Trigger 以外の Collider (isTrigger==false) を持つ GameObject が あるなら押し返されます (相手側に Rigidbody は必須ではありません)。 こちらも Collider の一種なので CharacterController にも押し返されます。 物理計算を伴うので厳密には力を受けて、動きます。 なお、Rigidbody だけでは衝突判定の形状を持っておらず判定ができないので、 Collider も持っている必要があります。
 
 
最後にコールバックが呼ばれる条件について見ていきます。 衝突判定関係のコールバックとしては、 OnTrigger〜、OnCollision〜、OnControllerColliderHit があります。 むやみにコールバックが呼ばれるとパフォーマンスに悪影響があるため、 それぞれの目的に合致した場合にしかコールバックは呼ばれないように 設計されているようです。 コールバックは衝突した両方の GameObject に対して呼ばれますが、 それぞれの目的の基準となる GameObject と他の GameObject の衝突と 考えると分かりやすいです。
 
OnTrigger 系コールバックは Trigger を持つ GameObject との 衝突を通知するコールバックです。 そのため Trigger (isTrigger==true の Collider) を持つ GameObject と、 他の GameObject の衝突にしか反応しません。 反応する相手の GameObject は Rigidbody か CharacterController を持っている 必要があります。 Rigidbody は Kinematic (isKinematic==true) でも問題ありあせんが、 Rigidbody だけでは衝突判定の形状を持っておらず判定ができないので、 Collider (Trigger も可能) も持っている必要があり、言い直すなら、 反応する相手の GameObject は Rigidbody+Collider か CharacterController を持っている必要がある、となります。 アイテムを取った時や特定の場所にプレイヤーが立った時などに処理するために 使うことが多いでしょう。
 
OnCollision 系コールバックは Rigidbody を持っている GameObject が、 押し返される時に通知するコールバックです。 そのため Kinematic ではない (isKinematic==false) Rigidbody を持っている GameObject と、他の GameObject の衝突にしか反応しません。 反応する相手の GameObject は Trigger 以外の Collider (isTrigger==false) を 持っている必要があります。 なお、Rigidbody だけでは衝突判定の形状を持っておらず判定ができないので、 Collider も持っている必要があります。
 
OnControllerColliderHit コールバックは CharacterController を持っている GameObject が、押し返される時に通知するコールバックです。 そのため CharacterController を持っている GameObject を 専用の Move/SimpleMove 関数で動かした時の、 他の GameObject の衝突にしか反応しません。 反応する相手の GameObject は Trigger 以外の Collider (isTrigger==false) を 持っている必要があります。 反応する相手が CharacterController を持っている GameObject の場合、 他のルールからはコールバックが呼ばれても良さそうですが、 この場合はなぜかコールバックは呼ばれません。
 

戻る