勉強不足で至らんブログ

勉強不足ですが、何か発信できればと思っています。

UnityのEditModeTestsをdotnetで実行させる

UnityのTestをUnityEditorを使わないでどうにか走らせれないか?

と考えのがきっかけです。

UnityのCIをCircleCIで頑張る - 勉強不足で至らんブログ

過去CircleCIでUnityEngineが入ったDockerを使っていたりしましたがDockerコンテナのセットアップで

f:id:MakeTake:20200204230713p:plain

長くて3分弱かかります早くて1分程度にはなります。 docker_layer_cachingを使えば10秒くらいのにもできますがUnityEngineの入ったDockerを使おうとすると工夫が必要になります。

工夫なく手軽に使えたらいいのに…と思ったところで

booth.pm

にある

  • 第5章:Unityと.NET Coreでのコード共有

を思い出しました。これでdotnetのtestから走らせればいいのでは…と思ってやってみました。

結果的に今回はEdit Mode Testsであれば可能でした。最後の方に記述しますがPlay Modeももしかするとできるかもしれません。

今回のリポジトリ github.com

CircleCIの結果 https://circleci.com/gh/MizoTake/UnityTestFromDotnet

セットアップ

基本的なセットアップ手順は 第5章:Unityと.NET Coreでのコード共有 にありますのでそちらの手順に添えば大丈夫です。 サンプルリポジトリも公開してるそうです。 github.com

自分は追加でdotnetのプロジェクトの.csprojに

  <ItemGroup>
    <PackageReference Include="NUnit" Version="3.12.0" />
    <PackageReference Include="Unity3D.UnityEngine" Version="2018.3.5.1" />
    <PackageReference Include="coverlet.collector" Version="1.0.1" />
    <PackageReference Include="NUnit3TestAdapter" Version="3.16.0" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
  </ItemGroup>

を追加しています。基本的にNuGetで検索すればでるはずなので導入方法はやりやすいものがいいと思います。

注意なのは Unity3D.UnityEngine のバージョンが2018台のいくつかしかないことです。バージョンを超える何かを行たい場合は工夫が必要になると思います。

Testを実行する

普通にC#でEdit Mode Testsを記述します。今回はサンプルで

using NUnit.Framework;

public class TestSample
{
    [Test]
    public void Passed()
    {
        Assert.AreEqual(true, true);
    }
    
    [Test]
    public void Failed()
    {
        Assert.AreEqual(true, false);
    }
}

としました。これをUnityのTestRunnerが認識するように作ります。

ただ、少し踏み入ってUnityEngineの参照を行おうとすると参照できないことがあるのでdllの解決をうまくやる必要がありそうです。自分も確認不足でしたのでわかり次第更新します。

あとはdotnet test を実行すればtestが走り、結果が得れます。

手元で得るよりはCIで走らせて見れた方がいいと思うのでCircleCIで走らせました。

version: 2.1

executors:
  dotnet:
    docker:
      - image: mcr.microsoft.com/dotnet/core/sdk:3.0

jobs:
  test:
    executor: dotnet
    steps:
      - checkout
      - run: dotnet tool install trx2junit
      - run: "dotnet test --logger trx || : ; dotnet trx2junit dotnetProject/TestResults/*.trx --output results"
      - store_test_results:
          path: results

workflows:
  version: 2
  test-flow:
    jobs:
      - test

途中でdotnet toolで trx2junit というのを導入していますが dotnet test だとtestの結果は.trxという形式で吐き出されるためです。CircleCIではJUnitCucumberしか対応していないので変換させるためにtrx2junitを使用します。

一度手元でdotnet tool install trx2junitを実行すれば.configというディレクトリに

{
  "version": 1,
  "isRoot": true,
  "tools": {
    "trx2junit": {
      "version": "1.3.0",
      "commands": [
        "trx2junit"
      ]
    }
  }
}

が作成されます。これがあればinstallできるぽいです。もしかすると.configはignoreしていいディレクトリかもしれません。

ともかくこれでCircleCIのTest Summaryに登録でき、エラーがあった場合に f:id:MakeTake:20200204234208p:plain という風に見れるので対処もやりやすくなると思います。

