仏像をぶつぞう！！！！というゲームを作りました

# はじめに
こんにちは。Unityプログラマの進捗ゼミです。私はKMCというサークルに所属しているのですが、今回はサークル活動の一環として学祭で展示したゲームである、「仏像をぶつぞう！！！！」について書きます。

この記事は[KMC Advent Calender](https://adventar.org/calendars/8840)の5日目の記事です。
昨日はtrdrさんの[積んだ本を晒す2023](https://itoooak.hatenablog.com/entry/2023/12/04/235756)でした。

# どんなゲーム？
仏像をぶつぞう！！！！は、その名の通り、ぶん殴り系アクションゲームです。チャージ時間5秒の間にスマホを全力で振り回してエネルギーをチャージして、その勢いのままに物体を破壊します。飛び散った物体の破片の飛距離によりスコアがつけられるというゲームです。
なお、コンプライアンス上の理由で、最終的には仏像ではなくスイカを爆砕するゲームになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1412657/6227fa68-5a8f-fbbe-7ad2-04ac13a53a9c.png)

これは開発中の映像です。(Unityレコーダーのバグなのか処理落ちなのか、音がズレてますね…)
<iframe width="560" height="315" src="https://www.youtube.com/embed/lAg75MUyj1E?si=6pWrdGQ2SZ6gQiaO" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

初めて遊ぶ人にもわかりやすく、老若男女楽しめるルールであり、1プレイ10秒で誰しも簡単に競い合えるゲームデザインです。演出もド派手で分かりやすく、NFでの展示用ゲームにぴったりですね。
このゲームは主に3つのパートから構成されています。

- 破壊演出
- コントローラーとスマホのリアルタイム通信
- スコア計算

# 破壊演出
今回の開発では、時間の制限があったため、演出を簡単に作成できるようになる[Feel](https://assetstore.unity.com/packages/tools/particles-effects/feel-183370)というアセットを使いました。
![fc631486-0294-4265-9cbb-1cfbbb4f5414.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1412657/d960743a-cd6d-2119-2de5-86be4eb446f0.jpeg)
FeelはFeedbackという形式でゲームに必要な演出をワンストップで提供してくれます。このアセットのおかげで演出のタイミングの調整のイテレーションの速度が爆上がりしました。演出に凝るゲームを作りたい場合はぜひ使ってみてください。
ここからは、個々の演出について説明していきます。

## DinoFractureによるメッシュの破壊
ゲームのコアとなる物体の爆破には、[DynoFracture](https://assetstore.unity.com/packages/tools/physics/dinofracture-a-dynamic-fracture-library-26599)というアセットを使用しました。サンプルシーンに爆破のサンプルがあったため、それをいい感じにコピペすることで爆速開発に成功しました。内部的にはメッシュを事前に数百個に分割して、適切なタイミングでそれをActiveにしているようです。ミニゲームの発想をするにあたってこういうツール系のアセットのサンプルシーンを眺めるといい感じのアイデアが降ってきやすいですし、開発も爆速になりやすいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1412657/f604bdcd-af3c-3c5f-f36f-a48e53b9847f.png)

## Lens Distortionによる衝撃の表現
Lens Distortionとは、レンズの歪みを表現するエフェクトです。このエフェクトを瞬間的に書けることで画面全体に衝撃が走ったことを表現できます。イメージとしては衝撃波があたって視界が歪んだ、とかですかね。このエフェクトをハンマーが物体に当たる瞬間に適用します。新標準レンダリングパイプラインである、URPの[ポストポロセッシング機能](https://docs.unity3d.com/ja/Packages/com.unity.render-pipelines.universal@14.0/manual/Post-Processing-Lens-Distortion.html)を使うことで実現できる演出のひとつです。
このエフェクトについては[エクスプラボさんの記事](https://ekulabo.com/whats-lens-distortion)を参考にしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1412657/143668cd-71d2-081e-d9c0-99a751ce891c.png)
<iframe width="560" height="315" src="https://www.youtube.com/embed/PjZq0VlMcBc?si=K6wbB3URLnTeCllw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

## Cinemachine Impulseによる衝撃の表現
本ゲームでは、カメラの操作用アセットとしてCinemachineを採用しました。
![239735275-2fa39321-f137-4863-85ab-f7b79a1fd954.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1412657/7445fc0e-acbe-7a1a-d3ee-5258ef79c7ac.png)
Cinemachineは、カメラの動きや画角などの各種プロパティを直感的かつ詳細に制御してカットシーンなどの作成を容易にするためのアセットです。Cinemachineが活用されているのは主に次の2点です。

1. 衝突までカメラがハンマーについていき、衝突する物体にズームする
2. 衝突の瞬間にカメラを揺らして衝撃を表現する

カメラの切り替えはCinemachineの基礎的な機能であり、3つ用意したVirtual Cameraをタイミングよく切り替えて、事前に定義した遷移アニメーションを再生することによりうまく対応しました。FeelのEvent機能を利用することで、適当なスクリプトを呼び出すことで実現をしています。
カメラの衝撃表現は、FeelのCinemachine ImpulseのFeedbackを利用しました。衝撃パターンは6Dにして、衝撃の大きさとスピードを調整することにより、いい感じにぶつかった威力を表現できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1412657/30e46598-f2f6-7a95-65ea-e0080a03e72f.png)
<iframe width="560" height="315" src="https://www.youtube.com/embed/TwrDewi5wxg?si=I_f6vMmb42oPzFzM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

## Lightによる光の表現
衝突の印象を更に強めるために、物体を下から照らして明度を上げます。Point Lightを適切なタイミングで光らせます。光の強さと光の範囲を手作業で調整しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1412657/0084ae52-cf79-1612-78d6-92834ddd0951.png)
<iframe width="560" height="315" src="https://www.youtube.com/embed/UABylWic39o?si=MLcZIdTxrC8OFxsc" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

## Timescale Modifierによるストップモーション
Timescale Modifierはいわゆるスローモーション演出を作ることができるFeedbackです。スピード感重視で読めなくてもいい部分と、詳細に書き込んで重要な情報を伝える部分とを両立させることができる演出であり、バトルアニメでも多用されています。今回は物体を高速に殴りつける部分と破壊する部分と破壊した物体が飛び散る部分の3つに分かれているため、スローモーション演出が特に適しています。
実装方法は簡単で、適当なタイミングで適当な時間だけ時間の流れる速度を0.1倍にしています。

## Particle Playによるエフェクト
パーティクルはエフェクトとして最もポピュラーなものの1つでしょう。Lowpoly Arsenalというアセットを所有していたため、そこから適当なものを選びました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1412657/302fd282-9604-84b7-d196-06e1ff9c9f56.png)

衝突の瞬間のパーティクルとして、コミカルかつ強いインパクトを示す文字付きの爆発エフェクトを採用しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1412657/48bb0dc4-1020-29e3-6183-8efbff427209.png)
<iframe width="560" height="315" src="https://www.youtube.com/embed/lwJ96z576jo?si=ZSjLKq5owf96wjtL" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
ハンマーのスピード感を表現するため、動くオブジェクトにアタッチすると煙があとを引くTrailing系のパーティクルを採用しました。
<iframe width="560" height="315" src="https://www.youtube.com/embed/YIWykpTVrQI?si=qAMrYgp4RAZY2HpV" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
パーティクルについてはあまり細かい制御を行なわずに、適切なタイミングで適切な場所に配置するに留めました。衝突エフェクトが衝突の瞬間にカメラの中心に現れるように、タイミングと場所をシビアに調整する必要がありました。

## BloomとSurface InputによるEmissionの表現
衝突のエネルギーによって鉄が赤熱する演出は、URPとシェーダーを組み合わせました。まず、シェーダーからいい感じの赤熱した光が出るようにするため、シェーダーのエミッションの項目を設定します。これは、シェーダーがかけられたマテリアルから光を出すかどうかの設定項目です。鉄のザラザラしたテクスチャから鈍い赤色の光を出すことにより、赤熱を表現できます。シェーダーの設定をリアルタイムに変更するため、DOTweenでタイミングよく赤熱にアニメーションさせました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1412657/2c7235a4-c4c0-475a-cbca-e1a623e916b6.png)

