I’m developing a real-time game that implements the command pattern for each unit’s commands. Normally I would pass a delegate to be called when the command is completed (so that my unit knows when to execute the next command in the queue). Here is a simplified example of what I’m trying to implement:
public abstract class UnitCommand
{
public event EventHandler OnCommandCompleted;
protected virtual void CommandComplete() => OnCommandCompleted?.Invoke(this, EventArgs.Empty);
protected Unit Unit;
public abstract void Execute();
}
public class MoveCommand : UnitCommand
{
public MoveCommand(Unit unit, Vector3 destination)
{
Unit = unit;
_destination = destination;
}
private Vector3 _destination
public override void Execute()
{
UnitMovement unitMovement = Unit.GetComponent<UnitMovement>();
unitMovement.StartMovementServerRpc(_destination, CommandComplete);
}
}
public class UnitMovement : NetworkBehaviour
{
private Vector3 _destination;
private Action _onMovementComplete;
private void Update()
{
// Only run this on Server as I am using default server authoritative NetworkTransform
if (!IsServer)
return;
// Update position each frame while calculating distance from destination...
// Destination reached... call the Action so our Unit knows to start the next one.
_onMovementComplete();
}
[ServerRpc]
public void StartMovementServerRpc(Vector3 destination, Action onMovementComplete)
{
_destination = destination;
_onMovementComplete = onMovementComplete;
}
}
Other classes include my UnitCommandManager class that starts the first Execute, subscribes to the UnitCommand.OnCommandCompleted event, and processes the Queue of commands as each command completes. Also not shown is the invoker class (UnitCommandGiver) that translates the user’s input into queueing these commands and passing the reference to the Unit that is selected.
The obvious problem I’m running into here is that ServerRpcs cannot take a reference type as a parameter, so I can’t pass the Action in StartMovementServerRpc.
Is there any way I can somehow serialize a reference type by implementing the INetworkSerializable interface or something similar?
If not, is there some simple workaround that I’m not seeing here?
Yes, you can do that, but you can only serialize value-type fields of that class. So when deserializing, you are actually creating a new instance of that class and assign the deserialized value-type fields to the new instance. This is probably not what you want, and it’s definitely not going to work with Action, Func or lambda methods.
I think you’re not in the right mindset regarding networking and RPC yet but once you do, the solution is straightforward though.
For one, you need to consider that you are defining the OnCommandCompleted event on the client side. You cannot pass its OnCommandCompleted “event method” to the server and even if you could, the server wouldn’t be able to make this method execute on the client side which I assume is the intention.
Instead, consider RPC message pairs: a request and a response.
Request: client to server to perform some action => StartMovementServerRpc()
Response: server to client to confirm or cancel said action => EndMovementClientRpc()
So basically you need to replace the call to _onMovementComplete(); in Update of UnitMovement with a call to EndMovementClientRpc() also defined in UnitMovement. On the client-side the ClientRpc method executs and would then invoke the _onMovementComplete(); event - this only works provided that each instance of a unit has its own UnitMovement script and there can only be one MovementCommand request/response going on at the same time (ie user action to move a unit is blocked the moment the first movement command is issued, and unblocked only after EndMovementClientRpc ran).
Otherwise you’d have to add more code to be able to enqueue several move commands per unit and be able to determine on both server and more importantly client side which of the movement commands the EndMovementClientRpc was issued for (they may not be received in the same order as the requests).
Thank you for the thorough response! All your assumptions were spot on (each Unit has its own instance of UnitMovement, each UnitCommand is processed one at a time, and user input is blocked when the UnitCommands are being executed).
Just as you said, the solution was straightforward:
Update MoveCommand so that its Execute method calls a non-RPC method on UnitMovement which stores the Action and begins the RPC pair:
public class MoveCommand : UnitCommand
{
public MoveCommand(Unit unit, Vector3 destination)
{
Unit = unit;
_destination = destination;
}
private Vector3 _destination
public override void Execute()
{
UnitMovement unitMovement = Unit.GetComponent<UnitMovement>();
unitMovement.StartMovement(_destination, CommandComplete);
}
}
On the UnitMovement script, store the Action for _onMovementComplete. Then pass the destination to the ServerRpc which initiates the actual movement. Replace _onMovementComplete with a ClientRpc (with a ClientRpcParam that targets OwnerClientId because each Unit is owned by their player).
public class UnitMovement : NetworkBehaviour
{
private Vector3 _destination;
private Action _onMovementComplete;
private void Update()
{
// Only run this on Server as I am using default server authoritative NetworkTransform
if (!IsServer)
return;
// Update position each frame while calculating distance from destination...
// Destination reached... call the Action so our Unit knows to start the next one.
EndMovementClientRpc(new ClientRpcParams { Send = new ClientRpcSendParams { TargetClientIds = new ulong[] { OwnerClientId } } });
}
public void StartMovement(Vector3 destination, Action onMovementComplete)
{
_onMovementComplete = onMovementComplete;
StartMovementServerRpc(destination);
}
[ServerRpc]
public void StartMovementServerRpc(Vector3 destination)
{
_destination = destination;
}
[ClientRpc]
private void EndMovementClientRpc(ClientRpcParams clientRpcParams)
{
_onMovementComplete();
}
}