またdotnet test実行時のコマンドは、この形式でないとテストがこけた際にtrxから変換ができずにTest Summaryが使えず終わってしまうのを回避するためのものです。

さいごに

ここまででUnityのEdit Mode Testsをdotnetで実行させる方法の説明は終わりです。UnityEngineがなくても実行できる環境というのは結構魅力的だと思っています。さらにはUnityのactivateが必要ないのでかなり良いです。

いずれPlay Mode Testsも実行させれるようにしてみたいため調べてみると www.nuget.org

があったので試しましたがそのままでは動きませんでした。ただ工夫をすればいけるような感じだったので時間のある時に挑戦してみようかと思います。

Unityでタイムラプスぽいことできないか試した

Unityでタイムラプスの表現できたら面白いのでは?

とアニメのOPなどをみて思いました。タイムラプスって面白いのでどうやったらそれっぽっく表現できるのか試しました。

Unityで表現するには簡単で Time.timeScale を弄って大きいする数字にだけでタイムラプスぽく見えるようになりました。

Time.timeScaleを大きくして早送りしてからキャラクターを表示非表示を一定の間隔で行えばいけるかな?と思ったらtimeScaleを弄るだけでおわりました…

一定の値を超えたあたりからそう見えるので閾値がありそうだなとは思いました…ができたことに満足したので終わりました。。。

Unityのネイティブプラグイン(macOS/iOS/tvOS)のソースコードを共通化する

ネイティブプラグインの動作確認

iOS/tvOSのネイティブプラグインはEditorでは動かせません。そこでmacOSで同じソースコードを動かせばいいのでは?と考えて動かしてみました。

ただ全てが動くわけではないと思いますし一部機能の制限が入ります。調べ尽くしてないので、もしかすると使えます。わかっているのは以下の一点です。

  • entitlementファイルを使う場合は.bundleにcodesignができれば動きそう(パッと試した感じはできなかった)

一点といいつつ結構範囲が大きいです。あくまでEditorで動かす.bundleにする際にできないというだけで.appにcodesignを使えば動くはずです。

リポジトリはこちら

github.com

Editor(macOS)での動作はもちろん、シミュレーターになりますがiOS/tvOSで動作確認をしました。

Editor(macOS) iOS tvOS
f:id:MakeTake:20200202160024p:plain f:id:MakeTake:20200202160042p:plain f:id:MakeTake:20200202160047p:plain

実装

今回Swiftにてコアのコードを記述しました。

import Foundation

@objcMembers
public class Sample : NSObject {
    
    public override init() {
        super.init()
    }
    
    public func Call() -> String {
        "From Swift Code"
    }
}

Objective-CでラップしてC#側でStringをTextMeshProForUGIに表示させています。

Editorで動かすためのXcodeProjectを作る

UnityのPluginsディレクトリに.swiftや.mmがある前提で話をします。

まずはXcodeでProjectの新規作成を行ます。 f:id:MakeTake:20200202160853p:plain

UnityEditorでは.bundleというフォーマットで動作をするのでBundleのテンプレートを選択します。

次にプロジェクト名を求められるのでお好きなプロジェクト名を入力します。

これで保存場所を選択して作成完了です。

これからUnityのPlugins配下にあるファイルを参照してXcodeのプロジェクトに含めます。 ファイルを選ぶ際にコピーするかどうかのオプションがりますが別ファイルになってしまうのでOFFにしておいた方が良いです。

info.plistの入っているディレクトリを右クリックして以下の項目を選択します。 f:id:MakeTake:20200202172214p:plain

次にSwiftを使用する設定です。 先ほどのファイル追加で処理が走り、Swiftなどの設定が増えます。そこで f:id:MakeTake:20200202172228p:plain

左の青いアイコンを選択して Build Settings を選びます。そこから Basic Customized All と選ぶポイントがあるので All にして検索ボックスに -swift.h と検索します。

そして Objective-C Generated Interface Header Name のところをUnityのPlayerSettingsで設定するProduct Nameを確認して {ProductName}-Swift.h と入力しておきます。

こうすることでUnityでiOS/tvOSのプロジェクトが吐かれた時にエラーが出なくなります。

これで準備ができたのでXcodeのRunボタンでDebugモードでビルドします。 f:id:MakeTake:20200202162612p:plain

