PR
連載: 

VRシューティングゲームを実装しよう

2016年10月5日(水)
酒井 駿介

前回は、ゲームの「設計」を行うことで、これから実装するシューティングゲームの大まかな機能を定義した。今回はこれらをもとに、Unity上で具体的な実装を行っていこう。過去連載の「モバイルVRの開発環境を準備しよう」と「サンプルプロジェクトをビルドしてみよう」(Gear VR・ハコスコ/Google Cardboard)も参照してほしい。

プレイヤーからの入力を実装する

プレイヤー機能の定義の1つに「HMDの動きやタッチパッド・コントローラからの入力を受け付けてVR 空間に反映させる」というものがあった。これを実現させるための手段を見ていこう。

HMDからの入力を受け付ける

「HMDからの入力」は、つまるところプレイヤーの頭部の動きのことだ。これをVR空間のカメラに回転情報として流し込めば、HMDの動きがVR空間でも同期されることになる。この時点で気づいた読者もいるかもしれないが、過去連載でも触れたとおり本機能はUnity本体、もしくはSDK自体がサポートしているものなので、特にプログラムを組む作業は不要だ。念のため、プラットフォームごとに簡単に振り返ってみよう。

Gear VRの場合はMain Cameraを置くだけ(ビルド時にはPlayer Settings > Other Settings > Virtual Reality Supportedを忘れずに)、ハコスコ/Google Cardboardの場合はSDKからGvrViewerMain.prefabを配置するだけで完了だ。

いずれのプラットフォームでもSceneにはMain Cameraが配置されている状態だと思うが、ここでは「プレイヤー」としての機能をMain Cameraゲームオブジェクトに集約していくことにする。

タッチパッド・コントローラからの入力を受け付ける

Gear VRではHMDの右側にタッチパッドが、Google Cardboardにも簡易的なタッチパッドが搭載されている。また、タッチパッドが搭載されていないハコスコでも、Bluetoothコントローラを接続すればボタン入力をゲームに反映できる。これらの入力によって何らかの処理を行うプログラムを書いてみよう。

Project Viewで右クリック > Create > C# Script を選択して新規クラスを作成後、名前を「Player」に変更する。ダブルクリックして編集できる状態にしたら、スクリプトのクラス名がファイル名と同一であることを確認しておこう。そうしないと、エラーとなりスクリプトが動作しない。

確認できたら、まず入力を受け付ける処理を書くためにMonoBehaviour関数を定義するが、ズバリ、ここはFixedUpdate関数を使う。FixedUpdateはUpdate関数と同じく毎フレーム呼び出される関数だが、処理速度によってフレームレートが変化しても影響を受けない。つまり、処理速度が低下して動作が遅延した場合でも必ず一定のタイミングで呼び出されるため、このような入力を受け付ける処理は必ずFixedUpdate関数に記述するのが鉄則だ。

public class Player : MonoBehaviour {
    void FixedUpdate () {
    }
}

FixedUpdate内では、if文で囲ったInput.GetButtonDown(“<ボタン名>”)を記述する。Input.GetButtonDown()は指定したボタンが押された場合にTrueを返す、つまり入力があったときif文内の処理が実行される。ボタン名は”Fire1” と指定しておく。

if (Input.GetButtonDown ("Fire1")) {
    // 処理
}

“Fire1”はUnity内部で定義されている値で、画面タップやコントローラの1ボタン(どの位置のボタンかはハードウェアによって異なる)、マウスの左クリックからの入力を識別できる共通の値である。つまり、このように記述するだけで画面タップとコントローラだけでなく、マウスにも対応した処理を一度に書けてしまうのだ。

なおEdit > Project Settings > Inputで他の入力の定義の値を確認できる

そして、Player.csクラスをプレイヤーゲームオブジェクトであるMain Cameraにアサインしておこう。

弾丸の発射

今度は、画面タップもしくはコントローラのボタンを押すとプレイヤーから弾丸が発射される仕組みを実装していく。まずは、弾丸となるゲームオブジェクトを作成しよう。Hierarchy ViewでCreate > 3D Object > Sphereを作成し、名前をBulletとする。BulletにはInspectorからAddComponentボタンをクリックしてRigidbodyコンポーネントをアサインしておく。その後Project Viewにドラッグ・アンド・ドロップしてプレハブ化したら、Hierarchy View上からは削除しておこう。

