読者です 読者をやめる 読者になる 読者になる

凹みTips

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

Unity 5 x WebGL について詳しく調べてみた

Unity HTML5 C# Emscripten JavaScript Node.js

f:id:hecomi:20141207233422p:plain

はじめに

本エントリは Unity Advent Calendar 2014 8日目の記事になります。

Unity 5 からは Build ターゲットに WebGL が追加されます。Unity 5 プリオーダ向けベータ版で現在試すことが出来ます。

今年の 3/18 に行われた GDC2014 で Unity 5 が発表されたタイミングで WebGL 対応が発表されました。日本でも 4/7、8 で行われた Unite 2014 においても WebGL についての講演があり、その動画や講演資料を公式サイトから閲覧することが出来ます。

これまでも様々なプラットフォーム対応を実現してきた Unity ですが、このリリースによって主要な Web ブラウザが動く環境ではプラグイン無しで Unity で作成したゲームで遊べるようになります。現状では ealy-access な形で部分的な対応(とはいっても大体動く)ですが、5.x のリリースサイクルで WebGL のフルサポートを目指しているようです。

本エントリでは、そんな Unity x WebGL でやってくる世界について、公式で紹介されているデモや自分で簡単に作ってみたデモを始め、仕組みやパフォーマンス、ビルド方法や現状の機能制限、ブラウザの JavaScript との連携、ビルド後の容量削減手法、その他周辺技術などを交えてご紹介します。

環境

WebGL ビルドを Web で体験してみる

Unity から公式で提供されているサンプルがいくつか上がっています。

デモ

また、自分でも簡単にビルドして公開することが出来ます。下記は WebGL ビルドしたものを Node.js で立てた Socket.IO サーバで複数のブラウザのシーンを同期しながら 3D チャットが出来るデモになります。

  • Unity WebGL Player | WebGLChat
    • WASD で移動、QE で回転、Space でジャンプです。名前やメッセージはボタン押下後のプロンプトで入力できます。
    • 誰も人がいなかったら動画のように 2 枚のブラウザで開いて見てみて下さい

Chrome では現状キー操作などが上手く動かないところもあるので、実行には Firefox を利用するのをオススメします。

後述の「実践例」にて仕組みについては詳しく解説します。

<追記: 2014/12/24>

GitHub にサンプルプロジェクトを上げました:

仕組み

概要については以下の公式ブログに詳しく書いてありますので、ここから得られる情報を元にざっと紹介します。

Unity を Web で動かすために、Unity のランタイムコードとユーザが記述したコード、この 2 つをどうにかして Web で動くコードに変換しなければなりません。

Unity のランタイムについて

まず下回りの C/C++ で記述された Unity のランタイムについては、コンパイラフロントエンドの Clang を通じて LLVM-IRコンパイラ基盤 LLVM の中間コード(ビットコード))へと変換、そして LLVM-IR を JavaScript に変換することが出来る Emscripten を使って Web 上で動くコードにコンパイルされます。

f:id:hecomi:20141207235025p:plain

このように C++JavaScript へと変換することは Emscripten が登場して以来様々なプロジェクトに対して行われていて、Vimgnuplot など各種ツールから RubyPerl といった各言語、Qt などのアプリケーションフレームワークに至るまで様々なものが変換されています。

Emscripten から吐き出される JavaScript asm.js という JavaScript のサブセットの形式(= asm.js に対応してないブラウザでも普通に動く形式)になっており、ロジックに影響しない形で様々な型アノーテーションをコードに付与して静的型付することで、JIT(Just-In-Time、実行時)コンパイル時に最適化することが可能になり、動的型付言語である JavaScript の速度的な弱点を改善しています。具体的に言うと a|0int 型の、+adouble 型などとして表現されます。

現在、asm.js が実装されたブラウザは Firefox のみです(Chrome が対応する?みたいな噂もあります)が、前述のように asm.js は JavaScript のサブセットとして記述されているので、実行速度は劣りますが Chrome でも問題なく動きます。

ユーザコードについて

一方、ゲーム中で利用されるロジック等のコードは C# や UnityScript で記述されています。通常はこれら C# や UnityScript のコードは CIL(Common Intermediate Language、.NET の共通中間言語)へとコンパイルされ、Mono によってこの CIL が JIT コンパイルされネイティブコードとなり実行されます(iPhone などではセキュリティの制約上 AOT(Ahead-Of-Time、事前)コンパイルされています)。Mono が中間言語を解釈して各プラットフォーム用のネイティブコードを吐き出してくれる形式をとることで、Windows でも Mac でも Linux でもその他諸々の環境でも動く仕組みになっているわけです。