ビルドすると Products ディレクトリ配下にbundleができるのでUnityのPluginsディレクトリ配下におきます。

f:id:MakeTake:20200202172243p:plain

Show in Finder を選択するとFinderでドラッグ&ドロップできるのでおすすめです。(xcodeのpostprocessに移動するscriptを書けばいいのですがめんどくさかった…)

Editorで動かす準備ができました。

C#プラグインを呼ぶ

C#で呼ぶ処理を書いて動かしてみましょう。

今回はC#でこう書きました。

using System.Runtime.InteropServices;
using TMPro;
using UnityEngine;

namespace ApplePlatformNativePlugin {

    public class SampleText : MonoBehaviour
    {

        [SerializeField] private TextMeshProUGUI centerText;
        
#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
        private const string LibraryName = "XcodeProject";
#elif (UNITY_IOS || UNITY_TVOS) && !UNITY_EDITOR
        private const string LibraryName = "__Internal";
#endif
        
        
        [DllImport (LibraryName)]
        private static extern string CallNativeString();
        
        void Start()
        {
            centerText.text = CallNativeString();
        }
    }
    
}

これでSceneに配置してUIに表示させています。

おわりに

以上がAppleのプラットフォームのネイティブプラグインを共通化させEditorで確認する方法でした。これであれば実機でしか動かせない!という時も処理だけならEditorで簡易に確認が可能かと思います。 資格情報の署名がわからず、このままでは機能が一部動作しませんが一旦はいいかなと思っています。

UnityでFirebaseのRealtime Databaseで位置同期検証してみた

Firebaseでキャラクター同期して遊べるゲームの検証

昨今UnityでもCrashlyticsやAnalyticsも内包しているFirebase使ってみようかなと思ったのがきっかけです。 特にFirebaseのRealtime Databaseを使うとFirebaseで値を保持してすぐにクライアントと同期してくれるというので試してみました。Firebaseはリアルタイムに同期するゲームには向いていないという意見をよくみてなんでだろう?と思ったので検証しました。

自分はサーバー側がわからなければFirebaseも初めて触っているので当たり前のことでもわからないため動かしました。

動かした結果は以下のツイートの動画になります。FirebaseのRealtime Databaseを使えるプロジェクトを用意してEditorで動かしました。位置は毎フレーム同期させています。

右のEditorのキャラクターの位置がすぐに左に反映されているのがわかると思います。 この結果だけみると「意外といけるのでは?」と思っていました。 しかし、Realtime Databaseには無料枠だと制限があります。

f:id:MakeTake:20200108105532p:plain

Firebaseから情報をダウンロードするのに月10GBの制限があるのですが、2つのクライアントから数分動かしただけで27.3MBかかっています。これが人数が増える or 情報が多くなるとものの数日や数時間で10GBに到達してしまいます。従量課金制のプランにすれば上限なしでかかった分だけ払えばいいのですが一旦無料でいけるところまでやりたいので今回は検証結果をまとめるだけまとめてスルーしておこうと思います。

次はMirrorでも試してみようかと思っています。 github.com

振り返ってみる【2019】

今年を振り返ってみる

初めてこういうことを書いてみるのですが今年は色々やった気がするのでまとめてみます。

結構初めてのことをやり散らかした年なのかなと思っています。

転職

新卒で入った会社を辞めました。

maketake.hatenablog.com

最初からとんでもなく大きい出来事ですが、これが1月に辞める旨の話を会社にして初めて転職を行ったので最初に来る出来事でした。今は新しいところで前職の知識も使いつつゲームを作る日々でございます。

初LT

maketake.hatenablog.com

イベントのLTを初めてしました。身内でのLTとかはやったことあったのですが外部イベントは初めてでした。どちゃくそ緊張してgdgdだった気しかしてないです。LTをやるのも慣れだなぁとは思います。

その後もまた

maketake.hatenablog.com

LTをして経験を積んでいけたと思います。

learning.unity3d.jp

動画まで残っているのは見返す良い機会なのですが…下手でしんどい…精進…

LINEスタンプの販売、音楽制作

プログラミング以外にも手を出し始めました。

store.line.me

soundcloud.com

