この記事はOculus Rift Advent Calendarの4日目です。
はじめに
こんにちは。Oculus Rift Advent Calendar 12/4担当のNeedleです。
先日11/9に開催された第一回Oculus Game Jamで作ったMikulus Kinect Onlineの、特にネットワーク部分について書こうと思います。
Mikulus Kinect Onlineとはこんなソフトです。
なお現在、第五回ニコニコ学会β 研究してみたマッドネスセッションの投票受付中です。投票は本日12/4 23:59までですので、もし良ければこちらのページから投票をよろしくお願いいたします!
Mikulus Kinectとは
Mikulus Kinect Onlineの話の前に、まずその元となったMikulus Kinectについて説明します。
Mikulus Kinectは、Oculus RiftとKinectとUnity3Dを組み合わせて、プレイヤーが自分の手足や体がVR内で見える状態でミクさんと握手や頭を撫でるといったコミュニケーションが取れるアプリです。
Oculusを着けて最初に感じたのが頭の角度に対する映像の追随の滑らかさでしたが、一方で感じたのが、頭の位置移動が反映されないことや自分の体が見えない事による違和感でした。そのときKinect登場時に話題になったNao_uさんの動画を思い出し、自分でもやってみようと思ったのがきっかけです。「VR内でAIキャラの目を見る」という体験は、モニタ越しのゲームとは全く違ったものとして感じられました。
ここで作ったMikulus Kinectはあくまで 人←→AI というシングルプレイヤーのデモでした。これをマルチプレイヤー化し、複数の人が同じVR空間内に入れるようにしたのが、Mikulus Kinect Onlineになります。
オンライン化にするにあたっての課題
オンライン対応にはネットワークミドルウェアのPhoton Cloudを使いました。以前ワークショップで少しだけ触ったことがあったからというのもありますが、ゲームジャムの会場がPhotonを日本展開されているGMOさんのオフィス内で、中の人に質問をしやすかったからでもあります。Unity用のPhoton CloudはUnity自身の内蔵ネットワーク機能に似た形で使えるよう作られているので、そちらを使う場合も同じ手法を応用できると思います。
ここからはMikulus Kinectをオンライン化する際に直面した課題について書いていきます。
アバターキャラの出現(○○さんがログインしました)
シングルプレイのMikulus Kinectではプレイヤーが操作するアバターキャラを最初からHierarchyの中に置いていましたが、ネットワーク環境では誰かがログインする度にその人のアバターキャラを出現させる必要があるため、Hierarchyにおいておくわけにはいきません。アバターキャラのGameObjectはPrefabにして、ProjectのResources内に置いておきます。
ログイン時のコードはこんな感じです。
(アバターキャラPrefabの名前はここではMKMotionCaptureContainer
となっています)
void OnJoinedRoom(){
// 参加者全員の世界に新規プレイヤーのアバターが出現する。
// PhotonNetwork.InstantiateでInstantiateできるのはResourcesフォルダの中身のみ。
// インスペクタでGameObjectメンバ変数にPrefabを割り当てて使ったりは出来ないので注意
GameObject character = PhotonNetwork.Instantiate("MKMotionCaptureContainer",
Vector3.zero, Quaternion.Euler(new Vector3(0,180,0)), 0);
// こっちの内容は参加者全員ではなく自分の世界でだけ反映されるので、
// 自分のアバターキャラにモーションキャプチャ&ヘッドトラッキングの関連付けを行う
character.name ="MKMotionCaptureContainer(Me)";
mocap.EngagedUsers[0] = character;
character.GetComponent<HeadFollowsEyeSmoothed>().enabled = true;
character.GetComponent<SetZigBias>().enabled = true;
myPhotonView = character.GetComponent<PhotonView>();
character.transform.parent = meObject.transform;
}
モーションキャプチャ関節の共有
Photon CloudにはPhotonView
というスクリプトがあります。基本的には、ネットワーク越しにプレイヤー間で共有したいGameObject
にこのスクリプトを貼っておくことで、そのGameObject
の位置・角度・パラメータといった状態を共有できるようになります。キャラクターの動きを共有するにはこれを使います。
通常のオンラインゲームでは、一人のプレイヤーが動かせるのは基本的に自分のプレイヤーキャラ1体です。つまり一人あたりのPhotonView
は1個で十分でした。
これがMikulus Kinect Onlineにおいては話が変わります。全身の関節をKinectでキャプチャし、MMDモデルの関節に割り当てているため、位置・角度をPhotonView
で共有するのも、キャプチャしている関節全てで行わなければなりません。 ((余談ですが、各関節に使うPhotonView
のState Synchronizationタイプは、「頻繁に位置が変わる」「送信失敗したデータを再送する必要はない」事から、Unreliable On Changeにしておくのが良さそうです。))
任意のキャラクターモデルへの対応
以上を踏まえて、単にミクのモデルだけを動かしたいのであれば、ミクモデルの各関節のGameObject
に黙々とPhotonView
を貼っていけばOKです。
しかし将来的にはKAITOやリン・レンなどアバターキャラ(モデル)の切替を可能にしたいので、決め打ち対応はなるべく避けたいところです。Kinectでキャプチャされる関節は24もあるので ((Kinect v2ではキャプチャされる関節数が更に増えているはずです。)) 、モデルを変える度にPhotonView
を貼る作業を24回繰り返すのは生産的ではありません。一方で、モデルによっては関節の数が違い ((シテヤンヨとかゆっくりとかMogg式ミクとか…)) 、24個全部を使わないこともありえます。
よって、PhotonView
の割り当ては初期化時に自動的に行えるとベストです。
PhotonViewスクリプトの自動割り当て
これらの問題を解決するため、当初は以下のような手順を考えました。
- アバターキャラPrefabの
Instantiate
時にモーションキャプチャで動かせる関節GameObject
の一覧を作る- その関節
GameObject
の配列をforeach
でループし、それぞれにAddComponent()
でPhotonView
スクリプトを生成して貼り付ける
- その関節
しかしこれだと動作しません。
調べて分かったのですが、PhotonView
スクリプトはPrefabのInstantiate
時点であらかじめ共有したいGameObject
に貼られていなければならず、初期化時に生成するのでは間に合わないようでした。
しばらく悩みましたが、発想を逆転し、結果として以下の様な手順になりました。
- Unityエディタを使い、
PhotonView
が貼られた空GameObject
を24個作って入れておく
- アバターキャラPrefabの
Instantiate
時にモーションキャプチャで動かせる関節GameObject
の一覧を作る - その関節
GameObject
の配列をforeach
でループし、それぞれに先ほど作っておいたPhotonView
付きGameObject
を関連付ける - モデルの関節数が24個未満だった場合、
PhotonView
付きGameObject
が関連付けられずに余るので、余った分はDestroy()
して始末する
- アバターキャラPrefabの
要は初期化時に生成するのがダメでも、初期化時に破壊するのはOKという性質を利用しています。
以下は該当部分のソース。
using UnityEngine;
using System;
public class NetworkPopulator : Photon.MonoBehaviour {
public GameObject jointPhotonViewContainer;
void Start () {
MatchZigfuSkeletonToMMDModel zigfuMMDModel =
GetComponent<MatchZigfuSkeletontoMmdModel>();
if (!zigfuMMDModel) {
Debug.LogError("No MatchZigfuSkeletonToMMDModel found");
return;
}
// skelTransformsにはトラッキングが有効な関節の一覧が入っている
Transform[] skelTransforms = zigfuMMDModel.trackedMMDBodyParts;
MKNetworkCharacter[] jointPhotonViews =
jointPhotonViewContainer.GetComponentsInChildren();
foreach (ZigJointId jId in Enum.GetValues(typeof(ZigJointId))) {
int joint = (int)jId; // 毎回キャストしないよう
if (skelTransforms[joint]){
// 該当関節があるならPhotonView割り当て
jointPhotonViews[joint].transform.parent = skelTransforms[joint];
jointPhotonViews[joint].transform.localPosition = Vector3.zero;
jointPhotonViews[joint].SetTargetTransform();
} else {
// ないなら破壊
Destroy(jointPhotonViews[joint].gameObject);
}
}
}
}
その他参考資料
2013/12/4現在、Photon公式のドキュメンテーションページが落ちてるようです。
公式ではないですが、こちらのスライドがPhotonの基本概念を理解するのに役立ちそうです。
まとめ&今後の展望
※この二人はVR空間内で抱き合っています #ogjj pic.twitter.com/TEy2wcqCkk
— Kenji Iguchi (@needle) November 10, 2013
こうしてなんとかゲームジャム終了までに動作する形にまとめる事ができ、その後のデモやTwitterにおいても好評を得ることが出来ました。(一緒に開発した@sinkukkuさん、ありがとうございます。)
Mikulus Kinectで実装した「VR内でAIキャラの目を見る」という体験も不思議な感覚でしたが、「VR内でネットワーク越しに他の人間の目を見る」というのは、生身の人間がアバターキャラの中にいると知っているだけに、また更に違う体験に感じられます。これについては、また別の機会に書ければと思います。
ただ、粗はまだまだ多いですし、当初やってみたかった事もできていないことは沢山あるので、今後少しずつやっていきたいところです。今後考えているところとしては
- 新規に入ってきた人の立ち位置や向きを重ならないようにずらす
- その為にPhoton, Kinect (Zigfu), Oculusそれぞれで向いている方角を揃える
- モーションキャプチャの動きを記録し、後から再生できるようにする
- 人間が動かすキャラとAIが動かすキャラを混在させる
- MecanimのIKを活用して肘関節などの位置・角度情報の共有を省き、通信量を減らす
- 表情制御(HMDかぶってるのにどうやって…?w)
- 公開する!
などなど。
長いエントリに最後までお付き合いいただき、ありがとうございます。再度になりますが、現在、第五回ニコニコ学会β 研究してみたマッドネスセッションの投票受付中です。投票は本日12/4 23:59までですので、もし良ければこちらのページから投票をよろしくお願いいたします!