Unityで簡単な3Dゲームをつくってみた

Unityで簡単な3Dゲームをつくったときに得られたノウハウを書きます。

 

なぜつくったか

コンピューターによる3Dモデル表示も身近になって、ゲーム専用機やパソコンだけでなく携帯端末でも3Dゲームがたくさん出ています。そんな中、Unityという開発環境が高品質の3Dゲームを無料で簡単につくることができるというので気になっていました。Unityの使い方を学ぶために、インターネット上の情報をもとに簡単なゲームをつくってみました。

 

なにをつくったか

3Dキャラクターを操ってモノを壊していくタイムトライアルゲームをつくりました。

Green Pasture (グリーンパスチャー)

Game 'Green Pasture'
馬の3Dキャラクターを駆ってキューブを壊してまわるゲーム

キーボードによってプレーヤーキャラクターの方向と速度を操作するだけの単純なゲームです。ゲームフィールドも平面で、ひたすら走りまわるという単純さです。すべてのターゲットを破壊するまでの時間を表示することでタイムトライアルタイプのゲームにしています。

プロジェクトで作成したアセットをパッケージにして以下に公開します。ライセンス条件のために馬や爆発など一部のアセットは入っていないので、このままでは動きません。記事の補足とお考えください。

パッケージの利用方法は「Unity – Unity Manual:プロジェクト間でアセットをどのように再利用するか?」を参照してください。

完成したゲームでは、これに加えて以下のアセットが利用されています。

 

開発環境をインストールする

Unity4.3をMacOSX 10.9上にインストールしました。フリー版ならEメールアドレスがあればインストールしてすぐに開発し始めることができます。

参考:

 

記述言語

UnityはJavascript、C#、Boo(Unity独自言語)でプログラミングできて、一つのアプリに複数言語を混在させることも可能です。今回はなじみのあるJavascriptを使いましたが、Unity独自の仕様がたくさんあって通称UnityScriptと呼ばれるくらい異なるものでした。JavascriptはC#よりも文法が緩く記述量が少ないので初心者には使いやすいのですが、少し複雑なことをし始めると標準のJavascriptの情報とUnityScriptの独自仕様とで混乱してハマります。Unityは実行速度の速いコードを書きやすいC#を使うのが良いようです。

参考:

 

操作方法

操作方法を知るためにはドットインストールのUnity入門が実践的で役に立ちました。特にシーン画面での視点やツールの変更方法は多用するので覚えておくと良いです。

参考:

 

地面をつくる

SceneにTerrainを配置することでゲーム内の地面を作ることができます。Terrainは風景をつくるために特化したオブジェクトなので、他のGameObjectと違って、凹凸をつくったり、テクスチャーをスプレーしたり、木を植えたりするツールが用意されています。

他のGameObjectと同様にTerrainもTransform(空間変換情報)を持ちますが、他のオブジェクトの地上の位置計算をするときに混乱を避ける為に、Position[0,0,0], Rotation[0,0,0], Scale[1,1,1]にしておくと後々楽です。

慣例としてゲーム内の長さは1単位で1メートルと考えられています。例えば、Terrainのサイズが2000×2000だと2平方キロメートルとなります。

 

落下して衝突する

オブジェクトにRigidBodyとColliderの二つのコンポーネントを付けることで、重力に従い、他のオブジェクトと衝突して反作用で動くオブジェクトをつくることができます。

参考:

例えば、メニュー[GameObject>Create Other>Cube]でScene内に立方体を新しくつくります。これはあらかじめ立方体のColliderが付いているので、RigidBodyを追加するだけで地面に落下して転がるオブジェクトになります。

オブジェクトの初期位置でColliderが地面(Terrain)に交差していると、ゲーム開始直後に地面の表面に飛び出す方向に動きます。

 

キャラクターを操作する

FPSタイプのゲームなどのプレイヤーキャラクターは物理挙動を無視した動きをしていますが、プレーヤーにとってはその方が自然に感じます。Unityにはこのような物理挙動に従わないキャラクター操作を簡単に実現するために専用のCharacter Controllerコンポーネントが用意されています。

