The library is made on top of the Unity Netcode package and saves you some time on configuring it / provides tools for client-server communication.
- Install
- Getting started
- Groups
- Command builders
- Command handlers
- Synchronizing entities
- Synchronizing components
- Managed RPC commands
I like to have this repo as a module without a unity project, so please go to another repo which I created just for a demo: ECSPowerNetcodeDemo.
You can either just put the files into Assets/Plugins/ECSEntityBuilder
or use it as a submodule:
git submodule add https://github.com/actionk/ECSPowerNetcode.git Assets/Plugins/ECSPowerNetcode
Important!
After adding a plugin, please add the Network
prefab into your scene from ECSPowerNetcode/Prefabs
folder. This will enable networking.
The library depends on:
These are required dependencies
ServerManager.Instance.StartServer(7979);
ClientManager.Instance.ConnectToServer(7979);
After doing so, the library will automatically establish a connection and create command handlers for each connection in both client & server world.
ClientManager.Instance.ConnectToServer(7979, "remote ip address");
Each connection is described by these parameters:
public struct ConnectionDescription
{
public int networkId; // unique network id value for client-server connection
public Entity connectionEntity;
public Entity commandHandlerEntity;
}
ClientManager.Instance.IsConnected
ClientManager.Instance.ConnectionToServer
-> ConnectionDescription
struct
ServerManager.Instance.AllConnections
- getting list of all connected clients of ConnectionDescription
struct
ServerManager.Instance.GetClientConnectionByNetworkId
You can set your handlers for OnConnected
& OnDisconnected
events:
ClientManager.Instance.OnConnectedHandler += MyHandler;
ClientManager.Instance.OnDisconnectedHandler += MyHandler;
You can set your handlers for OnConnected
& OnDisconnected
events for each player:
public delegate void OnPlayerDisconnected(int networkConnectionId);
ServerManager.Instance.OnPlayerConnectedHandler += MyHandler;
ServerManager.Instance.OnPlayerDisconnectedHandler += MyHandler;
ClientConnectionSystemGroup
ClientRequestProcessingSystemGroup
ClientNetworkEntitySystemGroup
ClientGameSimulationSystemGroup
Group | Description |
---|---|
ClientConnectionSystemGroup | Connection/Disconnection from server |
ClientRequestProcessingSystemGroup | Processing requests from server |
ClientNetworkEntitySystemGroup | Processing network entities |
ClientGameSimulationSystemGroup | All your game simulations on client side |
ServerConnectionSystemGroup
ServerRequestProcessingSystemGroup
ServerNetworkEntitySystemGroup
ServerGameSimulationSystemGroup
Group | Description |
---|---|
ServerConnectionSystemGroup | Connection/Disconnection from clients |
ServerRequestProcessingSystemGroup | Processing requests from clients |
ServerNetworkEntitySystemGroup | Processing network entities |
ServerGameSimulationSystemGroup | All your game simulations on server side |
For making your life easier, there are command builders for both client & server commands. First of all, you have to create an IRpcCommand yourself.
ClientToServerRpcCommandBuilder
.Send(new ClientPlayerLoginCommand {localPlayerSide = PlayerManager.LocalPlayerSide.LEFT})
.Build(PostUpdateCommands);
Where ClientPlayerLoginCommand
implements IRpcCommand
You can specify which client to send the command to:
ServerToClientRpcCommandBuilder
.SendTo(clientConnectionEntity, command)
.Build(PostUpdateCommands);
ServerToClientRpcCommandBuilder
.SendTo(networkConnectionId, command)
.Build(PostUpdateCommands);
Or you can simply broadcast:
ServerToClientRpcCommandBuilder
.Broadcast(command)
.Build(PostUpdateCommands);
Simply inherit from AClientReceiveRpcCommandSystem
to implement a client command handler for RPC command of type ServerPlayerLoginResponseCommand
(for example):
public class ClientPlayerLoginResponseSystem : AClientReceiveRpcCommandSystem<ServerPlayerLoginResponseCommand>
{
protected override void OnCommand(ref ServerPlayerLoginResponseCommand command, ConnectionDescription clientConnection)
{
// process your command
}
}
Simply inherit from AServerReceiveRpcCommandSystem
to implement a server command handler for RPC command of type ClientDropItemCommand
(for example):
public class ServerDropItemSystem : AServerReceiveRpcCommandSystem<ClientDropItemCommand>
{
protected override void OnCommand(ref ClientDropItemCommand command, ConnectionDescription clientConnection)
{
// process your command
}
}
Usually your way of organizing entities in client-server architecture with ECS would look like that:
That's for, the library provides you with a way of synchronizing entities without using ghost components:
You start with creating an entity builder by inheriting your builder from ServerNetworkEntityBuilder
:
public class ServerPlayerBuilder : ServerNetworkEntityBuilder<ServerPlayerBuilder>
{
protected override ServerPlayerBuilder Self => this;
public static ServerPlayerBuilder Create(int networkId, Entity connection, uint playerId, PlayerManager.LocalPlayerSide localPlayerSide)
{
return new ServerPlayerBuilder(networkId, connection, playerId, localPlayerSide);
}
private ServerPlayerBuilder(int networkId, Entity connection, uint playerId, PlayerManager.LocalPlayerSide localPlayerSide) : base()
{
CreateFromArchetype<PlayerArchetype>(WorldType.SERVER);
SetComponentData(new Scale {Value = 1});
AddComponentData(new ServerPlayer
{
networkId = networkId,
connection = connection,
playerId = playerId,
localPlayerSide = localPlayerSide
});
}
}
Then, you create an RPC command to send the entity to the clients:
[BurstCompile]
public struct PlayerTransferCommand : INetworkEntityCopyRpcCommand, IRpcCommand
{
public ulong NetworkEntityId => networkEntityId;
public int networkId;
public ulong networkEntityId;
public uint playerId;
public PlayerManager.LocalPlayerSide localPlayerSide;
public float3 position;
}
Then, for creating the command, you create a system inherited from AServerNetworkEntityTransferSystem
:
[UpdateInGroup(typeof(ServerNetworkEntitySystemGroup))]
public class ServerPlayerTransferSystem : AServerNetworkEntityTransferSystem<ServerPlayer, PlayerTransferCommand>
{
protected override PlayerTransferCommand CreateTransferCommandForEntity(Entity entity, NetworkEntity networkEntity, ServerPlayer selectorComponent)
{
return new PlayerTransferCommand
{
networkId = selectorComponent.networkId,
networkEntityId = networkEntity.networkEntityId,
playerId = selectorComponent.playerId,
localPlayerSide = selectorComponent.localPlayerSide,
position = EntityManager.GetComponentData<Translation>(entity).Value
};
}
}
There are also options for selection more components (2 and 3), such as AServerNetworkEntityTransferSystemT2
and AServerNetworkEntityTransferSystemT3
.
And the system for consuming this command on the client side:
[UpdateInGroup(typeof(ClientEarlyUpdateSystemGroup))]
public class ClientPlayerTransferSystem : AClientNetworkEntityTransferSystem<PlayerTransferCommand>
{
protected override void CreateNetworkEntity(ulong networkEntityId, PlayerTransferCommand command)
{
ClientPlayerBuilder
.Create(command)
.Build(EntityManager);
}
protected override void SynchronizeNetworkEntity(Entity entity, PlayerTransferCommand command)
{
EntityWrapper.Wrap(entity, PostUpdateCommands)
.SetComponentData(new Translation {Value = command.position});
}
}
That's it! When you server entity is created, it will be automatically transferred to the client side by using TransferNetworkEntityToAllClients
, which is described below
You have two possibilities of controlling that:
-
By adding
TransferNetworkEntityToAllClients
component to your network entity. This will automatically send the transfer command to all the clients connected -
By adding a buffer of
TransferNetworkEntityToClient
and specifing the client connection to send the entity to. You can use EntityWrapper to use an existing buffer or create one if it doesn't exist:
EntityWrapper.Wrap(entity, EntityManager)
.AddElementToBuffer(new TransferNetworkEntityToClient(reqSrcSourceConnection));
When you want to destroy the entity on the server and all the clients at the same time, you can just add a ServerDestroy
component to server entity and it will be automatically destroyed on all the clients:
PostUpdateCommands.AddComponent<ServerDestroy>(myServerEntity);
All:
ClientManager.Instance.NetworkEntityManager.All
ServerManager.Instance.NetworkEntityManager.All
By ID:
ClientManager.Instance.NetworkEntityManager[networkEntityId]
ServerManager.Instance.NetworkEntityManager[networkEntityId]
You have two ways of customizing your network entities:
The default network entity manager is DefaultNetworkEntityManager
which implements INetworkEntityManager
. You can just implement INetworkEntityManager
on your own and set the entity manager for server/client:
ClientManager.Instance.NetworkEntityManager = myEntityManager;
ServerManager.Instance.NetworkEntityManager = myEntityManager;
The default factory implementation is DefaultNetworkEntityIdFactory
. However, you can also implement your own factory by implementing INetworkEntityIdFactory
and replacing the default one:
ServerManager.Instance.NetworkEntityIdFactory = myEntityManager;
As you can see, it only works for server-side as the server is the one who assign the IDs.
As an alternatives to Unity Netcode's Ghosts, the lib provides a way of synchronizing components automatically from server to all clients.
Here is the example:
[assembly: RegisterGenericComponentType(typeof(CopyEntityComponentRpcCommand<Velocity, VelocityConverter>))]
namespace Entities.Players.Packets
{
public struct Velocity : IComponentData
{
public float3 value;
public bool IsZero => math.lengthsq(value) <= 0.001f;
}
public struct VelocityConverter : ISyncEntityConverter<Velocity>
{
public Velocity velocity;
public Velocity Value => velocity;
public void Convert(Velocity value)
{
velocity = value;
}
public void Serialize(ref DataStreamWriter writer)
{
writer.WriteFloat(velocity.value.x);
writer.WriteFloat(velocity.value.y);
writer.WriteFloat(velocity.value.z);
}
public void Deserialize(ref DataStreamReader reader)
{
velocity.value = new float3(
reader.ReadFloat(),
reader.ReadFloat(),
reader.ReadFloat()
);
}
}
public class VelocityRpcCommandSender : RpcCommandSendSystem<
CopyEntityComponentRpcCommand<Velocity, VelocityConverter>,
CopyEntityComponentRpcCommand<Velocity, VelocityConverter>>
{
}
}
This way when you add a Velocity
component to an entity which also has NetworkEntity
component, the Velocity
component will be automatically synchronized.
For triggering synchronization process for all the components on the entities, add Synchronize
component to the entity.
You can also synchronize all transform components in one command by just adding SyncTransformFromServerToClient
to any entity in server world which has NetworkEntity
component as well.
If you don't want to recieve such updates on the client side (for example, when you're controlling your character, you don't want to receive updates about his/her position), you can just add IgnoreTransformCopyingFromServer
component to this entity in client world.
Sometimes you want to have control over the requests you send to server. For example, when you send a request to perform an action and you wait until the server confirms it. In this case, you need to get a response to that exact request you sent and react to that response. You can do that with so-called managed rpc commands.
First of all, you should create your command which should implement IManagedRpcCommand
instead of IRPCCommand
. This interface adds a PacketId
field that will be filled automatically.
Then, you send a message to a server using ClientToServerManagedRpcCommandBuilder
:
ClientToServerManagedRpcCommandBuilder
.Send(new MyManagedCommand())
.AddEntityWaitingForResult(myEntity)
.Build(PostUpdateCommands)
To recieve the command on the server side and process it, you should create a system that implements AServerReceiveManagedRpcCommandSystem
:
class MyCommandRecieveSystem : AServerReceiveManagedRpcCommandSystem<MyManagedCommand> {
protected int OnCommand(ref T command, ref ReceiveRpcCommandRequestComponent requestComponent) {
// process command
return (int)MyStatusEnum.SUCCESS;
}
}
OnCommand
expects you to return a status int that will be sent back to the client.
Once you respond, the response will be sent back to the client and added to the entity which is waiting for it: .AddEntityWaitingForResult(myEntity)
.
The added component is described below:
public struct ManagedRpcCommandResponse : IComponentData
{
public ulong packetId;
public int result;
}