Bulletをプレハブ化したらScene上には不要だ

次に、先程作ったBulletを発射するための「場所」をMain Cameraに定義しよう。Hierarchy ViewでCreate > Create Emptyし、名前をShootPositionとする。ShootPositionをMain Cameraにドラッグ・アンド・ドロップしてMain Cameraの子ゲームオブジェクトにしたら、ShootPositionのPositionをMain Cameraの少し前方に置かれるように調整する。

ShootPositionからBulletが発射されるようにする

終わったらPlayerクラスの編集に戻り、メンバ変数に次の2つを定義しよう。

public GameObject bullet;
public GameObject shootPosition;

Hierarchy Viewに戻り、プレイヤーゲームオブジェクト(Main Camera)のInspector に注目すると、上記で定義した2つのメンバ変数を格納するフィールドが現れるので、bulletにはProject ViewからプレハブBulletを、shootPositionへはHierarchy ViewからShootPositionをアサインする。これで、プレイヤークラスからそれぞれのゲームオブジェクトにアクセスできるようになった。

このような見た目になればOKだ

最後に、弾丸が発射される仕組みを書いていこう。Input.GetButtonDown()の結果を判定するif文の中に、次のように記述する。

    if (Input.GetButtonDown ("Fire1")) {
        // Bullet のゲームオブジェクトを生成する
        GameObject bulletInstance = Instantiate<GameObject>(bullet);
        // 生成した Bullet の位置を shootPosition に合わせる
        bulletInstance.transform.position = shootPosition.transform.position;
        bulletInstance.GetComponent<Rigidbody> ().AddForce (shootPosition.transform.forward  * 1000f);
    }

基本的にはスクリプト上のコメントの通りだが、ポイント毎に解説すると次のようになる。

  • Instantiate<型>()で指定したオブジェクトをScene上に生成し、さらに指定した型で変数に格納できる
  • GetConponent<型>()で指定した型のコンポーネントを取得できる(ここでは行っていないが、そのまま変数にも代入できる)
  • RigidbodyコンポーネントのAddForce(方向)を使うと、物体を指定した方向に加速できる。transform.forwardは自身のtransformの前方を意味する。乗算している1000fは物体の速度パラメータ

最終的なコードは、次のようになる。変更点はAddForce()を行っている箇所で物体の速度をメンバ変数で定義し、後から値を変更できるようにした。また、SDKを利用するハコスコ/Google Cardboardの場合はStart()関数内の処理を加える必要がある。これはShootPositionの回転を正しく反映させるためだ。

public class Player : MonoBehaviour {

    public GameObject bullet;
    public GameObject shootPosition;
    public float shootSpeed = 1000f;

    /// <summary>
    /// ハコスコ/Google Cardboard のみ必要な処理(Gear VR の場合は不要)
    /// </summary>
    void Start() {
        shootPosition.transform.parent = transform.FindChild ("Main Camera Left");
    }

    void FixedUpdate () {
        if (Input.GetButtonDown ("Fire1")) {
            // Bullet のゲームオブジェクトを生成する
            GameObject bulletInstance = Instantiate<GameObject>(bullet);
            // 生成した Bullet の位置を shootPosition に合わせる
            bulletInstance.transform.position = shootPosition.transform.position;
            bulletInstance.GetComponent<Rigidbody> ().AddForce (shootPosition.transform.forward  * shootSpeed);
        }
    }
}

プレイヤー機能の仕上げ

プレイヤー機能の仕上げとして、最後にゲームの進行上必要なパラメータをPlayerクラスに設定しよう。ここは、HPパラメータをメンバ変数の形で設定しておく。

public int playerHP = 3;

また、Main CameraオブジェクトにAddComponentボタンからSphere Colliderコンポーネントをアサインしておこう。このColliderは衝突判定を行う際に必要で、後述するエネミー機能の実装で使用する。

緑の球体がSphere Colliderだ

エネミー機能の実装

今度はエネミー機能を実装していく。ここでは最低限の機能として、次の3つの要素を定義した。

  • HP・攻撃力・移動スピードのパラメータを持つ
  • ゲームが開始するとプレイヤーに向かってきて、プレイヤーと衝突したらプレイヤーのHPを攻撃力分減少させる
  • Bulletと衝突したら消滅する

エネミーの用意とパラメータ定義