<iframe width="560" height="315" src="https://www.youtube.com/embed/-eZx1eovj3g?si=Itu9X5yWdiM1vvwV" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

さて、単に光が出るだけでは赤熱の表現は難しいです。光のにじみを演出することでよりリアルな赤熱する物体を表現できます。光のにじみはURPのBloomの設定から行いました。

# コントローラーとのリアルタイム通信

コントローラーとしてAquos sense6sを採用しました。スマホの加速度センサーを用いて振り回しを検知します。このゲームを実現するためにはスマホとPCがリアルタイムに通信する必要があります。また、Webサーバーのようにすべての通信を受け取るのではなく、事前に登録された特定の端末だけでゲームをプレイすることが必要です。この要件を満たすため、TailscaleによるVPNとsocket.ioによるリアルタイム通信を採用しました。
Tailscaleはインターネット越しにVPNを作成してくれるサービスです。
![Tailscale-Logo-Black.svg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1412657/6a32f727-4413-dd67-fe5d-167614366d0a.png)
このサービスを使って作成した仮想ネットワーク上にある端末からの接続要求だけを受け付けることで、クライアント認証を達成しました。
socket.ioはWebSocketを利用したリアルタイム通信ライブラリです。
![062_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1412657/736cae4e-a73f-4450-12b3-8bc3bb4ab51f.png)