f:id:hecomi:20141207234926p:plain

ではこの C# / UnityScript で記述されたコードを JavaScript へと変換するためにどうしたか気になると思いますが、ここが凄いところで、Unity では色々な方法を模索した結果、驚くべきことに IL2CPP という C# / UnityScript のコード(正確には CIL)を C++ のコードに変換する独自のツールを開発することで解決したようです。これは 2 年前から実験的に進めていたと書かれています。

そして WebGL にビルドするためには、先程と同様に Clang / Emscripten によって得られた C++JavaScript へと変換するわけです。まとめると、以下の様な手順を行っているようです。

f:id:hecomi:20141208000232p:plain

このそれぞれのフェーズでどのようなコードが吐き出されているかについては、Unite 2014 での資料に書いてあり、とても面白いので是非ご覧ください。

IL2CPP の恩恵は WebGL 化だけにとどまらず、出力が C++ になることで静的解析や最適化によるパフォーマンス向上が可能となったり、大量にある各プラットフォームのアーキ依存の機械語と Mono のバインディングC++ に置き換えられることで大幅に軽減され、移植性やメンテナンス性も向上する見込みです(詳細は公式ブログ参照)。また、最近の iOS 64bit 対応のためにも IL2CPP の技術は使われています。

ある言語を C++ へと変化し高速化する、という話は FacebookHipHop が思い浮かびますが、あちらは色々な理由から JIT コンパイルに方針転換するなどしたりと、分野的には色々考察しがいがありそうなところですが、残念ながら私にあまり知見がないので、ここらのより具体的な話はどなたかが解説してくれることを期待しています。

パフォーマンス

動作速度

公式のブログのベンチマーク結果を見てみます。

f:id:hecomi:20141201180326p:plain

f:id:hecomi:20141201180336p:plain

私も手持ちの MacBook Pro Retina 13 Late 2013(Core i7 2.8GHz)と Alienware 17(Core i7 4910MQ、GTX 880M)の Chrome で動かしてみたところ、それぞれ 25694、89835 でした。以下、公式の考察を抜粋してみます。

  • asm.js が効く FirefoxChrome / Safari より速い
  • GPU が支配的な状況では WebGLネイティブと同等のパフォーマンスを発揮
  • ブラウザがネイティブより速い所があるが、これは IL2CPP の最適化のおかげ?
  • 3D の物理演算などではネイティブが圧倒的に勝るが、これはマルチスレッドや SIMD によるもの

GPU が支配的な表現はおおよそサクサク動き、CPU を使うロジックに関しても asm.js によりネイティブと比較しても遜色ないパフォーマンスが出る、ただしマルチスレッド系や SIMD など、シングルスレッドベースの JavaScript だと厳しいところで大きな差が出る、という感じです。マルチスレッド化や SIMD によって高速化する試みについては未だ入っていないが対応していきたいとのことです。ここあたりは Emscripten や Web やブラウザそのものと密接に結びついているので、業界全体を見ながら今後のアップデートに期待です。

ビルド方法

前置きが長くなりましたが、具体的に動作する WebGL アプリを出力する方法を紹介します。

ビルド

ビルドは簡単で Build Settings の Platform から WebGL (Preview) を選択してビルドするだけです。

f:id:hecomi:20141201171853p:plain

C++ から JS にコンパイルしていると思われる「Native Compile to JS」がとても長いです...。数分は待ちます。ビルドが完了すると以下の様なファイル構成でディレクトリが出力されます。

f:id:hecomi:20141201172157p:plain

<出力名>.js がメインの JS になり、その他の js は予め Unity で定義された便利関数(プログレスバーやロード、後述する Unity の世界で定義した関数とのやり取り等)などが記述されています。後はリソースが含まれている形になります(具体的に .data や .mem が何を格納しているかについては未確認です)。

ブラウザのセキュリティ的にローカルのままでは実行できないので、Apache や nginx などで適当なサーバを立てるかどこかにアップロードして確認して下さい。index.html を開くと以下の様な画面が表示されます。

f:id:hecomi:20141201173203p:plain

