littlewing

人間とコンピューターとメディアの接点をデザインするために考えたこと

Unityでビジュアル要素 (ヘッドレス) 無しのCLIアプリを作成する

Unityでアプリケーションを作成する際に、コマンドラインでも操作したい事があると思います。

Server Buildのオプションと、Mono.Options を利用することで、簡単に作成することができたのでその方法をメモします。

サンプルコードも最後にあります。

何に使えるのか?

ツール系アプリケーションをUnityで開発していると

  1. 起動時にコマンドラインでオプションを指定したい
  2. GUI無しでUnityで作成した処理を利用したい。
  3. マルチプレイ用のネットワークサーバとしてUI無しのヘッドレスモードで起動したい.
  4. アプリケーション実行中に標準入力を受け取りたい
  5. UbuntuCLI上で、ゴニョゴニョしたい。

ということがあり、活用できると思います。


CLIアプリ作成の前提知識

1. Server Build オプション

PC, Mac & Linux Standalone のPlatformには BuildSettings にServer Buildというオプションがあります。

f:id:pigshape:20210417212148p:plain

マニュアルは以下のように記述されています。

コマンドラインのオプションを全く使わずに、ビジュアル要素 (ヘッドレス) なしのサーバー用のプレイヤーをビルドします。これを有効にすると、UNITY_SERVER 定義を持つマネージスクリプトがビルドされます。これにより、アプリケーションにサーバー固有のコードを書くことができます。また、stdin と stdout にアクセスできるようにコンソールアプリケーションとして Windows バージョンにビルドすることもできます (Unity ログはデフォルトで stdout に保存されます)。

  • ServerBuildするとコンソールに以下のようなメッセージが表示されます。
NullGfxDevice:
    Version:  NULL 1.0 [1.0]
    Renderer: Null Device
    Vendor:   Unity Technologies
Begin MonoManager ReloadAssembly
- Completed reload, in  0.071 seconds
Microsoft Media Foundation video decoding to texture disabled: graphics device is Null, only Direct3D 11 and Direct3D 12 (only on desktop) are supported.

2. -batchmode オプション

  • Server Build オプションを有効にしなくても、通常ビルドで -batchmod のオプションを付けるとHeadlessで起動することもできます。
  • その場合 標準出力を受け取るには合わせて、 -logFile -のオプションも付与します。
./hoge.exe -batchmode -logFIle -

3. #if UNITY_SERVER ~ #endif

  • Server Build を有効にすると UNITY_SERVER のシンボルが有効になります。
  • これにより、引数や stdin/stdout の利用など、Server Build 時のみ有効なコードを記述する事ができます。
  • ただし、-batchmode を利用した場合は、UNITY_SERVER は有効にはなりません。

4. コマンドライン引数の解析

コマンドライン引数を利用するには入力オプションや値を解析する必要があります。

C# における、コマンドライン引数の解析は、いろいろライブラリがあるのですが、Mono.Options が追加ライブラリ不要で、そのままUnity内で利用できたので、それを今回は利用しています。

導入が容易で実装もシンプルなのが、良いですが正常系以外の処理などは充実していないので、高度なことをしたい場合は、他のライブラリも選定しても良いかもしれません。


Mono.Options について

  • Mono.Optionsに関してはこちらのサイト を見てもらうのがわかりやすいです。
  • UnityのC# でも、 using Mono.Options; を書くことで使うことができます。
  • 以下のサンプルコードにあるように、OptionSetを定義する事で簡単にコマンドラインの解析を行うことができます。

値を取りえるオプションの場合、以下のいずれの方法でも値の指定ができます。

-n value
-n=value
-name value
-name=value
--name value
--name=value
/n value
/n=value
/name value
/name=value

また、スイッチ系のオプションの場合、以下のような指定ができます。

# オンにする
-s
-s+
# オフにする
-s-

実際にコードを書く

  • Unityのシーンに、以下のコードを空のGameObjectにAttachして、ProjectSettingsでServerBuildをONすることで挙動を確認することができます。

  • 具体的には以下の処理を行うことができます。

    1. 起動時にコマンドライン引数を受け取る 例: xx.exe -v 10 —number=200 -x hoge

    2. 起動後の画面で標準入力としてオプションを与えることで、処理を制御することができる。 例 -v HOGE —number=200 -x FUGA

  • gistにもあります。

using Mono.Options;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;

public class StdinReader : MonoBehaviour
{
    private string stringValue = "";
    private int number = 0;
    private bool help = false;

    private OptionSet options;

    void Start()
    {

#if !UNITY_SERVER
        return;//Server Build 以外は無視する
#endif
        //CLIアプリケーションの場合、UIの描画はされないので、
        //処理内容によってはフレームレートを抑えることで
        //CPUの負荷も減らすことができる。
        Application.targetFrameRate = 10;

        options = new OptionSet()
        {
            // string 型の引数を取るオプション
            { "v|value=", "specify a value.", v => stringValue = v },
            // string 型以外の引数を取るオプション
            { "n|number=", "specify a number.", (int v) => number = v },
            // 引数を取らないオプション
            { "h|help", "show help.", v => help = v != null },
            // 引数を持たないメソッドをコール
            { "t|test", "call test method", v => TestMethod()},
            // 引数を持つメソッドをコール
            { "x|xmethod=", "call test method2", v => TestMethod2(v) },
        };

        string[] args = System.Environment.GetCommandLineArgs();
        ParseCommandLineArgs(System.Environment.GetCommandLineArgs());

        Task.Run(() =>
        {
            string line;
            Console.WriteLine("CLI task start");
            while (true)
            {
                if ((line = Console.ReadLine()) != null)
                {
                    args = line.Split(' ');
                    ParseCommandLineArgs(args);
                }
                Task.Delay(100);
            }
        });
    }

    void ParseCommandLineArgs(string[] args)
    {
        List<string> extra;

        try
        {
            extra = options.Parse(args);
            Console.WriteLine($"value={stringValue}");
            Console.WriteLine($"number={number}");

            if (help) options.WriteOptionDescriptions(Console.Out);

        }
        catch (OptionException e)
        {
            Console.WriteLine(e.Message);
        }

        Console.WriteLine("GetLine:" + string.Join(" ", args));
    }

    void TestMethod()
    {
        Console.WriteLine("call TestMethod");
    }

    void TestMethod2(string _v)
    {
        Console.WriteLine("call TestMethod2:" + _v);
    }
}

参考サイト

youtu.be