simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

AIに身体性を与えるためのマイクロワールドの構築13:Unityのプラグイン機構

前置き

今日のゲームの面白さは、決められた内容をコントローラをピコピコしながら進めるところにはありません。
いつしかゲームは絵本や映画とは異なり、何度プレイしても内容が変化する、飽きが来ない仕組みへと変化してきました。

それはいつしか自らの筆で描いた絵をゲーム内に持ち込み、ゲームを構成しているロジックを考えて導入できるまでとなり
自分が考えたゲームを遊び、さらにその面白さを他人と共有することで、もっと面白さが広がるようになりました。

後半はもう少し未来のことです。

さて、前置きはここまでにして、いよいよ地形生成とブロックのロジックまわりを手掛けていきます。
未来予想図に従うならば、ここを外部から変更できるようにプラグイン機構を用意して、容易にプレイヤーがゲームのロジックを変えられるようにしなければなりません。

実装メモ

Unity でプラグイン機構を作るには C# で DLL を配置するのがベストだと思います。
メリットとして作成時に Visual Studio などの IDE のコード補間機能やコンパイルエラーメッセージなどが確認できて、安全かつ開発者にやさしい点が挙げられます。

スクリプトを配置して実行時にコンパイルして使う方法もあります、こちらのメリットは容易にプラグインの内容がユーザーにも開示される点でしょうか。

