NaughtyAttributesのButtonをPlayModeだけ押せるようにしたい【Unity】

NaughtyAttributesとは

UnityのInpectorの拡張ライブラリには 有名なものとして Odinがあります。 こちらは非常に便利なのですが、値段がそこそこするのでできればOSSが嬉しいというのでOSSの中でもInspector拡張でトップレベルで有名(*1)なのが NaughtyAttributes です。

github.com

NaughtyAttributes は、Unity インスペクターの拡張機能です。

Unity が提供する属性の範囲が拡張されるため、カスタム エディターやプロパティ ドロワーを必要とせずに強力なインスペクターを作成できます。また、シリアル化されていないフィールドまたは関数に適用できる属性も提供します。

(*1) 界隈や立場にもよります

実現したいこと

Odin には Button Attributesを作成したときに 特定の時のみInspector上で押せるようになる機能があります。今回は こちらと同じような機能でかつ UnityEditorが PlayModeの時のみ押せるようなものを実現します

PlayModeのみ押せるようにするには

公式ドキュメントに [Button(enabledMode: EButtonEnableMode.Playmode)] で できるとの記載があります。

dbrizov.github.io

使ってみると以下のようになるため非常に簡単に実現できるそうです

Default Cursorを任意の画像に変更時にマウスのクリック位置がずれる問題の修正方法【Unity】

初めに

ゲームを作っているとゲーム のWindows内では任意のマウスアイコンに変更したい時があるかと思います。しかし、変更後マウスのクリック位置がずれていることがプロジェクト内で発見されて調査したので、メモしておきます。

環境

  • Unity 2021.3.3f1

困っていたこと

下の画像のようにマウスの中心位置が左上になっていることが分かります。

任意のボタンをプレイヤーに押してもらうときにかなりUXが悪い状態になっていることが分かります。

解決方法

使用している画像のサイズを確認

今回使用している画像は 500 × 約600です。

そのため、これらを半分にした数を設定すれば解決されます。

画像の半分のサイズを Project Settings → Player → Default Cursor の XとYにそれぞれ設定します。 結果いい感じにマウスの中心点を変更することができます

Google Sheetを使ったFungusのテキストデータの管理【Fungus】【Unity】【GoogleAppsScript】

概要

私たちが制作した「DreamIsland」(*1)というゲームでは、Fungusを使ってNPCやオブジェクトとの会話イベントを実装しています。その中でFungusのデータ管理(会話テキストやフロー)についていろいろ調査と試作をしていったん解決ができました。

今回は Fungusの会話フローやテキストデータを Google Sheetを使うことによって、開発時の開発フローの改善と生産性向上を行うことができると思っています。

(*1) 宣言もかねて、載せていただきます!是非プレイしてみてください

store.steampowered.com

デモ

デモ動画では、以下の流れで操作を行っています。

  1. Unity画面で会話テキストの表示
  2. Google Sheet側で会話テキストの変更
  3. 変更後の会話テキストの表示確認

リポジトリ

github.com

ターゲット

  • FungusとUnityをある程度使っている人
  • Fungusのデータ管理について困っている人

開発環境

  • Unity 2021.3.4f1
  • Fungus v3.13.8
  • UniTask 2.3.1
  • UniRx 7.1.0

課題と解決方法実装

これまでの課題

Fungusを使うときのデータ管理として、Fungus側は以下が準備されています。

  1. Flowchartにデータをそのまま埋め込む
  2. textデータをimport/exportして管理をする
  3. CSVデータをexportする(importはうまくいなかった)

上記の3つともローカルでは編集できるのですが、チームで開発を行うときにだれが更新するのか・そもそもどのファイルが最新版なのかがわからなくなってしまいます。また テキストデータを編集する人はUnityや実装のことをわかっていないポジションの人かもしれません。

解決方法

概要にもありますが、今回は Google Sheetで会話テキストと会話フローを管理することにしました。また Fungus(Unity)のデータ更新は Unity起動時に 最新版に更新することでシナリオを書く人と実装する人のコミュニケーションコストを無くすようにしました。