余談ですが、こういった小さなシーンでも、初回ロードはかなり時間がかかるので気長に待つ必要があります。この体感向上のために、ランタイム部分を CDN で提供したり FileSystem API の利用、AssetBundle のような仕組みの導入、非同期ストリーミングによるロードなど色々と課題がありそうです。後述しますが、gzip による転送量の削減はされていて、これによりかなりのネットワーク転送量の削減がされています。

WebPlayer が WebPlayerTemplates ディレクトリによって見た目を制御できるのに対し事前に WebGL がテンプレートをいじれるかどうかは不明です。勿論書きだした後はいくらでも修正可能です。

Optimization Level

書き出し時に、Optimization Level というプルダウンメニューがあり、3つのオプションが用意されています。

f:id:hecomi:20141201185530p:plain

  • Slow (fast builds)
  • Fast
  • Fastest (very slow builds)

ドキュメントがなかったので試した感触ですが、その名の通り Slow ではビルドが早い分、実行速度が遅いです。Fastest では実行が早い分、ビルドが激遅です。また出力サイズも Fastest、Fast、Slow の順で大きくなっていきます。容量についての詳細は後述します。

ビルドのカスタマイズ

「Edit > Project Settings > Player」に HTML5 のタブが追加されています。

f:id:hecomi:20141206012223p:plain

ここで WebGL のサイズなどの設定が出来ます。例えば Run In Background などはデスクトップアプリ等と同じようにチェックがオフの時はフォーカスが外れると(例えばブラウザのインスペクタにフォーカスがあたっている時)実行が止まり、オンだとフォーカスが当たっていなくても実行されるようになります。

その他の設定についてはまだ情報が少ないこともあり、あまり試せていません。

機能制限

ブラウザ上で動かすため、デスクトップアプリとして動いている全ての機能を使えるわけではありません。また、Preview 版であることから、本来 Web では出来るけれど現状では未対応なものもあります。

以下、現状出来ないもの一覧と何で出来ないかの考察などになります。

  • Runtime generation of Substance textures
  • MovieTextures
    • 動画テクスチャ(MovieTexture クラス)
  • Networking other then WWW class (a WebSockets plug-in is available)
    • WWW クラス以外のネットワーク機能
    • クロスドメイン制約の縛り有ると思うけど試してないです...
  • Support for WebCam and Microphone access
    • ウェブカメラやマイクへのアクセス
    • getUserMedia でアクセスできるけれど実装面倒そう
  • Hardware cursor support
    • ハードウェアカーソルのサポート
  • Most of the non-basic audio features
    • 多くの非標準オーディオ機能
    • なんだろう?
  • Script debugging
  • Threads
    • スレッド
    • WebWorker を使うとスレッド間でメモリの共有ができないので実装つらそうです
  • Any .NET features requiring dynamic code generation
    • 動的なコード生成を伴う .NET 機能
    • System.Reflection.Emit とか CSharpCodeProvider とか?

後はここには書かれていませんが、ローカルのファイルのアクセスやネイティブ DLL の利用などは WebPlayer 同様出来ません。

JavaScript と Unity 内で定義したコードの連携

Unity から JavaScript のコードを呼び出し

Unity で作成した世界と外の世界とのやり取りについては、Unite 2014 の資料に以下のように書いてあります。

  • You can just write a plugin directly in JavaScript and call functions from that from your C# scripts.
  • You can also just add C++ source files to your project, and call functions from them from your game scripts.
  • You can use Application.ExternalCall or Application.ExternalEval, like you would do in the Web Player.

2 番目については資料が見つからなかったため、ここでは、もっとも簡単な 3 番目(1 番目も含む)をやってみます。

Application.ExternalCall() は第 1 引数で指定された JavaScript の関数を第 2 引数以降を引数として呼び出す関数です。Application.ExternalEval() はその名の通り JavaScript 側で eval() するコードです。WebPlayer の頃に使われていたコードが WebGL 書き出しに関しても同じように使えるわけですね。簡単なテストをしてみます。

using UnityEngine;
using System.Collections;

public class ExternalCallTest : MonoBehaviour 
{
    void Update() 
    {
        if (Input.GetKeyDown(KeyCode.C)) {
            Application.ExternalCall("console.log", "hogehoge", 100, 123.456f);
        }
    
        if (Input.GetKeyDown(KeyCode.E)) {
            Application.ExternalEval("document.body.bgColor = '#000'");
        }
    }
}