やってみると意外とできるもんだんぁという感じですが、どちらもクオリティが高いものではなくて自分の第一歩かなと思います。いずれは頭に思ってるものをポンポン作っていけるようになりたいなぁとは思ってます。

同人誌への寄稿

前々から興味があった同人誌への寄稿を行いました。

booth.pm

初めての同人誌寄稿は文章校正とか大変で期限ぎりぎりで修正してました…スパン短くガンガン出してる人はすごいですね。。。苦労が少しわかるようになりました。楽しかったので良かったですけどね。

アドベントカレンダーへの参加

これも前から興味あって踏み出せてなかったひとつです。

maketake.hatenablog.com

自分が一番つかうUnityのアドベントカレンダーに参加してました。地味に役立つかもしれないものですね。仕事で欲しかったのでサクッと書いたものです。 「便利そう」って声を見かけたりしたので書いてよかったです。

MVのエキストラ参加

推しているMonsterZ MATEというユニットのMV参加募集があったので応募したら通った…

まじかよという気持ちをもちつつド平日に半休とったりして行きましたよ… 公開されたのがこちら

…最高か????いや…最高だよ… MVには写れてない気がするけど!!!いいんだ!!その空間にいたから!!!

まとめ

色々やったなぁ…って感じが強い…来年も頑張っていきたいけど今年くらい食い散らかせるかわからない。。。仕事で使う技術は掘り下げて知識蓄える気はあるけど、趣味でやることは食い散らかして色んな知識を吸収したいスタンスなのでやっていき…!

とりあえず来年はバンジージャンプすることは決まっているので飛んできます。よいお年を。

UnityのCIをCircleCIで頑張る

UnityのCIを無料で使いたい

業務だとJenkins等を使われてるイメージのあるUnityですが、個人作業だと自前でサーバー建てたくない(昔やったけど電気代がつらかった)し有料のCloudBuildを使うのも…僕はケチんぼなんでしたくない…サーバーレスで使えるCircleCIを使ってみようというのが始まりでした。

そこから CircleCIでUnityのTest&Buildを雰囲気理解で走らせた の記事を書き

Aiming Tech Book Vol.2 ここではCircleCIのOrbsを作った話をしていました。(OrbsはCircleCIのライブラリのようなもの) unity-ci(自分が作ったUnity用Orbs)でUnityのBuildやTestはある程度楽になったのですがライセンス周りは手動でどうにかする内容でした。

ここまでの記事ではUnityのバージョンごとにulfというUnityのライセンスファイルを手動で作ってCircleCIの環境変数にbasse64エンコードをかけて登録する必要がありました。 そこから先日 Unityの.alfファイルから自動で.ulfをダウンロードしたい! PuppeteerというjsのUIテストツールを使用して.alfというUnityのライセンスリクエストファイルからコマンドを叩けばUnityライセンス認証ファイルである.ulfをダウンロードするツールを使って自動化までしました。

これでulfも出来たから安泰…!というわけにはいかず、後からunity-license-activateのUnityアカウントのログインによる2要素認証処理が必要だったり、unity-ciもファイルからライセンス認証をする対応がが必要だったりしました。

そして最近やっと改修が落ち着いたのでまとめようと思います。改善点はまだまだありますが…一旦…

ここまでやって何が問題なの

ざっくりまとめると

  • unity-license-activateが2要素認証に対応してなかった
  • unity-ciがファイルからのライセンス認証に対応してなかった

ということになります。 個人開発だと安易にバージョンを上げることがあるのでバージョンの壁は自動で越えさせたかったというモチベーションがありました。

unity-license-activateとunity-ciを知らない方が多いと思いますので軽く紹介しますと

unity-license-activateはターミナル等で node activate.js $email $password $alf_file_path とすればemailとpasswordからアカウントでログインしてalfをulfに変えてくれるツールになります。

また、unity-ciはCircleCIでyamlを記述する際に

version: 2.1

orbs:
  unity-ci: mizotake/unity-ci@0.1.2

executors:
  unity_executor:
    docker:
      - image: gableroux/unity3d:2019.1.0f2

workflows:
  version: 2
  unity-ci-test:
    jobs:
      - unity-ci/build:
          name: Build Execute Win
          exec: unity_executor
          platform: Win
          method: Build.Method
          zip: true
      - unity-ci/test:
          name: Edit Mode Test
          exec: unity_executor
          mode: editmode
      - unity-ci/test:
          name: Play Mode Test
          exec: unity_executor
          mode: playmode

