I'm about to do something crazy! I'm going to hack in Composite Views for additive loading.

Options
Hey all, I am about to begin hacking at the source code and was hoping to touch base quickly to see if I'm not loosing my mind. What I'm hoping I might find here is an audit, in case it saves me lots of sweat and tears. This post also might help others who are trying to solve the same problem.

The problem:
  1. I have an open world game with additive scene loading.
  2. For various reasons I want to pre-place my entities in the scenes. This means pre-assigning views.
  3. This means tons of views and worse, conflicts between scenes when photon manually re-assigns view ids by the hundreds.
  4. This also means that when a scene is additively loaded, if the incoming view IDs are already used for some reason, the game state is knocked out of sync as they are re-assigned differently on each client.
  5. This ALSO means that while making tiny changes post release, if I screw up even a tiny bit, multiple scenes may be changed unnecessarily, meaning that our nice simple content update pipeline is as good as garbage.


The solution:
Use One single view per scene, and channel all synchronization and RPCs through that view. This way all views and IDs are created and set up before the game is built. nothing is ever created or assigned at runtime. This would require:
  1. Create a "CompositePhotonView" object for each scene that has a reference to all objects that need updating. This object is only different in that when it's serialized and the List<object> is generated, stores a byte as the first serialized object to remember which object the data is associated with on DeserializeView
  2. Edit RunViewUpdate() so that the serialization code checks to see if it's got a CompositePhotonView, in which case it iterates through the individually watched components. SerializeViewBatch system should work as normal here.
  3. RPCs need to store an additional byte so that they're assigned to the correct object.

That should work. I'm going to try and implement it now. If someone can tell me this is crazy, that would be wonderful. Otherwise, I'll post with my findings.

Comments

  • OverTheMoon
    edited June 2020
    Options
    Okay! Took me a little bit, but it's working now. I've got a single view for each scene, and many objects that channel their data through that view. For anyone who finds this thread and is trying to do something similar, here's what I did, more or less:

    First, The CompositePhotonView was created:
    1. Each scene has one parent object that everything sits under, with a CompositePhotonView component. This object basically inherits from PhotonView and overrides a few things, like Serialize and Deserialize.
    2. I implemented a CompositePhotonViewHandler, that works kinda like the PhotonViewHandler. When the scene's hierarchy is updated, the composite view searches through all its children for PhotonTransformViews, and adds them to its observed component list. This way I don't need to hook anything up.
    3. The same goes for RPCs, with one caveat - I made a blank interface called "RPCSender" and any monobehaviour that can send an RPC at any time gets this interface. I can then maintain a similar list of RPCS.
    4. I had to hack Photon's RPC code to add a "CompositeViewID" value that gets sent out with every RPC. default RPCs send a default value of (byte)255, whereas my CompositePhotonView sends out the index it has for the MonoBehaviour that wants to send the RPC.

    Now that the Composite View has been created, the I had to hack Photon's serialization batching a bit:
    1. From PhotonNetworkPart, inside RunViewUpdate, I check if the view is a Composite view and if so, I run the serialization code (get a batch container, serialize the view, send the batch if it's full) for each of the CompositePhotonView's observed components.
    2. The CompositePhotonView serializes one component at a time, and the first thing it does is load the stream with an int that marks which component index the outgoing List<object> applies to.

    Huge caveat here! OnSerializeWrite writes and re-writes to the same List<object> by default, which completely screws everything up. Because Lists are a reference type, each time one of the view's components is serialized, it'll re-write the old entry, which will send out a batch of the exactly same List<object> filled with the data from the last entry.

    Lastly, I didn't have to change any of the Deserialization code. My CompositePhotonView just reads the index I stored above and updates the appropriate component, instead of iterating through all of them like the default PhotonView does.

    There's a few little details and hackeries I had to do to get this working as well, but they're negligible so I won't mention them.