以下が大まかな構成図になります。

  1. Google Sheet上で会話テキストや会話フローを編集
  2. GASを使って特定のエンドポイントから最新版の会話データをjsonで取得
  3. Unity起動時に http経由で最新の会話データを取得
  4. Fungusのflowchartにipmortできる形にコンバートを行い、flowchartを最新に更新

実装方法

準備編

いったんいま実装している会話データをGoogle Sheet(CSV)の場合どのような書き方になるのかを確認します。

1 . Fungus(flowchart)で会話を実装

今回は以下のような会話を作りました。

2 . Localizationコンポーネントの追加

以下のMenuからCSVとしてexportできるための機能(Localization) コンポーネントを追加することができます。Tool -> Fungus -> Create -> LocalizationLocalization コンポーネントヒエラルキー上に追加することができます。

3. 会話データをCSVで出力

Localization コンポーネントExport Localization File というボタンがあるので、こちらを押すと 任意のパスに 会話データを CSVファイルとして出力することができます。

Google Sheet側(会話データ側)

1. 外部から会話データを取得できるGASスクリプトの作成

先ほど出力した CSVのフォーマットを そのまま Google Sheetにコピーします。

次にこのデータを Unity側で取得できるように GASを使ってjsonとして渡せるようにします。メニューから 拡張機能 -> App Script を開きます。

開くとスクリプトを書くことができるので、以下のようにして 任意のシート名のデータをjsonとして渡すスクリプトを作成します。

function doGet(e) {
  var sheetName = e.parameter.sheetName;
  var d = {"textData":getData(sheetName)};
  var out = ContentService.createTextOutput();
  out.setMimeType(ContentService.MimeType.JSON);
  out.setContent(JSON.stringify(d));
  return out;
}

function getData(sheetName) {
  const sheet = SpreadsheetApp.getActive().getSheetByName(sheetName);
  const rows = sheet.getDataRange().getValues();
  const keys = rows.splice(0, 1)[0];
  return rows.map(row => {
    const obj = {};
    row.map((item, index) => {
      obj[String(keys[index])] = String(item);
    });
    return obj;
  });
}

2. GASスクリプトのデプロイ

デプロイから新しいデプロイを選び、ウェブアプリを選択します。ユーザーは 自分を選択します。

このときの表示されるデプロイ URLは使うためどこかに控えておきます

Unity側(ゲーム側)

会話データ用をロードするためのクラスを以下のように作成します。

[Serializable]
    public class TextData
    {
        public string Key;
        public string Description;
        public string Standard;
    }

    [Serializable]
    public class TextDataList
    {
        public List<TextData> textData;
    }

次に GAS(http)経由で Google Sheetのデータを取得します。またFungusに沿ったテキストフォーマットに変更する必要があるため、二つの関数を用意しておきます。

/// <summary>
        /// ゲーム情報をスプレッドシートから取得
        /// </summary>
        /// <returns></returns>
        public static async UniTask<T> GetGameInfo<T>()
        {
            var request = UnityWebRequest.Get($"{url}?sheetName={sheetName}");
            await request.SendWebRequest();
            if (request.result is UnityWebRequest.Result.ConnectionError or UnityWebRequest.Result.ProtocolError or UnityWebRequest.Result.DataProcessingError)
            {
                Debug.Log("fail to get card info from google sheet");
            }
            else
            {
                var json = request.downloadHandler.text;
                var data = JsonUtility.FromJson<T>(json);
                return data;
            }

            return default;
        }

        public static string ConvertFungusTextFormat(TextDataList data)
        {
            var fungusText = "";
            foreach (var info in data.textData)
            {
                fungusText += $"#{info.Key}\n";
                fungusText += $"{info.Standard}\n";
                fungusText += "\n";
            }

            return fungusText;
        }

最後にこれらのスクリプトを呼び出し、Fungus側に起動時に反映すれば終了です

