Wednesday, April 24, 2013

Further Client Server Syncing

So last night I got basic synchronization between the client and server working.  Basically I had the server starting a zone, creating a single entity (a small spaceship) and forcing it to slowly rotate.

When the object was issued a command that updated it's attributes that data was then sent to any clients connected to the zone (GAMEOBJECT_UPDATE_MESSAGE).

The client would receive the data and update the entity.  Unless of course it didn't know about the entity, in which case it would discard the data and reply with a message telling the server it was ignorant of this entity (REQUEST_FULL_ENTITY_MESSAGE).

The server would then reply with (FULL_ENTITY_UPDATE_MESSAGE) which contained all the important bits of the entity (basically a serialization of all the entity's components.  I still want to write something on the entity-component system I'm using).  Since the components are purely data they were fairly simple to serialize and send across.  At first I simply replaced the entity's components with the newly materialized ones.

Bad idea though, as I want to used object pooling for everything where possible.

So, I used reflection to go into the objects and find every field decorated with the 'ProtoMember' attribute (I'm using to serialize), and copy to the same field on the other object.

Then I added some minor details.  Since the server is was just stuffing position data down to the client once per second (slowly for testing), I added some code to do linear interpolation between the server-says and client-says position/orientation/velocity data.  This made it move more smoothly.

On the other hand, that meant I couldn't just directly copy the data; I needed to maintain the original client-value, and copy the server data to another field.  I thought of a few ways to do it, none of them terribly clean.    I decided to use a custom attribute for this.  So in a component (Transform in this example) I'd have:

public Matrix world = Matrix.Identity;

That's the basic world matrix for the entity.  Now I added:

[SyncronizedValue("world"), ProtoIgnore]
public Matrix Syncronizeworld;

(ProtoIgnore means that this field isn't serialized... there is of course no point to sending a field that's only there to hold values to lerp toward on the client).

So when a serialized component is received over the network, the code iterates through the objects fields looking for ProtoMember decorated fields.  For each it finds it iterates through the fields decorated with SynchronizedValue and examines their fieldName value (which is always set to the field they want to synchronize with, i.e. 'world' in the case above).

This is unfortunately O(n2), but the lists should generally be quite small (three or four items at most).  Including all the reflection this is probably a bit slow.  However A) it 'just works' with no additional code, B) this is always on the client, so it isn't like it is adding heavy processing on the server.  So I'll simply go with this for now... I'm sure I can write some much much faster lookup code to do this, but I won't bother unless I know it will be a problem.

public void CopyProtoMemberFields(Object objTo, Object objFrom)
// Get all the fields on the incoming object that we are interested in synching between client/server
// (i.e. fields decorated with ProtoMember attribute so that they will serialize).
FieldInfo[] fromObjectfields = objFrom.GetType().GetFields();
IEnumerable fromObjectFieldInfo = fromObjectfields.Where(pi => pi.GetCustomAttributes(typeof(ProtoBuf.ProtoMemberAttribute), false).Length > 0);

// Now we want to copy those into the destination object.  On each destination object (thus far a Component) there are matching fields
// named Synchronizedxxx, i.e. SynchronizedVelocity or SynchronizedMass.  We will copy the incoming values to those fields.
// The 'Synchronized' fields will be used as a lerp target for the current client values, until they are the same.
// Thus if the server sends us values which do not match our client values (and they generally will not be exact matches due to latency, etc)
// the values on the client will over x frames smoothly come to match the update from the server.
FieldInfo[] syncFieldsOnToObject = objTo.GetType().GetFields();
IEnumerable tobjectSyncFieldInfo = syncFieldsOnToObject.Where(pi => pi.GetCustomAttributes(typeof(SyncronizedValue), false).Length > 0);

bool valueFound = false;
foreach (FieldInfo fromField in fromObjectFieldInfo)
 foreach (FieldInfo synchField in tobjectSyncFieldInfo)
  System.Attribute[] attrs = (System.Attribute[])synchField.GetCustomAttributes(typeof(SyncronizedValue), false);

  foreach (System.Attribute attribute in attrs)
   if (attribute is SyncronizedValue)
    if (((SyncronizedValue)attribute).fieldName == fromField.Name)
     synchField.SetValue(objTo, fromField.GetValue(objFrom));
     valueFound = true;

     ((BaseComponent)objTo).SyncPercent = 0;

  if (valueFound)
   valueFound = false;
But that was really just the start.

Normally I don't want to be sending positions back and forth (well, authoritative positions will never be sent from the client).  So today I sat down and wrote the code to synchronize commands rather than data.

In the entity component system I'm using ( each entity has a CommandQueue component.  As stuff is happening Commands get enqueued.  One of the systems that processes over the entities in the simulation engine is the QueueProcessingSystem.  This system processes any entities with a CommandQueue, going through each, dequeuing commands and processing them (currently it only does movement commands).

 This calls the MovementSystem.ProcessCommand, which handles making changes to the entity's transform and physics components.  Then (if the command didn't originate from the network), the simulationEngine adds that command to the entity's NetworkSync component.

 When the NetworkSync system processes over every entity with a NetworkSync component, it examines them for queued commands or data, and sends that to the appropriate Border Server, which then forwards the data on to the user.  So far it seems to be working well, data is flowing nicely back and forth between client and server.

The last thing I just added was that the server dumps all the active entities in the zone to the client after the client joins.  There were some problems with this.  I had the system set up such that:

Client requests to join a zone -> Server adds user/client to zone and replies with success/failure

In the second step there I was (if success) having the server start dumping entity data.  However, it wasn't working.  Turned out the old scene was requesting the new zone (at the click of a button).  If it got 'success' then it would create the appropriate scene object, add it as the active scene, then remove itself.

There is obviously the potential for timing problems there, but it was actually worse.  The old scene merely adds the new one to a short list of scenes to add (you can have multiple at once active).  The scene is actually initialized in the main Update function, rather than immediately.  This is done to keep scene switches synchronous with processing, rather than asynchronous on the network threads.

A lot simpler that way, but I hadn't remembered I'd done that so it lead to some frustration.  Ok, quite a bit, till after stepping through things I realized the new scene wasn't processing the FULL_ENTITY_UPDATE messages because... well it wasn't initialized yet.  Duh.

So I added

ZoneReadyOnClient zoneReady = new ZoneReadyOnClient();
zoneReady.messageType = BaseMessageMessageType.ZONE_READY_ON_CLIENT;
RogueMoonGame.NetClient.SendGameDataMessage(zoneReady, true); 

To the end of the scene init... now once it is up and ready to receive data it requests the entity dump.  And THAT works.  Whew.

No comments:

Post a Comment