C# .net、Unityアプリケーション間でMemoryMappedFileを用いてデータを共有する


やりたいこと

弊社の業務では、なんらかのセンサーを用いて情報を読み取り、その状況によって演出を行うといったことがよくあります。
演出をするアプリケーションはUnityで作成するとして、センサーを読み取る部分をどのように実装するかが問題になります。

ネイティブプラグインもしくはライブラリとして実装する手もありますが、センサーの調整や制御が少し面倒になるので、
- アプリケーションをC#.NETのWindowsフォームアプリケーションで作成
- 演出アプリケーションをUnityで作成
というように、2つのアプリケーションで分けて行うことにしました。

次のような恩恵がありました。
- センサー機器の担当者と演出の担当者の作業を分けることが出来る
- センサーの種類が変わっても対応できる

やりかた

MemoryMappedFileを用いて、メモリ内でファイルのようなものを扱い、アプリ間でデータのやりとりを行います。
MSDNのMemoryMappedFileクラスドキュメント
構造体を直接バイナリファイルに書き込んでポインタで参照したり、BinaryFormatterで書き込む手もあると思います。それだとパフォーマンスは間違いなく出ると思いますが怖いので……
今回は安全にxmlにシリアライズしたデータをやりとりします。

仕組みは単純で、センサーアプリ側ではデータを1つのクラスにまとめてxmlの形式にシリアライズし、メモリマップトファイルに書き込みます。演出アプリ側ではそれを読み取り、デシリアライズして元のクラスのインスタンスを復元することが出来ます。
ただし注意点として、2つのアプリでメモリを同時に読み書きしようとすると何らかの不具合が出るのは必至ですので、排他制御を確実に行います。

共通(データ定義)

以下が受け渡しを行うデータの例です。ここではSensorDataクラスがシリアライズするクラスです。
SerializableAttribute属性を付与していますが、実はXmlSerializerではSerializableAttribute属性は不要でした。
念のため残しております。

CensorData.cs
[Serializable]
struct SensorPoint {
    public float x;
    public float y;
}
[Serializable]
class SensorData {
    public float time;
    public List<SensorPoint> points;
}

書き込み側

Censor.cs
class hogeClass{
    private Mutex mutex; // 排他制御に使用
    MemoryMappedFile mmfile = null;
    // Formの作成時などに呼ばれる
    public void Initialize(){ 
        string mutexName = "SensorAppMutex";
        bool createdNew = false;
        mutex = new Mutex(false, mutexName, out createdNew);
    }
    // アプリ終了時に呼ばれる
    public void Finalize(){
        mmfile?.Dispose();
        mutex?.Dispose();
    }

    // 共有するデータを保存したいタイミングで呼ぶ
    public void SaveSensorData(const SensorData data){
        // シリアライザーの作成
        System.Xml.Serialization.XmlSerializer serializer =
            new System.Xml.Serialization.XmlSerializer(typeof(SensorData));

        bool getMutex = false;
        try {
            if (getMutex = mutex.WaitOne(5000)) {
                mmfile?.Dispose();
                mmfile = MemoryMappedFile.CreateNew("SensorAppData", 1024 * 1024 * 1);
                using (MemoryMappedViewStream stream = mmfile.CreateViewStream())
                {
                    serializer.Serialize(stream, data);
                }
            }
        }
        finally{
            if (getMutex) mutex.ReleaseMutex();
        }
    }
}

読み込み側

Unityアプリ側の処理です。
センサーアプリから渡された座標に、オブジェクトを生成しています。

Spawner.cs
// センサーアプリから渡された座標に、オブジェクトを生成する
public class Spawner : MonoBehaviour
{
    Mutex mutex; // 排他制御
    // 下記のSampleObjectコンポーネントを取り付けたゲームオブジェクトのプレハブ
    [SerializeField] GameObject sampleObject; 

    void Start() {
        string mutexName = "SensorAppMutex";
        bool createdNew = false;
        mutex = new Mutex(false, mutexName, out createdNew);
    }
    void OnDestroy() {
        mutex?.Dispose();
    }

    void Update() {
        System.Xml.Serialization.XmlSerializer serializer =
            new System.Xml.Serialization.XmlSerializer(typeof(SensorData));
        // センサーから取得した結果
        SensorData data = null;
        bool getMutex = false;
        try {
            if (getMutex = mutex.WaitOne(3000)) {
                using (MemoryMappedFile mmfile = MemoryMappedFile.OpenExisting("SensorAppData"))
                using (MemoryMappedViewStream stream = mmfile.CreateViewStream()) {
                    result = (SensorData)serializer.Deserialize(stream);
                }
            }
            catch (Exception ex) {
                // なにかまずいことが起こった場合、ログに残す
                Debug.LogError($"exception type: {ex.GetType()} msg: {ex.Message}"); 
                throw ex;
            }
            finally {
                if (getMutex) mutex.ReleaseMutex();
            }
        }

        // データが取得できたので、その位置にオブジェクトを生成する
        if (data != null) {
            foreach (var point in data.points) {
                Vector3 pos = new Vector3(point.x, point.y, 0);
                GameObject obj = Instantiate<GameObject>(samplePrefab, pos, Quaternion.identity, this.transform);
            }
        }
    }
}
SampleObject.cs
// 生成されて一定時間で消えるゲームオブジェクトの例
public class SampleObject : MonoBehaviour
{
    const float lifeTimeMax = 0.5f;
    float lifeTime = lifeTimeMax;

    void Update()
    {
        lifeTime -= Time.deltaTime;
        if (lifeTime < 0)
        {
            Destroy(this.gameObject);
        }
    }
}