public class UpdateFungusText : MonoBehaviour
    {
        private Localization _localization;
        [SerializeField] private GameStarted gameStarted;

        private void Awake()
        {
            _localization = GetComponent<Localization>();
        }

        private async void Start()
        {
            var data = await LoadTextData.GetGameInfo<TextDataList>();
            var fungusText = LoadTextData.ConvertFungusTextFormat(data);
            _localization.SetStandardText(fungusText);
            gameStarted.enabled = true;
        }
    }

省略している部分や少し変更されている部分もありますので、最新版はRepositoryをご確認ください

終わりに

FungusのデータをGoogle Sheetで管理することによって、テキストのローカライズの対応がより容易、テキストの確認作業の高速化、エンジニア側とシナリオ側のコミュニケーションの削減など多くのメリットがあります。

またFungusを使ったときの会話テストをEditor Windowのみで完結させる拡張機能は以下で紹介していますので、ご覧ください

ayousanz.hatenadiary.jp

OSSをForkしてUPM・OpenUPMの登録を行う【Unity】【upm】【OpenUPM】

はじめに

個人製作でいろいろやっているとOSSを使うことがありますが、最終更新日が数年前のものがよくあります。しかし、ライブラリとしては使いたいけどいろいろいまのversionとはあっていないものがあるので、今回はその辺の更新とUPM、OpenUPMの登録をする方法をメモとして置いておきます。

みなさんも古いものは更新してどんどんほかの人(自分が)使えるようにしていきましょう!

対象者

  • Unityで UPM,OpenUPMを登録したい人(今回はUnityで限定させていただきます)
  • ライブラリを自分用にカスタマイズしたい人

ライブラリのfork

まずは自分がメンテナンスしたいライブラリがないと始まらないため、今回は 以下のライブラリをメンテナンスして UPMとOpenUPMで使えるようにしていきます。

github.com

ボタンを押すといろいろ設定ができますが、そのまま作成します。

(以下の画像は別のリポジトリのforkなので名前等は自分のものにしてください)

まずはこちらから fork を押して自分のリポジトリにforkしていきます

github.com

(今回は すでにforkされている方がいたので、fork されていたものをforkしています。ほぼ同じなので好きなところからforkしてください)

upmの登録

基本的には以下の記事を参考に進めていきます。

qiita.com

upmとして公開するには、以下が必要になってきます。もともとのライブラリによってすでに作成済みになっている場合があるので適宜スキップしてください。

  1. ライブラリのフォルダの作成
  2. package.json の作成
  3. package.json の内容を適切に記述

フォルダの作成

上の画像のように ライブラリ用のフォルダを作成していきます。名前は適切なものにして、ほかの人が見たときにわかるようにしておきましょう。

package.json の作成と記述

フォルダの中に package.json を作成します。

次に記述を以下のようにします。

{
  "name": "com.ayutaz.uigradients" ,
  "displayName": "UI Gradients" ,
  "version": "1.0.1" ,
  "unity": "2021.3" ,
  "description": "A small collections of scripts to add gradient effects to UGUI elements." ,
  "keywords": [
    "Gradient" ,
    "UI"
  ]
}

確認作業

  1. 新規のプロジェクトを作成して、PackageManagerを開きます。

  2. Add Package from Git URL から自分のURLを追加します。

私の場合は、https://github.com/ayutaz/Unity-UIGradient.git?path=Assets/UIGradients になります。

これで以下のようにライブラリが問題なく追加できれば upmの対応は終了です。

遭遇するかもしれないエラーと対応方法

~ is not valid JSON: Unexpected token in JSON at position 0

UnityのPackge Managerから登録した際に 上記のエラーが表示されることがあります。こちらは、BOM が入っていることがあるのでこちらを取り除いていきます。

Editorによって少し操作が異なってきますが、以下のようなメニューがあるので エンコードを指定して開きなおす などを押して適切なエンコード (だいたいは utf-8 ) で開いて保存すれば直ります。

更新したときの作業

修正点が見つかって、更新したときには package.jsonversion を更新しておきましょう。