まずは、Hierarchy上でCreate > 3D Object > Cubeを作成し、エネミーゲームオブジェクトにしよう。次に、AddComponentをクリックしてRigidbodyコンポーネントをアサインし、Use Gravityのチェックを外しておく。最後は、例によってEnemy.csを作成し、エネミーゲームオブジェクトにアサインする。

このオプションを外すとゲーム開始時に重力によって無限に-Y方向へ移動してしまうのを防ぐ

続けて、HP・攻撃力・移動スピードのパラメータをメンバ変数として記述する。

public int enemyHP = 1;
public int enemyAttack = 1;
public float enemySpeed = 1;

エネミーの移動処理を実装する

エネミーの移動機能は、まずStart()関数でプレイヤーを探しGameObject.Find (名前)、毎フレーム分の処理を実行するUpdate()関数内で必ずプレイヤーをエネミーの正面に捉えるようにし(transform.LookAt(方向))、 transform.Translate(方向)でエネミーの前方へ移動させるようにする。これで、エネミーはどのような位置にいても必ずプレイヤーに向かって移動するようになるわけだ。試しに、ゲーム実行中にプレイヤー(Main Camera)を手動で上に移動してみると、エネミーは追いかけるようについてくるはずだ。

private Player player;

void Start () {
    // プレイヤーゲームオブジェクトを探し、Playerコンポーネント(クラス)をメンバ変数に格納する
    player = GameObject.Find ("Main Camera").GetComponent<Player> ();
}

void Update () {
    // プレイヤーの方を向く
    transform.LookAt (player.transform);
    // 自分の前方(forward)へ移動する
    transform.Translate (transform.forward * enemySpeed, Space.World);
}

エネミーの衝突検知

ゲームオブジェクト同士がぶつかりあったとき、次の2つの条件であれば、その衝突を検知できる。

  • ゲームオブジェクト同士がColliderコンポーネントを持つ
  • ゲームオブジェクトのうち片方・もしくは両方にRigidbodyコンポーネントがある

今回はこの衝突検知を使って、次のケースを実装してみよう。

  • プレイヤーから発射された弾がエネミーに当たったとき
  • プレイヤーに接触したとき

EnemyクラスでOnCollisionEnter(Collision 衝突)を使って実装していく。引数のCollisionで衝突した相手のゲームオブジェクト名を取得できるので、プレイヤーに衝突した場合と弾丸で撃墜された場合とで処理を分けることができる。Destroy()関数はMonoBehaviour関数の1つで、引数に指定したゲームオブジェクトをScene上から削除できる。ここでは、プレイヤーに衝突・もしくは弾丸と衝突した場合に自身を削除するようにしている。

void OnCollisionEnter(Collision collision) {

    GameObject collisionTarget = collision.gameObject;

    if (collisionTarget.name.Contains ("Main Camera")) {
        // プレイヤーの HP を攻撃力分減らす
        collisionTarget.GetComponent<Player> ().playerHP -= enemyAttack;
        // 自身(エネミー)を Scene 上から削除
        Destroy (gameObject);
    }
    else if(collisionTarget.name.Contains("Bullet"))
    {
        // 自身(エネミー)を Scene 上から削除
        Destroy (gameObject);
    }
}

ひと通りの実装ができたら、実機にビルドするなどしてテストプレイをしてみよう。エネミーゲームオブジェクトを[Ctrl]+[D]キー(macなら[cmd]+[D]キー)で複製し、それぞれのエネミーのパラメータや位置を調整するなどして難易度を変えてみるのも良いだろう。

複数のエネミーを配置してマテリアルの色も替えることで調整がしやすくなる

おわりに

次回は、残る機能「ゲームサイクル」の実装と、グラフィックを強化しゲームの「にぎやかし」要素を増やすことで、ゲームの完成度を高めていく。お楽しみに!

アプリ制作会社などでモバイルアプリの開発業務を経て、2015年よりグリー株式会社所属。

Technical Artistチームにて、3D アートアセットパイプラインの構築やシェーダ開発、処理負荷の最適化などにあたっている。

Unity Certified Developer(2016)

連載記事一覧

Think IT会員サービスのご案内

Think ITでは、より付加価値の高いコンテンツを会員サービスとして提供しています。会員登録を済ませてThink ITのWebサイトにログインすることでさまざまな限定特典を入手できるようになります。

Think IT会員サービスのご案内

関連記事