参考:

Character Controllerは縦カプセル形のColliderも含んでいるので、これを使用した場合は他のColliderを追加しなくても衝突判定が行われます。

キャラクター移動はスクリプトでCharacterController.Move(Vector3)を呼び出すことによって行います。キャラクター移動スクリプトの例は以下のスクリプト・リファレンスに載っています。

参考:

 

効果音を鳴らす

ゲーム内で音を鳴らす為にはGameObjectにAudio Sourceコンポーネントを追加してAudioClipを再生します。サウンドファイルをプロジェクトにインポートするとAudioClipとしてゲームに利用できるようになります。GameObjectにAudio Sourceを追加しておくとスクリプトの中でaudioとして参照できます。あらかじめ AudioSource.clip にAudioClipをいれておいて AudioSource.Play() で再生します。複数の効果音を重ねて鳴らしたいときには AudioSource.PlayOneShot(AudioClip) を使います。

インスペクターでスクリプトへ任意のAudioClipを接続してJumpボタンで再生するスクリプトは以下のようになります。

@script RequireComponent(AudioSource)

var jumpingSound : AudioClip;

function playActingSound(audioClip : AudioClip) {
    if (audio.isPlaying) {
        audio.Stop();
    }
    audio.clip = audioClip;
    audio.Play();
}

function Update () {
    if (Input.GetButtonDown("Jump")) {
         playActingSound(jumpingSound);
    }
}

スクリプト内の audio にはこのスクリプトが属する GameObject に追加された AudioSource がバインドされます。playActingSound(AudioClip)  は AudioSource が再生中ならそれを停止してから新しくAudioClipを再生し始めます。

 

爆発させる

爆発には無料で簡単に派手な演出ができる Detonator Explosion Framework を使うのが定番のようです。

参考:

 

Unity4 ではインポートする途中で警告が出て、そのままインポートしただけでは煙幕に四角い枠が付いてしまいます。この枠を外すには、インポート後に [Detonator Explosion Framework>Resrouces>Detonator>Textures] フォルダ内のファイルの ‘Texture Type’ が ‘Normal Map’ になっているところを ‘Texture’ に変更します。

参考:

 

ゲームを統括するオブジェクトをつくる

ゲームを統括して制御し易くするオブジェクトを一つつくっておくと点数やゲーム状態を保持するために便利です。空のGameObjectをつくって’GameController’タグを設定しておけば、他のオブジェクトからタグ指定で取り出せます。インスペクターで特定のオブジェクトをGameControllerとして設定するか、それが無かったときにAwakeイベントでタグ指定で取り出すスクリプトは以下のようになります。

var gameController : GameObject;

function Awake () {
	if (!gameController) {
		gameController = GameObject.FindWithTag("GameController");
	}
}

 

他のオブジェクトのメソッドを呼び出す

他のオブジェクトのメソッドを呼び出す方法には直接指定する方法と SendMessage によるメッセージ名を指定した方法の2種類あります。

SendMessage は一対多の疎結合で、該当オブジェクトに追加されたスクリプトの中でメッセージ名に合致したメソッドがすべて実行され、合致するメソッドが一つも無いときにエラーを出すかどうかも引数によって設定できます。

参考:

 

 スコアをつける

GameControllerオブジェクトにスコアを管理する以下のようなスクリプト(Scorekeeper)を追加しておきます。

#pragma strict

var score : int = 0;

function ScoreChanged () {
}

function GetScore () : int {
	return(score);
}

function SetScore (newScore : int) {
	score = newScore;
	ScoreChanged();
}

function AddScore(adding : int) {
	SetScore(score + adding);
}

他のオブジェクトからこの Scorekeeper.AddScore(int) などを呼び出せばスコアを変更できます。

次のようなスクリプトでGUITextにスコアを表示します。

#pragma strict

var scoreText : GUIText;
var gameController : GameObject;

