【Unity】エディターで再生中に外部ツールで変更したファイルを自動で反映させる覚書【FileSystemWatcher】
はじめに
Unity を利用していくと外部ツールや json ファイルなどで出力したファイルを読み込みゲームに反映したいことが多々あります。
ScriptableObject や MonoBehaviour で [Serializable] 設定した メンバ をHierarchy で変更すればいいやん。と思われるでしょう。Unity的にはそれが正解ですが、コンバータなどで一括変換して Unity に読み込ませたいこともあります。
ファイルの状態を監視する FileSystemWatcher
System.IO.FileSystemWatcher はファイルの状態を監視して 変更された時に通知が来るよう設定ができるクラスです。
例
エディットでプレイ中に
Assets/StreamingAssets/config.json ファイルが手動・外部ツールでコンバートされた場合に リアルタイムで反映させるサンプルコードです。
using System;
using System.IO;
using System.Collections.Generic;
using UnityEngine;
public class TestClass : MonoBehaviour
{
private static System.IO.FileSystemWatcher m_FileSystemWatcher = null;
private struct Config
{
public int m_Level;
} // struct Config
private static List<Config> m_ConfigChangedList = new List<Config>();
//----------------------------------------------------------------------------
private void Awake()
{
// FileSystemWatcher を作成
m_FileSystemWatcher = new System.IO.FileSystemWatcher();
// 更新日時が変更になったら知らせる設定
m_FileSystemWatcher.NotifyFilter = System.IO.NotifyFilters.LastWrite;
// 監視するディレクトリ
m_FileSystemWatcher.Path = "Assets/StreamingAssets/";
// 監視するファイル
m_FileSystemWatcher.Filter = "config.json";
// 変更時に呼ばれるデリゲート
m_FileSystemWatcher.Changed += new System.IO.FileSystemEventHandler( ( source, e) =>
{
var pash = "Assets/StreamingAssets/config.json";
string json_text = "";
// ここが呼ばれるのは別スレッドかもしれないので、呼べる関数は制限される
using( var fileStream = new FileStream( pash, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using( var reader = new StreamReader( fileStream, System.Text.Encoding.UTF8))
{
json_text = reader.ReadToEnd();
}
}
var newConfig = UnityEngine.JsonUtility.FromJson<Config>( json_text);
m_ConfigChangedList.Add( newConfig);
});
// 監視開始
m_FileSystemWatcher.EnableRaisingEvents = true;
}
//----------------------------------------------------------------------------
public void OnDestroy()
{
// 一応後始末
if( m_FileSystemWatcher != null)
{
m_FileSystemWatcher.EnableRaisingEvents = false;
m_FileSystemWatcher = null;
}
m_ConfigChangedList = null;
}
//----------------------------------------------------------------------------
private void Update()
{
foreach( var config in m_ConfigChangedList)
{
// config を利用して何かしらの設定を反映する
hoge.Level = config.m_Level;
}
m_ConfigChangedList.Clear();
}
} // class TestClass
解説
FileSystemWatcher の初期化
まず FileSystemWatcher をインスタンスし監視する対象の設定をします。
NotifyFilter には監視する方法を設定します。
- Attributes ファイルまたはフォルダーの属性。
- CreationTime ファイルまたはフォルダーが作成された時刻。
- DirectoryName ディレクトリの名前。
- FileName ファイルの名前です。
- LastAccess ファイルまたはフォルダーを最後に開いた日付。
- LastWrite ファイルまたはフォルダーへの最終書き込み日付。
- Security ファイルまたはフォルダーのセキュリティ設定。
- Size ファイルまたはフォルダーのサイズ。
Path は 監視するディレクトリのパス
“Assets/StreamingAssets” だとデータが Raw を保つのでとりあえずここにしてみた。
Filter にはアスタリスク も設定でき “*.json” など複数のファイルを監視対象にすることもできます。
// FileSystemWatcher を作成
m_FileSystemWatcher = new System.IO.FileSystemWatcher();
// 更新日時が変更になったら知らせる設定
m_FileSystemWatcher.NotifyFilter = System.IO.NotifyFilters.LastWrite;
// 監視するディレクトリ
m_FileSystemWatcher.Path = "Assets/StreamingAssets/";
// 監視するファイル
m_FileSystemWatcher.Filter = "config.json";
更新時のイベントを登録
Changed には変更があった場合に呼ばれるイベントを登録します
FileSystemEventHandler でデリゲートを登録します。今回はラムダ式で登録してます。
// 変更時に呼ばれるデリゲート
m_FileSystemWatcher.Changed += new System.IO.FileSystemEventHandler( ( source, e) =>
{
var pash = "Assets/StreamingAssets/config.json";
string json_text = "";
// ここが呼ばれるのは別スレッドかもしれないので、呼べる関数は制限される
using( var fileStream = new FileStream( pash, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using( var reader = new StreamReader( fileStream, System.Text.Encoding.UTF8))
{
json_text = reader.ReadToEnd();
}
}
var newConfig = UnityEngine.JsonUtility.FromJson<Config>( json_text);
m_ConfigChangedList.Add( newConfig);
});
呼ばれるタイミングが良くわからなかったのですが、MainThread 以外から呼ばれた場合は UnityEngine での呼ばえう関数は限られるので、一旦リストに登録するようにしてます。
監視開始
設定の最後に EnableRaisingEvents を有効にすることで監視開始します。
m_FileSystemWatcher.EnableRaisingEvents = true;
Unity 側での更新対応
後は Unity の MainThread での Update() など安全な場所で リストを見てなにか入っていたら対応していきます。対応後はリストをクリアしてます。
foreach( var config in m_ConfigChangedList)
{
// config を利用して何かしらの設定を反映する
hoge.Level = config.m_Level;
}
m_ConfigChangedList.Clear();
後始末
一応後始末しておきます。
OnDestroy() で良いのかちょっと不安 OnDisable() の方が良いかも
// 一応後始末
if( m_FileSystemWatcher != null)
{
m_FileSystemWatcher.EnableRaisingEvents = false;
m_FileSystemWatcher = null;
}
最後に
これでリアルタイムで外部ファイルの更新を 実行中のゲームに反映させれました。ゲームのレベルデザインなので、ちょくちょくデータベースをを書き換えてキャラの調整などする場合に効果的かと思います。
以前見た現場で エクセル書き換えて コンバート後に Unity エディッタで プレイし、また止めて、エクセルで・・・の繰り返しを見て超非効率な状況を目の当たりにして、少し考えてみました。