IBM の developerWorks サイトの記事 Watson×Unity!初心者でもできる、VR 空間で Unity ちゃんとおしゃべりアプリ! を現時点(2018年4月15日)の最新環境(バージョン)で試してみました、ところ… 初っ端から Configuration Editor が無くて困ってしまいました。
その顛末は 試してみる や 続・試してみる にまとめて、書き直したサンプルプログラムを公開しました。
とりあえず落ち着きましたので、そもそもどうしてこうなったのか?どう対応するのが正解なのか?を2回にわけてちゃんと考えたいとおもいます。後編 も公開しました。
消えたConfiguration Editor
元の dW記事 は公開が昨年の9月と古いもので Unity 5.6.2f1 に、Watson Unity-SDK v0.13.0 を使用していて、以下のメニューがあります。
私の環境は Unity 2017.4.0f1 に、Watson Unity-SDK v2.2.1 を使用していて、以下のメニューしかありません。
今回のdW記事でよく使用しているConfiguration Editorが無いので、いきなり最初で詰まってしまいますね。どこに行ってしまったのでしょう?
以前の投稿 にまとめましたが、便利ツールであるConfiguration Editorを使わず、利用するコード側でCredentials管理をするのがv1.0.0以降のお作法であり、正解なのかな?が私の結論です。
でもConfiguration Editorって、実際には何をしてくれていたのでしょうか?そして今後はどう実装したら良いのでしょうか?
Unityエディター拡張から探る
エディター拡張入門 サイトの情報が役に立ちます。学びつつ、探っていきましょう。
第1章 エディター拡張で使用するフォルダー によりますと、Editorフォルダーが大事なようなので、Unity-SDK v0.13.0 のフォルダ内を眺めると… 確かにありますね。
第8章 MenuItem あたりなど参考に、まずは ConfigEditor.cs を眺めてみましょう。192行目にそれっぽいコードがあります。
[MenuItem("Watson/Configuration Editor", false, 0)]
private static void EditConfig()
{
GetWindow<ConfigEditor>().Show();
}
うん、やはり。この ConfigEditor.cs ファイルで定義されている ConfigEditor クラスが、消えたConfiguration Editorの本体のようです。この中身をざっと眺めてみました。
ConfigurationEditorクラスの役割
ConfigurationEditorクラスで GUI を作成する、OnGUI クラスの主要部分のコードを以下にまとめます。222行目あたりから。
まずは上部にあるサービスの一覧表示の部分のGUI作成コード。詳細は削っているので注意。
private void OnGUI()
{
Config cfg = Config.Instance;
GUILayout.Label(m_WatsonIcon); // 左上のWatsonアイコン
if (GUILayout.Button("Register for Watson Services")) // 最初のボタン
Application.OpenURL(BLUEMIX_REGISTRATION);
foreach (var setup in SERVICE_SETUP)
{
Config.CredentialInfo info = cfg.FindCredentials(setup.ServiceID);
bool bValid = GetIsValid(setup);
GUILayout.BeginHorizontal(); // 【サービスの行表示】 開始
// 各サービスのステータスのアイコン表示
if (m_ServiceStatus.ContainsKey(setup.ServiceID))
{
if (m_ServiceStatus[setup.ServiceID])
GUILayout.Label(m_StatusUp, GUILayout.Width(20));
else
GUILayout.Label(m_StatusDown, GUILayout.Width(20));
}
else
GUILayout.Label(m_StatusUnknown, GUILayout.Width(20));
// 各サービスの説明テキスト
GUILayout.Label(
string.Format(
"Service {0} {1}.",
setup.ServiceName,
bValid ? "CONFIGURED" : "NOT CONFIGURED"
),
labelStyle
);
// 各サービスのアクションボタン
if (GUILayout.Button("Configure", GUILayout.Width(100)))
Application.OpenURL(setup.URL);
if (bValid && GUILayout.Button("Clear", GUILayout.Width(100)))
cfg.Credentials.Remove(info);
GUILayout.EndHorizontal(); // 【サービスの行表示】 終了
}
各サービスのCredential情報、つまり「資格情報」を読み込んでいます。ポイントとしては
- info = cfg.FindCredentials(setup.ServiceID) で資格情報を読み込む
- bValid = GetIsValid(setup) でサービスがきちんと設定されているか確認する
あたりで、ここらへんを追っていけば資格情報の格納先がわかりそうです。
ちなみに、ステータスのアイコン表示に使用されているリソースは以下です。
以下、引き続き OnGUI 関数の後半をみていきます。
GUILayout.Label("PASTE CREDENTIALS BELOW:");
// Credential情報を貼り付けるテキストエリア
m_PastedCredentials = EditorGUILayout.TextArea(m_PastedCredentials);
// Credential情報を反映するApplyボタン
GUI.SetNextControlName("Apply");
if (GUILayout.Button("Apply Credentials"))
{
Config.CredentialInfo newInfo = new Config.CredentialInfo();
if (newInfo.ParseJSON(m_PastedCredentials))
{
foreach (var setup in SERVICE_SETUP)
{
if (newInfo.m_URL.EndsWith(setup.ServiceAPI))
{
newInfo.m_ServiceID = setup.ServiceID;
// 過去にCredential情報があれば削除する処理
cfg.Credentials.Add(newInfo);
}
}
}
// 成功もしくは失敗のダイアログを表示
SaveConfig();
}
}
こちらは「資格情報」の書き込みの処理ですが、重要なのはたぶん以下の3ポイントだと思われます。
- newInfo = new Config.CredentialInfo() で新しい資格情報を生成
- newInfo.ParseJSON 関数でJSON形式の資格情報を取り込み
- cfg.Credentials.Add(newInfo) で新しい資格情報を登録
資格情報に関しては、以下のオブジェクトが担っているのがわかります。
Config cfg = Config.Instance;
このオブジェクトについて、もう少し見ていきましょう。
Configクラスから探る
Config.cs ですが、Editor ではなく Utilities フォルダの中にありました。
まず読み出しのための FindCredentials関数を探してみると、217行目にあります。
public CredentialInfo FindCredentials(string serviceID)
{
foreach (var info in m_Credentials)
if (info.m_ServiceID == serviceID)
return info;
return null;
}
うん、単に探しているだけの関数ですね。そして肝心の資格情報の格納場所である m_Credentials は以下で設定されていました。
[fsProperty]
private List<CredentialInfo> m_Credentials = new List<CredentialInfo>();
public List<CredentialInfo> Credentials {
get { return m_Credentials; }
set { m_Credentials = value; }
}
うん、m_Credentials は単なるCredentialInfoのリストのようですね。すると上にある[fsProperty]属性が重要な気がします。
で、この属性がどこで定義してあるのかわからず悩んだのですが、結論から言ってこれ FullSerializer という独立したライブラリでした。コードは ThirdParty フォルダの中にあります。
というわけで、ここでわかったのは
- Config オブジェクトに資格情報が保存されている
- Config オブジェクトはFullSerializerというライブラリを用いてSerialize可能にされている
ですね。Configクラスはデータを抽象化したクラス(モデル)であり、アプリケーションに依存したロジックは無いことがわかります。
Serializeされた情報がどこに保存されるのか、Configクラスを眺めただけではわからない、ということになります。残念ですが、ひとつ前に戻って探し直しましょう。
再びConfigurationEditorクラス
そういえばConfiguration Editorの下のほうに「Save」というボタンがありますね。さきほどの OnGUI関数の最後のほう、その部分のコードを見てみましょう。
if (GUILayout.Button("Save"))
SaveConfig();
おや?この SaveConfig って先ほどもありましたね。めっちゃ怪しい名前じゃないですか?そのコードを見てみると…
private static void SaveConfig()
{
if (!Directory.Exists(Application.streamingAssetsPath))
Directory.CreateDirectory(Application.streamingAssetsPath);
File.WriteAllText(
Application.streamingAssetsPath + "/Config.json",
Config.Instance.SaveConfig()
);
RESTConnector.FlushConnectors();
}
なるほど File.WriteAllText関数がありますので、思いっきりテキスト情報をファイルに書き込んでいますね。ココでしたか!そして使用されている streamingAssetsPath なんですが、Unityマニュアルにある ストリーミングアセット ですよね。嫌な予感がします…
Unity プロジェクトにおける StreamingAssets と呼ばれるフォルダーに配置したファイルはビルド先のプラットフォームの、特定のフォルダーにそのまま何も変換されない状態で保持されます。
ノーガード戦法キター!新しいバージョンでサポートされていないハズですよ、平文でIDやパスワード保持しちゃってた、ってことですよね?
第3章 データの保存 にある 3.2 EditorUserSettings.Set/GetConfigValue であれば、まだ暗号化されたものを…
SpeechToText APIから逆に探る
なんとなく見えてきた気がしますが… 念のため逆から、信頼情報を利用するWatson API側も見ておきましょう。
試してみる で最初に確認した SpeechToText API ですが、元のサンプルコードは
private SpeechToText m_SpeechToText = new SpeechToText();
と引数無しでSpeechToTextインスタンスを生成していたわけです。ということは、その内部でConfig Editorが保存した「信頼情報」を参照しているハズ! Services/SpeechToText フォルダにある SpeechToText.cs ファイルを見てコードを確認してみましょう。
以下、音声認識を実行する関数の先頭部分です。
public bool Recognize(AudioClip clip, OnRecognize callback)
{
if (clip == null)
throw new ArgumentNullException("clip");
if (callback == null)
throw new ArgumentNullException("callback");
RESTConnector connector = RESTConnector.GetConnector(SERVICE_ID, "/v1/recognize");
if (connector == null)
return false;
RESTConnector.GetConnector のところが怪しい感じです。v2.2.1のほうは同じ部分が以下のようになっており、渡した Credentials をちゃんと利用しています。
RESTConnector connector = RESTConnector.GetConnector(Credentials, "/v1/recognize");
if (connector == null)
return false;
やはりRESTConnectorが怪しい!というわけで、Connection フォルダにある RESTConnector.cs コードを追ってみましょう。
public static RESTConnector GetConnector(string serviceID, string function, bool useCache = true)
{
RESTConnector connector = null;
string connectorID = serviceID + function;
if (useCache && sm_Connectors.TryGetValue(connectorID, out connector))
return connector;
Config cfg = Config.Instance;
Config.CredentialInfo cred = cfg.FindCredentials(serviceID);
if (cred == null)
{
Log.Error("Config", "Failed to find credentials for service {0}.", serviceID);
return null;
}
RESTConnectorクラスのGetConnectorメソッドの中にありましたね、FindCredentials(serviceID) と信頼情報をゲットしている部分が。こんな下のサービスレベルで信頼情報をやりとりしていたとは… で、この信頼情報を格納した Config オブジェクトは Instance メソッドにより
public static Config Instance {
get { return Singleton<Config>.Instance; }
}
とシングルトンで得られるわけで。なるほどねぇ。そしてConfigクラスのコンストラクターが、例のConfig.jsonファイルを読みだしていることが確認できました。
public Config()
{
LoadConfig();
}
public void LoadConfig()
{
if (!Directory.Exists(Application.streamingAssetsPath))
Directory.CreateDirectory(Application.streamingAssetsPath);
LoadConfig(System.IO.File.ReadAllText(Application.streamingAssetsPath + Constants.Path.CONFIG_FILE));
}
public bool LoadConfig(string json)
{
// 実際の処理は省略
}
いやぁ、なかなか長い確認の旅でしたw
まとめ
Watson Unity-SDK にあったConfiguration Editorですが、信頼情報をConfiguration Editorに入力すると、内部でConfigオブジェクトに情報をまとめ、UnityのストリーミングアセットとしてConfig.jsonというファイルに保存してくれていた。
SpeechToTextなどのWatson APIのほう、サービス利用側は信頼情報など気にせずサービス用のオブジェクトを生成する。すると内部でRESTConnectorクラスがConfigオブジェクトを呼び出し、サービスの種類に応じた信頼情報を自動的に利用してくれていた。
以上、非常に便利な仕組みだが、たぶん欠点が2つあって…
- Unityのストリーミングアセットに(たぶん)平文としてIDやパスワードが保存されている
- サービスの種類から信頼情報を自動的にセットすることしかできない(明示的に指定できない)ので、サービスごとに1つの設定しか利用できない
という流れで、この仕組みはお蔵入りしたのではないか?と想像します。
というわけで
Watson Unity-SDK にあったConfiguration Editorですが、ストリーミングアセット に Config.json という大胆な名前でシリアライズ化した信頼情報を保存していた、というなかなか豪快な事実がわかりました。
正式っぽい v1.0.0 以降から Configuration Editor が消えた理由が、なんとなく想像できる気がしますね… そしてこのままの復活は難しいであろうことが予想できます。
あたらしい Unity-SDK がそのあたりを明確にガイドしてくれていれば良いのですが、いまのところは無さそうです。自分たちで考えなければいけないかもしれません。
というわけで対応策を考えたいのですが… 長くなりましたし、いったん落ち着きましたので、本日はこれで。もうすぐ GW なので、じっくり考えてまとめたいとおもいます!
GWにまとめました> 後編
ではでは。