はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】外部アプリ(exe)を起動して標準入出力を用いてやり取りをする

はじめに

Unityからexeを起動して、標準入出力によりやり取りを行う処理を書く必要がありました。

f:id:hanaaaaaachiru:20211212182810p:plain
Unityと外部プロセスとの標準入出力を用いたやり取り

その手法について簡単の書き残しておきたいと思います。

exeを準備する

まずは実験を行うにあたってexeを用意します。

using System;

namespace ConsoleApp
{
    static class Program
    {
        private const string ExitLine = "exit";

        private static void Main(string[] args)
        {
            while(true)
            {
                var line = Console.ReadLine();

                if (line is null or ExitLine)
                    break;

                Console.WriteLine(line);
            }
        }
    }
}

標準入力で与えられた文字列をそのまま標準出力するコンソールアプリを作成してみました。

f:id:hanaaaaaachiru:20211212145308p:plain
作成したコンソールアプリケーション

これをUnityに入れて、UnityからExeを起動、標準入出力でやり取りを行い、使い終わったらExeを終了する処理を書いていきたいと思います。

UnityにExeを取り込む

UnityプロジェクトのAsset/以下にStreamingAssetsというフォルダを作成してその中にexeファイルを入れます。

f:id:hanaaaaaachiru:20211212150225p:plain
StreamingAssetsフォルダの中にexeファイルを入れる

なぜStreamingAssetsに入れるのかというと、UnityではStreamingAssetsフォルダは特殊なフォルダになっていて、今回の利用にあたって都合がいいからです。

  • アセットをそのままの状態でアプリに格納される
  • プラットフォームが異なっても、パスを指定するコードを書き換えなくてよい

標準入出力を用いてやり取りする

public class ConsoleClient : MonoBehaviour
{
    // StreamingAssetsフォルダ内なので、プラットフォームが違っても以下のパス指定で大丈夫
    private static readonly string FolderPath = Application.streamingAssetsPath + "/ConsoleApp";
    private static readonly string FilePath = FolderPath + "/ConsoleApp.exe";
    
    private Process _process;

    private void Awake()
    {
        _process = new Process();

        // プロセスを起動するときに使用する値のセットを指定
        _process.StartInfo = new ProcessStartInfo
        {
            FileName = FilePath,                        // 起動するファイルのパスを指定する
            UseShellExecute = false,                    // プロセスの起動にオペレーティング システムのシェルを使用するかどうか(既定値:true)
            WorkingDirectory = FolderPath,              // 開始するプロセスの作業ディレクトリを取得または設定する(既定値:"")
            RedirectStandardInput = true,               // StandardInput から入力を読み取る(既定値:false)
            RedirectStandardOutput = true,              // 出力を StandardOutput に書き込むかどうか(既定値:false)
            CreateNoWindow = true,                      // プロセス用の新しいウィンドウを作成せずにプロセスを起動するかどうか(既定値:false)
        };
        
        // 外部プロセスのStandardOutput ストリームに行を書き込む度に発火されるイベント
        _process.OutputDataReceived += OnStandardOut;
        
        //外部プロセスの終了を検知する
        _process.EnableRaisingEvents = true;
        _process.Exited += DisposeProcess;

        // プロセスを起動する
        _process.Start();
        _process.BeginOutputReadLine();
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Z))
        {
            _process?.StandardInput.WriteLine("Hello, World!");
        }

        if (Input.GetKeyDown(KeyCode.X))
        {
            _process?.StandardInput.WriteLine("Onaka suita.");
        }
    }

    private void OnDestroy()
        => DisposeProcess();

    private static void OnStandardOut(object sender, DataReceivedEventArgs e)
        => UnityEngine.Debug.Log($"外部プロセスの標準出力 : {e.Data}");
    
    private void DisposeProcess(object sender, EventArgs e)
        => DisposeProcess();

    private void DisposeProcess()
    {
        if (_process == null || _process.HasExited) return;
        
        _process.StandardInput.Close();
        _process.CloseMainWindow();
        _process.Dispose();
        _process = null;
    }
}

コードの説明はコメントを見てもらえればと思うのですが、まず???となりやすいのはProcessStartInfoの箇所だと思います。

こちらはプロセス起動のときに用いる設定のことになります。設定できる項目は二十数個ありますので、以下の公式ドキュメントのプロパティの箇所を閲覧してみるとそれぞれの仕様を把握することができます。
docs.microsoft.com

後気を付けなければいけないのは以下の点くらいでしょうか。

  • RedirectStandardInputtrueなら、UseShellExecutefalseでなければならない
  • RedirectStandardOutputtrueなら、UseShellExecutefalseでなければならない
  • Process.StandardInputを利用するなら、RedirectStandardInputtrueでなければならない
  • Process.BeginOutputReadLineを利用するなら、RedirectStandardOutputtrue でなければならない