参考
プログラムでコンパイルを行う - .NET Tips (VB.NET,C#...)
文字列の計算式の計算結果を取得する - .NET Tips (VB.NET,C#...)

ただ、この件を同僚に相談したところ、プラグイン開発も容易に行えるツールも作れるわけで、最初は簡単な方から手掛けて
後から変更が利くように作っておけばなんでも良いという意見をいただきました。

まったくその通りであります。
ということで、経験もあり容易に実装できるマネージドDLLプラグイン形式で外部からブロックのロジックを提供するようにします。

ここで、地形作成さえもプラグイン化することで、ユーザーに独自の世界を楽しんでもらえるようにしようと思いつきました。

プラグインの作り方などはこちら
docs.unity3d.com

C:\Windows\Microsoft.NET\Framework\v4.0.30319 に csc.exe ファイルがあり、コマンドラインよりプラグインが作れます。
csc.exe を使用したコマンド ラインからのビルド | Microsoft Docs
コンパイラオプションは -help コマンドで確認できます。

さっそく地形生成処理に使ってみましょう。

テストに使ったプラグインコードはこちら

using System;
using UnityEngine;

namespace DLLTest
{

    public class MyUtilities
    {

        public int c;

        public void AddValues(int a, int b)
        {
            c = a + b;
        }

        public static int GenerateRandom(int min, int max)
        {
            System.Random rand = new System.Random();
            return rand.Next(min, max);
        }
    }
}

実際にビルドに使ったコマンドがこちら

C:\Users\simpl>C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /r:"C:\Program Files\Unity2017.3\Editor\Data\Managed\UnityEngine.dll" /target:library /out:"F:\Work\GitHub\SimpleHexWorld\Assets\SimpleHexWorld\Scripts\UnityWorld\Model\External\MyUtilities.dll" F:\Work\GitHub\SimpleHexWorld\Assets\SimpleHexWorld\Scripts\UnityWorld\Model\External\MyUtilities.cs

作られた DLL を Assets 下に配置して、競合する型の警告が出る元の .cs ファイルを削除
次のテストコードを書いて、適当なオブジェクトにアタッチします。

using UnityEngine;
using System.Collections;
using DLLTest;

public class Test : MonoBehaviour
{

    void Start()
    {
        MyUtilities utils = new MyUtilities();
        utils.AddValues(2, 3);
        print("2 + 3 = " + utils.c);
    }

    void Update()
    {
        print(MyUtilities.GenerateRandom(0, 100));
    }
}

問題なく動きました。

が、競合するクラス定義がプロジェクト内に二つ以上あると、片方を削除しなければならないという問題があります。(一応警告が出るだけで実行はできる)
プラグイン側の定義のみ有効にする方法は無いのでしょうか?

。。。ひらめきました。

using System;

#if !USE_PLUGINS
namespace SimpleStar
{
    public class MyUtilities
    {

        public int c;

        public void AddValues(int a, int b)
        {
            c = a + b;
        }

        public static int GenerateRandom(int min, int max)
        {
            System.Random rand = new System.Random();
            return rand.Next(min, max);
        }
    }
}
#endif

USE_PLUGINS を Project Settings の Scripting Define Symbols にてCROSS_PLATFORM_INPUT;USE_PLUGINS のようにすることで
スクリプト側の実装を隠し、プラグイン側の実装のみを有効にすることができます。

USE_PLUGINS を外しておけば、プラグインのコードのデバッグも容易ですし、これは良いことを思いつきました。

続いて、動的に読み込み利用するようにテストコードを修正します。

参考にしたのは次のページ
zecl.hatenablog.com

LINQ という配列を入力に幾重にも条件フィルタリングしながら、時には変換して最終的に配列を返す書式をうまくつかってコードをすっきりまとめています。
素晴らしいですね。問題なく機能しました。

なお、ビルドに際してはプラグインもエディタも Interface を共通の dll から利用する必要があるので、少し工夫が必要です。
私が行ったのはインタフェースを定義するファイルを用意し、プリプロセッサ定義によってエディタでは無効になりつつも、プラグインでは有効になる仕組みを利用しました。

#if !USE_PLUGIN_INTERFACES
namespace SimpleHexWorld
{
    public interface IWorldCreation
    {
        int BlockId(int i);
    }
}
#endif

プラグインのインタフェースDLLが無い時は上記コードを有効にしてテストを進め、安定したある段階でプラグイン化して、プロジェクト側で USE_PLUGIN_INTERFACES を定義してプラグインの方を有効にします。
このファイルからインタフェースがまとまった DLL を作成するコマンドは次の通り

C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /target:library /out:"F:\Work\GitHub\SimpleHexWorld\Assets\SimpleHexWorld\Scripts\UnityWorld\Model\Plugin\PluginInterfaces.dll" F:\Work\GitHub\SimpleHexWorld\Assets\SimpleHexWorld\Scripts\UnityWorld\Model\Plugin\PluginInterfaces.cs

そして、このインタフェースを継承するクラスを定義するプラグインを作成するコードが次の通りで

using System;

#if !USE_PLUGINS
namespace SimpleHexWorld
{
    public class TestPlugin : IWorldCreation
    {
        public int BlockId(int i)
        {
            return i + 2;
        }
    }
}
#endif

このファイルとインタフェースDLLからプラグインを作成するコマンドは次の通り

Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /r:"F:\Work\GitHub\SimpleHexWorld\Assets\SimpleHexWorld\Scripts\UnityWorld\Model\Plugin\PluginInterfaces.dll" /target:library /out:"F:\Work\GitHub\SimpleHexWorld\Plugins\TestPlugin.dll" F:\Work\GitHub\SimpleHexWorld\Assets\SimpleHexWorld\Scripts\UnityWorld\Model\Plugin\TestPlugin.cs

あとは USE_PLUGINS のプリプロセッサをプロジェクト設定で定義して、プラグインを動的読み込みするコードを実行します。

        IWorldCreation _worldCreationPlugin; // @debug
        public void LoadPlugins(string dataPath)
        {
#if USE_PLUGINS
            foreach (IWorldCreation worldCreation in PluginUtil.LoadPlugins<IWorldCreation>(dataPath + @"\..\Plugins\"))
            {
                _worldCreationPlugin = worldCreation;
                break;
            }
#else
            _worldCreationPlugin = new TestPlugin();
#endif
        }

問題なくプラグインは読み込まれ、インタフェースを実行してプラグインの機能を呼び出すことに成功しました。
チャンクのセーブデータが無ければ WorldCreation のプラグインが指定するブロックマテリアルを設定するようにして、次のような結果を得ました。

インタフェースはもう少しチャンクの情報やブロックの情報を渡すように改良する必要がありますが、プラグイン機構がおおむね用意できましたので、次の記事へ進みます。

f:id:simplestar_tech:20180111002630j:plain