If anyone takes the time to read this, thank you in advance.
Aim: Have a Local Client that can function within the ServerWorld
Test Case: Share camera position of all connected clients / local client via ghost entities.
Step 1: Server World Setup
- Setup ServerWorld to include
WorldSystemFilterFlags.LocalSimulation
and WorldSystemFilterFlags.Presentation
- Donât spawn ClientWorld if
PlayType.ClientAndServer
Step 2: Local Client Network Connection
Create NetworkConnection entity mimic for local client with networkId=0:
public partial struct LocalConnectionEntitySingleton : IComponentData{
public Entity Entity;
public static implicit operator Entity(LocalConnectionEntitySingleton element) => element.Entity;
}
[WorldSystemFilter(WorldSystemFilterFlags.LocalSimulation)]
[BurstCompile]
public partial struct LocalConnectionSystem : ISystem{
[BurstCompile]
public void OnCreate(ref SystemState state) {
Entity networkConnectionLocal = state.EntityManager.CreateEntity();
state.EntityManager.AddComponentData(networkConnectionLocal, new NetworkId() {
Value = 0
});
state.EntityManager.AddComponent<NetworkStreamInGame>(networkConnectionLocal);
state.EntityManager.CreateSingleton(new LocalConnectionEntitySingleton() {
Entity = networkConnectionLocal
});
}
}
This is obviously very naive since itâs not really a NetworkConnection like the ones that spawn for external clients.
I tried to create a new NetworkDriver and register it to the store but that was too complex.
Perhaps a LocalNetwork
component could be added that gets handled differently for RPCs/ghosts.
Step 3: RPC System
Create a system that runs on both LocalSimulation (ServerWorld) and ClientSimulation (ClientWorld):
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.LocalSimulation)]
[UpdateInGroup(typeof(CameraSystemGroup))]
public partial struct ClientCameraLocationRpcProviderSystem : ISystem {
public void OnCreate(ref SystemState state) {
if (state.World.IsClient()) {
state.RequireForUpdate<NetworkStreamConnection>();
}
state.RequireForUpdate<EndSimulationEntityCommandBufferSystem.Singleton>();
}
public void OnUpdate(ref SystemState state) {
if (state.World.IsClient()) {
NetworkStreamConnection connectionState = SystemAPI.GetSingleton<NetworkStreamConnection>();
if (connectionState.CurrentState != ConnectionState.State.Connected) {
return;
}
}
foreach (RefRO<CameraControl> cameraControl in SystemAPI.Query<RefRO<CameraControl>>().WithChangeFilter<CameraControl>()) {
EntityCommandBuffer ecb = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged);
Entity rpcEntity = ecb.CreateEntity();
ecb.AddComponent(rpcEntity, new CameraLocationRPC() {
Position = cameraControl.ValueRO.CurrentPosition
});
if (state.World.IsClient()) {
ecb.AddComponent(rpcEntity, new SendRpcCommandRequest());
}
else {
if (SystemAPI.TryGetSingleton(out LocalConnectionEntitySingleton singleton)) {
ecb.AddComponent(rpcEntity, new ReceiveRpcCommandRequest() {
SourceConnection = singleton
});
}
}
}
}
}
There are some ways in which i could remove if (state.World.IsClient()) checks, i could always send a
SendRpcCommandRequest and have a system that converts it to a ReceiveRpcCommandRequest before it gets picked up. idk.
Steps 4/5: Ghost Management and RPC Handling
Single system handling both ghost instantiation (prefabs created with GhostPrefabCreation) and RPC processing:
public struct CameraLocationReference : IComponentData {
public Entity CameraLocationEntity;
};
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
[UpdateInGroup(typeof(CameraSystemGroup), OrderFirst = true)]
public partial struct CameraLocationHandlerSystem : ISystem {
private ComponentLookup<CameraLocation> _cameraLocationLookup;
private ComponentLookup<CameraLocationReference> _cameraLocationReferenceLookup;
public void OnCreate(ref SystemState state) {
state.RequireForUpdate<LocalConnectionEntitySingleton>();
state.RequireForUpdate<CameraLocationPrefabSingleton>();
state.RequireForUpdate<EndSimulationEntityCommandBufferSystem.Singleton>();
_cameraLocationLookup = SystemAPI.GetComponentLookup<CameraLocation>(false);
_cameraLocationReferenceLookup = SystemAPI.GetComponentLookup<CameraLocationReference>(true);
}
public void OnUpdate(ref SystemState state) {
_cameraLocationReferenceLookup.Update(ref state);
_cameraLocationLookup.Update(ref state);
EntityCommandBuffer ecb = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged);
foreach ((RefRO<NetworkStreamInGame> _, RefRO<NetworkId> id, Entity entity) in
SystemAPI.Query<RefRO<NetworkStreamInGame>, RefRO<NetworkId>>().WithNone<CameraLocationReference>().WithEntityAccess()) {
CameraLocationPrefabSingleton locationPrefabSingleton = SystemAPI.GetSingleton<CameraLocationPrefabSingleton>();
Entity instance = ecb.Instantiate(locationPrefabSingleton.Entity);
ecb.SetComponent(instance, new GhostOwner() {
NetworkId = id.ValueRO.Value
});
ecb.AddComponent<CameraLocationReference>(entity, new CameraLocationReference() {
CameraLocationEntity = instance
});
Debug.Log($"creating camera location entity for client: {id.ValueRO.Value}]");
}
foreach ((RefRO<ReceiveRpcCommandRequest> receiveRpcCommandRequest, RefRO<CameraLocationRPC> cameraLocationRPC, Entity entity) in
SystemAPI.Query<RefRO<ReceiveRpcCommandRequest>, RefRO<CameraLocationRPC>>().WithEntityAccess()) {
Entity sourceConnection = receiveRpcCommandRequest.ValueRO.SourceConnection;
CameraLocationReference cameraLocationReference = _cameraLocationReferenceLookup[sourceConnection];
RefRW<CameraLocation> cameraLocation = _cameraLocationLookup.GetRefRW(cameraLocationReference.CameraLocationEntity);
cameraLocation.ValueRW.Position = cameraLocationRPC.ValueRO.Position;
ecb.DestroyEntity(entity);
}
}
Effect: Achieved local client request functionality in the ServerWorld without needing a separate ClientWorld.
While this workaround functions, having an official way of doing this would be nice.
If anyone from the Unity team could comment on whether this is a planned feature or if thereâs a better approach, it would be greatly appreciated.
I got something similar to work with ICommandData too.