上記のように記述すればCircleCIでBuildやTestが可能になるOrbsになります。

今回の追加対応した大雑把な流れ

  1. 開発対象のUnityバージョンで.alfを生成
  2. unity-license-activateをgitで落としてくる
  3. npmで環境設定してunity-license-activateを叩いて.ulfをダウンロード
  4. ダウンロードした.ulfでUnityのライセンス認証

をCircleCI上で自動化しました。

開発対象のUnityバージョンで.alfを生成

これはUnityのコマンドラインにあるので素直に /opt/Unity/Editor/Unity -quit -batchmode -nographics -logFile -createManualActivationFile || exit 0

createManualActivationFileで生成します。しかし、終了コードがすべて失敗になるので強制的に成功させます。

unity-license-activateをgitで落としてくる

CircleCI上で行っているので、素直にgitで落としてくるものの alpine/git のDockerImageを使いCircleCIの機能である persist_to_workspace を使って次のjobでnodejsのImageで実行させるようにしています。

npmで環境設定してunity-license-activateを叩いて.ulfをダウンロード

以前書いた記事 Unityの.alfファイルから自動で.ulfをダウンロードしたい! で可能になったのですが初めて実行するPCでは2要素認証をクリアする必要がでてきます。そこで node activate.js $email $password $alf_file_path という引数がデフォルトですが node activate.js $email $password $alf_file_path $verify_code 第4引数に2要素認証のコードを入れて進めるようにしました。 CircleCIでどうやってクリアするのかというと

f:id:MakeTake:20191230092038p:plain

ここにあるsshでCircleCIにアクセスする方法をとります。まず2要素認証をメールなどではなくてGoogleの「認証システム」アプリなどにします。このアプリだと認証コードが一分ほど有効です。そのためsshで接続してunity-license-activateを叩いて認証するまで時間的には間に合います。 一度認証が通ればその後は必要になりませんがプロジェクトごとに一度ssh接続して認証してあげる必要がありそうです。

手法としては力技という感じがしますが自分としては他に道が見えなかったのでこうしました!!!

unity-ciのアップデート

そして自前で作っているCircleCIのOrbsがファイルからのライセンスアクティベートに対応していなかったので対応しました。

対応してOrbsを使うときには固定の文字列を入れたりしないといけないのでまだ改修が必要な感じがしますが一旦こうなります。

version: 2.1

orbs:
  unity-ci: mizotake/unity-ci@0.1.2

executors:
  unity_executor:
    docker:
      - image: gableroux/unity3d:2019.3.0f1

jobs:
  setup:
    machine: true
    steps:
      - checkout
      - run: sudo mkdir -p /activateTools
      - run: sudo chmod -R 777 /activateTools
      - run: touch /activateTools/ProjectVersion.txt && cat ~/project/ProjectSettings/ProjectVersion.txt > sudo /activateTools/ProjectVersion.txt
      - restore_cache:
          key: unity-license-{{ checksum "/activateTools/ProjectVersion.txt" }}
      - persist_to_workspace:
          root: /activateTools
          paths:
            - .
  unity-ci-test-editmode:
    executor: unity_executor
    steps:
      - setup_remote_docker:
          docker_layer_caching: true
      - attach_workspace:
          at: /activateTools
      - checkout
      - unity-ci/unity_activate
      - unity-ci/test:
          mode: editmode
      - save_cache:
          key: unity-license-{{ checksum "/activateTools/ProjectVersion.txt" }}
          paths:
            - /activateTools/Unity.ulf
      - persist_to_workspace:
          root: /activateTools
          paths:
            - .

workflows:
  version: 2
  test:
    jobs:
      - setup
      - unity-ci/outputAlf:
          exec: unity_executor
          requires:
            - setup
      - unity-ci/cloneActivateTool:
          requires:
            - unity-ci/outputAlf
      - unity-ci/outputUlf:
          requires:
            - unity-ci/cloneActivateTool
      - unity-ci-test-editmode:
          requires:
            - unity-ci/outputUlf