これを適当なオブジェクトにアタッチしてビルド、C / E キーを押下してみるとこんな感じになります。

f:id:hecomi:20141202223814p:plain

eval() により背景が黒くなり、console.log() によってログが書き出されていますね。

他にも jslib ファイルと [DllImport("__Internal")] attribute を利用する方法もあるようです(未ドキュメント)。サンプルを用意中とのことで、近いうちに情報が上がるのではないでしょうか。

JavaScript から Unity のコードを呼び出し

逆に、JavaScript から Unity のコードを呼び出すにはどうしたら良いでしょうか。WebPlayer では UnityObject2 オブジェクトを通じて SendMessage() してやりとりしていました。

WebGL では SendMessage() 関数が UnityConfig.js の中でグローバルな関数として用意されており、そのまま使用することが出来ます。

試しに Unity で以下のようなコードを書いてみます。

using UnityEngine;
using System.Collections;

public class CubeGenerator : MonoBehaviour 
{
    public GameObject cube;

    void Generate()
    {
        var pos = Vector3.up * 10f + Random.onUnitSphere;
        Instantiate(cube, pos, Quaternion.identity);
    }
}

インスペクタで指定された GameObject をポチポチ生成する感じです。適当ですがこれを Main Camera にアタッチして適当な Prefab を cube に放り込んでおきます。これを JS からキー押下がある度に呼び出してみます。

document.addEventListener("keydown", function() {
    SendMessage("Main Camera", "Generate");
});

SendMessage() では第 1 引数にメッセージを送る GameObject の名前を、第 2 引数に関数名、第 3 引数に渡すパラメタを記述します。これでキー押下がある度に上で定義した Generate が呼ばれる形になります。実際にビルドしてテストしてみました。

f:id:hecomi:20141202234424p:plain

とても良い感じに動きます。

これは結構面白くて、例えば Unity 側で書くのが面倒なことは適宜便利な HTML5 の機能等を使いながら JS 側で書いてしまって、結果だけ Unity に伝える、みたいなことが簡単にできます。グリングリン Web 上で動く Twitter クライアント、なんかも簡単に作れそうですし、WebSocket でサーバとやりとりしてブラウザ上でリアルタイムに複数人でコラボレーションできるリッチなゲームも簡単に出来ます(後述)。

ただし、Application.ExternalCall()SendMessage() は戻り値で結果が取得できないので、Unity の中の世界の値を外から問い合わせて取ってくるには、行き帰りの 2 段階のコールバックを経由しなければなりません。しかしながら WebPlayer とは異なり、ビルド後は Unity の世界も同じコンテキストの上で動く JavaScript になるため、プロセスまたぎが発生せず、Application.ExternalCall()SendMessage() 直後に呼び出し先のコードが実行されます。そのため往復しても遅延が発生しないので、うまくラップしてあげれば便利にできると思います。

容量の削減

何もないシーン(Main Camera と Directional Light のみ存在)を Slow 設定でビルドしてみます。容量を見ると、何と 132 MB もあります。これは主にメインの JS ファイル(Data/PROJECT_NAME.js)が 110 MB という大きな容量を占めているからです。Slow 設定では JS ファイルが minify されていないためこのような大きなサイズになっていますが、Fastest 設定でビルドをすると minify が効く結果、全体は 35 MB 程度、メインの JS も 23 MB になります。それでも WebPlayer などと比較するとちょっと大きいサイズです。

これについては以下のスレッドで議論されています。

ここでは、出力されたディレクトリには DataCompressed という 2 つのディレクトリがあり、その中のリソースが .htaccess によってよしなに割り振られていることが書いてあります。ディレクトリ出力されたディレクトリにある .htaccess を見てみます。

Options +FollowSymLinks
RewriteEngine on

RewriteCond %{HTTP:Accept-encoding} gzip
RewriteRule (.*)Data(.*)\.js $1Compressed$2\.jsgz [L]
RewriteRule (.*)Data(.*)\.data $1Compressed$2\.datagz [L]
RewriteRule (.*)Data(.*)\.mem $1Compressed$2\.memgz [L]
RewriteRule (.*)Data(.*)\.unity3d $1Compressed$2\.unity3dgz [L]
AddEncoding gzip .jsgz
AddEncoding gzip .datagz
AddEncoding gzip .memgz
AddEncoding gzip .unity3dgz

