凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity から Node.js を起動時に裏で実行・通信して諸々の処理を肩代わりしてもらう方法考えてみた

f:id:hecomi:20140415010624p:plain

はじめに

Unity C# の世界で完結して色々と実行してくれるのはライブラリの利用者側から見るととても楽ですが、プロトタイプをそれで作ろうとすると結構大変です。そこで、Processing なり oF なり自分の慣れ親しんだ環境で作成したものを別途動かし、そこから OSC なりなんなりで通信して Unity の世界に反映させる、ということが良くやられていると思います。しかしながら、これを配布して別の人に使ってもらおうと考えると、別途色々と動かしてもらわないとならず大変です。

そこで、ここをうまい具合にやる方法を思いついたのでご紹介します。

アイディア

アイディアとしては、何かしらのコマンドStreamingAssets 下に置いたバイナリSystem.Diagnostics.Process から実行するというシンプルなものです。先行事例だと、Unity でリアルタイムにリップシンクする MMD4Mecanim LipSync Plugin を作ってみた - 凹みTips では、Open JTalk を実行して Wave ファイルを StreamingAssets 内のテンポラリディレクトリに生成、それを読み込んで 3D サウンドとして Unity の世界で再生、再生後 rm コマンドで消す、ということをやっています。これを応用して、Awake 時に Node.js でサーバを立ち上げ、適宜 OSC WebSocket 等でやり取りをし、OnApplicationQuit で終了する、みたいなことをやれば、裏で自動的にサーバの立ち上げと立ち下げをやってくれる形になります。ここを詳しくご紹介したいと思います。

ダウンロード

今回の実験用のプロジェクトは以下で公開しています(Mac 用、Windows もバイナリ入れ替えれば動くと思います)。

そのうち機能拡充して Unity 側と Node.js 側併せてライブラリ化します。

原理実験

原理実験をしてみます。まず、StreamingAssets 直下に Node.js のバイナリ(私は nodebrew でビルドしたものを利用)を配置し、以下の様なコードを書きます。

NodeJS.cs
using UnityEngine;
using System.Collections;

public class NodeJS : MonoBehaviour
{
    private static readonly string NodeBinPath =
        System.IO.Path.Combine(Application.streamingAssetsPath, "node");

    private System.Diagnostics.Process process_;
    public string scriptPath = "";

    public bool isRunning { get; set; }

    void Awake()
    {
        isRunning = false;
        Run();
    }

    void OnApplicationQuit()
    {
        if (process_ != null && !process_.HasExited) {
            process_.Kill();
            process_.Dispose();
        }
    }

    void Run()
    {
        process_ = new System.Diagnostics.Process();
        process_.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
        process_.StartInfo.FileName    = NodeBinPath;
        process_.StartInfo.Arguments   = scriptPath;
        process_.EnableRaisingEvents   = true;
        process_.Exited += OnExit;
        process_.Start();
        isRunning = true;
    }

    void OnExit(object sender, System.EventArgs e)
    {
        isRunning = false;
        if (process_.ExitCode != 0) {
            Debug.LogError("Error! Exit Code: " + process_.ExitCode);
        }
    }
}

このスクリプトを適当な GameObject にアタッチし、Script Path で適当な js をフルパスで指定してあげます。例えば以下の様なものを書いてみます。

require('http').createServer(function(req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello, world!');
}).listen(8080);

f:id:hecomi:20140414214916p:plain

そしてゲームを実行し、ブラウザから http://localhost:8080/ にアクセスしてみます。

f:id:hecomi:20140414215107p:plain

無事表示されました。ゲームを終了して再度アクセスすると Not Found になり、サーバが立ち下がっているのが分かります。

通信をしてみる

OSC で通信することにしてみます。まず JS 側の用意をします。Unity では JS は Unity の JS としてコンパイルされてしまうので、Assets 以下の普通のディレクトリに置くことは出来ません。そこで、コンパイルされない WebPlayerTemplates というディレクトリを用意すると便利ですが、こちらはビルドすると見えなくなってしまいます。そこで StreamingAssets 以下に "." から始まるディレクトを用意し、ここに一連のファイルを置くとビルド時にも含まれ且つコンパイルされない形になります。

$ cd PATH_TO_YOUR_PROJECT/Assets/StreamingAssets/
$ mkdir .node
$ cd .node
$ npm install node-osc
$ vim osc.js

そして以下の様なスクリプトを書いてみます。

osc.js
var util   = require('util');
var osc    = require('node-osc');
var client = new osc.Client('127.0.0.1', 6666);
var cnt    = 0;
var freq   = 60;

setInterval(function() {
	var x = Math.sin(cnt / freq * Math.PI);
	var y = Math.cos(cnt / freq * Math.PI);
	var z = 0;
	var pos = util.format('%s,%s,%s', x, y, z);
	client.send('NodeJs/Test', pos);
	++cnt;
}, 1000/60);

60 fps で回転する座標を送信する形になっています。

次に受け手の Unity 側の用意の用意をします。まずは、.node を参照するように先ほど作成した NodeJs.cs を書き換えます。

...
public class NodeJs : MonoBehaviour
{
    ...
    private static readonly string NodeScriptPath =
        System.IO.Path.Combine(Application.streamingAssetsPath, ".node/");
    ...    
    void Run()
    {
        ...
        process_.StartInfo.Arguments   = NodeScriptPath + scriptPath;
        ...
    }
    ...
}

そして、Unity 側で OSC を受ける必要があります。これには keijiro さんの unity-osc が軽量で使いやすいためお借りすることにします。


サーバ本体コードは以下のものをお借りします。


こちらのコードでは、"Hoge/Fuga" というアドレスで指定された OSC のメッセージを "Hoge_Fuga" ゲームオブジェクトへ OnOscMessage を SendMessage で伝える形になっています。そこで "NodeJS_Test" という適当な名前をつけた GameObject を作成して、以下のスクリプトをアタッチします。

NodeJsTest.cs

using UnityEngine;
using System.Collections;

public class NodeJsTest : MonoBehaviour
{
    void OnOscMessage(object msg)
    {
        var p = msg.ToString().Split(
            new string[]{","}, System.StringSplitOptions.None);
        transform.position = new Vector3(
            float.Parse(p[0]), float.Parse(p[1]), float.Parse(p[2]));
    }
}

OSC で送られてきたメッセージを元に座標を書き換えています。これで Script Path を osc.js に設定して実行すると以下のようになります。



おわりに

Twitter クライアントとか簡単に作れると思うので、やってみたいと思います。また、WebSocket 使うとブラウザとの連携も簡単にできるので、socket.io を裏で動かして Unity とブラウザでやりとりをする的なネタは別途エントリ書きます。