権限付与が必要だったり、Unityプロジェクトのpathがルートにある前提だったりと改修要素が見えますがファイル認証の対応を優先して行いました。 これでバージョンの壁も突破して実行が可能になると思います。

おわり

UnityのCIをCircleCIで行うために頑張ると以上のようなことも必要になってきました…素直にCloudBuild使う方が楽な気はしますが色々と突破するための技術を学ぶのも中々幅が広くて面白いです。 しかし、Orbsである程度省略して書けるようになったので良きかなと思ってます。ご興味あれば使ってみてください…!

UnityEditorのTitlebarにディレクトリの場所を表示する

この記事はUnity Advent Calendar 2019の8日目の記事です。

レビューをするときにEditorを複数起動する

業務でも業務でなくてもコードレビューをすることがあると思います。そして、PRにもよりますがEditorで挙動確認をしたいことがあります。 その時に、自分のタスクでbranch切ってるUnityEditorで一旦PRのブランチを切ってレビューをすることもありましたが、ブランチを切ってImportさせるのは時間を食います。

そこで、私は別のReviewディレクトリを作ってそこにProjectをcloneしてレビューをするようにしてます。

ex. Unity Project ディレクト

├── A (Repository root)

└── Review

   └── A' (Repository root)

※AとA'は同じgit Repository

例えば上記のように、Aは自分のタスクを行う場所、Reviewディレクトリ配下はレビューを行う場所としておけばUnityの多重起動をしてスムーズに切り替えれると思います。レビューのProjectをImportする間は自分のタスクが行えるので時間の無駄にもなりにくいです。

ただここで問題が起こります。 Unityの多重起動をすると、Editorの違いが見た目でわかりません!どっちがレビュー用だっけ?となりがちです。

そこで、最初はEditorの色を変えればいいかなと思いましたが無料で個人開発で同じようなことをする場合にできないこともあると思ったのでUnityEditor WindowのTitlebarを変えることにしました。(そのためWindows専用の処理になっています)

gistはこちら MizoTake/ChangeEditorTitlebar

できたもの

before after
before.jpg review.jpg

今回のEditor拡張の有無です…地味ですがTitlebarの後ろに「Review」という文字が追加されています。 正直、わかりづらいですがこれでReview用のUnityEditorかどうか判別が可能になります。

実装

using System;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;

namespace ChangeEditorTitlebarFromPath
{
#if UNITY_EDITOR_WIN
    [InitializeOnLoad]
    public class ChangeEditorTitlebar
    {
        
        [DllImport("user32.dll")]
        static extern IntPtr GetActiveWindow();
        
        [DllImport("user32.dll", EntryPoint = "SetWindowText")]
        static extern bool SetWindowText(System.IntPtr hwnd, System.String lpString);
        
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        static extern int GetWindowTextLength(IntPtr hWnd);

        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
        
        static ChangeEditorTitlebar()
        {
            // 検索対象の文字列をディレクトリパスに含まれているかどうか判別
            var target = "Review";
            var path = Application.dataPath;
            var isMatch = Regex.IsMatch(path, target);
            if (!isMatch) return;
            
            // 現在ActiveなWindowを取得(InitializeOnLoadでUnityEditorが選択されている前提)
            var window = GetActiveWindow();
            if (window == IntPtr.Zero) return;
            
            // 既にあるWindowの文字列を取得
            var length = GetWindowTextLength(window);
            var stringBuilder = new StringBuilder(length + 1);
            GetWindowText(window, stringBuilder, stringBuilder.Capacity);
            
            // 取得した文字列の後ろに今回の検索対象の文字列を追加
            var titleBarText = stringBuilder.ToString();
            SetWindowText(window, titleBarText + $" - {target}");
        }
    }
#endif
}

かなりシンプルです。この1ファイルのみを追加すれば大丈夫です。 Windowsのみですが、これで用途別に同じプロジェクトを開いても判別できると思います。

さいごに

これで一旦、用途別のWindowの判断が可能になりました。タスク用途とReviewで分けるのであれば有料版を持っていればEditorの色を黒とデフォルトで分ければいいのですが、無料版や3つ以上となるとTitlebarになるのかなと思いました。 拡張をいれるとするならプロジェクト内ではなくてUnity.exe単位の拡張で行うのが良いのかなと思っています。