ここでは gzip による圧縮が許容されていれば gzip 形式のファイルを代わりに使用するルールが記述されています。

モダンなブラウザでは gzip を解凍してくれるので転送量の削減ができるわけですね。つまり Data ディレクトリ内の .js.data.mem.unity3d はモダンなブラウザのみを考慮すれば削除しても良いことになります。

gzip による転送を効かせるため、例えば Mac & Apache で作業している人は /private/etc/apache2/httpd.conf の 2 箇所を書き換えます。まず、RewriteCond / RwriteRule が効くように mod_rewrite をオンにします。

# 以下のコメントを外す
LoadModule rewrite_module libexec/apache2/mod_rewrite.so

そして、.htaccess から使えるように AllowOverrideAll にします。

DocumentRoot "PATH_TO_YOUR_WWW_DIR"
<Directory "PATH_TO_YOUR_WWW_DIR">
    ...
    # AllowOverride None を All にする
    AllowOverride All 
    ...
</Directory>

これでレスポンスヘッダを確認すると以下のように Content-Encodinggzip になっていることを確認できます。

f:id:hecomi:20141202015424p:plain

なお、2回目のロードはキャッシュから読み込まれる(304)ので速くなります。

f:id:hecomi:20141202015539p:plain

要らないファイル(Data ディレクトリ内で Rwrite される対象のファイル)を消去しても動作し、Slow ビルドでは全体は 16.5 MB ほどになり、元と比較しておよそ 1/8 のサイズになりました。Fastest では何と 6.2 MB にまで小さくなります。

ただ、gzip 転送ができるのはあくまで .htaccess によって設定できる環境に限られるので、Dropbox のような環境では Data 内のファイルも全部とっておく必要があります(逆に Compressed は要らないです)。

実践例

サンプルとして冒頭のデモで作成した、Node.js で立てた Socket.IO による複数のブラウザ間を同期する方法について紹介します。仕組みとしては、Unity による WebGL の世界からは、先に紹介した Application.ExternalCall を利用して自分の書いた JavaScript のコードに自機の位置データ等を引き渡し、それを Socket.IO を通じて他のクライアントへ転送、そちらでは SendMessage() して Unity の WebGL の世界へ位置データ等を伝える形になります。

自機の位置を他のブラウザへ送信

まず、自機の情報をサーバに伝えるところを見てみます。

f:id:hecomi:20141206162527p:plain

この処理は例えば以下のようなシンプルな形になります。

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class PlayerController : MonoBehaviour 
{
    public Text textUi;

    private Vector3 prePosition_;
    private Quaternion preRotation_;

    void Update() 
    {
        // ...(略: プレイヤーを動かす処理)

        EmitPosition();
        EmitRotation();
    }

    void EmitPosition()
    {
        // 位置が更新されていたらサーバに伝える
        var pos = transform.position;
        if (pos != prePosition_) {
            prePosition_ = pos;
            Application.ExternalCall("socket.emit", "move", pos.x, pos.y, pos.z); 
        }
    }

    void EmitRotation()
    {
        // 向きが更新されていたらサーバに伝える
        var rot = transform.rotation;
        if (rot != preRotation_) {
            preRotation_ = rot;
            Application.ExternalCall("socket.emit", "rotate", rot.x, rot.y, rot.z, rot.w); 
        }
    }

    void Talk(string message)
    {
        // メッセージを更新してサーバに伝える
        textUi.text = message;
        Application.ExternalCall("socket.emit", "talk", message); 
    }
}

socket.emit 出来るように、JavaScript では Socket.IO を読み込んで以下のように socket をグローバルに置いておきます。

var socket = io() || {};

こうすることで、Unity のコードから、この socket オブジェクトを直接参照できるようになるため、socket.emit() 経由でデータをサーバへと送れるようになります。

サーバ側で送られてきた情報を転送

そうして送られてきた情報を、Node.js サーバでは以下の様なコードを書いて情報を転送します。

var io = require('socket.io')(3000);

io.on('connection', function(socket) {
    var id = socket.id;

    socket.on('move', function(x, y, z) {
        socket.broadcast.emit('move', id, x, y, z);
    });

    socket.on('rotate', function(x, y, z, w) {
        socket.broadcast.emit('rotate', id, x, y, z, w);
    });

    socket.on('talk', function(message) {
        socket.broadcast.emit('talk', id, message);
    });

    socket.on('disconnect', function(message) {
        socket.broadcast.emit('destroy', id);
    });
});

