Unityで2Dアクションゲームを制作したときの知見

# はじめに

【<b>まずは公式リファレンスを見よう</b>】

一番言いたいことを最初に書きました。

UnderRocketというゲーム制作を通して得た知見を(自分用のメモとして)残しておきます。
Unityを学び始めて1年未満くらいの人向けの内容です。

特に以下のようなものを実装したい人向けです
・2Dのアクションゲーム
・WebGLビルドをし、公開する
・ハイスコアを保存、共有したい

(UnderRocketは以下のサイトで遊べるので、ぜひプレイしてみてください^^)
[UnderRocket | unityroom](https://unityroom.com/games/underrocket)

汚いですがコードはこちら
[UnderRocket GitHub](https://github.com/Papyrustaro/UnityRocketGame/tree/master/Assets/Scripts)


# TilemapとCinemachine

## ステージ外を移さないカメラ移動
<b>Cinemachine</b>を利用します。
インストールの仕方を調べると、AssetStoreからインストールという記事がたくさんありますが、
<b>現在はUnityエディタからインストールできます。</b>

一番注意する点として、カメラの枠より小さい範囲を映そうとすると上手くいきません。
ステージの大きさや、映す範囲には注意してください。

[Cinemachineインストール方法](https://dkrevel.com/makegame-beginner/make-2d-action-move-camera/)
[Cinemachineの基本的な使い方](https://blogs.unity3d.com/jp/2018/07/17/cinemachine-for-2d-tips-and-tricks/)

## Tilemapを利用する
2Dゲームで素直にsceneにgameObjectを置いてステージを作成しようとすると、だいぶ大変です。
Tilemapを利用すると、簡単に描画・Collider配置ができます。

以下の2サイトを見れば、できると思うので、利用したことがない人はぜひ
[コガネブログ](https://baba-s.hatenablog.com/entry/2018/04/08/130000)
[Qiita](https://qiita.com/RyotaMurohoshi/items/5c3cdd24b7e09bfd8ed3)

### Tilemapのtileひとつひとつの間隔が空いてしまう
![tilemap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276180/3f337800-7421-ce44-668c-595b05eca099.png)

原因: SpritesのPixels Per Unitのサイズがタイルに合っていない
![tilemap2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276180/a7be0872-f435-5da9-5a15-c39c9745bd37.png)

![tilemap3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276180/7d382901-26f8-2547-510b-0bf4023511c1.png)

上の画像の場合は、Pixels Per Unitの値を16にする

## TilemapとCinemachineを共存させる方法
Cinemachineの映す範囲を、Cinemachine ConfinerのBounding Shape2Dで指定します。
![Cinemachine0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276180/aed3a15c-4575-ccc0-eaaa-8fd5f2dd3adb.png)

ここで、代入するものは、Tilemapとは別でステージの範囲に合わせて作成したCompositeCollider2D。

![Cinemachine1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276180/1b846bbf-a7b5-4d74-f8f7-8c95146284eb.png)

当然ですが、TilemapのcompositeColliderをカメラの枠にするとうまく動きません。
そのため、ステージの枠用のCompositeCollider2Dを別で用意する必要があるのです。

ただし、Tilemapと別ステージ枠を設定することで、本来狭すぎてCinemachineで映せなかった部分を、実際のステージより大きくすることで映すということができます(ステージ外が映ってしまいますが)。

# NCMB

## NCMBを利用し、オンラインランキング機能を実装する

今回オンラインランキング機能を実装するにあたり、
NCMB(Nifty Cloud Mobile Backend)というサービスを利用しました。
無料でひと月100万回APIリクエストできるすごいやつです。
[NCMB公式サイト](https://mbaas.nifcloud.com/)

公式のマニュアルや、記事を漁れば環境構築と基本的な実装は難しくないはずです。
[クイックスタート(公式)](https://mbaas.nifcloud.com/doc/current/introduction/quickstart_unity.html)
[機能別コード例](https://qiita.com/Hirai0827/items/f7461991f658f86a559b)
[コルーチンをいい感じに](http://dividebyzero.hatenablog.com/entry/2017/12/15/155358)

## 前座: NCMBはUnityのWebGLビルドに対応しているのか
開発している途中で、実装は間違っていないはずでUnityエディタ上では動くのに、ブラウザ上では上手く動かないということがありました。
調べると「NCMBはUnityのWebGLに対応していない」とか「WebGL専用のスクリプトを用意してます」みたいなニュアンスの記事を目にします。

結論を言うと専用のスクリプトを使わなくても、WebGL版でもちゃんと動きます。
動かないとすれば、ビルドの設定やその他諸々の問題です。

## createDateが参照できない
NCMBのデータストアを利用する際、最初からデータ格納日時である「createDate」とデータ更新日時である「updateDate」が用意されています。
しかし、このcreateDateが参照できなく...ない...ない...
...ありました。

自分で作成したDBでいうところのカラム(PlayerName, Scoreなど)はobj["PlayerName"]などと参照しているので、てっきりobj["createDate"]で参照できると思っていました。
obj.CreateDateでした。
この仕様は一般的なんですかね...私はわかりません。

```cs
private NCMBObject obj;

Debug.Log(obj["createDate"].ToString()); //error
Debug.Log(obj.CreateDate.ToString()); //ok
```

## データストアへの登録が終わったかわからない
NCMBObjectに用意されているSaveAsyncを利用してデータストアへのセーブ作業をしているのですが、この関数、エラーは返しても結果は返してくれません。
[SaveAsync](https://mbaas.nifcloud.com/assets/sdk_doc/unity_3.2.3/Help/classNCMB_1_1NCMBObject.html#a2e429d428a70cb7ef9ad6230b0cd2837)

例えばスコアランキングを作っていたとして、
①プレイヤーのスコアをデータストアに登録
②データストアからスコアが高い順にn人取得
とやったときに①が完了する前に②をおこなうと、②の結果が変わる可能性があります。

この問題は、プレイヤーの送信予定のデータとデータストアから持ってきたデータを比較することで解決しました。
①データストアからスコアが高い順にn人取得
②n人のスコアと、プレイヤーのデータで比較、処理(クライアント側)。
③プレイヤーのスコアをデータストアに登録

# 実装
## 当たり判定を残したまま、壁で跳ね返す
これには様々な方法があると思いますが、今回はRigidbody2Dを利用した方法です。

・Rigidbody2DのBodyTypeをDynamic
・CircleCollider2DのIsTriggerをfalse
・動きの制御はRigidbody2Dで
これさえ守れば、PhysicsMaterialの値次第で様々な跳ね返りができますね。

しかし、「壁でだけ跳ね返ってほしいのに、他のgameObjectとも物理的な衝突処理がされてしまう」という問題になりがちです。

この問題の解決策のひとつとして、「壁衝突用、他衝突フラグ用でふたつのgameObjectを用意する」があります。
・壁衝突用...CircleCollider(IsTrigger = false)、Layerを壁用のLayerと検知(他不要なものは検知しない)
![collider1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276180/9b1d1978-589a-43bc-350d-272351676963.png)

・他衝突フラグ用...見た目に合わせたCollider(IsTrigger = true)、Layerは特に指定なし(Layer単位で無視したいものがあれば)
![collider0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276180/c41ba864-fd5b-b72e-8651-aacca8ccaecc.png)

Layer同士無視するかどうかは、Edit->ProjectSettings->LayerCollisionMatrixから設定できます
![collider2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276180/3e19f802-767d-d86a-9ca5-eeb0a28212f0.png)

## 毎回データストアを参照しない
UnderRocketの話をすると、各ステージごとにクリアタイム順10名と最近クリア10名の名前とタイム(or日付)を表示します。
プレイヤーがミッションクリアしたときは、ランクインしている可能性があるのでデータストアから最新の情報を読み取ります。
しかし、ミッション失敗したときはどうでしょう？私は最新の情報である必要性は低いと考えました。
そのため、ランキングデータを初回取得時にクライアント側に格納し、ランキング一覧表示や失敗時には、すでにあるデータはクライアント側から、無いデータだけをデータストアからとってくるという処理にしました。

データはDictionary<string, dataClass>として格納
keyはミッション名(SceneManager.GetActiveScene().nameと一致)
valueはstring2つ(playerNameと、timeまたはdate)を持つクラス


## プロパティもシリアライズ化したい
「プロパティだけでいいのに、SerializeFieldでInspector上から編集したいから、仕方なくフィールドを用意。プロパティの初期値をフィールドから参照。」

```cs
[SerializeField] private int hp;
public int Hp {get; private set;}
private void Awake(){
    this.Hp = this.hp;
}
```

こういうとき、ないですか？
プロパティの初期値をSerializeFieldで変えられたら...最高ですよね？

そんな魔法がこちら↓
[コガネブログ](https://baba-s.hatenablog.com/entry/2018/12/19/085000)

※公式で意図されたものかわからないのでご用心

## フォント

WebGLのビルドでは、Unityのデフォルトのフォント(Arial)を利用していると日本語が表示されません。
日本語に対応したフォントに変更する必要があります。
また、フォントによって大きさが違うので、表示の変化に注意してください。

フォントを一括で変更するには、以下のサイトを参考にしました↓
[フォント一括変更](https://www.cg-method.com/entry/ugui-font-replace/)

## ツイートボタン

WebGL版でツイートする場合、現在開いているウィンドウとは別ウィンドウで開く、という処理が必要になります。
「エディタ上ではできるけど、ブラウザ上からはツイートできない」みたいな人は必見↓

[WebGLでツイートボタン](http://negi-lab.blog.jp/Tweet)



## Maximum call stack size exceeded

![maximum.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276180/9895d4e5-4936-8fc1-4c6f-d94a911aea0f.png)
⇈エラー画像

NCMBを利用して初めてのビルドをし、unityroomに反映させたら出たエラー。
当時、ロードシーンにNCMBSettings等のgameObjectや、BGMなどをロードするgameObjectを置いていました。
そしてロードシーンのStart()でタイトルシーンへ移動するようにしていたのです。

つまり、最初の処理でやることが多すぎ、ということだったんですね。
「NCMBがダメなのか？」と当時試行錯誤しましたが、NCMB関連のgameObjectをタイトルシーンに移動させることで解決しました。


## フルスクリーンのときに文字の表示が違う(大きさ)
フルスクリーンにしたときに、Canvas内の見え方が変わるというお話。
今まで見えていたTextがフルスクリーンにすると範囲をはみ出て見えなくなってしまった、なんてこともありました。

Canvasの設定でなんとかできそうですが、今はどちらのサイズでも注意する、とだけしています。
どなたか知りませんか(小声)

## 停止処理をtimeScaleに頼らない
動作停止の簡単な実装方法としてTime.timeScaleを0にするというものがあります。
これによりUpdateは呼ばれたままで、Time.deltaTimeが0になったり、FixedUpdateが呼ばれなくなったりします。
つまり、プレイヤーの入力は受け取るまま、時間経過によるgameObjectの動きを止められます。

最初この方法を使っていたのですが、NCMBを利用するにあたって、主にコルーチンが動かしたいときに動かせない状態になってしまいました。
自作のものなら実時間で測るコルーチンを利用すれば、できなくはないのですが...。

他の機能も実装しづらくなる可能性があるので、timeScaleを使わずフラグによって処理する形にしました。
Update()の冒頭で、止まっているフラグならRigidbody2D.simulated = falseにするなどです。

# Unityの便利機能

## ColliderとRigidbodyのPhysicsMaterialの違い
ColliderとPhysicsの両方にPhysicsMaterialを代入できます。

ColliderとPhysicsのPhysicsMaterialには優先順位があります。
Colliderのほうが優先順位が高く、
<b>基本となるPhysicsMaterialはRigidBodyに、特定の部分だけ変える場合はColliderに</b>
という認識でよさそうです。

ちなみに、何も代入しない場合のデフォルトは
friction = 0.4、bounciness = 0

詳しくはこちら↓
[UnityEngine.Rigidbody2D-sharedMaterial](https://docs.unity3d.com/ja/current/ScriptReference/Rigidbody2D-sharedMaterial.html)

## UnityEvent

[UnityEvent 公式リファレンス](https://docs.unity3d.com/ja/current/ScriptReference/Events.UnityEvent.html)

Buttonで出てくるこれ
![unityevent.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276180/abdcf308-2ead-f401-3856-56cc40dbfc62.png)

Inspector上でボタンが押されたときにする処理を設定できて、便利ですよね。

注意する点
・UnityEventにInspector上から何も代入してないときに、Invoke()で呼ぶと、エラーにならず次の処理に進む。
・呼ぶ関数の引数は1つまで

### サンプルコード
```cs
using UnityEngine.Events;

/*省略*/

[SerializeField] private UnityEvent haveDamaged; //攻撃を受けた際の処理
[SerializeField] private UnityEvent destroyMe; //自身が破壊されるときの処理
[SerializeField] private UnityEvent contactEnemy; //敵にあたったときの処理

private void OnTriggerEnter2D(Collider2D collision)
{
    if (collision.CompareTag("Enemy"))
    {
        this.contactEnemy.Invoke();
    }
    if (collision.CompareTag("AttackToPlayer"))
    {
        this.hp--;
        this.haveDamaged.Invoke();
        if (this.hp <= 0)
        {
            this.destroyMe.Invoke();
            Destroy(this.gameObject);
        }
    }
}
```


## スクロールビュー
[テラシュールブログ](http://tsubakit1.hateblo.jp/entry/2014/12/18/040252)

Tilemap同様、使ってみたら意外と便利だったシリーズ。
大事なのは、contentにvertualGroupをいれることです。
contentの中に空のgameObject→その中に入れたいものを複数入れるという流れ
contentに入るContentSizeFitterは「Unconstrained」にしておくことで自由にwidth/heightを変えることができます

[公式リファレンス](https://docs.unity3d.com/ja/2018.4/Manual/script-ScrollRect.html)

## OnEnable()
gameObjectがactiveになったときに呼ばれます。
Awake()より後、Start()より前です。
OnDisable()もあります。
Update()等目立ちすぎて、影薄くなりがちですが、大変便利です。

[呼ばれる順番](https://docs.unity3d.com/jp/current/Manual/ExecutionOrder.html)

## クリックされたときに呼ばれたいが、ボタンのように選択してほしくない
これはUnderRocketの右上歯車アイコンを押すと、設定が開く、といったときの話です。
Buttonの画像だけ変えるとそれっぽくなりますが、どうしてもゲームの入力中に間違って選択してしまい、そのままEnter/Spaceで押してしまったりします。

そこで、EventTriggerをアタッチしてPointerClickの処理を追加することで、簡単にマウスクリック時の処理ができます。
知ってるかどうかの問題ですが、本当便利ですね。
なお、クリックされたかの判定は、Imageの画像の範囲に自動で合わせてくれていました。
![onClick.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276180/fd38d6cb-72de-0689-16da-ad85f1ba5337.png)