クリスマスプレゼントにCardboardでゲームを作ろう

この記事は、「[Xamarin / MAUI Advent Calendar 2021](https://qiita.com/advent-calendar/2021/xamarin)」の、21日目の記事です。

今年もアドベントカレンダーが始まりましたね。私も何か書きたいと思ったのですが、なかなかいいアイデアが思い浮かびません。そこで今年は、クリスマスプレゼント代わりに、ゲームを１つ作ろうと思います。

私は以前から、Xamarin.AndroidでCardboardアプリを作成しています。Cardboardとは、スマホで簡単に以下のようなVRアプリを作成することができるライブラリです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245196/229f256d-8d25-a214-c03e-d56be061487a.png)

Cardboardアプリを作成するにはSDKを利用する必要がありますが、Xamarinでは直接、利用できません。AARやJARを「Xamarinバインドライブラリ」という仕組みで利用したり、NDK経由で呼び出す必要があり、簡単に利用することができませんでした。
そこで私は[昨年のアドベントカレンダー](https://qiita.com/secile/items/147acc872f63a7cffa49)で、デコンパイルされたSDKのソースコードをC#でかきなおし、簡単にXamarinからCardboardアプリを作成することができるライブラリを公開していました。

https://github.com/secile/CardboardXamarin

今回はそのライブラリを利用して、できるだけCardboardの特徴を活かしたゲームを作ろうと思います。なお、Cardboardアプリときいて「どうせ、VRゴーグルが必要なんでしょ？」と思うかもしませんが、VRゴーグルなしでも楽しめるアプリにしたいと思います。

# どんなゲームを作ろうか
色々考えた結果、「だるまさんがころんだ」ゲームを作ることにします。

- あなたは雪原の中心にいて、雪だるまが周りを取り囲んでいます。
- スマートフォンをもって周りをキョロキョロ見まわしてみましょう。遠くの方に雪だるまが見えます。
- 雪だるまは、あなたが見ているときは動けませんが、見ていないときは、少しずつ近づいてきます。
- 雪だるまが近づいたとき、雪だるまを攻撃すると、倒すことができます。
- 雪だるまにタッチされると、負けです。

# 3Dプログラムの知識も少し必要です。
Cardboardアプリを作成するには、3Dプログラムの知識が必要です。Androidで3Dアプリを作成するにはOpenGLを利用します。Xamarinで直接OpenGLを利用することもできるのですが、ここでは、C#でOpenGLを利用するための「OpenTK」というライブラリを利用しています。この記事内ではOpenGLやOpenTKについての説明は行いませんが、知識がなくても、雰囲気だけは理解できるように注意して記載したつもりです。

## ここからしばらくプログラムの話がつづきます
作成したゲームが気になる方は、下の「こんなゲームができました」を先にご覧ください。

ゲームのソースコードはgithubで公開しています。

https://github.com/secile/Darumasan

完成したゲームはGoogle Playからダウンロードして遊ぶことができます。

https://play.google.com/store/apps/details?id=github.secile.darumasan

## 3Dプログラムの座標系
AndroidやWindowsのフォームプログラムなどでは、画面に描画するときは、左上が原点で、右側がX軸のプラス、下側がY軸のプラスです。それに対してOpenGLの世界では、中心が原点で、右側がX軸のプラス、上側がY軸のプラス、手前側がZ軸のプラスとなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245196/8f638dd7-d145-508d-3099-8db97dfbd0dc.png)

そしてこの世界の中に、カメラやキャラクターを設置し動かすことで、ゲームを作るのです。

## あなたは雪原の中心にいて、
あなたの場所＝カメラの視点ですので、カメラを原点に設置します。OpenGLでカメラの場所を指定するには、「座標」「みる場所」「どっちが上」の３要素を指定する必要があります。ここでは「場所＝原点」「みる場所＝Z軸のマイナス方向」「どっちが上＝Y軸のプラス方向」にしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245196/d156f1b3-8cd5-4bd0-31f3-91b05ae70c81.png)

プログラムでは「原点=Vector3.Zero」「みる場所=-Vector3.UnitZ」「どっちが上＝Vector3.UnitY」という情報からMatrix4.LookAt()を呼び出して、カメラ視点マトリクスを作成します。カメラ視点マトリクスについては後でまた説明します。

```cs
var lookat = Matrix4.LookAt(Vector3.Zero, -Vector3.UnitZ, Vector3.UnitY);
```

## 雪だるまが周りを取り囲んでいます。
雪だるまを、原点の周りにぐるっと取り囲むように配置するために、雪だるま（Enemyクラス）に「Angle」と「Distance」の２つのパラメータを用意します。

```cs
class Enemy
{
    /// <summary>表示位置角度をラジアンで。</summary>
    public double Angle { get; set; }

    /// <summary>中心からの距離。0.0-1.0範囲</summary>
    public double Distance { get; set; }

    /* ...以下省略... */
}
```

「Angle」は角度の意味で、Z軸のマイナス方向を基準として、そこから何度回転したところにいるかを意味することとします。角度は0°～360°で指定するのではなく、0～2π範囲の「ラジアン」で指定します。
「Distance」は距離の意味で、原点からどれだけ離れているかを意味することとします。0.0～1.0範囲とし、0で原点、1.0で雪原のはしっことします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245196/a12aefe0-5115-90a7-e6af-42e6e714ba8b.png)

そしてそれを、カメラの正面(-Z軸)を起点に、Y軸を中心として、角度Angleだけ回転した場所に、雪だるまを配置します。距離も、Distanceの分だけ離れて配置します。

```cs
/* 雪だるまの描画関数より抜粋 */

// カメラの正面(-Z軸)を起点にY軸中心に回転。
Shader.Rotate((float)enemy.Angle.RadianToDegree(), Vector3.UnitY);
// 距離を離す。
Shader.Translate(0, 0, (float)enemy.Distance * -20);
```