ミソとしてはクライアントに割り振られた socket.id を同時に転送することです。これをユーザの識別子として後で使います。

他機の位置を自分のブラウザ上に反映

最後に送られてきた情報を元に自分のシーンに相手を出します。

f:id:hecomi:20141206162557p:plain

まず、サーバから送られてきたデータを Unity の世界に伝えます。先ほど用意した socket に続けて以下のように書きます。

var socket = io() || {};
socket.isReady = false; // <-- Awake() のタイミングで true になる

window.addEventListener('load', function() {
    var execInUnity = function(method) {
        // シーンがロードされるまでデータの同期はしない
        if (!socket.isReady) return;
        // "Network Player Manager" ゲームオブジェクトに SendMessage(method, args) する
        //  引数は 1 つしか取れないので , 区切りの文字列で引き渡す
        var args = Array.prototype.slice.call(arguments, 1);
        SendMessage('Network Player Manager', method, args.join(','));
    };

    socket.on('move', function(id, x, y, z) {
        execInUnity('Move', id, x, y, z);
    });

    socket.on('rotate', function(id, x, y, z ,w) {
        execInUnity('Rotate', id, x, y, z, w);
    });

    socket.on('talk', function(id, message) {
        execInUnity('Talk', id, message);
    });

    socket.on('destroy', function(id) {
        execInUnity('DestroyPlayer', id);
    });
});

シーンのロードが完了する前に Unity の世界へ SendMessage() を行うとエラーになるため、isReady というフラグを設けています。

直接オブジェクトに SendMessage() しても良いのですが、SendMessage 対象のオブジェクトが存在する/しない判定をしないとならず面倒なので、 Network Player Manager という仲介役を設けてやりとりすることにしました。これで Unity の世界へ SendMessage() で情報を伝えられるようになったので、最後に Unity 側で同期する処理を書きます。

using UnityEngine;
using System.Collections.Generic;

public class NetworkPlayerManager : MonoBehaviour 
{
    public GameObject networkPlayerPrefab;

    private Dictionary<string, NetworkPlayerController> players_ = 
        new Dictionary<string, NetworkPlayerController>();
    static private readonly char[] Delimiter = new char[] {','};

    void Awake()
    {
        Application.ExternalEval("socket.isReady = true;");
    }

    void Move(string argsStr) 
    {
        var args = argsStr.Split(Delimiter);
        GetPlayer(args[0]).Move(new Vector3(float.Parse(args[1]), float.Parse(args[2]), float.Parse(args[3])));
    }

    void Rotate(string argsStr) 
    {
        var args = argsStr.Split(Delimiter);
        GetPlayer(args[0]).Rotate(new Quaternion(
            float.Parse(args[1]), float.Parse(args[2]), float.Parse(args[3]), float.Parse(args[4])));
    }

    void Talk(string argsStr)
    {
        var args = argsStr.Split(Delimiter);
        GetPlayer(args[0]).Talk(args[1]);
    }

    NetworkPlayerController GetPlayer(string id)
    {
        return players_.ContainsKey(id) ? players_[id] : CreatePlayer(id);
    }

    NetworkPlayerController CreatePlayer(string id)
    {
        var obj = Instantiate(networkPlayerPrefab) as GameObject;
        obj.name = id;
        var player = obj.GetComponent<NetworkPlayerController>();
        players_.Add(id, player);
        return player;
    }

    void DestroyPlayer(string id)
    {
        var player = GetPlayer(id);
        player.Talk("[LOGOUT] Good by!");
        players_.Remove(id);
        Destroy(player.gameObject, 3f);
    }
}

送られてきたメッセージを id に応じて送り先を割り振って実行するスクリプトです。id に紐付けられたオブジェクトが存在しない場合は新規に作り、接続が切れた(= ブラウザが閉じられた)ら対象のオブジェクトを消去する役割も担っています。

最後に、ここでネットワーク側のオブジェクトを動かす NetworkPlayerController を作って同期用オブジェクトの Prefab にアタッチしておきます。

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class NetworkPlayerController : MonoBehaviour 
{
    public Text textUi;

    public void Move(Vector3 position) 
    {
        transform.position = position;
    }
    
    public void Rotate(Quaternion rotation) 
    {
        transform.rotation = rotation;
    }

    public void Talk(string message)
    {
        textUi.text = message;
    }
}

