UnityEditor「で」遊べるクリッカーゲームを作ってみた

# 使用バージョン
Unity 2021.2.7f1(古いバージョンでも動くはず)

# 記事の内容のプロジェクト（Unlicense）
https://github.com/ttyyamada/ClickerInEditor


# 概要
まずはこちらをご覧ください
![Clicker In UnityEditor](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49946/b4affab2-40dc-576e-257a-f0111d325ccd.gif)

はい。見ての通り、クッキーをクリックすると数が増えていき、そのクッキーでお買い物をしてさらにクッキーをクリックする効率が増えるゲームです

今回はUnityのEditor拡張を使って、このクリッカーゲームをUnityEditor内でプレイできるようにしてみました


これでUnityで開発しながらゲームができるよ！


# ゲームの仕様
今回のクリッカーゲームは非常にシンプルな仕様になっています
1. クッキーをクリックすると数字が増える
1. 一定秒数でクリックしなくても数字が増える
1. アイテムを購入すると上記二つの数字の増える効率が上がる
1. アイテムを購入すると新しいアイテムが解放される
1. 上記進捗がセーブされる

と、ゲームとしては最低限の体裁で作られています。今回はこれをEditor拡張で作ってみましょう
Editor拡張の細かい解説は他にも優良な記事がたくさんありますので今回は割愛します

# ウィンドウのレイアウトに対応して位置をある程度調整する
先ほどの動画であった通り、このプロジェクトではウィンドウのサイズによって、ある程度レイアウトが自動的に設定されています
![レイアウトの調整](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49946/70397363-b604-fcc3-1049-25f350d6e2ca.gif)

