はじめに
アカツキは5周年を迎え、先日パーティーが開かれました。その時に展示物として披露したUnityとOculus Rift DK2を使ったゲームを作った話をしようと思います。
今回作ったものは隕石を避けながらコインを取って行ってゴールを目指す、レースゲームです。この記事では様々なアセットを組み合わせてUnityを使ってどのようにして短期間でレースゲームを作り上げたかを紹介したいと思います。
前提
開発し始めたときの私はこのような感じでした。
- Unityは少しかじったことがある
- Oculus開発は初
- 一般的なプログラミングスキルは持っている
あと、平日は業務で忙しいため制作はもっぱら土日の開いた時間で やりました。よって実働時間は4、5日といったところです。
また、本投稿で紹介するソースコードはわかりやすくするためにUnity上必要な部分を省いているところもあるため、このままでは動作しません。あしからずご了承ください。
コンセプトメイキング
パーティのコンセプトが「祭」ということもあり、祭にちなんだアセットをつかってみようということでこのアセットを利用したゲームを作ることになりました。
ゼンリンが無料で公開しているJapanese Matsuri Cityというアセットです。
この段階でのイメージでは
- 街を歩き回る
- コインとかを集めるドットイート系ゲーム
- 制限時間内にどれくらいのコインを集められるかを競う
感じのゲームにしようかなと構想しました。
まずはJapanese Matsuri Cityの中を走り回るようにすることを目指しました。
試作1
Unityのバージョンは最新の5.0系を使いました。 UnityでOculusを使うのは非常に簡単です。 OculusのDeveloperページからOculus PC SDK Unity 4 Integration 0.6.0.0-betaをダウンロードしてきます。Unity 5でもそのまま利用することができます。
UnzipするとOculusUnityIntegration.unitypackageというファイルがあるので、そのファイルをUnityのプロジェクトにimportします。
importするとOVR/PrefabsのなかにOVRCameraとOVRPlayerControllerという2つのprefabがあります。これはそれぞれ
- OVRCamera:Oculusから見る固定位置のカメラ(移動不可)
- OVRPlayerController:Oculusから見るキャラクタ(コントローラーにより移動可)
です。今回は動きまわるアプリを作りたいのでOVRPlayerControllerを使います。 OVRPlayerControllerをSceneの中に配置します。そして、Japanese Matsuri Cityのオブジェクトに衝突判定を加えるために、Mesh Colliderを追加します。
Oculus Riftをつなげて実行するとこんな感じで見えるようになりました。
改良
試作1の段階では
- 画面を回転させると酔う
- すぐに画面の端に到達してしまって面白く無い
などの課題がありました。 これに対応するためにゲームの方向性を変える必要が出てきました。
酔いにくいコースを作る
VR酔いの主な原因は、現実世界にない不自然な移動によるものです。特に画面をコントローラーで回転させる操作はほんの数秒でも酔ってしまいます。そこで、Japanese Matsuri Cityのアセットをそのまま使うという計画を変更して、Oculusに適した下のようなまっすぐなコースを作成しました。コースの端に透明な壁を用意してプレーヤーが誤ってコースから出てしないよう細工をしています。
邪魔キャラを作る
何もなくただまっすぐのコースを走るのはつまらないので邪魔キャラを用意することにしました。邪魔キャラは今回天から落ちてくる隕石ということにしました。 下記のようなコードで隕石を出現させています。 intervalという変数で隕石が出現する周期を設定します。フレーム単位で呼ばれるUpdateメソッドで時刻を進めていき、周期に達したらInstantiateで隕石を作成します。隕石を作成する座標はプレーヤーの現在位置をもとにしています。
# pragma strict
var meteo : GameObject;
var interval : float = 0.1;
private var timer : float;
private var prevPlayerZ : float = 0.0f;
function Start () { timer = 0.0; }
function Update () {
timer -= Time.deltaTime;
if ( timer < 0.0 ) {
var player : GameObject = GameObject.FindWithTag("Player");
var velocityZ : float = player.transform.position.z - prevPlayerZ;
var offsetX : float = Random.Range(0, 20) - 10;
var offsetZ : float = Random.Range(0, 5) - 20 + velocityZ * 10;
var position : Vector3 = Vector3(offsetX + player.transform.position.x, 20, offsetZ + player.transform.position.z);
prevPlayerZ = player.transform.position.z;
Instantiate(meteo, position, transform.rotation);
Instantiate(meteo, position + Vector3(0, 0, 20), transform.rotation);
Instantiate(meteo, position + Vector3(0, 0, 30), transform.rotation);
timer = interval;
}
}
隕石はSphereを元に隕石を表現するマテリアルを追加しています。今回は火の玉っぽい感じにしてみました。
コインを作る
パワーアップアイテム、およびスコアアップのアイテムとしてコインを作りました。
単にスコアアップさせるだけでなく、プレーヤーをスピードアップさせる効果を実現しました。この操作はOVR Player ControllerのComponentの中のAccelarationという変数を操作することで実現できます。スピードアップする一方になるとゲームバランス的に易しすぎるため、隕石にあたった場合はスピードダウンさせる効果をつけます。Accelarationの値が0、もしくは負数になってしまうとゲームが成り立たなくなるため、Accelarationは初期値未満にはならないようにします。
このアプリはJavascriptを使って制作していましたが、OVR Player ControllerはC#のクラスだったため、GetComponentを使ってのアクセスはできませんでした。このため、Accelarationを操作するC#のクラスとメソッドを持つComponentを独自に用意して、Javascript側からはSendMessageを使ってアクセスするという方法を取りました。 (調べてみると他にも方法があるみたいです。) また、ゲームオーバーの時にコントローラー操作をできないようにするため、controllerのenabledをfalseに変更しています。
呼ぶ側
function Meteo() {
var player : GameObject = GameObject.FindGameObjectWithTag("Player");
player.SendMessage("Meteo");
}
function GetCoin() {
var player : GameObject = GameObject.FindGameObjectWithTag("Player");
player.SendMessage("GetCoin");
}
呼ばれる側
using UnityEngine;
using System.Collections;
public class PlayerEventListener : MonoBehaviour {
private float initAcceleration;
void GetCoin() {
OVRPlayerController controller = GetComponent<OVRPlayerController> ();
controller.Acceleration += 0.1f; // コインを取ったら加速
}
void Meteo() {
OVRPlayerController controller = GetComponent<OVRPlayerController> ();
if (controller.Acceleration > initAcceleration) controller.Acceleration -= 0.1f;
// 隕石にあたったら減速
}
}
制限時間をつける
ゲームらしくするために制限時間をつけます。このためには ゲーム進行中→ゲーム終了という状態遷移と時間を管理するオブジェクトが必要です。今回は「GameModel」という名前の空のGameObjectを追加して、そこで時間を管理させることにしました。状態遷移に関してはgameStateという変数を用意して扱います。
# pragma strict
var time : float = 60.0;
var readyTime : float = 5.0;
var timeObject : GameObject;
private var gameState : int = 0;
// 0 -> 待機状態
// 1 -> ゲーム中
// 2 -> ゲームオーバー
function Update () {
if(gameState == 0) {
// ゲーム開始を待機
readyTime -= Time.deltaTime;
if (readyTime <= 0.0f){
gameState = 1;
}
} else if(gameState == 1) {
// ゲーム中
time -= Time.deltaTime;
if ( time <= 0.0f ) {
time = 0.0f;
GameOver(false);
}
}
}
function GameOver(isCleared : boolean) {
gameState = 2;
// ゲームオーバー
}
スコアを作る
スコアは隕石に触れると-100点、コインに触れると500点に設定しました。 ゴールすると5000+残り時間×500点を加算します。 スコア計算と制限時間は「GameModel」に集約しています。 触れた時にはAudioSourceを使って音声を鳴らします。
var score : int = 0;
public var meteoDownScore : int = 100;
public var coinUpScore : int = 500;
function ScoreUp (up : int) {
if (gameState == 1) ChangeScore(up);
}
function ScoreDown (down : int) {
if (gameState == 1) ChangeScore(-down);
}
private function ChangeScore(change : int) {
score += change;
}
function Meteo() {
var player : GameObject = GameObject.FindGameObjectWithTag("Player");
player.SendMessage("Meteo"); ScoreDown(meteoDownScore);
var src = GetComponent(AudioSource);
src.clip = meteoClip;
src.Play();
}
function GetCoin() {
var player : GameObject = GameObject.FindGameObjectWithTag("Player");
player.SendMessage("GetCoin");
ScoreUp(coinUpScore);
var src = GetComponent(AudioSource);
src.clip = coinClip;
src.Play();
}
function Goal() {
var goalscore = 5000 + Mathf.Floor(500 * time);
ScoreUp(goalscore);
GameOver(true);
}
時間やスコアを表示する
スコアや時間をOculusの画面上に表示するにはOVRPlayerControllerからツリーをたどったところにあるCenterEyeAnchor/OVRTrackerBounds/Canvasの下にPanelを追加して、このPanelにTextMeshをもたせます。
画面上の位置は今のところ実機で表示させながら調整していくしかなさそうです。画面端に表示させると読めないので、なるべく中央に寄せておくとよいです。
ゴール演出を作る
最後にゴール演出を追加しました。 ゴールした時には「Congratulations!」の表示とゴール地点に表示される花火のオブジェクトで祝福します。
振り返り
もう少し時間が取れたら
- お邪魔キャラ(人間タイプ)をマップに登場させる
- コントローラー操作をもう少しレース風に改良する
- ライバルカーを登場させて競走する
といった要素を加えたかったなと思いました。
Unityの最新版の5.1ではOculusがアセットという形ではなく標準サポートされてさらに使いやすくなるようなので、今後試してみたいと思います。