こちらを更新しないと 追加したときのversionの数字が更新されません。

OpenUPMの登録

Open UPM( packge.json からインストールする) ためには登録する必要があります。

openupm.com

こちらから以下のように申請していきましょう

  1. リポジトリの登録

  1. 詳細の設定

Defaultのブランチや ライセンスなどを記載していきます。特にこだわらなければ、リポジトリと同じものにしましょう

  1. PRの提出とデプロイ待ち

あとは上記のものを PRを出して、テストが通れば数時間くらいでデプロイが完了します。

このときに package.jsonのversionと同じgit tagを設定しないとPackage Managerから見てない ことに注意してください

PRがマージされると package.jsonに記載すると PackageManagerに表示されるようになります!

OpenUPM のサイトに登録されていれば今後自分が登録したものが使えるようになります!

  1. 登録の確認 以下登録した終わった今回のライブラリ

openupm.com

GitHub Actionsでビルドが終了時に DiscordでartifactのダウンロードページURLを受け取る【GitHub Actions,Discord,Python】

はじめに

GitHub ActionでCI/CD環境を組んでいて、ビルドができても完了通知やビルドデータのダウンロードをするためには毎回 GitHubを開く必要があります。

かなり手間になっていたので、ビルドが環境するとアーティファクトがあるページのURLをDiscordに送信するシステムを作りました。

(デモリポジトリはUnity用に作成していますが、ほかのものでも代用できると思います)

public Repositoryではないと動かないことが判明したため、調査中です

成果物

以下のように Discordにビルドで作成されたアーティファクトがあるページのURLが、 discordでビルド環境時にメッセージとして送られてきます。

Repository

github.com

動作環境

使い方

1. Actions secretsの登録

まずは今回は GitHub APIと discordのwebHookを使っているため、以下の画像のように GitHub環境変数WebHookURLGitHub Personal Token 登録をします。

tokenは最低限のものを許可していれば大丈夫です。

この際に GitHub上の設定で、Setting → Actions → General → Workflow permissionsRead and write permissions になっていることを確認してください。 動かない可能性があります。

2. GitHub Actionのymlの作成

ymlの実行したい箇所に以下を追加します

      - name: Set up Python 3.9
        uses: actions/setup-python@v3.1.2
        with:
          python-version: 3.9
      - name: Install dependencies
        run: |
          pip install -r .github/send_download_artifact_url/requirements.txt
      - name: send download artifact page url
        env:
          GITHUB_REPOSITORY: ${{ github.repository }}
        run: |
          python .github/send_download_artifact_url/send-download-artifact-page-url.py

3. スクリプトの作成と配置

実行するスクリプト( send-download-artifact-page.py ) と requirements.txt を以下のように .github/send_download_artifact_url 以下に配置します。

ファイル名や配置パスは、ymlを書き換えれば任意のものに変更しても大丈夫です。

discordに送信するスクリプトは以下になっています。

import os
import requests

from dotenv import load_dotenv

load_dotenv()

headers = {
    "Accept": "application/vnd.github.v3+json",
    "Authorization": os.getenv("PERSONAL_ACCESS_TOKEN"),
}

req = requests.get(f" https://api.github.com/repos/{os.getenv('GITHUB_REPOSITORY')}/actions/artifacts",
                   headers=headers).json()


def get_download_url(content):
    for artifact in content["artifacts"]:
        if artifact["name"] == "Build-StandaloneWindows64":
            run_id = artifact["workflow_run"]["id"]
            url = f"https://github.com/{os.getenv('GITHUB_REPOSITORY')}/actions/runs/{run_id}"
            return url
    return None


def message(url):
    content = {
        "username": "ビルドダウンロードページ",
        "content": "ビルドが終了しました。"
                   + f"\nダウンロードページ: {url}"
    }
    return content


requests.post(os.getenv("DISCORD_WEBHOOK_URL"), message(get_download_url(req)))

実装の詳細

最新のビルドデータを取得