描画部分は全体として
[BaseWindow.cs](https://github.com/ttyyamada/ClickerInEditor/blob/main/Assets/Editor/EditorClicker/Scripts/BaseWindow.cs)
のファイルのOnGUI()で行なっています

```
GUILayout.BeginArea(new Rect(0, 0, currentWindow.position.size.x, currentWindow.position.size.y));
```

というはじまりをしていますが、
これは「現在のWindowのサイズで描画を行う」という処理になります
GUILayoutでBeginAreaとEndAreaを行うことで、ショップの情報、インフォメーション情報、クッキーの情報を並べています
ここは通常のEditor拡張の表示と変わりはありません

## 数字のアニメーションを表示する
さて、次は数字の処理ですが、これが曲者でした
単純に生成して、毎フレーム上にいき、一定秒数立ったら消えていく。というだけの処理ではありますが、
UnityEngineと比べてUnityEditorではこの処理はなかなかに曲者です
理由としては、
1.Coroutineが使えない
1.OnGUI()などUpdate()の代わりになるようなものの呼ばれるタイミングが通常と違う
1.Time.deltaTimeが正常に動作しない

というものがEditor拡張ではあるからです
ではどうすれば良いのか。そう、自作です

[CookieAnimation.cs](https://github.com/ttyyamada/ClickerInEditor/blob/main/Assets/Editor/EditorClicker/Scripts/CookieAnimation.cs)
のファイルでクッキーのアニメーションを管理しています

ここではコンストラクタとして以下の情報を渡しています

```
        public CookieAnimation(Rect targetRect,Rect fromRect, double number,float animationSpeed)
        {
            this.targetRect = targetRect;
            this.number = number;
            this.currentRect = fromRect;
            this.animationSpeed = animationSpeed / 10f;
            step = 0;
        }
```

順番に、対象の位置、初期位置、表示する数字、アニメーションの速度。です

これらの情報を元に、
[BaseWindow.cs](https://github.com/ttyyamada/ClickerInEditor/blob/main/Assets/Editor/EditorClicker/Scripts/BaseWindow.cs#L133)に以下の処理があります

```
        private void ExecuteNumberAnimation(double deltaTime)
        {
            var style = new GUIStyle();
            style.fontSize = 30;
            style.normal.textColor = Color.white;
            for(var i = currentAnimationList.Count - 1; i >= 0; i--)
            {
                var animation = currentAnimationList[i];
                GUI.Label(animation.NextStep(deltaTime), animation.number.ToString(CultureInfo.InvariantCulture), style);

                if (animation.IsComplete())
                {
                    currentAnimationList.Remove(animation);
                }
            }
        }
```

要は「今登録されている数字に対して、経過時間を渡して、完了していたら完了する」という処理ですね。
ここでキモとなるのはdeltaTimeの計算と`animation.NextStep`の処理です

### deltaTimeの計算方法
[BaseWindow.cs](https://github.com/ttyyamada/ClickerInEditor/blob/main/Assets/Editor/EditorClicker/Scripts/BaseWindow.cs#L52)でdeltaTimeを計算しています。
先ほど少し触れましたが、EditorApplicationではTime.deltaTimeが正常に動作しません
なので、注目したのが[EditorApplication.timeSinceStartup](https://docs.unity3d.com/ja/current/ScriptReference/EditorApplication-timeSinceStartup.html)です

これはEditorを起動してからの時間をとれるため、これを利用してOnGUIが呼ばれるたびに最後にOnGUIが呼ばれた時間を代入し、差分の計算に使っています
これでOnGUIの更新の間の時間が取れました
更新が処理的に早すぎるのなら、この差分の時間が一定になったらアニメーションを実行する。という処理を入れても良いでしょう

![時間の更新処理](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49946/13af32e8-9dd2-1d13-4b33-09880d6f3ee8.png)

### AnimationのStep実行
次に、数字のアニメーションの処理です
これは[CookieAnimation.cs](https://github.com/ttyyamada/ClickerInEditor/blob/main/Assets/Editor/EditorClicker/Scripts/CookieAnimation.cs#L25)のNextStep関数で表示位置を返しています
ここでの処理は非常にシンプルです

```
        public Rect NextStep(double deltaTime)
        {
            step += (float)deltaTime * animationSpeed;
           
            var y = Mathf.Lerp(currentRect.y, targetRect.y, step);
            return new Rect(currentRect.x, y, currentRect.width, currentRect.height);
        }
```

独自で物理計算やアニメーションなどやっている人にはお馴染みの
[Mathf.Lerp](https://docs.unity3d.com/jp/current/ScriptReference/Mathf.Lerp.html)ですが、使ったことある人には違和感があると思います
普通は「秒数」を基準に、何秒かけて目標に行くか。とやるのですが、ここでは単純にアニメーションの速度だけしかみていません
普通に秒数を基準に厳格にやっても良いのですが、なんかこうした方が上までしっかり行って消えてくれたのでそのままにしています
この部分はゲーム性には関わらないので厳格にはやっていません

![数字アニメーション](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49946/8f8ea111-26f2-42f6-76db-f906ba3d510e.gif)

例えばここを厳格にしてみたり、消える時にColorのフェードを入れても良いかもしれませんね

# データ周り
さて、お次はデータ周りです。もしこのプロジェクトを使って遊びたい場合は、このデータ周りを触る必要があります
データとして大きく持っているのは
1. スキルデータ
1. セーブデータ
1. リソースデータ

の3つです
特にスキルデータの扱いをメインに説明いたします

## スキルの追加・編集・登録
スキルのデータは`Resources/SkillData`の中に入っています

![スキルデータ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49946/6719a970-9f3b-c711-72f3-ecad6662d3e2.png)

データの持ち方としては[ScriptableObject](https://docs.unity3d.com/ja/2018.4/Manual/class-ScriptableObject.html)で、起動時にその中のデータを読んで、セーブデータと照らし合わせています

右クリックをして作成メニューを選ぶか、既存のアイテムをコピーして作りましょう

データの実態としては
[SkillData.cs](https://github.com/ttyyamada/ClickerInEditor/blob/main/Assets/Editor/EditorClicker/Data/SkillData.cs)の中をご覧ください

要素を一個ずつ説明していきます
1. 連番<dd>連番です。これが重複しているとバグると思います</dd>
1. 名前<dd>アイテムの名前です</dd>
1. 値段<dd>購入にかかるクッキーの数です</dd>
1. スキル説明<dd>説明文です。表示するところはまだありませんが、作ると楽しいと思います</dd>
1. 1クリックでいくらもらえるか<dd>1クリックするときに`追加で増える`クッキーの数です</dd>
1. クリックしなくてもいくらもらえるか<dd>クリックをしなくても`自動で追加で増える`クッキーの数です</dd>
1. オートでもらえる秒数<dd>このスキルで自動で増えるクッキーの数が何秒に一回もらえるかの秒数です</dd>
1. 購入したら解放されるアイテムの番号<dd>これを購入することで、新しくショップに並ぶアイテムの番号です。連番と合わせます（連番対応はバグりやすいので設計としては微妙なので、ScriptableObjectの参照でもよかった）</dd>
1. メモ<dd>その名の通りメモ欄です。何を書いても良いです</dd>

これらのデータを追加することで、新しいアイテムが作成できます
例えばチートとして、
「伝説のクッキー職人」「無料でもらえる」「購入するとクッキーがクリックのたびに10000枚」「自動で12345枚のクッキーを0.5秒でもらえる」
アイテムを作ってみましょう

![伝説のクッキー職人](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49946/fcf04121-46f1-f3d6-2da0-3167b47e429d.png)

作りました。ただこれで終わりではなく、このアイテムの解放条件を入れる必要があります
最初から持ってるデフォルトアイテムの解放アイテムに、今作ったアイテムの番号を入れてみましょう

![解放アイテムの更新](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49946/be5e795a-1e6e-1f01-d2d7-c9c021dfbeb4.png)

この状態でWindowを読み込み直すと

![スクリーンショット 2022-03-21 14.38.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49946/7d7f9098-3f77-cd25-7c4b-761aa310e40a.png)

無事商品に「伝説のクッキー職人」が並びました。購入してみましょう

![伝説の力](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49946/5dea2402-5da4-01b2-56f2-f06141696a93.gif)

はい。ゲーム性が壊れました。インフレも良いところです

## セーブデータ
ゲームとしてチートしてしまい、つまらなくなってしまったのでデータを初期化しましょう
セーブデータは
[SaveService.cs](https://github.com/ttyyamada/ClickerInEditor/blob/main/Assets/Editor/EditorClicker/Scripts/SaveService.cs)で行なっています

```
public void Save(UserData data)
        {
            string dataJson = JsonUtility.ToJson(data);
           
            using (StreamWriter streamWriter = new StreamWriter(DefaultData.savePath, false))
            {
                streamWriter.Write(dataJson);
            }
        }

        public UserData Load()
        {
            // セーブデータがつくられてなければnullを返す
            if (!File.Exists(DefaultData.savePath)) return null;
            using var streamReader = new StreamReader(DefaultData.savePath);
            try
            {
                var dataJson = streamReader.ReadToEnd();
                var data = JsonUtility.FromJson<UserData>(dataJson);
                if (data != null)
                {
                    return data;
                }

            }
            catch (Exception e)
            {
                Debug.LogError("LoadError: " + e);
            }

            return null;
        }
```

やってることはシンプル。UserDataをJsonにしてファイルに書き出し、セーブデータがなければ作っているだけです
なのでSaveData.txtを削除して初期化しましょう

![何もなかった。いいね？](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49946/6b568270-6267-16f1-4e6d-2e631ef5d8ec.png)

## リソースデータ
最後にリソースのデータです。実はGithubを見てもらった人にはわかると思うのですが、クッキーのアイコンはGithubには上がっていません。再配布になってしまいますからね

[ResourcesNames.cs](https://github.com/ttyyamada/ClickerInEditor/blob/main/Assets/Editor/EditorClicker/Data/ResourcesNames.cs)の中で読み込む画像やスキルのリソースが入っているフォルダなどを設定しています

```
    public static class ResourcesNames
    {
        #if USE_DEBUG_RESOURCE
        // デバッグようのリソース
        private static readonly string resourcesPrefix = "Ignore/";
        #else
        private static readonly string resourcesPrefix = "";
        #endif
        // クッキーの画像のリソースパス
        public static readonly string CookieButtonImagePath = resourcesPrefix + "cookieButton";
        // スキルデータの格納されているフォルダ名
        public const string SkillPathFolder = "SkillData";
    }
```

先ほどからのサンプルでは、`USE_DEBUG_RESOURCE`のシンボルを使って、Ignoreというフォルダの下のcookieButtonを見ています
Ignoreフォルダ以下はGitに上がらないようにしているので、著作権的にも考慮されています

さて、それでは`Ignore/cookieButton`というファイルを差し替えてみましょう

![チョコチップクッキー](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49946/3560470a-5373-91f1-08c0-672243766c60.png)
はい。チョコチップクッキーになりました。美味しそうですね

## 文字データ
最後に、文字のデータについて少しお話しします
例えばクッキーとして作っていたけど、世の中には無量大数に到達するレベルのクッキーがすでに生産されたと聞きます。
だったらクッキーではなく[マラサダ](https://ja.wikipedia.org/wiki/%E3%83%9E%E3%83%A9%E3%82%B5%E3%83%80)に変えることで独自性を出そうと思います

まず、マラサダの画像をDLして差し替えます
![マラサダ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49946/3e9d6334-9ce8-650f-64a8-2d85444a9aaa.png)
はい。マラサダになりました。美味しそうですね

しかし問題として、通貨の単位がクッキーになってしまっています
![クッキー](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49946/1785cd68-e8b2-67b7-7b2d-fd2e15715b12.png)

こういう時のために、
[StringList.cs](https://github.com/ttyyamada/ClickerInEditor/blob/main/Assets/Editor/EditorClicker/Data/StringList.cs)というファイルに文言を入れています

```
    public static class StringList
    {
        public static readonly string title = "クリッカーInEditor";
        public static readonly string resourcesName = "クッキー";
        public static readonly string unit = "個";
    }
```

変えられるのはWindowのタイトル、増やしたいやつの名前、単位です。これをマラサダ風に変えてみましょう
![マラサダ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49946/61a761e1-e8be-f917-abec-a9157319a99c.png)

はい。無事マラサダになりましたね。スキル名…？　みないでください

このように、データとしていじりやすいようになっているので、ぜひ君だけの最強のEditorクリッカーを作ってください

# 最後に
今回はEditor拡張のTipsを交えつつ、クリッカーゲームを作ってみました
ここでは紹介していない機能などもあるので、ぜひみなさん一度Githubに目を通していただけると幸いです

Githubはこちら
https://github.com/ttyyamada/ClickerInEditor