Another frequently asked question is how to handle serialization for classes which are inherited by multiple other classes. These are often used to allow RPCs to use the base class as a parameter, while permitting other inheriting types to be used as arguments. This approach is similar to Interface serializers.
Class Example
Here is an example of a class you want to serialize, and two other types which inherit it.
publicclassItem:ItemBase{publicstring ItemName;}publicclassWeapon:Item{publicint Damage;}publicclassCurrency:Item{publicbyte StackSize;}//This is a wrapper to prevent endless loops in//your serializer. Why this is used is explained//further down.publicabstractclassItemBase {}
Using an RPC which can take all of the types above might look something like this.
publicvoidDoThing(){Weapon wp =newWeapon() { Itemname ="Dagger", Damage =50, };ObsSendItem(wp);}[ObserversRpc]privatevoidObsSendItem(ItemBase ib){ //You could check for other types or just convert it without checks //if you know it will be Weapon. //EG: Weapon wp = (Weapon)ib;if (ib isWeapon wp)Debug.Log($"Recv: Item name {wp.ItemName}, damage value {wp.Damage}.");}
Creating The Writer
Since you are accepting ItemBase through your RPC you must handle the different possibilities of what is being sent. Below is a serializer which does just that.
When using this approach it is very important that you check for the child-most types first.
For example: Weapon is before Item, and so is Currency, so those two are checked first. Just as if you had Melee : Weapon, then Melee would be before Weapon, and so on.
publicstaticvoidWriteItembase(thisWriter writer,ItemBase ib){if (ib isWeapon wp) { // 1 will be the identifer for the reader that this is Weapon.writer.WriteByte(1); writer.Write(wp); }elseif (ib isCurrency cc) {writer.WriteByte(2)writer.Write(cc); }elseif (ib isItem it) {writer.WriteByte(3)writer.Write(it); }}publicstaticItemBaseReadItembase(thisReader reader){byte clsType =reader.ReadByte(); //These are still in order like the write method, for //readability, but since we are using a clsType indicator //the type is known so we can just compare against the clsType.if (clsType ==1)returnreader.Read<Weapon>();elseif (clsType ==2)returnreader.Read<Currency>();elseif (clsType ==1)returnreader.Read<Item>(); //Unhandled, this would probably result in read errors.elsereturnnull;}
You can still create custom serializers for individual classes in addition to encapsulating ones as shown! If for example you had a custom serializer for Currency then using the code above would use your serializer for Currency rather than the one Fish-Networking generates.
Finally, disclosing why we made the ItemBase class. The sole purpose of ItemBase is to prevent an endless loop in the reader. Imagine if we were able to return only Item, and we were also using that as our base. Your reader might look like this...
publicstaticItemReadItem(thisReader reader){byte clsType =reader.ReadByte(); //These are still in order like the write method, for //readability, but since we are using a clsType indicator //the type is known so we can just compare against the clsType.if (clsType ==1)returnreader.Read<Weapon>();elseif (clsType ==2)returnreader.Read<Currency>();elseif (clsType ==1)returnreader.Read<Item>(); //Unhandled, this would probably result in read errors.elsereturnnull;}
The line return reader.Read<Item>(); is the problem. By calling read on the same type as the serializer you would in result call the ReadItem method again, and then the line return reader.Read<Item>(); and then ReadItem again, and then, well you get the idea.
Having a base class, in our case ItemBase, which cannot be returned ensures no endless loop.