同期用のオブジェクトに Rigidbody コンポーネントを使う場合は Is Kinematic をオン、Use Gravity をオフにしておきます。これで冒頭のデモの完成です。

抽象化しないでゴリゴリ書いてしまったのでシンプルではあるものの若干手順は多いです。また、現状の仕組みではビルドしないとデバッグ出来ないため、書き出しに時間のかかる WebGL ビルドでは苦行感があります。なので、実際に開発する際には、ブラウザでの実行か Editor での実行かを見て処理を切り分けてくれるレイヤをはさんであげた方が良いと思います。とはいえ仕組みはシンプルなので、なんとでもやり様はあると思いますので是非いろいろ作ってみてください。

その他のゲームエンジンWebGL 対応

Unreal Engine

昨年の時点で Unreal Engine 3 のサンプルを Mozilla と協力して WebGL に出力するデモが公開されていました。

そして現在の Unreal Engine 4 でビルドした WebGL サンプルもいくつか上がっています。

現在はソースコードからビルドすると HTML5 出力を試せるようです。

仕組みとしては同じで、Emscripten によって asm.js 形式の JS を吐き出されます。 容量や速度による比較などはあまり上がっていないようです。Unreal Engine は Unity のように下に Mono のレイヤがいるわけではなく C++ / Blueprint なので Unity よりも変換が楽なのかなぁ、と素人目線では思ってしまうのですが実際はどうなのでしょうか。

Esenthel Engine

Esenthel Engine でも WebGL ビルドが出来るようです。

その他

C++ から JavaScript への変換は Emscripten が担っているため、EmscriptenWiki に利用しているメジャーなプロジェクト一覧がのっています。

その他気になる動向

WebGL 2.0

現行の WebGLOpenGL ES 2.0 ベースですが、WebGL 2.0 では OpenGL ES 3.0 ベースになります。これにより色々と表現力が上がるのですが、この対応に関しては Unity 5.x にて考えていると Unite 2014 の資料に記載されていました。

WebVR

Oculus Rift 以外にも Morpheus や Google Cardboard、GearVR のようなスマホ系デバイスなど色々な VR デバイスがありますが、これらバラバラな機器に対して描画やヘッドトラッキング、ポジショントラッキングといった機能を統合的に扱おうと ChromeFirefox では WebVR を開発しています。いくつかサンプルも出てき始めています。

来年辺りにはゲームエンジンから WebGL 書き出しでポチッとビルドしてどこかのサーバに置けば皆がアクセスするだけでコンテンツを楽しめる、みたいな世界が来ていそうですね。

Chrome の NPAPI 対応打ち切りによる WebPlayer の廃止

2015/1 から Chrome の NPAPI 対応を打ち切ると Google が発表しました。

Chrome としてはこれにより NPAPI に依存する WebPlayer は、Firefox / Safari / IE では動作しますが、Chrome 上では動かなくなります。Unite 2014 でも NPAPI に関してセキュリティリスクに触れられてはいましたが、こんなに早く廃止に踏み切るとは...という感じだと思います。公式ではこれを受け止めて、これからは WebGL をがんばるぞい、と言っています。

.NET のオープンソース

先日、Microsoft .NET のコアランタイムがオープンソース化されました。

これが果たして Unity にどう影響するのか、色々議論されてますがよくわかりません(誰か教えてください)。

ちなみに、ブログには Microsoft とも今日協力して .NET のアップグレードを進めている、と記載されています。

早く新しい文法使ってコード書きたいですね。

本文中に記載していない参考文献

おわりに

ゲームエンジンから WebGL 書き出しできることにより、Three.js などがメインだったこれまでの Web の 3D コンテンツの敷居が大きく下がり、また表現力はより豊かなものとなります。ノンコーダーなデザイナの方でもかなりのものが作れると思います。サンプルとして作成したデモを見てもお分かり頂けるように、既存の Web に対する知見との親和性も高いです。今後よりバラエティ豊かなコンテンツが出てくることがとても楽しみです。

次回の 12/9 はキルロボさん(@kirurobo)の「KinectMMDモデルを動かす話」になります。 * キルロボブログ: KinectでMMDモデルを動かす話