Unity NetCode のチュートリアルを動かす
https://docs.unity3d.com/Packages/com.unity.netcode@0.1/manual/getting-started.html
こちらのチュートリアルを動かそうと思ったら大分躓いたのでメモ。上記チュートリアルページを適時参照しながらお読みください。
NetCodeとは
https://blogs.unity3d.com/jp/2019/06/13/navigating-unitys-multiplayer-netcode-transition/
プレイヤー数が 80名以上、500 個以上のオブジェクトや AI の同期が可能
サーバーコードによるチート防止が可能
レイテンシ耐性がある
2020 年 4 月 27 日現在:Unity Transport and Reliability はプレビュー版が公開
2021 年第 2 四半期:製品品質の DOTS-Netcode
といった感じ。
環境
バージョン:Unity NetCode preview.6 0.1.0
動作環境: Unity 2019.3.b11以上
適時プロジェクトorシーンを作成してください。
プロジェクトの設定
Package ManagerからAdvanced->Show preview packagesでプレビューパッケージを表示。以下のパッケージをインストール。
- Entities(NetCodeが対応していないため0.10.0ではなく、0.9.0をインストール。こちらにあるように古いバージョンのみ対応している模様)
- Hybrid Renderer(これも最新ではなく0.4.1をインストール),
- NetCode
- Transport
シーンを作成
空のGameObjectを作成し、名前をSharedDataに変更して、ConvertToClientServerEntityコンポーネントを追加します。
そして、SharedData を選択して 3D Object > Plane を追加します。
ghost Prefabの作成
次に、ghost Prefabを作成していきます。まず、SharedData を選択して 3D Object > Cube を追加。
作成したCubeを選択して、ProjectビューのAssetフォルダにドラッグ&ドロップしてプレハブにします。
次にスプリクトMovableCubeComponent.cs
を次のように作成して、プレハブに追加します。
using Unity.Entities;
using Unity.NetCode;
[GenerateAuthoringComponent]
public struct MovableCubeComponent : IComponentData
{
[GhostDefaultField]
public int PlayerId;
}
次に、Ghost Authoring Componentを同じく追加し。Update Component Listを押して、チュートリアルの通りに設定しますが、ここで説明とは異なり、Predicting player newtwork id を Predicting player network idを設定しないとエラーが出るので気をつけてください。
Ghost Collection の作成
GhostCollectionのセットアップをします。SharedDataを選択して、空のGameObjectを作成。
名前をGhostCollectionに変更して、GhostCollectionAuthoringComponentを追加します。
Update ghost list を押すとGhostsリストに反映されます。
接続を確立する
次のようにGame.cs
を作成します。
using Unity.Entities;
using Unity.NetCode;
using Unity.Networking.Transport;
using Unity.Burst;
// Control system updating in the default world
[UpdateInWorld(UpdateInWorld.TargetWorld.Default)]
public class Game : ComponentSystem
{
// Singleton component to trigger connections once from a control system
struct InitGameComponent : IComponentData
{
}
protected override void OnCreate()
{
RequireSingletonForUpdate<InitGameComponent>();
// Create singleton, require singleton for update so system runs once
EntityManager.CreateEntity(typeof(InitGameComponent));
}
protected override void OnUpdate()
{
// Destroy singleton to prevent system from running again
EntityManager.DestroyEntity(GetSingletonEntity<InitGameComponent>());
foreach (var world in World.AllWorlds)
{
var network = world.GetExistingSystem<NetworkStreamReceiveSystem>();
if (world.GetExistingSystem<ClientSimulationSystemGroup>() != null)
{
// Client worlds automatically connect to localhost
NetworkEndPoint ep = NetworkEndPoint.LoopbackIpv4;
ep.Port = 7979;
network.Connect(ep);
}
#if UNITY_EDITOR
else if (world.GetExistingSystem<ServerSimulationSystemGroup>() != null)
{
// Server world automatically listens for connections from any host
NetworkEndPoint ep = NetworkEndPoint.AnyIpv4;
ep.Port = 7979;
network.Listen(ep);
}
#endif
}
}
}
RpcCommandは以下のように
[BurstCompile]
public struct GoInGameRequest : IRpcCommand
{
public void Deserialize(ref DataStreamReader reader)
{
}
public void Serialize(ref DataStreamWriter writer)
{
}
[BurstCompile]
private static void InvokeExecute(ref RpcExecutor.Parameters parameters)
{
RpcExecutor.ExecuteCreateRequestComponent<GoInGameRequest>(ref parameters);
}
static PortableFunctionPointer<RpcExecutor.ExecuteDelegate> InvokeExecuteFunctionPointer =
new PortableFunctionPointer<RpcExecutor.ExecuteDelegate>(InvokeExecute);
public PortableFunctionPointer<RpcExecutor.ExecuteDelegate> CompileExecute()
{
return InvokeExecuteFunctionPointer;
}
}
次に、RpcCommandRequestSystemを作成します。
// The system that makes the RPC request component transfer
public class GoInGameRequestSystem : RpcCommandRequestSystem<GoInGameRequest>
{
}
次に、CubeInput.cs
というスクリプトファイルを作成し、次のコードを記述します。ここもチュートリアルページのコードでは動作しないので注意。
using Unity.NetCode;
using Unity.Networking.Transport;
public struct CubeInput : ICommandData<CubeInput>
{
public uint Tick => tick;
public uint tick;
public int horizontal;
public int vertical;
public void Deserialize(uint tick,ref DataStreamReader reader)
{
this.tick = tick;
horizontal = reader.ReadInt();
vertical = reader.ReadInt();
}
public void Serialize(ref DataStreamWriter writer)
{
writer.WriteInt(horizontal);
writer.WriteInt(vertical);
}
public void Deserialize(uint tick,ref DataStreamReader reader, CubeInput baseline,
NetworkCompressionModel compressionModel)
{
Deserialize(tick,ref reader);
}
public void Serialize(ref DataStreamWriter writer, CubeInput baseline, NetworkCompressionModel compressionModel)
{
Serialize(ref writer);
}
}
public class NetCubeSendCommandSystem : CommandSendSystem<CubeInput>
{
}
public class NetCubeReceiveCommandSystem : CommandReceiveSystem<CubeInput>
{
}
次に、SampleCubeInput.cs
を以下のように作成します。
using Unity.Entities;
using Unity.NetCode;
using UnityEngine;
using Unity.Transforms;
[UpdateInGroup(typeof(ClientSimulationSystemGroup))]
public class SampleCubeInput : ComponentSystem
{
protected override void OnCreate()
{
RequireSingletonForUpdate<NetworkIdComponent>();
RequireSingletonForUpdate<EnableNetCubeGhostReceiveSystemComponent>();
}
protected override void OnUpdate()
{
var localInput = GetSingleton<CommandTargetComponent>().targetEntity;
if (localInput == Entity.Null)
{
var localPlayerId = GetSingleton<NetworkIdComponent>().Value;
Entities.WithNone<CubeInput>().ForEach((Entity ent, ref MovableCubeComponent cube) =>
{
if (cube.PlayerId == localPlayerId)
{
PostUpdateCommands.AddBuffer<CubeInput>(ent);
PostUpdateCommands.SetComponent(GetSingletonEntity<CommandTargetComponent>(), new CommandTargetComponent {targetEntity = ent});
}
});
return;
}
var input = default(CubeInput);
input.tick = World.GetExistingSystem<ClientSimulationSystemGroup>().ServerTick;
if (Input.GetKey("a"))
input.horizontal -= 1;
if (Input.GetKey("d"))
input.horizontal += 1;
if (Input.GetKey("s"))
input.vertical -= 1;
if (Input.GetKey("w"))
input.vertical += 1;
var inputBuffer = EntityManager.GetBuffer<CubeInput>(localInput);
inputBuffer.AddCommandData(input);
}
}
[UpdateInGroup(typeof(GhostPredictionSystemGroup))]
public class MoveCubeSystem : ComponentSystem
{
protected override void OnUpdate()
{
var group = World.GetExistingSystem<GhostPredictionSystemGroup>();
var tick = group.PredictingTick;
var deltaTime = Time.DeltaTime;
Entities.ForEach((DynamicBuffer<CubeInput> inputBuffer, ref Translation trans, ref PredictedGhostComponent prediction) =>
{
if (!GhostPredictionSystemGroup.ShouldPredict(tick, prediction))
return;
CubeInput input;
inputBuffer.GetDataAtTick(tick, out input);
if (input.horizontal > 0)
trans.Value.x += deltaTime;
if (input.horizontal < 0)
trans.Value.x -= deltaTime;
if (input.vertical > 0)
trans.Value.z += deltaTime;
if (input.vertical < 0)
trans.Value.z -= deltaTime;
});
}
}
ここで、EnableNetCubeGhostReceiveSystemComponent
はEnable+プロジェクト名+ GhostReceiveSystemComponent
という名前になっているので注意。
最後のステップ
最後に、Game.cs
に以下を追記します。
[UpdateInGroup(typeof(ClientSimulationSystemGroup))]
public class GoInGameClientSystem : ComponentSystem
{
protected override void OnCreate()
{
}
protected override void OnUpdate()
{
Entities.WithNone<NetworkStreamInGame>().ForEach((Entity ent, ref NetworkIdComponent id) =>
{
PostUpdateCommands.AddComponent<NetworkStreamInGame>(ent);
var req = PostUpdateCommands.CreateEntity();
PostUpdateCommands.AddComponent<GoInGameRequest>(req);
PostUpdateCommands.AddComponent(req, new SendRpcCommandRequestComponent { TargetConnection = ent });
});
}
}
[UpdateInGroup(typeof(ServerSimulationSystemGroup))]
public class GoInGameServerSystem : ComponentSystem
{
protected override void OnUpdate()
{
Entities.WithNone<SendRpcCommandRequestComponent>().ForEach((Entity reqEnt, ref GoInGameRequest req, ref ReceiveRpcCommandRequestComponent reqSrc) =>
{
PostUpdateCommands.AddComponent<NetworkStreamInGame>(reqSrc.SourceConnection);
UnityEngine.Debug.Log(string.Format("Server setting connection {0} to in game", EntityManager.GetComponentData<NetworkIdComponent>(reqSrc.SourceConnection).Value));
var ghostCollection = GetSingleton<GhostPrefabCollectionComponent>();
var ghostId =
NetCubeGhostSerializerCollection.FindGhostType<CubeSnapshotData>();
var prefab = EntityManager.GetBuffer<GhostPrefabBuffer>(ghostCollection.serverPrefabs)[ghostId].Value;
// プレイヤー(キューブを作成)
var player = EntityManager.Instantiate(prefab);
EntityManager.SetComponentData(player, new MovableCubeComponent { PlayerId = EntityManager.GetComponentData<NetworkIdComponent>(reqSrc.SourceConnection).Value});
PostUpdateCommands.AddBuffer<CubeInput>(player);
PostUpdateCommands.SetComponent(reqSrc.SourceConnection, new CommandTargetComponent {targetEntity = player});
PostUpdateCommands.DestroyEntity(reqEnt);
});
}
}
ここで、NetCubeGhostSerializerCollectionも、プロジェクト名+GhostSerializerCollection
になっているので注意。
あとはMultiplayer > PlayMode Tools から PlayMode Type が Client & Server になっていることを確認して、再生ボタンを押すと、W,A,S,D,で箱を移動させることが出来ているはずです。
参考
とても参考になった記事(中国語):https://zhuanlan.zhihu.com/p/110986295