## スマートフォンをもって周りをキョロキョロ見まわしてみましょう。
Cardboardでは、「OnNewFrame->OnDrawEye(左目)->OnDrawEye(右目)」が繰り返して呼ばれます。
スマートフォンの動きに合わせてカメラ映像を動かし、左目または右目の視点にするには、OnDrawEyeのEyeTransform引数のGetEyeView()を呼び出してMatrixを取得し、それをカメラ視点Matrixに掛けます。

```cs
var mat = transform.GetEyeView().ToMatrix4();
var lookat = Matrix4.LookAt(Vector3.Zero, -Vector3.UnitZ, Vector3.UnitY);
Shader.SetLookAt(lookat * mat);
```

また、あなたが向いている方向にプレイヤーキャラクターを向ける必要があります。あなたがどこを向いているかは、OnNewFrameのheadTransform引数のgetForwardVector()から取得することができます。float[4]で取得できますが、それをOpenTKの「Vector3」に変換しておくと便利です。Vector3（Vectorはベクトルと呼びます）はOpenTKで3次元の座標や方向を表すための構造体です。

```cs
private float[] Forward = new float[3];
public void OnNewFrame(HeadTransform transform)
{
    // 顔の向いている方向を取得してPlayerにそっちを向かせる。
    transform.getForwardVector(Forward, 0);
    Player.Forward = new Vector3(Forward[0], Forward[1], Forward[2]);
}
```

## 雪だるまは、あなたが見ているときは動けませんが、見ていないときは、少しずつ近づいてきます。
雪だるまの場所を指定する「Angle」と、「あなたがどこを向いているか」の「Forward」をつかって判定します。ベクトルが同じ方向を向いているかは、2つのベクトルの内積を取ればわかります。長さ1のベクトル同士の場合、同じ方向を向いているほど、内積は1.0に近くなります。ただし、Angleはまだベクトルではありませんので、ベクトルに変換して判定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245196/c9d566e4-7aec-9267-f682-c1df27b36964.png)

## 雪だるまが近づいたとき、雪だるまを攻撃すると、倒すことができます。
Cardboardアプリで問題になることの１つが、どうやって「攻撃」したことにするかです。通常のアプリであれば、画面タッチで攻撃にすればいいのですが、CardboardアプリをVRゴーグルに入れている場合、ユーザーは画面にタッチできません。そこでこのアプリでは、スマートフォンのセンサーの１つである「加速度センサー」を利用することにします。センサーに一定以上の加速度が加わったら、ユーザーが「攻撃した」と判定することとします。
なお、おすすめは、VRゴーグル使用時はジャンプ！そうでない場合は腕をのばしてスマホで雪だるまを「たたく」イメージです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245196/62e83965-cea1-275f-174a-09b2020c5f55.png)

VRゴーグルに入れない場合は画面タッチできますので、画面タッチでも攻撃できることにします。

## 雪だるまにタッチされると、負けです。
雪だるまのDistanceが一定値以下になったら、ゲームオーバーとなります。

# こんなゲームができました。

- スマートフォンを振るか、画面タッチでゲーム開始。

- 遠くの方に雪だるまがいて、あなたを取り囲んでいます!
![Screenshot_20211220-082503.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245196/04b84ed9-6daf-4f39-cd23-c6b6591e2b6b.jpeg)

- どんどん近づいてきますので、近づいたら、スマホを振るか画面タッチで攻撃しましょう。

![Screenshot_20211220-082432.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245196/bd48afba-3666-0e21-f6a3-3e31500b1f58.jpeg)

- 攻撃して倒したところ。雪だるまがころびます。

![Screenshot_20211220-082443.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245196/d0ba3ef2-14a4-3d72-7fcb-f28e78f47a31.jpeg)

- すべての雪だるまを倒したら、ステージクリア。次のステージは雪だるまが少しずつ増え、だんだん難しくなっていきます。

- 雪だるまが止まっているか、動いているか、わかりにくいので、止まっているときは細目、動いているときはまるい目玉にしました。

![Screenshot_20211220-090723.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245196/b6fb9296-7d24-e3fd-8139-ac3f47a3b8f3.jpeg)

- 後ろにいる雪だるまに気が付かず、突然ゲームオーバーになっても気が付かなかったので、ゲームオーバーになったときは、地面の色をピンク色になるようにしました。また、オプションで、「📳チェックボックス」をオンにすると、ゲームオーバー時に振動するようになり、ドキドキ感がアップしました。


![Screenshot_20211220-090754.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245196/65654bb6-2524-9ea7-a145-54232a56b506.jpeg)



- ステージクリア時もわかりにくかったので地面の色がみどり色になるようにしました。

![Screenshot_20211220-090648.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245196/7e9b067f-c921-9053-ae22-68147e518cba.jpeg)


- VRボタンを押すとステレオ⇔モノラルを切り替えることができます。

![Screenshot_20211220-082820.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245196/eab42c95-df23-dffb-9465-f14e1eb74023.jpeg)


- またVIEWボタンを押すと、通常視点と上空視点を切り替えることができます。

![Screenshot_20211220-090705.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245196/af96435c-11fa-c391-ab0a-1bd691e2ced5.jpeg)



- ステージが進むと、雪だるまがどんどん増えてきて、それをバンバンやっつけるのが爽快になってきました。

![movie.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245196/7e6b07ea-4d72-9f92-1272-347d8cbb5285.gif)


# まとめ
私はたしか「だるまさんがころんだ」風のゲームを作るつもりだったのですが、出来上がったのはどうやら、どんどん近づいてくる雪だるまを、ピストルでガンガン撃ちまくる、ガンシューティングができてしまったようです。