https://api.github.com/repos/OWNER/REPO/actions/artifacts を使い、Repositoryのartifactのリストを取得しています。その中から ビルドデータ (今回の場合は、Build-StandaloneWindows64 ) に一致するもので最新のものを取得しています。

https://docs.github.com/ja/rest/actions/artifacts#list-artifacts-for-a-repository

実際にAPIを叩いてみると、以下のようなデータが返ってくることが分かります。

artifactのあるページのURL作成

GitHub上でartifactがあるページは https://github.com/owner_name/repository_name/actions/runs/run_id となっています。こちらは直接取得できないため、↑のでビルドデータの中に run_id が含まれているので、そこからidを取得して URLを生成しています。

(ブックマークは気にしないでください)

リポジトリのownerとRepositoryNameを取得

Repositoryに依存しない実装にしたかったので、Repository名やowner名をスクリプト上に記載したくはなかったので この二つを 環境変数として取得しています。

${{ github.repository }} で取得できるため、 ymlファイルでは以下のように設定をして Python上で環境変数を読んでいます。

        env:
          GITHUB_REPOSITORY: ${{ github.repository }}

参考サイト

zenn.dev

終わりに

本来は直接artifactをダウンロードできるURLが欲しかったのですが、以下のようにダウンロードURLは発行できるものの1分しか持たないみたいです。

github.com

そのため、今回は妥協してダウンロードできるページのURLを送信するようにしました。

Unityで3D音源の距離と音量からAudioVisualiserを作成する【Unity】

はじめに

前回以下のような 距離から音量を取得する機能を実装しました。こちらをせっかくなら いい感じの見た目にしようと思い、Audio Visualiser の作成を行いました

成果物

動作環境

  • Unity 2021.3.4f1

実装

各音源から距離と音量を取得・表示する

以前に実装について書いたので以下をご覧ください。

ayousanz.hatenadiary.jp

Visualiser UIの作成

今回のAudio Visualiserは 16等分割した円の画像を使って向きと大きさを表現しています。

まずはこちらのUIを作る作業からやっていきます

(完成UIの拡大版)

(完成したUIのパーツUI : 白で塗りつぶししているため、Web上だと見えないため画像をダウンロードして使用してください)

画像作成アプリは、なんでもいいですが 今回は firealpaca を使っていきます。

放射線上の下書きと円形の下書きを書いて、1/16の円を作っていきます。

次に Unity上で、以下の感じで Image の Fill Amountを変更してUIに変化を持たせたいため、画像を横向きに変更します (画像作成時に横向きにしておくとこの作業がスキップできます)

画像の回転は Windows標準機能のフォトアプリの編集機能が優秀だったため、こちらを使っていきます。 回転する角度は 11.25度(360 / 32) ですが、小数点以下は指定できないみたいなので11度回転させてあげます

できた画像を Unity上にimportして、Texture Typeを Spriteにしておきます。 そのあとに 円の中心に回転したいため Sprite Editorを使って Pivotを円の中心に変更します

シーン上に配置すると、以下のようにImageが円の中心を軸にきれいに回転するようになっています。

Audioデータから Visualiser UIをリアルタイで更新する

今回は Visualiserは以下のように要件定義して実装をしていきます

  1. 聞こえている音とVisualiser上の色を同じ色にする(見やすくするため)
  2. 聞こえている方向を 360度の円形上で表現する
  3. 聞こえている音量を 一つの軸の高さで表現する

上を満たすための AudioData は以下のようにしました。

    public class VisualizerAudioData
    {
        public float AudioValue;
        public Vector2 AudioVector;
        public Color32 AudioColor;
    }

1 はほぼ何もしていないので、詳細は省略します。

2.3については以下のようにしています。

簡単に説明すると Visualiserの画像数(表現できる音の種類 = 16個) の配列に その方向の音量で一番大きいデータを入れる 処理をしています。

まずは Player と Audioの位置から 角度を以下のように計算します

        public static float Vector2ToAngle(Vector2 vector2)
        {
            var angle = Mathf.Atan2(vector2.y, vector2.x) * Mathf.Rad2Deg;
            if (0f <= angle) return angle;
            return 360f + angle;
        }

