新增FishNet文档

This commit is contained in:
2026-09-20 15:53:11 +08:00
parent c403145db9
commit 179c282405
431 changed files with 15476 additions and 0 deletions
@@ -0,0 +1,98 @@
---
description: >-
Custom serializers are useful where an automatic serializer may not be
possible, or where you want data to be serialized in a specific manner.
---
# Custom Serializers
When creating a custom serializer there are a few important things to remember. When you follow the proper steps your custom serializer will be found and used by Fish-Networking. Your custom serializers can also override automatic serializers, but not included ones.
* [x] Your method must be static, and within a static class.
* [x] Writing method names must begin with _Write_.
* [x] Reading method names must begin with _Read_.
* [x] The first parameter must be _this Writer_ for writers, and _this Reader_ for readers.
* [x] Data must be read in the same order it is written.
Although _Vector2_ is already supported, the example below uses a Vector2 for simplicity sake.
```csharp
// Write each axis of a Vector2.
public static void WriteVector2(this Writer writer, Vector2 value)
{
writer.WriteSingle(value.x);
writer.WriteSingle(value.y);
}
// Read and return a Vector2.
public static Vector2 ReadVector2(this Reader reader)
{
return new Vector2()
{
x = reader.ReadSingle(),
y = reader.ReadSingle()
};
}
```
Custom serializers are more commonly used for conditional situations where what you write may change depending on the data values. Here is a more complex example where certain data is only written when it's needed.
```csharp
/* This is the type we are going to write.
* We will save data and populate default values
* by not writing energy/energy regeneration if
* the enemy does not have energy. */
public struct Enemy
{
public bool HasEnergy;
public float Health;
public float Energy;
public float EnergyRegeneration;
}
public static void WriteEnemy(this Writer writer, Enemy value)
{
writer.WriteBoolean(value.HasEnergy);
writer.WriteSingle(value.Health);
// Only need to write energy and energy regeneration if HasEnergy is true.
if (value.HasEnergy)
{
writer.WriteSingle(value.Energy);
writer.WriteSingle(value.EnergyRenegeration);
}
}
public static Enemy ReadEnemy(this Reader reader)
{
Enemy e = new Enemy();
e.HasEnergy = reader.ReadBoolean();
e.Health = reader.ReadSingle();
// If there is energy also read energy values.
if (e.HasEnergy)
{
e.Energy = reader.ReadSingle();
e.EnergyRenegeration = reader.ReadSingle();
}
return e;
}
```
Often when creating a custom serializer you want to use it across your entire project, and all assemblies. Without taking any action further your custom serializer would **only be used on the assembly it is written.** Presumably, that's probably not what you want.
But making a custom serializer work across all assemblies is very simple. Simply add the \[UseGlobalCustomSerializer] attribute of the type your custom serializer is for, and done!
Example:
```csharp
[UseGlobalCustomSerializer]
public struct Enemy
{
public bool HasEnergy;
public float Health;
public float Energy;
public float EnergyRegeneration;
}
```
@@ -0,0 +1,135 @@
---
description: Learn how to serialize classes and any their children classes.
---
# Inheritance Serializers
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 ](../../network-communication/remote-procedure-calls.md)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.](interface-serializers.md)
## Class example
Here is an example of a class you want to serialize, and two other types which inherit it.
```csharp
public class Item : ItemBase
{
public string ItemName;
}
public class Weapon : Item
{
public int Damage;
}
public class Currency : Item
{
public byte StackSize;
}
/* This is a wrapper to prevent endless loops in
* your serializer. Why this is used is explained
* further down. */
public abstract class ItemBase {}
```
Using an RPC which can take all of the types above might look something like this.
```csharp
public void DoThing()
{
Weapon wp = new Weapon()
{
Itemname = "Dagger",
Damage = 50,
};
ObsSendItem(wp);
}
[ObserversRpc]
private void ObsSendItem(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 is Weapon 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.
{% hint style="warning" %}
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.
{% endhint %}
```csharp
public static void WriteItembase(this Writer writer, ItemBase ib)
{
if (ib is Weapon wp)
{
// 1 will be the identifer for the reader that this is Weapon.
writer.WriteByte(1);
writer.Write(wp);
}
else if (ib is Currency cc)
{
writer.WriteByte(2)
writer.Write(cc);
}
else if (ib is Item it)
{
writer.WriteByte(3)
writer.Write(it);
}
}
public static ItemBase ReadItembase(this Reader 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)
return reader.Read<Weapon>();
else if (clsType == 2)
return reader.Read<Currency>();
else if (clsType == 3)
return reader.Read<Item>();
// Unhandled, this would probably result in read errors.
else
return null;
}
```
{% hint style="success" %}
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.
{% endhint %}
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...
```csharp
public static Item ReadItem(this Reader 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)
return reader.Read<Weapon>();
else if (clsType == 2)
return reader.Read<Currency>();
else if (clsType == 3)
return reader.Read<Item>();
// Unhandled, this would probably result in read errors.
else
return null;
}
```
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.
@@ -0,0 +1,140 @@
# Interface Serializers
## General
Interfaces are very commonly used in most Unity Projects. Since Interfaces are not classes, even if the interface only uses serializable fields, a custom serializer is still needed in order for SyncTypes, and RPCs to serialize them properly over the network.
***
## Serializing the interfaces entire class
In most cases you will want to interrogate an Interface as what its class type is, and serialize the entire types class over the network. This allows you to interrogate the interface later on the receiving client/server and have the data match what the sender has at the time it was sent as well.
If the Interface is a NetworkBehaviour you might as well send it as one because Serializing them over the network is only sending an ID for the receiving client to look up. Very Little network traffic, and you still get all of the data!
### Creating the writer
Since Interfaces are not classes you must design the writer to be able to interrogate the class the Interface is, and serialize the class over the network. If an interface can be many types of classes, you will need to account for each class the Interface can be.
#### Example
```csharp
public static void WriteISomething(this Writer writer, ISomething som)
{
if(som is ClassA c1)
{
// 1 will be the identifer for the reader that this is a ClassA.
writer.WriteByte(1);
writer.Write(c1);
}
else if(som is ClassB c2)
{
// 2 will be the identifier for the reader that this is a ClassB.
writer.WriteByte(2)
writer.Write(c2);
}
}
```
### Creating the reader
When reading the interface, we have to read the byte that identifies what class the interface actually is first. Then use the reader to read that classes data. Finally casting it as the interface we need.
#### Example
```csharp
public static ISomething ReadISomething(this Reader reader)
{
// Gets the byte of what class type we should be reading the next bit of data as.
byte clsType = reader.ReadByte();
// Remember we assigned 1 to be ClassA.
if(clsType == 1)
return reader.Read<ClassA>();
// And 2 for ClassB.
else if(clsType == 2)
return reader.Read<ClassB>();
// Fall through, unhandled. This would be bad.
return default;
}
```
***
## Serializing only the interfaces properties
Sometimes you may only want to serialize just the Interface properties over the network, just keep in mind that if you cast it as the Type it actually is on the receiving client, the values of fields not apart of the interface will be their default values!
### Creating the writer
You still will have to use an Identifier to send what class the Interface is, but we will not be sending the entire class over the network. Just the Interface Properties.
#### Example
```csharp
public interface ISomething
{
string Name;
int Health;
ushort Level;
}
```
```csharp
public static void WriteISomething(this Writer writer, ISomething som)
{
// Defining a blank Class Type Indentifier
byte clsType = 0; // Default
if(som is CustomClass1 cc1)
writer.WriteByte(1);
else if(som is CustomClass2 cc2)
writer.WriteByte(2);
// Fall through, indicating unknown type.
else
writer.WriteByte(0);
// Remember the order the data is written, is the order it must be read.
writer.WriteString(som.Name);
writer.WriteInt32(som.Health);
writer.WriteUInt16(som.Level);
}
```
### Creating the reader
When reading, we will get the class type from the identifier, create a new class, cast the class as the interface, and then assign the custom serialized values to the interface!
#### example
```csharp
public static ISomething ReadISomething(this Reader reader)
{
/* Getting the Class Type Indentifier.
* Read all values first. to clear out the
* reader. */
byte clsType = reader.ReadByte();
string name = reader.ReadString();
int health = reader.ReadInt32();
ushort level = reader.ReadUInt16();
ISomething som = default;
// Check to see what class the interface is
if(clsType == 1)
som = new CustomClass1();
else if(clasType == 2)
som = new CustomClass2();
// Value was not set, so we cannot populate it.
if(som == default(ISomething))
return null;
som.Name = name;
som.Health = health;
som.Level = level;
return som;
}
```