private var scorekeeper : Scorekeeper;

function Awake() {
	if (!gameController) {
		gameController = GameObject.FindWithTag("GameController");
	}
	scorekeeper = gameController.GetComponent("Scorekeeper") as Scorekeeper;
	if (!scoreText) {
		scoreText = gameObject.GetComponent(GUIText);
	}
}

function Update () {
	if (scoreText) {
		scoreText.text = scorekeeper.GetScore().ToString();
	}
}

 

ゲーム状態をつくる

ゲームの状態遷移はGameControllerオブジェクトに持たせて、他から参照しやすくします。ゲームによってだいぶ変わるところですが、例えば、スコアが一定以上になったらクリアとすると以下のようなスクリプトになります。

var completedScore : int = 10;

private var state : String;
private var scorekeeper : Scorekeeper;

function Awake () {
	scorekeeper = gameObject.GetComponent("Scorekeeper") as Scorekeeper;
	SetState("awaked");
}

function Start() {
	SetState("started");
}

function Update() {
	var score : int = scorekeeper.GetScore();
	if (score >= completedScore) {
		SetState("completed");
	}
}

function StateChanged (newState : String, oldState : String) {
}

function IsFinished () : boolean {
	return(GatState() == "completed");
}

function IsCompleted () : boolean {
	return (GatState() == "completed");
}

function GatState () : String {
	return(this.state);
}

function SetState (newState : String) {
	if (newState == state) return;
	var oldState = state;
	state = newState;
	StateChanged(newState, oldState);
}

 

経過時間を表示する

以下のスクリプトで Start() から時間を数え始めて Update() で経過時間の表示を変更しています。

var countText : GUIText;
var gameController : GameObject;

private var gameState : GameState;

private var min : int;
private var sec : int;
private var fraction : int;
private var timeCount : float;
private var startTime : float;

function Awake() {
	if (!gameController) {
		gameController = GameObject.FindWithTag("GameController");
	}
	gameState = gameController.GetComponent("GameState");
	if (!countText) {
		countText = gameObject.GetComponent(GUIText);
	}
}

function Start () {
	startTime = Time.time;
}

function Update () {
	if (gameState.IsFinished()) {
		countText.color = Color.blue;
		return;
	}
	timeCount = Time.time - startTime;
	min = (timeCount/60f);
	sec = (timeCount % 60f);
	fraction = ((timeCount * 10) %10);
	countText.text = String.Format("{00:00}:{1:00}:{2:00}",min,sec,fraction);
}

参考:

 

ゲームを一時停止させる

ゲーム内の物理挙動の計算に利用される時間経過の速さは Time.timeScale で変更できます。 Time.timeScale = 1.0 が最高の速さで、少なくなるに従ってスローモーションになり 0.5 で1/2、0.0で静止します。

Time.timeScale が影響するのはrigidbodyへのforceやvelocityなどの物理挙動と FixedUpdate() の発行です。 Update() はTime.timeScale に関係なく発行されます。

参考:

 

地上にオブジェクトをばらまく

TerrainはTerrainDataに地形などのTerrain特有の情報を持っています。これを使って地上のランダムな位置にオブジェクトを発生させるコードの例は以下のようになります。

var mono : Transform;
var margin : int = 100;

function SpawnMono() {
	var trr = Terrain.activeTerrain;
	var pos : Vector3;
	pos.x = Random.Range(margin, trr.terrainData.size.x - margin);
	pos.z = Random.Range(margin, trr.terrainData.size.z - margin);
	pos.y = trr.terrainData.GetHeight(pos.x, pos.z);
	Instantiate(mono, trr.transform.TransformPoint(pos), Quaternion.identity);
}

Terrain.activeTerrainで現在のTerrainをとって、terrainData.sizeで地形全体の範囲を 得ます。terrain.GetHeight(int, int)で指定位置の標高を得ています。transform.TransformPoint(Vector3)でTerrain内のローカル座標から World上のグローバル座標へ変換しています。

参考:

 

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です