次に上で計算した角度がどの向きグループに属するのかを計算します

例えば 角度が 0度の場合は 配列[0]に属する、角度が 120度の場合は 120 / (360/16) = 5.33 ≒ 6なので 配列[6] に属する。

        private int VisualizerDataIndex(float angle)
        {
            return Mathf.CeilToInt(angle / (360f / _visualiserData.Length)) - 1;
        }

すでに入っているAudioDataの音量よりも大きいかどうかを比較して、大きい場合のみ更新します。

        public void UpdateVisualizerData(VisualizerAudioData data)
        {
            if (data.AudioValue <= _minValue) return;
            var angle = Vector2ToAngle(data.AudioVector);

            var oldValue = _visualiserData[VisualizerDataIndex(angle)].AudioValue;
            if (oldValue < data.AudioValue)
            {
                _visualiserData[VisualizerDataIndex(angle)] = data;
            }
        }

これで データクラスの更新が終わったので、配列データを View側に 渡して Visualiser UIを更新していきます。

MVP の設計にしているため、 PresenterがModelのデータ配列をもらって View側(MonoBehaviour) を更新します

サンプルプロジェクトのクラス図

まとめ

ModelとViewを分割しているため、3D Visualiserとかにも応用できそう??

HoloLensやOculusQuestとかで周りの環境音を取得して、表示したら面白そうですね!(どこかでやりたい)

Unity WebGLビルド後 特定のブランチ GitHubに自動デプロイする【Unity,WebGL】

はじめに

UnityでWebGL開発を行っているときにビルド後デプロイしたい時があります。OSSとして公開しているプロジェクトの場合、デプロイ先としてGitHub Pageを選んだとしてもアセットをpublicにすることができません。 今回は、ローカルでビルド後 WebGLデプロイ用のブランチに自動で pushする機能を作りました。

成果物

WebGL Deploy URL

ayutaz.github.io

プロジェクトリポジトリ

github.com

サンプルプロジェクトの成果物として、InputSystemをいろいろ使っているプロジェクトです

動作環境

  • Window10
  • Unity 2021.3.3f1
  • Git,GitHubが使えるPC

実装

Build後に特定の処理を行う

ビルド処理が終わった後に処理を挟みたい場合は、IPostprocessBuildWithReport を継承したスクリプトを使用します。

2019.1以前は IPostprocessBuild がありましたが、こちらは現時点では非推奨になっています。 docs.unity3d.com

githubへのcommit/pushは Deploy.bat にまとめています。

外部のファイルを実行する際に process.StartInfo.FileName に実行するファイル名を設定します。
また process.StartInfo.CreateNoWindow を trueにすることで、cmdが立ち上がらずにバックグラウンド処理になります。

public class WebGLDeploy : IPostprocessBuildWithReport
    {
        public int callbackOrder { get; }
        private string _batPath;

        public void OnPostprocessBuild(BuildReport report)
        {
            _batPath = Path.Combine(GetBuildFolderPath(), "Build");
            RunCommand(_batPath);
        }

        private static string GetBuildFolderPath()
        {
            return Directory.GetParent(Application.dataPath)?.FullName;
        }

        private static void RunCommand(string buildFolderPath)
        {
            var process = new Process();
            process.StartInfo.WorkingDirectory = buildFolderPath;
            process.StartInfo.FileName = Path.Combine(buildFolderPath, "Deploy.bat");
            process.StartInfo.UseShellExecute = false;
            process.StartInfo.RedirectStandardInput = false;
            process.StartInfo.RedirectStandardOutput = true;
            process.StartInfo.CreateNoWindow = true;
            process.Start();
            process.WaitForExit();
        }
    }

BuildデータをGitHubスクリプト上から Commit/Pushする

事前に Deploy.bat を Buildフォルダに入れておきます。

Batファイルは、以下のように定義しています。

git add *
git commit -m "Deploy"
git push origin webgl_build