サーバー側ではAndroidソケットとWindowsソケットを用意しておいて、Android端末からのメッセージをそのままWindows端末に送信し、Windows端末からのメッセージをそのままAndroidに送信するリレーサーバーの役割を行います。今回の展示では1ペアだけ動けばよかったのでスマホとPCのペアリングの仕組みは実装しませんでした。上記の仕組みを実装するコードはわずかこれだけです。
```JavaScript
const http = require('http');
const socketIo = require('socket.io');
const server = http.createServer();
const io = socketIo(server, {
  cors: {
    origin: "*",
    methods: ["GET", "POST"]
  }
});
const windows = io.of('/windows');
const android = io.of('/android');
windows.on('connection', (socket) => {
  console.log('Windows user connected: ' + socket.id);
  socket.on('begin-charge', () => {
    console.log('Begin charge received');
    android.emit('begin-charge');
  });
  socket.on('end-charge', () => {
    console.log('End charge received');
    android.emit('end-charge');
  });
});
android.on('connection', (socket) => {
  console.log('Android user connected: ' + socket.id);
  socket.on('swing', () => {
    console.log('Swing received from Android');
    windows.emit('swing');
  });
});
server.listen(29061, () => {
  console.log('Server is running on port 29061');
});
```
サーバーはいつもお世話になっている[Conoha VSP](https://www.conoha.jp/vps/)の512MBプランを利用しました。誕生日クーポン500円で稼働したので実質無料でした。
![og.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1412657/be75eabe-a660-75a1-84cd-ac8842532790.png)

クライアント側はUnityなのでsocket.ioを使う知見がそこまで多いわけではありませんでした。そこで、1年ほど前にセールで購入したBest HTTP2（現在は[Best HTTP Bundle](https://assetstore.unity.com/packages/tools/network/best-http-bundle-268834)）というアセットを使うことにしました。幸い、socket.ioを扱う機能を利用して簡単に接続を行うことができました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1412657/e54a387c-34e7-262c-8587-8baec0e14bf8.png)

# スコア計算
適切にスマホを振りまわしたことを判定するためにはスマホの加速度センサーやジャイロセンサーを使うことが必要です。
さて、適切な計算式を作成するためスマホから毎フレームセンサーの値をUDPでPCに送りつけそれをCSVに書き出すアプリケーションを開発してエクセルで分析しました。すると、加速度センサーによって検知された加速度ベクトルの前フレームからの変位ベクトルの絶対値が適切であることが分かったため、その条件を満たしたフレーム数によって計算を行うことにしました。
```C#
// スマホを振り回したことを検知するロジック
public class TestDisplaySensorValue : MonoBehaviour
{
    private Vector3 lastAcceleration; // 前フレームのaccelerationを保存するための変数
    private AndroidEffectControl androidEffectControl;
    public SocketForAndroid socket;

    void Start()
    {
        androidEffectControl = FindObjectOfType<AndroidEffectControl>();
        lastAcceleration = Input.acceleration; // 初期値として現在のaccelerationをセット
    }

    void Update()
    {
        Vector3 currentAcceleration = Input.acceleration; // 現在のaccelerationを取得

        float difference = Mathf.Abs((lastAcceleration - currentAcceleration).magnitude); // 直前のaccelerationと今回のaccelerationの差分のmagnitudeの絶対値を計算

        if (difference > 5.5f)
        {
            androidEffectControl.Shake();
            socket.SendSwing();
        }

        lastAcceleration = currentAcceleration; // 現在のaccelerationを次のフレームのために保存
    }
}

```
次に、適切な回数のスマホの振り回しによって適切に破片を吹き飛ばす処理を実装します。破片の飛び散り具合は物理エンジンとの相談になるため、適切な場所から適切な向きに振り回すほどに派手に飛び散るようにする必要がありました。最終的に、中心座標を物体のやや下側に配置してすくい上げるように振り回した回数に比例する威力の衝撃を与えることが最適であると結論づけました。
最後に飛び散った破片をもとにスコアを計算します。物理エンジンの計算結果を確認すると、一部の小さい破片および物体の下部の重たい破片は極端に飛ぶ飛ばないが分かれるため、これらを考慮にいれると実行ごとに不安定なスコアになることが予想されました。そのため、破片のうち上位20%と下位20%を除いた60%の破片の平均飛距離をそのままスコアに採用しました。これにより、おおよそ200程度に収まるスコアリングになり、毎回のプレイごとにプレイヤーの甘さが説得力のある形で数値化され、競技性が生まれました。
```C#
// スコア計算アルゴリズム
public class CalcScore : MonoBehaviour
{
    public Transform center;
    public int GetScore()
    {
        var distances = new List<float>(transform.childCount);
        foreach(Transform child in transform)
        {
            distances.Add((child.position - center.position).magnitude);
        }
        return (int)distances.OrderBy(x => x).Skip(transform.childCount / 5).Take(transform.childCount / 5 * 3).Average();
    }
}

```

# おわりに
「仏像をぶつぞう！！！！」またの名を「スイカゲーム」は学祭の展示で大いに盛り上がってくれました。またこういうゲームを作ってリリースしていきたいと思います。
明日のアドベントカレンダーはWalnutsさんの[【待望】walnuts.devを支える技術]()です。