Unity NetCode 入門 (4) - RPC
以下の記事を参考にして書いてます。
・RPCs
1. RPC
「NetCode」は、「RPC」を使用してイベント処理を行います。「SendRpcCommandRequestComponent」や「ReceiveRpcCommandRequestComponent」などのコンポーネントを含むエンティティを作成するフローを使用できます。
2. IRpcCommandの拡張
「IRpcCommand」を拡張してコマンドを作成します。
[BurstCompile]
public struct OurRpcCommand : IRpcCommand
{
// シリアライズ
public void Serialize(ref DataStreamWriter writer)
{
}
// デシリアライズ
public void Deserialize(ref DataStreamReader reader)
{
}
// 呼び出し実行の関数ポインタの取得
public PortableFunctionPointer<RpcExecutor.ExecuteDelegate> CompileExecute()
{
}
// 呼び出し実行
[BurstCompile]
private static void InvokeExecute(ref RpcExecutor.Parameters parameters)
{
}
static PortableFunctionPointer<RpcExecutor.ExecuteDelegate> InvokeExecuteFunctionPointer =
new PortableFunctionPointer<RpcExecutor.ExecuteDelegate>(InvokeExecute);
}
IRpcCommandインターフェイスには、Serialize、Deserialize、CompileExecuteの3つのメソッドがあります。SerializeとDeserializeはデータをパケットに格納しますが、CompileExecuteはBurstを使用してFunctionPointerを作成します。コンパイルする関数は、以下を含むrefによるRpcExecutor.Parametersを取ります。
・DataStreamReader リーダー
・Entity コネクション
・EntityCommandBuffer.Concurrent コマンドバッファ
・int jobIndex
関数は静的であるため、「RPC」を実行する前にDeserializeを使用して構造体データを読み取る必要があります。次に、「RPC」はコマンドバッファを使用して接続エンティティを変更するか、それを使用してより複雑なタスクの新しい要求エンティティを作成します。その後、別のシステムでコマンドを後で適用します。つまり、「RPC」を受信するために追加の操作を実行する必要はありません。そのExecuteメソッドは、受信側で自動的に呼び出されます。
「RPC」を保持するエンティティを作成するには、関数ExecuteCreateRequestComponent<T>を使用します。これを行うには、前のInvokeExecute関数の例を次のように拡張します。
[BurstCompile]
private static void InvokeExecute(ref RpcExecutor.Parameters parameters)
{
RpcExecutor.ExecuteCreateRequestComponent<OurRpcCommand>(ref parameters);
}
これにより、ReceiveRpcCommandRequestComponentおよびOurRpcCommandコンポーネントを持つエンティティが作成されます。
IRpcCommandを作成したら、RpcCommandRequestシステムがそれを取得することを確認する必要があります。これを行うには、次のようにRpcCommandRequestシステムを拡張するシステムを作成します。
public class OurRpcCommandRequestSystem : RpcCommandRequestSystem<OurRpcCommand>
{
}
RpcCommandRequestシステムはRpcQueueを内部で使用して、発信RPCをスケジュールします。
3. コマンドの送受信
例を完了するには、作成したコマンドを送信および受信するエンティティをいくつか作成する必要があります。コマンドを送信するには、エンティティを作成し、コマンドと特別なコンポーネントSendRpcCommandRequestComponentをエンティティに追加する必要があります。このコンポーネントには、このコマンドの送信先のリモート接続を参照するTargetConnectionというメンバーがあります。
【注意】TargetConnectionがEntity.Nullに設定されている場合は、メッセージをブロードキャストします。サーバーに送信するだけなので、クライアントではこの値を設定する必要はありません。
以下は、単純な送信システムの例です。
[UpdateInGroup(typeof(ClientSimulationSystemGroup))]
public class ClientRcpSendSystem : ComponentSystem
{
protected override void OnCreate()
{
RequireSingletonForUpdate<NetworkIdComponent>();
}
protected override void OnUpdate()
{
if (Input.GetKey("space"))
{
var req = PostUpdateCommands.CreateEntity();
PostUpdateCommands.AddComponent(req, new OurRpcCommand());
PostUpdateCommands.AddComponent(req, new SendRpcCommandRequestComponent());
}
}
}
このシステムは、ユーザーがキーボードのスペースバーを押すとコマンドを送信します。
前の例では、IRpCommandへの関数呼び出しにより、フィルタにかけることができるエンティティが作成されます。これが機能するかどうかをテストするために、次の例ではOurRpcCommandを受け取るシステムを作成します。
[UpdateInGroup(typeof(ServerSimulationSystemGroup))]
public class ServerRpcReceiveSystem : ComponentSystem
{
protected override void OnUpdate()
{
Entities.ForEach((Entity entity, ref OurRpcCommand cmd, ref ReceiveRpcCommandRequestComponent req) =>
{
PostUpdateCommands.DestroyEntity(entity);
Debug.Log("We received a command!");
});
}
}
RpcSystemはすべての要求を自動的に検出して送信し、送信要求を削除します。リモート側では、それらは同じIRpcCommandおよびReceiveRpcCommandRequestComponentを持つエンティティとして表示されます。これを使用して、要求の受信元の接続を識別できます。
4. シリアライズ
RpcCommandにアタッチしたいデータがあるかもしれません。これを行うには、データをコマンドのメンバーとして追加し、Serialize関数とDeserialize関数を使用して、どのデータをシリアル化するかを決定する必要があります。この例については、次のコードを参照してください。
[BurstCompile]
public struct OurDataRpcCommand : IRpcCommand
{
public int intData;
public short shortData;
public void Serialize(ref DataStreamWriter writer)
{
writer.WriteInt(intData);
writer.WriteShort(shortData);
}
public void Deserialize(ref DataStreamReader reader)
{
intData = reader.ReadInt();
shortData = reader.ReadShort();
}
public PortableFunctionPointer<RpcExecutor.ExecuteDelegate> CompileExecute()
{
}
[BurstCompile]
private static void InvokeExecute(ref RpcExecutor.Parameters parameters)
{
RpcExecutor.ExecuteCreateRequestComponent<OurDataRpcCommand>(ref parameters);
}
static PortableFunctionPointer<RpcExecutor.ExecuteDelegate> InvokeExecuteFunctionPointer = new PortableFunctionPointer<RpcExecutor.ExecuteDelegate>(InvokeExecute);
}
【注意】問題を回避するには、シリアライズとデシリアライズの呼び出しが対称であることを確認してください。上記の例では、intにshortを書き込んでいるため、コードはint、shortの順序で読み取る必要があります。値の読み取りを省略したり、値の書き込みを忘れたり、コードの読み書きの順序を変更したりすると、予期しない結果が生じる可能性があります。
5. RpcQueue
RpcQueueは、発信RPCをスケジュールするために内部的に使用されます。ただし、OnGetOrCreateManagerを使用して手動で独自のスケジュールを作成できます。これを行うには、以下を呼び出し、アプリケーションの存続期間を通してキャッシュします。
m_RpcQueue = World.GetOrCreateManager<RpcSystem>().GetRpcQueue<OurRpcCommand>();
キューがある場合は、エンティティからOutgoingRpcDataStreamBufferComponentを取得してキュー内のイベントをスケジュールし、以下を呼び出します。
rpcQueue.Schedule(rpcBuffer, new OurRpcCommand);
[UpdateInGroup(typeof(ClientSimulationSystemGroup))]
public class ClientQueueRcpSendSystem : ComponentSystem
{
private RpcQueue<OurRpcCommand> rpcQueue;
protected override void OnCreate()
{
RequireSingletonForUpdate<NetworkIdComponent>();
rpcQueue = World.GetOrCreateSystem<RpcSystem>().GetRpcQueue<OurRpcCommand>();
}
protected override void OnUpdate()
{
if (Input.GetKey("space"))
{
Entities.ForEach((Entity entity, ref NetworkStreamConnection connection) =>
{
var rpcFromEntity = GetBufferFromEntity<OutgoingRpcDataStreamBufferComponent>();
if (rpcFromEntity.Exists(entity))
{
var buffer = rpcFromEntity[entity];
rpcQueue.Schedule(buffer, new OurRpcCommand());
}
});
}
}
}
この例では、ユーザーがキーボードのスペースバーを押すと、RpcQueueを使用して「RPC」を送信します。
この記事が気に入ったらサポートをしてみませんか?