Oculus Avatar
このガイドでは、PUNでOculus Avatar SDKを使用する方法を説明します。 まずは、新しいUnityプロジェクトを使い、次のパッケージをインポートしましょう。
- Asset StoreからPUN 2
- Asset StoreからOculus Integration
はじめに
インポートが完了したら既存のコンポーネントの拡張が可能です。 まずは「LocalAvatar」と「RemoteAvatar」の2つのプレハブがある 「Assets/Oculus/Avatar/Content/Prefabs」を表示します。 次にこれら2つのプレハブをそのまま使用するか、コピーを作成します。
今回のケースではそれぞれのプレハブのコピーが「Assets/Resources」にあります。
Avatarの同期
次のステップでは、複数のクライアントの同期を処理するPhotonView
コンポーネント(後で添付します)に監視されるスクリプトの実装が必要です。
新しいスクリプトを作成し、PhotonAvatarView
と名付け以下の3つのレファレンスをコードに追加します。
private PhotonView photonView;
private OvrAvatar ovrAvatar;
private OvrAvatarRemoteDriver remoteDriver;
さらに、他のクライアントに実際に送信する前にAvatarからデータを格納するため、バイト配列のリストも必要です。
private List<byte[]> packetData;
UnityのStart
機能を使用すると、前のレファレンスとオブジェクトを全て設定できます。
public void Start()
{
photonView = GetComponent<PhotonView>();
if (photonView.IsMine)
{
ovrAvatar = GetComponent<OvrAvatar>();
ovrAvatar.RecordPackets = true;
ovrAvatar.PacketRecorded += OnLocalAvatarPacketRecorded;
packetData = new List<byte[]>();
}
else
{
remoteDriver = GetComponent<OvrAvatarRemoteDriver>();
}
}
PhotonView
コンポーネントにリファレンスを取得したあと、isMine
条件を直接使用して、「Local」と「Remote Avatar」を明確に区別できるようになります。
インスタンス化されたオブジェクトが私たちのものであれば、次のステップでこれを使用してOvrAvatar
コンポーネントへのリファレンスを取得し、このデータをネットワーク経由で送信する前にすべてのアバター関連の入力イベントを格納するバイト配列のリストをインスタンス化します。
オブジェクトが別のクライアントに属している場合は、後でOvrAvatarRemoteDriver
コンポーネントへの参照を取得します。
次に私たちのジェスチャーを含む記録パケットの停止するのに使う、UnityのOnDisable
メソッドが必要です。
public void OnDisable()
{
if (photonView.IsMine)
{
ovrAvatar.RecordPackets = false;
ovrAvatar.PacketRecorded -= OnLocalAvatarPacketRecorded;
}
}
次のステップではこのパケットはPUNでサポートされているバイト配列にシリアライズ化されます。 その後で前もって作成していたリストが追加され、ネットワーク上で送信される準備が整います。 不必要なデータの送信を避け、メッセージ量の超過により起こり得る接続が切れるのを防止するには、はじめに機能の他の部分を処理する必要かがあるか確認する条件を実装します。 以下を参照してください。
private int localSequence;
public void OnLocalAvatarPacketRecorded(object sender, OvrAvatar.PacketEventArgs args)
{
if (!PhotonNetwork.InRoom || (PhotonNetwork.CurrentRoom.PlayerCount < 2))
{
return;
}
using (MemoryStream outputStream = new MemoryStream())
{
BinaryWriter writer = new BinaryWriter(outputStream);
var size = Oculus.Avatar.CAPI.ovrAvatarPacket_GetSize(args.Packet.ovrNativePacket);
byte[] data = new byte[size];
Oculus.Avatar.CAPI.ovrAvatarPacket_Write(args.Packet.ovrNativePacket, size, data);
writer.Write(localSequence++);
writer.Write(size);
writer.Write(data);
packetData.Add(outputStream.ToArray());
}
}
シリアライザーがあるので、受信したパケットをデシリアライズするためにデシリアライザーも必要です。
このため、次のタスクはこの機能を実装することです。
今回のケースではDeserializeAndQueuePacketData
と呼ばれ、以下のようになっています。
private void DeserializeAndQueuePacketData(byte[] data)
{
using (MemoryStream inputStream = new MemoryStream(data))
{
BinaryReader reader = new BinaryReader(inputStream);
int remoteSequence = reader.ReadInt32();
int size = reader.ReadInt32();
byte[] sdkData = reader.ReadBytes(size);
System.IntPtr packet = Oculus.Avatar.CAPI.ovrAvatarPacket_Read((System.UInt32)data.Length, sdkData);
remoteDriver.QueuePacket(remoteSequence, new OvrAvatarPacket { ovrNativePacket = packet });
}
}
この機能は、入ってくるバイト配列をデシリアライズし、 パケットデータをOvrAvatarRemoteDriver
コンポーネントにキューに入れてジェスチャーを再生します。
このセクションでコーディングする最後の部分は、記録されたパケットの交換を追加することです。
この場合、定期的に自動的に呼び出され、結果として定期的な更新とスムーズな検索をおこなうOnPhotonSerializeView
を使用します。
この機能を正しく追加および使用するには、IPunObservable
インターフェースの実装が必要です。
今回のケースでは、public class PhotonAvatarView : MonoBehaviour, IPunObservable
となります。
このインターフェースはOnPhotonSerializeView
機能の実装を強制します。
public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
if (stream.IsWriting)
{
if (packetData.Count == 0)
{
return;
}
stream.SendNext(packetData.Count);
foreach (byte[] b in packetData)
{
stream.SendNext(b);
}
packetData.Clear();
}
if (stream.IsReading)
{
int num = (int)stream.ReceiveNext();
for (int counter = 0; counter < num; ++counter)
{
byte[] data = (byte[])stream.ReceiveNext();
DeserializeAndQueuePacketData(data);
}
}
}
わかりやすくするためにこれを2つのパートに分けます。
1つ目はIsWriting
条件で Game Objectの所有者によって実行されます。
はじめにデータを送信する必要があるかどうか確認します。
送信する必要があれば、初めに送られた値はパケットの数字です。
この情報は受信者にとって重要です。
最終的に全ての記録され、シリアライズ化されたパケットを送信し、それ以前のリストに保管されていたパケットデータを消去します。このパケットデータはもう必要ではありません。
ただしIsReading
条件は、オブジェクトを持たないリモートクライアント上でのみ実行されます。
これははじめに処理しなくてはいけないパケットの数を確認して、前もって実装していた機能を呼び出します。全てのパケットデータを段階的にデシリアライズしキューに入れます。
最後のステップではPhotonView
コンポーネントと実装したPhotonAvatarView
えお両方のプレハブに添付します。
PhotonAvatarView
コンポーネントをPhotonView
の監視コンポーネントに追加するのを忘れないでください。
Avatareのインスタンス化
残念ながらPhotonNetwork.Instantiate
を呼び出すだけではネットワークAvatarをインスタンス化できません。
理由は、インスタンス化する必要のあるAvatarが2つあるからです。
1つはインスタンス化を行うプレイヤーの'LocalAvatar'で、もう1つは他のプレイヤーの'RemoteAvatar'です。
したがって手動のインスタンス化を使用する必要があります。
以下のコードは、既存のNetwork Manager
や既にネットワークロジックを処理するその他のスクリプトに設定することができます。これは、少し前で作成したPhotonAvatarView
に属しません。
OnJoinedRoom
コールバックを使用します。
コールバックが確実に呼び出されるようにするには、Unityの OnEnable
を使用してクラスを登録する必要があります。また、OnDisable
で不要になったクラスを登録解除する必要もあります。
using System.Collections.Generic;
using ExitGames.Client.Photon;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
public class NetworkManager : MonoBehaviour, IMatchmakingCallbacks
{
public const byte InstantiateVrAvatarEventCode = 1; // コード例。1〜199の値におきかえてください
private void OnEnable()
{
PhotonNetwork.AddCallbackTarget(this);
}
private void OnDisable()
{
PhotonNetwork.RemoveCallbackTarget(this);
}
#region IMatchmakingCallbacks
public void OnJoinedRoom()
{
GameObject localAvatar = Instantiate(Resources.Load("LocalAvatar")) as GameObject;
PhotonView photonView = localAvatar.GetComponent<PhotonView>();
if (PhotonNetwork.AllocateViewID(photonView))
{
RaiseEventOptions raiseEventOptions = new RaiseEventOptions
{
CachingOption = EventCaching.AddToRoomCache,
Receivers = ReceiverGroup.Others
};
PhotonNetwork.RaiseEvent(InstantiateVrAvatarEventCode, photonView.ViewID, raiseEventOptions, SendOptions.SendReliable);
}
else
{
Debug.LogError("Failed to allocate a ViewId.");
Destroy(localAvatar);
}
}
public void OnFriendListUpdate(List<FriendInfo> friendList)
{
}
public void OnCreatedRoom()
{
}
public void OnCreateRoomFailed(short returnCode, string message)
{
}
public void OnJoinRoomFailed(short returnCode, string message)
{
}
public void OnJoinRandomFailed(short returnCode, string message)
{
}
public void OnLeftRoom()
{
}
#endregion
}
このケースではLocal Avatarのインスタンス化をはじめにローカルで行います。
PhotonViewへIDを正常に割り振ることができていた場合、RaiseEventOptions
とSendOptions
を定義しています。
それ以外の場合、エラーをログに記録し、ローカルにインスタンス化されたプレハブを破棄します。
RaiseEventOptions
を使用して、カスタムManual Instantiationイベントがルームのキャッシュに格納されていることを確認します。(後から参加するクライアントもこのイベントを受信します。)そして、自分のオブジェクトは既にローカルでインスタンス化しているので、他のクライアントにのみ同イベントを送信します。
SendOptions
では、カスタムイベントが信頼性をもって送信されたかを確認します。
その後でRaiseEvent
関数を使用してカスタムManual Instantiationイベントをサーバーに送信します。
今回はInstantiateVrAvatarEventCode
を使用しまが、これは単にこの特定のイベントを表すバイト値です。
PhotonViewへのID配布割り当てが失敗していた場合は、エラーメッセージをログし以前にインスタンス化していたオブジェクトを破壊します。
カスタムイベントを受信し処理するには2つの方法があります。
今回のケースで説明するのはそのうちの1つで、そのまま続けてIOnEventCallback
の実装を行います。
ここで説明できない方法については、技術資料のRPCsとRaiseEventで確認していただけます。
IOnEventCallback
インターフェースを実装すると、クラスはコードスニペットの様になります。
public class NetworkManager : MonoBehaviour, IMatchmakingCallbacks, IOnEventCallback
{
// クラスボディの残り部分。上記参照。
public void OnEvent(EventData photonEvent)
{
}
}
次に、ロジックをいくつかOnEvent
コールバックハンドラに追加します。これはRemote Avatarプレハブがインスタンス化され、正常に設定されているか確認できるものです。
public void OnEvent(EventData photonEvent)
{
if (photonEvent.Code == InstantiateVrAvatarEventCode)
{
GameObject remoteAvatar = Instantiate(Resources.Load("RemoteAvatar")) as GameObject;
PhotonView photonView = remoteAvatar.GetComponent<PhotonView>();
photonView.ViewID = (int) photonEvent.CustomData;
}
}
ここでは、受信したイベントが、自分のカスタムManual Instantiationイベントであることを確認します。
その場合、Remote Avatarプレハブをインスタンス化します。
その後でオブジェクトのPhotonViewコンポーネントにリファレンスを取得して、受信したViewID
を割り当てます。
これにより、接続済みの全てのクライアントの正しいAvatarが、ルームに入室しているか否かにかかわらずインスタンス化されます。
テスト
Avatarを同期するには2つの方法があります。
1つ目は、少なくとも2つのコンピューターが必要です。それぞれOculusデバイスに接続しておいてください。(RiftとTouch Controller)。 Unityエディターからゲームを開始しても、ビルドをはじめに構築して、後から両方のコンピューターで実行してジェスチャーの同期が問題ないか確認するやり方でも大丈夫です。
もう1つの方法は2つ目のテストアプリケーションをビルドすることです。 これは事実上「空の」プロジェクト(とはいっても、このページの始めに既述しているプラグインは必要です)ということになります。このプロジェクトにはメインカメラを配置し、RemoteAvatarがインスタンス化される地点に向かせます。 今回のケースではカメラの向く方向は(0/0/0)です。 Photonに接続する時、同じAppIdとAppversionを使用していること、イベントのインスタンス化を処理するためにOnEventコールバックを実装しかつ登録していることを確認してください。 この方法のメリットは、1つのOculusデバイス(RiftとTouch Controllers)を持つ1つのコンピュータで完結できることです。
既知の問題
InvalidCastException
OnPhotonSerializeView
もしくはDeserializeAndQueuePacketData(byte[] data)
内のInvalidCastExceptionsで問題が発生したら、フォーラムユーザ cloud_canvasから提供されている回避策を確認してください。
この回避策を適用するには、OvrAvatarクラスを開いて修正します。
まずはじめに、public bool Initialized = false;
を追加します。
この一行は、例えばパブリックフィールド定義の最後に付け足しても大丈夫です。
その後でCombinedMeshLoadedCallback(IntPtr assetPtr)
関数を表示し、ここの最後にInitialized = true;
を追加します。
これをすることで、オブジェクトのインスタンス化の状態の詳細を知ることができます。
この機能とOculus Platformを活用して、Avatar posesをパッケージを送受信する準備が整っているかどうか判断します。
このためにはPhotonAvatarView
クラスにいくつか変更を加える必要があります。
OnLocalAvatarPacketRecorded
関数で、ルームに入室しているかどうか、そしてルーム内にプレイヤーが少なくとも2人いるかどうかを確認します。
前述の条件もここで追加します。
この条件は頻繁に使っていくので、プロパティを作成してPhotonAvatarViewクラスに追加してしまいましょう。
private bool notReadyForSerialization
{
get
{
return (!PhotonNetwork.InRoom || (PhotonNetwork.CurrentRoom.PlayerCount < 2) ||
!Oculus.Platform.Core.IsInitialized() || !ovrAvatar.Initialized);
}
}
このプロパティは、少なくともクライアントが2人いるルームにいて、Oculus PlatformとAvatarが初期化されている場合、true
を返します。
この場合、Avatar posesを送受信する準場が整っているという意味になります。
このプロパティを活用するには再度OnLocalAvatarPacketRecorded
関数を表示してif (!PhotonNetwork.InRoom || (PhotonNetwork.CurrentRoom.PlayerCount < 2))
条件を新しいif (notReadyForSerialization)
条件に置換します。
さらに、 DeserializeAndQueuePacketData(byte[] data)
関数の頭に、同じif (notReadyForSerialization) { return; }
条件を追加します。
ここまで完了したら、問題が解決されたか確認します。
まだ問題が残っていたら、上記の条件をOnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
関数の頭に追加してみてください。
ヒント:OvrAvatarクラスを修正しているので、プロジェクト内でOculus統合をアップデートするたびに、この変更を適用する必要があります。(少なくともこの特定のファイルが変更されたときは毎回適用します。)