新增FishNet文档
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
description: The following are guides on the many features Fish-Networking provides.
|
||||
---
|
||||
|
||||
# Features
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
description: How to manage prefab and scene addressables with FishNet.
|
||||
---
|
||||
|
||||
# Addressables
|
||||
|
||||
Both addressable scenes and prefabs work over the network.
|
||||
|
||||
## Scene addressables
|
||||
|
||||
Scene addressables utilize Fish-Networking's [Custom Scene Processors](scene-management/custom-scene-processors/). With a few overrides you can implement addressable scenes using [this guide](scene-management/custom-scene-processors/addressables.md).
|
||||
|
||||
## Prefab addressables
|
||||
|
||||
To work reliably each addressables package must have a unique ushort Id, between 1 and 65535. **Never** use 0 as the Id, as the preconfigured SpawnablePrefabs use this Id. You may assign your addressable Ids however you like, for instance using a dictionary that tracks your addressable names with Ids.
|
||||
|
||||
Registering addressable prefabs with Fish-Networking is easy once each package has been given an Id.
|
||||
|
||||
The code below shows one way of loading and unloading addressable prefabs for the network.
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// Reference to your NetworkManager.
|
||||
/// </summary>
|
||||
private NetworkManager _networkManager => InstanceFinder.NetworkManager;
|
||||
/// <summary>
|
||||
/// Used to load and unload addressables in async.
|
||||
/// </summary>
|
||||
private AsyncOperationHandle<IList<GameObject>> _asyncHandle;
|
||||
|
||||
/// <summary>
|
||||
/// Loads an addressables package by string.
|
||||
/// You can load whichever way you prefer, this is merely an example.
|
||||
/// </summary>
|
||||
public IEnumerator LoadAddressables(string addressablesPackage)
|
||||
{
|
||||
/* FishNet uses an Id to identify addressable packages
|
||||
* over the network. You can set the Id to whatever, however
|
||||
* you like. A very simple way however is to use our GetStableHash
|
||||
* helper methods to return a unique key for the package name.
|
||||
* This does require the package names to be unique. */
|
||||
ushort id = addressablesPackage.GetStableHash16();
|
||||
|
||||
/* GetPrefabObjects will return the prefab
|
||||
* collection to use for Id. Passing in true
|
||||
* will create the collection if needed. */
|
||||
SinglePrefabObjects spawnablePrefabs = (SinglePrefabObjects)_networkManager.GetPrefabObjects<SinglePrefabObjects>(id, true);
|
||||
|
||||
/* Get a cache to store networkObject references in from our helper object pool.
|
||||
* This is not required, you can make a new list if you like. But if you
|
||||
* prefer to prevent allocations FishNet has the really helpful CollectionCaches
|
||||
* and ObjectCaches, as well Resettable versions of each. */
|
||||
List<NetworkObject> cache = CollectionCaches<NetworkObject>.RetrieveList();
|
||||
|
||||
/* Load addressables normally. If the object is a NetworkObject prefab
|
||||
* then add it to our cache! */
|
||||
_asyncHandle = Addressables.LoadAssetsAsync<GameObject>(addressablesPackage, addressable =>
|
||||
{
|
||||
NetworkObject nob = addressable.GetComponent<NetworkObject>();
|
||||
if (nob != null)
|
||||
cache.Add(nob);
|
||||
});
|
||||
yield return _asyncHandle;
|
||||
|
||||
/* Add the cached references to spawnablePrefabs. You could skip
|
||||
* caching entirely and just add them as they are read in our LoadAssetsAsync loop
|
||||
* but this saves more performance by adding them all at once. */
|
||||
spawnablePrefabs.AddObjects(cache);
|
||||
|
||||
// Optionally(obviously, do it) store the collection cache for use later. We really don't like garbage!
|
||||
CollectionCaches<NetworkObject>.Store(cache);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads an addressables package by string.
|
||||
/// </summary>
|
||||
public void UnoadAddressables(string addressablesPackage)
|
||||
{
|
||||
// Get the Id the same was as we did for loading.
|
||||
ushort id = addressablesPackage.GetStableHash16();
|
||||
|
||||
/* Once again get the prefab collection for the Id and
|
||||
* clear it so that there are no references of the objects
|
||||
* in memory. */
|
||||
SinglePrefabObjects spawnablePrefabs = (SinglePrefabObjects)_networkManager.GetPrefabObjects<SinglePrefabObjects>(id, true);
|
||||
spawnablePrefabs.Clear();
|
||||
// You may now release your addressables!
|
||||
Addressables.Release(_asyncHandle);
|
||||
}
|
||||
```
|
||||
|
||||
{% hint style="warning" %}
|
||||
When using addressables be sure the client has addressable bundles loaded before the server sends spawn messages for the objects; the above example demonstrates loading addressables.\
|
||||
\
|
||||
For example: if you are going to load a scene with addressables ensure the client has already loaded the addressables bundle for that scene before the server is to add the client to the scene.
|
||||
{% endhint %}
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
description: >-
|
||||
FishNet's code stripping feature allows your server-only code to be
|
||||
automatically stripped from client builds and client-only code to be stripped
|
||||
from server builds.
|
||||
---
|
||||
|
||||
# Code Stripping (Pro Feature)
|
||||
|
||||
## Why use code stripping?
|
||||
|
||||
It helps secure your game server by removing sensitive logic that players shouldn’t see. This makes it harder for cheaters to figure out how the server works and keeps sensitive server data hidden.
|
||||
|
||||
## What gets stripped
|
||||
|
||||
FishNet Pro can remove server code from [non-server builds](#user-content-fn-1)[^1] and client code from [server builds](#user-content-fn-2)[^2]. The code it strips is code within [RPCs](network-communication/remote-procedure-calls.md), code marked with the [Server or Client attributes](server-and-client-identification/executing-on-server-or-client.md#method-attributes), as well as server and client only callbacks such as [OnStartServer/OnStartClient](networked-gameobjects-and-scripts/network-behaviour-guides.md#callbacks).
|
||||
|
||||
## How to use it
|
||||
|
||||
1. To use FishNet's code stripping you will firstly need to have **FishNet Pro** installed, you can find details for how to install it here: [upgrading-to-fishnet-pro.md](../updating-fishnet/upgrading-to-fishnet-pro.md "mention")
|
||||
2. Next, you can navigate to the **FishNet Configuration menu** through the Unity toolbar: 
|
||||
|
||||
<div align="center" data-with-frame="true"><figure><img src="../../.gitbook/assets/toolbar-menu.png" alt=""><figcaption><p><strong>(Tools → Fish-Networking → Configuration)</strong></p></figcaption></figure></div>
|
||||
3. On the Configuration page you will see the option to **"Strip Release Builds"**, enable it. 
|
||||
|
||||
<div data-with-frame="true"><figure><img src="../../.gitbook/assets/code-stripping-enabled.png" alt=""><figcaption></figcaption></figure></div>
|
||||
4. There is now the **"Stripping Type"** option that you can switch between **Redirect** and **Empty\_Experimental**. **Redirect** will replace the methods and calls with a dummy method that doesn't contain any of the logic, while **Empty\_Experimental** will keep the same method, but will strip its method body, leaving it empty. You can use either option you want to.
|
||||
5. Now you will need your game to recompile its scripts, you can trigger this by changing something in your code. If your code is in a separate assembly, be sure to trigger that assembly to recompile as well. This is only needed after you change the FishNet stripping settings.
|
||||
6. That's all you need to do, you can now build your game, and FishNet will automatically strip the code for you.
|
||||
|
||||
## Confirming it worked
|
||||
|
||||
It's always a good idea to check if the sensitive code was indeed stripped from the builds. For Unity Mono builds you can easily do so using a tool such as [ILSpy](https://github.com/icsharpcode/ILSpy).
|
||||
|
||||
You can download the above-mentioned tool or another of your choosing. Then you'll want to run it and load the chosen assembly in your built game's files. If you find your script, you should see how FishNet stripped its code according to the build.
|
||||
|
||||
In the picture below you can see how the `ClientMethod` has had its method body emptied in this server build.
|
||||
|
||||
<figure><img src="../../.gitbook/assets/ilspy-stripping.png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
[^1]: Server builds are considered builds made using Unity's "Dedicated server build profile" or its "headless mode" option in older Unity versions.\
|
||||
Any other builds are considered non-server builds.
|
||||
|
||||
[^2]: Server builds are considered builds made using Unity's "Dedicated server build profile" or its "headless mode" option in older Unity versions.␍ \
|
||||
Any other builds are considered non-server builds.
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
description: >-
|
||||
Serialization and deserialization of data for the purpose of sending it across
|
||||
the network.
|
||||
---
|
||||
|
||||
# Data Serialization
|
||||
|
||||
Anytime you use a type within a [communication ](../../high-level-overview/terminology/communicating.md)Fish-Networking automatically recognizes you wish to send the type over the network, and will create a serializer for it. FishNet will only attempt to automatically serialize public fields and properties. You do not need to perform any extra steps for this process, but if you would like to exclude such fields from being serialized use `[ExcludeSerialization]` above the field.
|
||||
|
||||
For example, _Name_ and _Level_ will be sent over the network but not _Victories_.
|
||||
|
||||
```csharp
|
||||
public class PlayerStat
|
||||
{
|
||||
public string Name;
|
||||
public int Level;
|
||||
[ExcludeSerialization]
|
||||
public int Victories;
|
||||
}
|
||||
|
||||
[ServerRpc]
|
||||
public void RpcPlayerStats(PlayerStat stats){}
|
||||
```
|
||||
|
||||
Fish-Networking is also capable of serializing inherited values. In the type _MonsterStat_ below, _Health, Name, and Level_ will automatically serialize.
|
||||
|
||||
```csharp
|
||||
public class Stat
|
||||
{
|
||||
public float Health;
|
||||
}
|
||||
public class MonsterStat : Stat
|
||||
{
|
||||
public string Name;
|
||||
public int Level;
|
||||
}
|
||||
```
|
||||
|
||||
In very rare cases a data type cannot be automatically serialized; a _Sprite_ is a good example of this. It would be very difficult and expensive to serialize the actual image data and send that over the network. Instead, you could store your sprites in a collection and send the collection index, or perhaps you could create a [custom serializer](custom-serializers-guides/).
|
||||
|
||||
{% hint style="success" %}
|
||||
FishNet should be able to automatically serialize many basic C# and Unity types as well as classes and structs made with serializable types.
|
||||
{% endhint %}
|
||||
|
||||
### The ExcludeSerialization attribute
|
||||
|
||||
{% tabs %}
|
||||
{% tab title="Description" %}
|
||||
This attribute is part of the `FishNet.CodeGenerating` namespace, and may be used to prevent a field from being serialized by FishNet's automatic serializer and thus sent over the network when using your own types. 
|
||||
{% endtab %}
|
||||
|
||||
{% tab title="Example" %}
|
||||
This simple example shows how to use **ExcludeSerialization** to _hide_ a field from FishNet's automatic serialization.
|
||||
|
||||
```csharp
|
||||
public class PlayerStats
|
||||
{
|
||||
public float Health;
|
||||
public float MoveSpeed;
|
||||
/* In this example ControllerIndex is only used
|
||||
* for local multiplayer. This data does not need to be sent
|
||||
* over the network so it's been marked with ExcludeSerialization. */
|
||||
[ExcludeSerialization]
|
||||
public int ControllerIndex;
|
||||
}
|
||||
```
|
||||
{% endtab %}
|
||||
{% endtabs %}
|
||||
+98
@@ -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;
|
||||
}
|
||||
```
|
||||
+135
@@ -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.
|
||||
+140
@@ -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;
|
||||
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
# InstanceFinder
|
||||
|
||||
There is a lot of useful information you can get from [NetworkBehaviours ](../../fishnet-building-blocks/components/network-behaviour-components/)but there may be some cases that your script does not inherit from NetworkBehaviour. This is where InstanceFinder can help you out.
|
||||
|
||||
InstanceFinder provides you quick access to commonly needed references or information. Some examples are: [SceneManager](../../fishnet-building-blocks/components/managers/scenemanager.md), IsClientStarted, [TimeManager](../../fishnet-building-blocks/components/managers/time-manager.md), and [more](https://fish-networking.com/FishNet/api/api/FishNet.InstanceFinder.html#properties).
|
||||
|
||||
```csharp
|
||||
public class MyButton : MonoBehaviour
|
||||
{
|
||||
public Image ButtonImage;
|
||||
|
||||
/* It wouldn't make sense to update the UI
|
||||
* color on the server since it will have
|
||||
* no graphical interface. To save performance
|
||||
* we're only going to update color if client.
|
||||
* However, since this is a MonoBehaviour class
|
||||
* you do not have access to base.IsClientStarted.
|
||||
* Instead the InstanceFinder may be used. */
|
||||
private void SetColor(Color c)
|
||||
{
|
||||
if (InstanceFinder.IsClientStarted)
|
||||
ButtonImage.color = c;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
# Lag Compensation
|
||||
|
||||
Lag compensation, also known as collider rollback, is the act of placing colliders back in time on the server to provide accurate raycast hit detection regardless of client latency. 
|
||||
@@ -0,0 +1,227 @@
|
||||
# Projectiles
|
||||
|
||||
Projectiles which move over time can have lag hidden by accelerating the projectiles for the server and spectators, while allowing the firing client to show the projectile immediately.
|
||||
|
||||
Another bonus to this approach is you are not networking the projectile movement each tick, which saves tremendously on bandwidth and performance.
|
||||
|
||||
Here is an example of what this guide will cover. Notice how even though the client has a 220ms ping the projectile is still aligned on the server. Other clients would align the projectile as well with the same guide code.
|
||||
|
||||
<figure><img src="../../../.gitbook/assets/predicted-projectile-example.gif" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
First the local client, or owning client, fires the projectile. The projectile is spawned locally, then the client tells the server to also fire the projectile. The MAX\_PASSED\_TIME constant is covered in the next code snippet.
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// Projectile to spawn.
|
||||
/// </summary>
|
||||
[Tooltip("Projectile to spawn.")]
|
||||
[SerializeField]
|
||||
private PredictedProjectile _projectile;
|
||||
/// <summary>
|
||||
/// Maximum amount of passed time a projectile may have.
|
||||
/// This ensures really laggy players won't be able to disrupt
|
||||
/// other players by having the projectile speed up beyond
|
||||
/// reason on their screens.
|
||||
/// </summary>
|
||||
private const float MAX_PASSED_TIME = 0.3f;
|
||||
|
||||
/// <summary>
|
||||
/// Local client fires weapon.
|
||||
/// </summary>
|
||||
private void ClientFire()
|
||||
{
|
||||
Vector3 position = transform.position;
|
||||
Vector3 direction = transform.forward;
|
||||
|
||||
/* Spawn locally with 0f passed time.
|
||||
* Since this is the firing client
|
||||
* they do not need to accelerate/catch up
|
||||
* the projectile. */
|
||||
SpawnProjectile(position, direction, 0f);
|
||||
// Ask server to also fire passing in current Tick.
|
||||
ServerFire(position, direction, base.TimeManager.Tick);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns a projectile locally.
|
||||
/// </summary>
|
||||
private void SpawnProjectile(Vector3 position, Vector3 direction, float passedTime)
|
||||
{
|
||||
PredictedProjectile pp = Instantiate(_projectile, position, Quaternion.identity);
|
||||
pp.Initialize(direction, passedTime);
|
||||
}#
|
||||
```
|
||||
|
||||
When the server receives the fire request it will calculate how long it took the client to send the fire message using the provided tick. Later on the projectile will be accelerated based on this time passed.
|
||||
|
||||
{% hint style="info" %}
|
||||
You will notice we aren't calling `Spawn` for the projectiles, that is because they aren't NetworkObjects, just regular game objects. Do not make the PredictedProjectile script a NetworkBehaviour, or give it any network components.
|
||||
{% endhint %}
|
||||
|
||||
After the _passedTime_ is calculated, spawn the projectile on the server, then tell spectators to also spawn the projectile; spectators being other clients.
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// Fires on the server.
|
||||
/// </summary>
|
||||
/// <param name="position">Position to spawn projectile.</param>
|
||||
/// <param name="direction">Direction to move projectile.</param>
|
||||
/// <param name="tick">Tick when projectile was spawned on client.</param>
|
||||
[ServerRpc]
|
||||
private void ServerFire(Vector3 position, Vector3 direction, uint tick)
|
||||
{
|
||||
/* You may want to validate position and direction here.
|
||||
* How this is done depends largely upon your game so it
|
||||
* won't be covered in this guide. */
|
||||
|
||||
// Get passed time. Note the false for allow negative values.
|
||||
float passedTime = (float)base.TimeManager.TimePassed(tick, false);
|
||||
/* Cap passed time at half of constant value for the server.
|
||||
* In our example max passed time is 300ms, so server value
|
||||
* would be max 150ms. This means if it took a client longer
|
||||
* than 150ms to send the rpc to the server, the time would
|
||||
* be capped to 150ms. This might sound restrictive, but that would
|
||||
* mean the client would have roughly a 300ms ping; we do not want
|
||||
* to punish other players because a laggy client is firing. */
|
||||
passedTime = Mathf.Min(MAX_PASSED_TIME / 2f, passedTime);
|
||||
|
||||
// Spawn on the server.
|
||||
SpawnProjectile(position, direction, passedTime);
|
||||
// Tell other clients to spawn the projectile.
|
||||
ObserversFire(position, direction, tick);
|
||||
}
|
||||
```
|
||||
|
||||
Observers use the same technique to calculate the passed time and spawn the projectile. There are a few small things to note.
|
||||
|
||||
First, this RPC is sent to everyone but the owner. The owner does not need to receive the RPC because it already spawned the projectile locally.
|
||||
|
||||
Second, the passed time calculation is not limited by half. This is to support the maximum possible passed time. This is not a requirement but is recommended.
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// Fires on all clients but owner.
|
||||
/// </summary>
|
||||
[ObserversRpc(ExcludeOwner = true)]
|
||||
private void ObserversFire(Vector3 position, Vector3 direction, uint tick)
|
||||
{
|
||||
// Like on server get the time passed and cap it. Note the false for allow negative values.
|
||||
float passedTime = (float)base.TimeManager.TimePassed(tick, false);
|
||||
passedTime = Mathf.Min(MAX_PASSED_TIME, passedTime);
|
||||
|
||||
// Spawn the projectile locally.
|
||||
SpawnProjectile(position, direction, passedTime);
|
||||
}
|
||||
```
|
||||
|
||||
With the projectile spawned all that's left is showing how to use the calculated passed time. You likely noticed the `SpawnProjectile` method was initializing the projectile with some values. Here's what that looks like:
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// Direction to travel.
|
||||
/// </summary>
|
||||
private Vector3 _direction;
|
||||
/// <summary>
|
||||
/// Distance remaining to catch up. This is calculated from a passed time and move rate.
|
||||
/// </summary>
|
||||
private float _passedTime = 0f;
|
||||
/// <summary>
|
||||
/// In this example the projectile moves at a flat rate of 5f.
|
||||
/// </summary>
|
||||
private const float MOVE_RATE = 5f;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes this projectile.
|
||||
/// </summary>
|
||||
/// <param name="direction">Direction to travel.</param>
|
||||
/// <param name="passedTime">How far in time this projectile is behind te prediction.</param>
|
||||
public void Initialize(Vector3 direction, float passedTime)
|
||||
{
|
||||
_direction = direction;
|
||||
_passedTime = passedTime;
|
||||
}
|
||||
```
|
||||
|
||||
After initializing with the specified passed time and direction all that's left to accelerate the projectile is a move method. The provided example is a very basic implementation of a move method, while also applying the acceleration.
|
||||
|
||||
If there is passed time to apply, then additional delta is added using the `passedTimeDelta` variable.
|
||||
|
||||
The percentage applied when assigning the _step_ variable decides how fast your projectile will catch up to the predicted value. In this code I am using 8% of the passed time per Move call. Higher values will result in the projectile catching up faster.
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// Move the projectile each frame. This would be called from Update.
|
||||
/// </summary>
|
||||
private void Move()
|
||||
{
|
||||
// Frame delta, nothing unusual here.
|
||||
float delta = Time.deltaTime;
|
||||
|
||||
// See if to add on additional delta to consume passed time.
|
||||
float passedTimeDelta = 0f;
|
||||
if (_passedTime > 0f)
|
||||
{
|
||||
/* Rather than use a flat catch up rate the
|
||||
* extra delta will be based on how much passed time
|
||||
* remains. This means the projectile will accelerate
|
||||
* faster at the beginning and slower at the end.
|
||||
* If a flat rate was used then the projectile
|
||||
* would accelerate at a constant rate, then abruptly
|
||||
* change to normal move rate. This is similar to using
|
||||
* a smooth damp. */
|
||||
|
||||
/* Apply 8% of the step per frame. You can adjust
|
||||
* this number to whatever feels good. */
|
||||
float step = (_passedTime * 0.08f);
|
||||
_passedTime -= step;
|
||||
|
||||
/* If the remaining time is less than half a delta then
|
||||
* just append it onto the step. The change won't be noticeable. */
|
||||
if (_passedTime <= (delta / 2f))
|
||||
{
|
||||
step += _passedTime;
|
||||
_passedTime = 0f;
|
||||
}
|
||||
passedTimeDelta = step;
|
||||
}
|
||||
|
||||
// Move the projectile using moverate, delta, and passed time delta.
|
||||
transform.position += _direction * (MOVE_RATE * (delta + passedTimeDelta));
|
||||
}
|
||||
```
|
||||
|
||||
Very last, keep in mind these projectiles are fired locally and are not networked. Because of this you may want to perform different actions based on if client or server. The code below demonstrates what a collision event might look like.
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// Handles collision events.
|
||||
/// </summary>
|
||||
private void OnCollisionEnter(Collision collision)
|
||||
{
|
||||
/* These projectiles are instantiated locally, as in,
|
||||
* they are not networked. Because of this there is a very
|
||||
* small chance the occasional projectile may not align with
|
||||
* 100% accuracy. But, the differences are generally
|
||||
* insignifcant and will not affect gameplay. */
|
||||
|
||||
// If client show visual effects, play impact audio.
|
||||
if (InstanceFinder.IsClient)
|
||||
{
|
||||
// Show VFX.
|
||||
// Play Audio.
|
||||
}
|
||||
// If server check to damage hit objects.
|
||||
if (InstanceFinder.IsServer)
|
||||
{
|
||||
PlayerShip ps = collision.gameObject.GetComponent<PlayerShip>();
|
||||
/* If a player ship was hit then remove 50 health.
|
||||
* The health value can be synchronized however you like,
|
||||
* such as a syncvar. */
|
||||
if (ps != null)
|
||||
ps.Health -= 50f;
|
||||
}
|
||||
|
||||
// Destroy projectile (probably pool it instead).
|
||||
Destroy(gameObject);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,70 @@
|
||||
# Raycast
|
||||
|
||||
{% hint style="info" %}
|
||||
Fish-Networking [Pro](../../../overview/readme/pro-projects-and-support.md) is required for raycast lag compensation. Over-time projectiles may use another approach and do not require Pro. See [Projectiles ](projectiles.md)for more information.
|
||||
{% endhint %}
|
||||
|
||||
This feature can be setup easily in a few steps.
|
||||
|
||||
To begin I am entering the ColliderRollbackDemo scene included with Fish-Networking Pro. You do not need to open the scene, but if you wish to follow along visually you may.
|
||||
|
||||
## Hitboxes
|
||||
|
||||
In my scene is an object named EnemySetup. The screen capture below shows two boxes on the 'enemy'. One for the body, and one for the head.
|
||||
|
||||
<figure><img src="../../../.gitbook/assets/collider-rollback-hitboxes.png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
Each hitbox must be on a new object. Generally speaking, the hitbox would be a child of whichever component moves. With an actual humanoid rig, you would probably place the head collider on a child beneath the neck bone.
|
||||
|
||||
Notice the 'hitbox' objects are children of the moving component, and only have a collider on them. You may use any collider type.
|
||||
|
||||
<figure><img src="../../../.gitbook/assets/collider-rollback-children-objects.png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
{% hint style="info" %}
|
||||
When using a [Tick Smoother](../../../fishnet-building-blocks/components/tick-smoothers/), ensure that the hitboxes are placed on the "target" objects rather than smoothed objects (the object with the smoother component or one of its children). When a rollback occurs, the server automatically interpolates (lerps) hitboxes between positions, making local smoothing unnecessary. To maintain consistency, set the smoother’s **Flat Interpolation** value to match the **Additional Ticks** configured in your **Rollback Manager**.
|
||||
{% endhint %}
|
||||
|
||||
Next the ColliderRollback script must be added somewhere within your prefab or object. For the sake of simplicity I have mine on the root, with the NetworkObject.
|
||||
|
||||
Once added, specify each object you added as a collider within the **Collider Parents** collection.
|
||||
|
||||
{% hint style="info" %}
|
||||
To make things easier, place the suffix 'Hitbox' on all your hitbox objects. You can then lock the inspector on your ColliderRollback object, search Hitbox, and quickly drag all entries in at once.
|
||||
{% endhint %}
|
||||
|
||||
<figure><img src="../../../.gitbook/assets/collider-rollback-setup.png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
You've completed all the steps required for setting up an object.
|
||||
|
||||
## Rollback Manager
|
||||
|
||||
To utilize the lag compensation you must also add the RollbackManager script to your Networkmanager object. We have a section on the [RollbackManager ](../../../fishnet-building-blocks/components/managers/rollbackmanager-pro-feature.md)component to understand what each field does. Once you review the component settings we can move onto the code.
|
||||
|
||||
## Performing a rollback
|
||||
|
||||
The RollbackManager must know how far back in time to place colliders to obtain accurate hit results. When your client is to fire their weapon you will want to gather the current PreciseTick and include it with your Fire RPC.
|
||||
|
||||
```csharp
|
||||
[Client]
|
||||
private void Fire()
|
||||
{
|
||||
// Use LastPacketTick to get the best tick alignment.
|
||||
PreciseTick pt = base.TimeManager.GetPreciseTick(TickType.LastPacketTick);
|
||||
// Call fire on the server.
|
||||
ServerFire(pt);
|
||||
}
|
||||
|
||||
[ServerRpc]
|
||||
private void ServerFire(PreciseTick pt)
|
||||
{
|
||||
// Rollback using the precise tick sent in.
|
||||
// Using Physics for 3d rollback, Physics3D for 2d rollback.
|
||||
// Both physics types can be used at once.
|
||||
base.RollbackManager.Rollback(pt, RollbackManager.PhysicsType.Physics, base.IsOwner);
|
||||
// Perform your raycast normally.
|
||||
RaycastHit hit;
|
||||
if (Physics.Raycast(transform.position, transform.forward, out hit)) { }
|
||||
// Return the colliders to their proper positions.
|
||||
base.RollbackManager.Return();
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,84 @@
|
||||
# States
|
||||
|
||||
States sometimes need to be synchronized between clients and server to prevent desynchronization. This is generally only true when using client-side prediction. However, it is possible this guide could be used outside prediction so please keep that in mind when reading.
|
||||
|
||||
Ideally when using prediction the client should run all states locally as they would normally occur, just like the server. For example, if you were to drive a ship into the wall and want to apply a slow, the client and server would apply the state immediately when hitting the wall. In such a scenario the states would most likely exist in the replicate and reconcile data. However, there are cases when only a server is aware of a state, and if the server applies the state before the client does a desynchronization will occur. If this is the design you are intending, consider reworking your logic so clients and server run the same simulation instead; otherwise, continue reading!
|
||||
|
||||
A common state that requires synchronizing as example given is something that would impact movement. In this guide I'll be demonstrating on how to apply an EMP.
|
||||
|
||||
Since only the server is aware when the state is applied we will perform a check on the server, using the _base.IsServer_ check. When the condition is satisfied as by our pretend _ApplyEmp()_ method a RPC is sent.
|
||||
|
||||
```csharp
|
||||
|
||||
private void TimeManager_OnTick()
|
||||
{
|
||||
/* We're going to pretend there is
|
||||
* some logic here which happens only on the
|
||||
* server resulting in applying a EMP.
|
||||
*
|
||||
* The EMP is set to apply 50ms in the future.
|
||||
* However many ticks used are up to you. The goal is
|
||||
* to give clients a chance to get the emp tick
|
||||
* before that tick actually runs on their end.
|
||||
* This will have clients all run the EMP at roughly
|
||||
* the same time, keeping in mind there is sometimes a
|
||||
* small margin of error of 1 tick. */
|
||||
if (base.IsServer && ApplyEMP())
|
||||
{
|
||||
uint futureTicks = base.TimeManager.TimeToTicks(0.05f, TickRounding.RoundUp);
|
||||
ObserversSetEMPTick(base.TimeManager.LocalTick + futureTicks);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// RunLocally is used to the server also sets the emp tick.
|
||||
[ObserversRpc(RunLocally = true)]
|
||||
private void ObserversSetEMPTick(uint serverTick)
|
||||
{
|
||||
// Converts the server EMP tick to local tick for this client.
|
||||
_empStartTick = base.TimeManager.TickToLocalTick(serverTick);
|
||||
// Set end tick. In our example the EMP will last 1 second.
|
||||
_empEndTick = _empStartTick + base.TimeManager.TimeToTicks(1f, TickRounding.RoundUp);
|
||||
}
|
||||
```
|
||||
|
||||
When the RPC is received two fields are set, a tick when to start the EMP and when to end it.
|
||||
|
||||
Next in our replicate method if the clients or servers LocalTick is within that EMP duration we prevent any further motor movement. As mentioned this example uses prediction but you could apply this to an Update loop as well if not using prediction.
|
||||
|
||||
```csharp
|
||||
// Tick when to start emp.
|
||||
private uint _empStartTick = uint.MaxValue;
|
||||
// Tick when to end emp.
|
||||
private uint _empEndTick = uint.MaxValue;
|
||||
|
||||
/* This is an example of a replicate data excluding the extra
|
||||
* parameters, given they have no context in this example. */
|
||||
[Replicate]
|
||||
private void Replicate(MotorData md)
|
||||
{
|
||||
uint localTick = base.TimeManager.LocalTick;
|
||||
/* If localTick is between EMP range then return.
|
||||
* It's important to not reset the emp values because
|
||||
* we want these to be the same during replays. If you reset
|
||||
* values soon as the condition of exiting emp was satisfied
|
||||
* then emp would not be properly set during a replay.
|
||||
*
|
||||
* Note that this logic runs on the server and owner,
|
||||
* and if using prediction v2 can run on other clients
|
||||
* as well. */
|
||||
if (localTick >= _empStartTick && localTick <= _empEndTick)
|
||||
{
|
||||
/* Since under emp exit method early
|
||||
* to not process MotorData. In this example
|
||||
* we are using an EMP state, therefor the motor
|
||||
* will not work. You'll adjust this to whatever
|
||||
* your game needs. */
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal motor logic here.
|
||||
}
|
||||
```
|
||||
|
||||
Just like that the client is set to apply the EMP marginally in the future, and as will the server. Yes, this does create a very small delay on when the EMP or state begins but this is often considered acceptable to maintain a reliable simulation. In addition, also as said before, ideally the client will be running the same simulation as the server at all times which would let you avoid this sort of setup.
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
description: >-
|
||||
FishNet's LOD system enables a reduction in network traffic by reducing the
|
||||
network data sent for objects that are further away.
|
||||
---
|
||||
|
||||
# Level of Detail (Pro Feature)
|
||||
|
||||
## What is LOD?
|
||||
|
||||
The Level of Detail (LOD) system is a Pro-tier optimization feature designed to reduce network traffic and improve client-side performance. By intelligently scaling the frequency of data synchronization based on the distance between non-owned objects and the client’s owned objects, the system ensures that nearby interactions remain precise while distant objects consume significantly fewer resources.
|
||||
|
||||
## Global configuration
|
||||
|
||||
The core logic for the LOD system is handled within the [ObserverManager](../../fishnet-building-blocks/components/managers/observermanager/). This is where you can enable the system and define the global "rules" for how updates are throttled.
|
||||
|
||||
You can read up more detail about each available option in the page about the [observermanager](../../fishnet-building-blocks/components/managers/observermanager/ "mention"). By setting your [Level of Detail Distances](../../fishnet-building-blocks/components/managers/observermanager/#level-of-detail-distances), you establish brackets for synchronization: objects within the **closest distance** sync at full speed, while those exceeding your furthest threshold sync at the [Maximum Send Interval](../../fishnet-building-blocks/components/managers/observermanager/#maximum-send-interval).
|
||||
|
||||
<figure><img src="../../.gitbook/assets/observer-manager-lod-example.png" alt="Example of the Observer Manager component with Level of Detail enabled and configured"><figcaption><p>Example LOD values set in the <strong>ObserverManager</strong></p></figcaption></figure>
|
||||
|
||||
## Implementation and usage
|
||||
|
||||
**NetworkObject Settings**
|
||||
|
||||
The LOD system is opt-in at the NetworkObject level. On your NetworkObject component, you will find a [Use Level of Detail](../../fishnet-building-blocks/components/network-object.md#settings-1) toggle. When enabled, this specific object will adhere to globally defined distance rules.
|
||||
|
||||
**Prediction & Performance**
|
||||
|
||||
The LOD system prioritizes the optimization of Reconciles.
|
||||
|
||||
* Traffic Reduction: By sending fewer reconciles for distant objects, you significantly reduce the server's outgoing bandwidth.
|
||||
* Client CPU Gains: Since the client doesn't have to process reconciles for every distant object every frame, local performance is improved.
|
||||
* Movement Consistency: Replicates always continue to send regardless of LOD. This ensures that while an object might sync its state less frequently, it will never simply "stop" moving or disappear.
|
||||
|
||||
## Local state creation (Prediction Manager)
|
||||
|
||||
To prevent "popping" or desyncs when a client rapidly approaches a distant object, the system works in tandem with the [Prediction Manager’s Local State Creation](../../fishnet-building-blocks/components/managers/predictionmanager.md#create-local-states).
|
||||
|
||||
Even if the server hasn't yet recalculated the LOD for an object you just sprinted toward, the client will automatically begin reconciling nearby objects locally. This prevents physical desyncs—such as walking through an object—before the server's next scheduled LOD update. This feature is enabled by default and requires no additional setup.
|
||||
@@ -0,0 +1,120 @@
|
||||
---
|
||||
description: >-
|
||||
You can take advantage of numerous available events to stay informed about the
|
||||
current state of the network.
|
||||
---
|
||||
|
||||
# Network State Events
|
||||
|
||||
{% hint style="info" %}
|
||||
This is not a comprehensive list of available events, but merely some of the most commonly used ones. To view all the available ones check out the [API pages](https://fish-networking.com/FishNet/api/api/index.html).
|
||||
{% endhint %}
|
||||
|
||||
## Server side events
|
||||
|
||||
### [**OnAuthenticationResult**](https://fish-networking.com/FishNet/api/api/FishNet.Managing.Server.ServerManager.html#FishNet_Managing_Server_ServerManager_OnAuthenticationResult)
|
||||
|
||||
| **Name:** | <mark style="color:green;">`ServerManager.OnAuthenticationResult`</mark> |
|
||||
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Namespace:** | <mark style="color:blue;">`FishNet.Managing.Server`</mark> |
|
||||
| **Parameters:** | [<mark style="color:orange;">NetworkConnection</mark>](https://fish-networking.com/FishNet/api/api/FishNet.Connection.NetworkConnection.html)<mark style="color:orange;">, bool</mark> |
|
||||
|
||||
This event is called once a client has either been authenticated or failed to authenticate. The parameters are the relevant [NetworkConnection](server-and-client-identification/networkconnections.md), and a Boolean representing if that client successfully authenticated or not. This event can be very useful to use to store the NetworkConnection of a client that has successfully connected to the server or to send some message to the newly connected client.
|
||||
|
||||
### [**OnServerConnectionState**](https://fish-networking.com/FishNet/api/api/FishNet.Managing.Server.ServerManager.html#FishNet_Managing_Server_ServerManager_OnServerConnectionState)
|
||||
|
||||
| **Name:** | <mark style="color:green;">`ServerManager.OnServerConnectionState`</mark> |
|
||||
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Namespace:** | <mark style="color:blue;">`FishNet.Managing.Server`</mark> |
|
||||
| **Parameters:** | [<mark style="color:orange;">ServerConnectionStateArgs</mark>](https://fish-networking.com/FishNet/api/api/FishNet.Transporting.ServerConnectionStateArgs.html) |
|
||||
|
||||
This event is called when the server's [state ](#user-content-fn-1)[^1]changes. In other words, immediately when the server starts or stops running. This event is very useful for performing network actions on the server such as loading initial scenes or spawning initial objects.
|
||||
|
||||
### [**OnRemoteConnectionState**](https://fish-networking.com/FishNet/api/api/FishNet.Managing.Server.ServerManager.html#FishNet_Managing_Server_ServerManager_OnRemoteConnectionState)
|
||||
|
||||
| **Name:** | <mark style="color:green;">`ServerManager.OnRemoteConnectionState`</mark> |
|
||||
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Namespace:** | <mark style="color:blue;">`FishNet.Managing.Server`</mark> |
|
||||
| **Parameters:** | [<mark style="color:orange;">NetworkConnection</mark>](https://fish-networking.com/FishNet/api/api/FishNet.Connection.NetworkConnection.html)<mark style="color:orange;">,</mark> [<mark style="color:orange;">RemoteConnectionStateArgs</mark>](https://fish-networking.com/FishNet/api/api/FishNet.Transporting.RemoteConnectionStateArgs.html) |
|
||||
|
||||
This event is called when a client's [state ](#user-content-fn-1)[^1]changes with the server, in other words, immediately when a client connects or disconnects from the server. This event is very useful for detecting when a client has disconnected as well as for authentication scripts to handle sending data or checking if the server is full before authenticating a connection.
|
||||
|
||||
***
|
||||
|
||||
## Client side events
|
||||
|
||||
### [**OnAuthenticated**](https://fish-networking.com/FishNet/api/api/FishNet.Managing.Client.ClientManager.html#FishNet_Managing_Client_ClientManager_OnAuthenticated)
|
||||
|
||||
| **Name:** | <mark style="color:green;">`ClientManager.OnAuthenticated`</mark> |
|
||||
| -------------- | ----------------------------------------------------------------- |
|
||||
| **Namespace:** | <mark style="color:blue;">`FishNet.Managing.Client`</mark> |
|
||||
|
||||
This event is called when the local client has successfully been authenticated with the FishNet server. The client will now have a Client ID and be added to the [ServerManager.Clients](https://fish-networking.com/FishNet/api/api/FishNet.Managing.Server.ServerManager.html#FishNet_Managing_Server_ServerManager_Clients) and [ClientManager.Clients](https://fish-networking.com/FishNet/api/api/FishNet.Managing.Client.ClientManager.html#FishNet_Managing_Client_ClientManager_Clients) collections. This is a good event to use to know when your client is fully connected to the server and ready to play the game.
|
||||
|
||||
### [**OnClientConnectionState**](https://fish-networking.com/FishNet/api/api/FishNet.Managing.Client.ClientManager.html#FishNet_Managing_Client_ClientManager_OnClientConnectionState)
|
||||
|
||||
| **Name:** | <mark style="color:green;">`ClientManager.OnClientConnectionState`</mark> |
|
||||
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Namespace:** | <mark style="color:blue;">`FishNet.Managing.Client`</mark> |
|
||||
| **Parameters:** | [<mark style="color:orange;">ClientConnectionStateArgs</mark>](https://fish-networking.com/FishNet/api/api/FishNet.Transporting.ClientConnectionStateArgs.html) |
|
||||
|
||||
This event is called when the local client's [state ](#user-content-fn-1)[^1]changes. In other words, immediately when the client makes or contact with the Fish-Networking server or is disconnected from it. This event is useful for detecting when you are disconnected from the server and when you initially connect. Authentication scripts will often use this event to send initial data to the server so that the server can authenticate the client.
|
||||
|
||||
{% hint style="warning" %}
|
||||
When this event is invoked the client hasn't yet been added to the [ClientManager.Clients](https://fish-networking.com/FishNet/api/api/FishNet.Managing.Client.ClientManager.html#FishNet_Managing_Client_ClientManager_Clients) collection.
|
||||
{% endhint %}
|
||||
|
||||
### [**OnRemoteConnectionState**](https://fish-networking.com/FishNet/api/api/FishNet.Managing.Client.ClientManager.html#FishNet_Managing_Client_ClientManager_OnRemoteConnectionState)
|
||||
|
||||
| **Name:** | <mark style="color:green;">`ClientManager.OnRemoteConnectionState`</mark> |
|
||||
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Namespace:** | <mark style="color:blue;">`FishNet.Managing.Client`</mark> |
|
||||
| **Parameters:** | [<mark style="color:orange;">RemoteConnectionStateArgs</mark>](https://fish-networking.com/FishNet/api/api/FishNet.Transporting.RemoteConnectionStateArgs.html) |
|
||||
|
||||
This event is called when a remote[^2] client's [state ](#user-content-fn-1)[^1]changes. This event is useful for detecting when another player has connected or disconnected from the game.
|
||||
|
||||
{% hint style="info" %}
|
||||
This is only available when using [ServerManager.ShareIds](https://fish-networking.com/FishNet/api/api/FishNet.Managing.Server.ServerManager.html#FishNet_Managing_Server_ServerManager_ShareIds).
|
||||
{% endhint %}
|
||||
|
||||
***
|
||||
|
||||
## Shared events
|
||||
|
||||
### [**OnClientLoadedStartScenes**](https://fish-networking.com/FishNet/api/api/FishNet.Managing.Scened.SceneManager.html#FishNet_Managing_Scened_SceneManager_OnClientLoadedStartScenes)
|
||||
|
||||
| **Name:** | <mark style="color:green;">`SceneManager.OnClientLoadedStartScenes`</mark> |
|
||||
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Namespace:** | <mark style="color:blue;">`FishNet.Managing.Scened`</mark> |
|
||||
| **Parameters:** | [<mark style="color:orange;">NetworkConnection</mark>](https://fish-networking.com/FishNet/api/api/FishNet.Connection.NetworkConnection.html)<mark style="color:orange;">,</mark> <mark style="color:orange;">bool</mark> |
|
||||
|
||||
Called when a client loads the initial scenes after connecting, for example any global networked scenes. \
|
||||
Since this event will be called on the client who loaded the start scenes as well as the server, there is available a Boolean parameter which will be true if the event was called as the server. This can be used on the host player to prevent logic in this method from running twice. This will invoke even if the SceneManager is not used when the client completes fully connecting to the server.
|
||||
|
||||
### [**OnPreTick**](https://fish-networking.com/FishNet/api/api/FishNet.Managing.Timing.TimeManager.html#FishNet_Managing_Timing_TimeManager_OnPreTick)
|
||||
|
||||
| **Name:** | <mark style="color:green;">`TimeManager.OnPreTick`</mark> |
|
||||
| -------------- | ---------------------------------------------------------- |
|
||||
| **Namespace:** | <mark style="color:blue;">`FishNet.Managing.Timing`</mark> |
|
||||
|
||||
This event is called right before a network tick occurs, as well as before data is read.
|
||||
|
||||
### [**OnTick**](https://fish-networking.com/FishNet/api/api/FishNet.Managing.Timing.TimeManager.html#FishNet_Managing_Timing_TimeManager_OnTick)
|
||||
|
||||
| **Name:** | <mark style="color:green;">`TimeManager.OnTick`</mark> |
|
||||
| -------------- | ---------------------------------------------------------- |
|
||||
| **Namespace:** | <mark style="color:blue;">`FishNet.Managing.Timing`</mark> |
|
||||
|
||||
This event is called when a network tick occurs. This can be useful for sending data to the server at a set rate to prevent sending data too frequently and flooding the connection.
|
||||
|
||||
### [**OnPostTick**](https://fish-networking.com/FishNet/api/api/FishNet.Managing.Timing.TimeManager.html#FishNet_Managing_Timing_TimeManager_OnPostTick)
|
||||
|
||||
| **Name:** | <mark style="color:green;">`TimeManager.OnPostTick`</mark> |
|
||||
| -------------- | ---------------------------------------------------------- |
|
||||
| **Namespace:** | <mark style="color:blue;">`FishNet.Managing.Timing`</mark> |
|
||||
|
||||
This event is called just after a network tick occurs; physics would have already been simulated if using [PhysicsMode.TimeManager](../../fishnet-building-blocks/components/managers/time-manager.md).
|
||||
|
||||
[^1]: The available states are **Started** and **Stopped**
|
||||
|
||||
[^2]: This means some client other than the local client itself.
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: >-
|
||||
Read more about the various methods you can use to communicate over the
|
||||
network!
|
||||
---
|
||||
|
||||
# Network Communication
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
# Broadcasts
|
||||
|
||||
Broadcasts allow you to send messages to one or more objects without them requiring a NetworkObject component. This could be useful for communicating between objects which are not necessarily networked, such as a chat system.
|
||||
|
||||
Like [Remote Procedure Calls](remote-procedure-calls.md), broadcasts may be sent reliably or unreliably. Data using broadcasts can be sent from either from the client to the server, or server to client(s). Serializers are automatically generated for Broadcasts as well.
|
||||
|
||||
Broadcasts must be structures, and implement _IBroadcast_. Below demonstrates what values a chat broadcast may possibly contain.
|
||||
|
||||
```csharp
|
||||
public struct ChatBroadcast : IBroadcast
|
||||
{
|
||||
public string Username;
|
||||
public string Message;
|
||||
public Color FontColor;
|
||||
}
|
||||
```
|
||||
|
||||
Since broadcasts are not linked to objects, they must be sent using the [ServerManager](../../../fishnet-building-blocks/components/managers/server-manager.md), or [ClientManager](../../../fishnet-building-blocks/components/managers/clientmanager.md). When sending to the server you will send using ClientManager, and when sending to clients, use ServerManager.
|
||||
|
||||
{% hint style="success" %}
|
||||
The following examples use the InstanceFinder to get a reference to the ClientManager and ServerManager, but you can store a reference yourself, use the NetworkManager's public references or a NetworkBehaviour's.
|
||||
{% endhint %}
|
||||
|
||||
Here is an example of sending a chat message from a client to the server.
|
||||
|
||||
```csharp
|
||||
public void OnKeyDown_Enter(string text)
|
||||
{
|
||||
// Client won't send their username, server will already know it.
|
||||
ChatBroadcast msg = new ChatBroadcast()
|
||||
{
|
||||
Message = text,
|
||||
FontColor = Color.white
|
||||
};
|
||||
|
||||
InstanceFinder.ClientManager.Broadcast(msg);
|
||||
}
|
||||
```
|
||||
|
||||
Sending from the server to client(s) is done very much the same, but you are presented with more options. For a complete list of options I encourage you to view the [API](https://fish-networking.com/FishNet/api/api/FishNet.Managing.Server.ServerManager.html#methods). Here is an example of sending a broadcast to all clients which have visibility of a specific client. This establishes the idea that clientA sends a chat message to the server, and the server relays it to other clients which can see clientA. In this example clientA would also get the broadcast.
|
||||
|
||||
```csharp
|
||||
// When receiving broadcast on the server which connection
|
||||
// sent the broadcast will always be available.
|
||||
public void OnChatBroadcast(NetworkConnection conn, ChatBroadcast msg, Channel channel)
|
||||
{
|
||||
// For the sake of simplicity we are using observers
|
||||
// on conn's first object.
|
||||
NetworkObject nob = conn.FirstObject;
|
||||
|
||||
// The FirstObject can be null if the client
|
||||
// does not have any objects spawned.
|
||||
if (nob == null)
|
||||
return;
|
||||
|
||||
// Populate the username field in the received msg.
|
||||
// Let us assume GetClientUsername actually does something.
|
||||
msg.Username = GetClientUsername(conn);
|
||||
|
||||
// If you were to view the available Broadcast methods
|
||||
// you will find we are using the one with this signature...
|
||||
// NetworkObject nob, T message, bool requireAuthenticated = true, Channel channel = Channel.Reliable)
|
||||
//
|
||||
// This will send the message to all Observers on nob,
|
||||
// and require those observers to be authenticated with the server.
|
||||
InstanceFinder.ServerManager.Broadcast(nob, msg, true);
|
||||
}
|
||||
```
|
||||
|
||||
Given broadcasts are not automatically received on the object they are sent from you must specify what scripts, or objects can receive a broadcast. As mentioned previously, this allows you to receive broadcast on non-networked objects, but also enables you to receive the same broadcast on multiple objects.
|
||||
|
||||
While our example only utilizes one object, this feature could be useful for changing a large number of conditions in your game at once, such as turning off or on lights without having to make them each a networked object.
|
||||
|
||||
Listening for a broadcast is much like using events. Below demonstrates how the client will listen for data from the server.
|
||||
|
||||
```csharp
|
||||
private void OnEnable()
|
||||
{
|
||||
// Begins listening for any ChatBroadcast from the server.
|
||||
// When one is received the OnChatBroadcast method will be
|
||||
// called with the broadcast data.
|
||||
InstanceFinder.ClientManager.RegisterBroadcast<ChatBroadcast>(OnChatBroadcast);
|
||||
}
|
||||
|
||||
// When receiving on clients broadcast callbacks will only have
|
||||
// the message. In a future release they will also include the
|
||||
// channel they came in on.
|
||||
private void OnChatBroadcast(ChatBroadcast msg, Channel channel)
|
||||
{
|
||||
// Pretend to print to a chat window.
|
||||
Chat.Print(msg.Username, msg.Message, msg.FontColor);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
// Like with events it is VERY important to unregister broadcasts
|
||||
// When the object is being destroyed(in this case disabled), or when
|
||||
// you no longer wish to receive the broadcasts on that object.
|
||||
InstanceFinder.ClientManager.UnregisterBroadcast<ChatBroadcast>(OnChatBroadcast);
|
||||
}
|
||||
```
|
||||
|
||||
As a reminder, a receiving method on the server was demonstrated above. The method signature looked like this.
|
||||
|
||||
```csharp
|
||||
public void OnChatBroadcast(NetworkConnection conn, ChatBroadcast msg)
|
||||
```
|
||||
|
||||
With that in mind, let's see how the server can listen for broadcasts from clients.
|
||||
|
||||
```csharp
|
||||
private void OnEnable()
|
||||
{
|
||||
// Registering for the server is exactly the same as for clients.
|
||||
// Note there is an optional parameter not shown, requireAuthentication.
|
||||
// The value of requireAuthentication is default to true.
|
||||
// Should a client send this broadcast without being authenticated
|
||||
// the server would kick them.
|
||||
InstanceFinder.ServerManager.RegisterBroadcast<ChatBroadcast>(OnChatBroadcast);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
// There are no differences in unregistering.
|
||||
InstanceFinder.ServerManager.UnregisterBroadcast<ChatBroadcast>(OnChatBroadcast);
|
||||
}
|
||||
```
|
||||
|
||||
If you would like to view a working example of using Broadcast view the **PasswordAuthentictor.cs** file within the `Demos/Authenticator/Scripts` folder.
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
---
|
||||
description: >-
|
||||
Information about how to run methods across the network using remote procedure
|
||||
calls.
|
||||
---
|
||||
|
||||
# Remote Procedure Calls
|
||||
|
||||
Remote Procedure Calls (RPCs) are a type of [communication](../../high-level-overview/terminology/communicating.md#server-and-host-2) that are received on the same object they are sent from. The three types of RPCs are ServerRpc, ObserversRpc, and TargetRpc. RPCs are easy to use and are just like calling another method. Since Remote Procedure Calls are object bound, they must be called on scripts which inherit from [NetworkBehaviour](../../../fishnet-building-blocks/components/network-behaviour-components/).
|
||||
|
||||
Fish-Net will [generate serializers](../data-serialization/) for your types automatically, including arrays and lists. In rare cases Fish-Net may not be able to create a serializer for you. When this occurs you must create a [custom serializer](../data-serialization/custom-serializers-guides/).
|
||||
|
||||
{% hint style="info" %}
|
||||
Remote Procedure Call methods do not need to have prefixes as shown in the examples, but it's good practice to use them to keep your intentions known.
|
||||
{% endhint %}
|
||||
|
||||
***
|
||||
|
||||
## ServerRpc <a href="#serverrpc" id="serverrpc"></a>
|
||||
|
||||
A ServerRpc allows a client to run logic on the server. The client calls a ServerRpc method, and the data to execute that method is sent to the server. To use a ServerRpc the client must be active, and the method must have a _ServerRpc_ attribute.
|
||||
|
||||
```csharp
|
||||
private void Update()
|
||||
{
|
||||
// If owner and space bar is pressed.
|
||||
if (base.IsOwner && Input.GetKeyDown(KeyCode.Space))
|
||||
RpcSendChat("Hello world, from owner!");
|
||||
}
|
||||
|
||||
[ServerRpc]
|
||||
private void RpcSendChat(string msg)
|
||||
{
|
||||
Debug.Log($"Received {msg} on the server.");
|
||||
}
|
||||
```
|
||||
|
||||
By default only the [owner](../ownership/) of an object may send a ServerRpc. It is however possible to allow ServerRpcs to be called by any client, owner or not. This is done by setting RequireOwnership to false in the _ServerRpc_ attribute. You may optionally know which connection is calling the RPC by using a NetworkConnection type with a null default as the last parameter. The NetworkConnection parameter will automatically be populated with whichever client is calling the ServerRpc; you do not need to specify the connection.
|
||||
|
||||
```csharp
|
||||
[ServerRpc(RequireOwnership = false)]
|
||||
private void RpcSendChat(string msg, NetworkConnection conn = null)
|
||||
{
|
||||
Debug.Log($"Received {msg} on the server from connection {conn.ClientId}.");
|
||||
}
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## ObserversRpc <a href="#observersrpc" id="observersrpc"></a>
|
||||
|
||||
ObserversRpc allows the server to run logic on clients. Only observing clients will get and run the logic. Observers are set by using the [Network Observer](../../../fishnet-building-blocks/components/network-observer.md) component. To use ObserversRpc add the _ObserversRpc_ attribute to a method.
|
||||
|
||||
```csharp
|
||||
private void FixedUpdate()
|
||||
{
|
||||
RpcSetNumber(Time.frameCount);
|
||||
}
|
||||
|
||||
[ObserversRpc]
|
||||
private void RpcSetNumber(int next)
|
||||
{
|
||||
Debug.Log($"Received number {next} from the server.");
|
||||
}
|
||||
```
|
||||
|
||||
In some instances you might want the owner of an object to ignore received ObserversRpc. To accomplish this set ExcludeOwner to true on the _ObserversRpc_ attribute.
|
||||
|
||||
```csharp
|
||||
[ObserversRpc(ExcludeOwner = true)]
|
||||
private void RpcSetNumber(int next)
|
||||
{
|
||||
// This won't run on owner.
|
||||
Debug.Log($"Received number {next} from the server.");
|
||||
}
|
||||
```
|
||||
|
||||
You may also have the latest values sent through an ObserversRpc also automatically send to new clients. This can be useful for values that only change through RPCs. Doing so is done by setting BufferLast to true on the _ObserversRpc_ attribute. The example below will not send to owner, and will also update new joining clients.
|
||||
|
||||
```csharp
|
||||
[ObserversRpc(ExcludeOwner = true, BufferLast = true)]
|
||||
private void RpcSetNumber(int next)
|
||||
{
|
||||
// This won't run on owner and will send to new clients.
|
||||
Debug.Log($"Received number {next} from the server.");
|
||||
}
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## TargetRpc <a href="#targetrpc" id="targetrpc"></a>
|
||||
|
||||
Lastly is TargetRpc. This RPC is used to run logic on a specific client. You can implement this feature by adding the _TargetRpc_ attribute. When sending a TargetRpc the first parameter of your method must always be a NetworkConnection; this is the connection the data goes to.
|
||||
|
||||
```csharp
|
||||
private void UpdateOwnersAmmo()
|
||||
{
|
||||
/* Even though this example passes in owner, you can send to
|
||||
* any connection that is an observer. */
|
||||
RpcSetAmmo(base.Owner, 10);
|
||||
}
|
||||
|
||||
[TargetRpc]
|
||||
private void RpcSetAmmo(NetworkConnection conn, int newAmmo)
|
||||
{
|
||||
// This might be something you only want the owner to be aware of.
|
||||
_myAmmo = newAmmo;
|
||||
}
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## Multi-Purpose Rpc
|
||||
|
||||
It is possible to have a single method be both a TargetRpc, as well an ObserversRpc. This can be very useful if you sometimes want to send a RPC to all observers, or a single individual. A chat message could be an example of this.
|
||||
|
||||
```csharp
|
||||
[ObserversRpc][TargetRpc]
|
||||
private void DisplayChat(NetworkConnection target, string sender, string message)
|
||||
{
|
||||
Debug.Log($"{sender}: {message}."); // Display a message from sender.
|
||||
}
|
||||
|
||||
[Server]
|
||||
private void SendChatMessage()
|
||||
{
|
||||
// Send to only the owner.
|
||||
DisplayChat(base.Owner, "Bob", "Hello world");
|
||||
// Send to everyone.
|
||||
DisplayChat(null, "Bob", "Hello world");
|
||||
}
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## Channels <a href="#channels" id="channels"></a>
|
||||
|
||||
Remote procedure calls can be sent as **reliable or unreliable**. To utilize this feature add a _Channel_ type as the last parameter to your RPC. You may also use your added Channel parameter to know which Channel the data arrived on.
|
||||
|
||||
```csharp
|
||||
private bool _reliable;
|
||||
private void FixedUpdate()
|
||||
{
|
||||
// Reliable or unreliable can be switched at runtime!
|
||||
_reliable = !_reliable;
|
||||
|
||||
Channel channel = (_reliable) ? Channel.Reliable : Channel.Unreliable;
|
||||
RpcTest("Anything", channel);
|
||||
}
|
||||
|
||||
/* This example uses ServerRpc, but any RPC will work.
|
||||
* Although this example shows a default value for the channel,
|
||||
* you do not need to include it. */
|
||||
[ServerRpc]
|
||||
private void RpcTest(string txt, Channel channel = Channel.Reliable)
|
||||
{
|
||||
if (channel == Channel.Reliable)
|
||||
Debug.Log("Message received! I never doubted you.");
|
||||
else if (channel == Channel.Unreliable)
|
||||
Debug.Log($"Glad you got here, I wasn't sure you'd make it.");
|
||||
}
|
||||
```
|
||||
|
||||
If your RPC is a ServerRpc and you are using a NetworkConnection with a null default as the last parameter then the Channel parameter must be added second to last, just before the NetworkConnection parameter.
|
||||
|
||||
```csharp
|
||||
/* Example of using Channel with a ServerRpc that
|
||||
* also provides the calling connection. */
|
||||
[ServerRpc(RequireOwnership = false)]
|
||||
private void RpcTest(string txt, Channel channel = Channel.Reliable, NetworkConnection sender = null)
|
||||
{
|
||||
Debug.Log($"Received on channel {channel} from {sender.ClientId}.");
|
||||
}
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## RunLocally
|
||||
|
||||
All RPC types may use the RunLocally field within the RPC attribute. When RunLocally is true the method will send, but also run on the calling side. For example, if a client calls a ServerRpc with RunLocally true, the ServerRpc will be put in the clients buffer to send, then the client will run the method logic locally.
|
||||
|
||||
```csharp
|
||||
// Keeping in mind all RPC types can use RunLocally.
|
||||
[ServerRpc(RunLocally = true)]
|
||||
private void RpcTest()
|
||||
{
|
||||
// This debug will print on the server and the client calling the RPC.
|
||||
Debug.Log("Rpc Test!");
|
||||
}
|
||||
```
|
||||
|
||||
{% hint style="info" %}
|
||||
Only transports which support unreliable messages may send RPCs as unreliable. If you attempt to send a RPC as unreliable when the transport does not support it the RPC will default to reliable.
|
||||
{% endhint %}
|
||||
|
||||
***
|
||||
|
||||
## DataLength
|
||||
|
||||
If you have intentions to send very large amounts of data you may specify the maximum potential size of your data in the RPC attribute. Doing so will use a reserved serializer to prevent resizing and allocations, increasing performance. Every RPC type supports this feature.
|
||||
|
||||
```csharp
|
||||
// This implies we know the data will never be larger than 3500 bytes.
|
||||
// If the data is larger then a resize will occur resulting in garbage collection.
|
||||
[ServerRpc(DataLength = 3500)]
|
||||
private void ServerSendBytes(byte[] data) {}
|
||||
```
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
# SyncTypes
|
||||
|
||||
Like[ remote procedure calls](../remote-procedure-calls.md), **SyncTypes** are another type of [communication](../../../high-level-overview/terminology/communicating.md). These are fields which automatically synchronize over the network to clients when the server changes them. There are a variety of **SyncTypes** available: [SyncVar](syncvar.md), [SyncDictionary](syncdictionary.md), [SyncList](synclist.md), [custom SyncTypes](custom-synctype.md), and more.
|
||||
|
||||
When changes are made to a SyncType, only the changes are sent. For example, if you have a SyncList of 10 values and add another, only the just added entry will be sent.
|
||||
|
||||
Any changes made to SyncTypes in Awake will be performed on server and client without requiring synchronization. This is a great opportunity to add to lists or dictionaries. If you wish values to be synchronized after initializing use OnStartServer.
|
||||
|
||||
{% hint style="warning" %}
|
||||
SyncVar Values will be reset before `OnDestroy` runs on the object, so you won't be able to get them in that method.
|
||||
{% endhint %}
|
||||
|
||||
By altering the **SendRate,** you can slow down how often SyncTypes can at most often sync their changes. This can be useful if you make several changes in quick succession and only want the latest change to be sent every couple of seconds for example. The values are per second, and setting a SendRate of `0f` will allow SyncTypes to send changes as often as possible, which is every network tick if your game is running on a [non-variable timing mode](../../../../fishnet-building-blocks/components/managers/time-manager.md#timing-type). 
|
||||
|
||||
SyncTypes will also [automatically generate serializers](../../data-serialization/) for custom types.
|
||||
|
||||
{% hint style="info" %}
|
||||
When SyncTypes are set to use reliable messaging (the default), their changes will arrive on clients in the reverse order that they are defined inside the script. This is due to the way the code generation works.
|
||||
{% endhint %}
|
||||
|
||||
## Host client limitations
|
||||
|
||||
There is a small limitation with all **SyncTypes** when running both the client and server in a single build.
|
||||
|
||||
While as the host client, the previous value, when applicable, in callbacks will be the current value or null if the `asServer` parameter is false. This is mostly noticed in [SyncVars](syncvar.md), note the example below:
|
||||
|
||||
```csharp
|
||||
// This example assumes you are acting as the host client.
|
||||
// We will imagine previous value was 10, and next is 20.
|
||||
private void _mySyncVar_OnChange(int prev, int next, bool asServer)
|
||||
{
|
||||
// If asServer if true then the prev value will correctly be 10,
|
||||
// with the next being 20.
|
||||
|
||||
// If asServer is false then the prev value will be 20, as well the
|
||||
// next being 20. This is because the value has already been changed
|
||||
// on the server, and the previous value is no longer stored.
|
||||
|
||||
DoSomething(prev, next);
|
||||
}
|
||||
```
|
||||
|
||||
There is a fairly simple way to accommodate this scenario if you plan on using a host client in your game.
|
||||
|
||||
```csharp
|
||||
// This example assumes you are acting as the host client.
|
||||
// We will imagine previous value was 10, and next is 20.
|
||||
private void _mySyncVar_OnChange(int prev, int next, bool asServer)
|
||||
{
|
||||
// Only run the logic using the previous value if being called
|
||||
// with asServer true, or if only the client is started.
|
||||
if (asServer || IsClientOnlyStarted)
|
||||
DoSomething(prev, next);
|
||||
}
|
||||
```
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
description: >-
|
||||
With a customized SynType you can decide how and what data to synchronize, and
|
||||
make optimizations as you see fit.
|
||||
---
|
||||
|
||||
# Custom SyncType
|
||||
|
||||
For example: if you have a data container with many variables you probably don't want to send the entire container when you change it, as a SyncVar would. By making a custom SyncType you can customize the behavior entirely; this is how other SyncType work.
|
||||
|
||||
```csharp
|
||||
/* If one of these values change you
|
||||
* probably don't want to send the
|
||||
* entire container. A custom SyncType
|
||||
* is perfect for only sending what is changed. */
|
||||
[System.Serializable]
|
||||
public struct MyContainer
|
||||
{
|
||||
public int LeftArmHealth;
|
||||
public int RightArmHealth;
|
||||
public int LeftLegHealth;
|
||||
public int RightLeftHealth;
|
||||
}
|
||||
```
|
||||
|
||||
Custom SyncTypes follow the same rules as other SyncTypes. Internally other SyncTypes inherit from SyncBase, and your type must as well. In addition, you must implement the _ICustomSync_ interface.
|
||||
|
||||
```csharp
|
||||
public class SyncMyContainer : SyncBase, ICustomSync
|
||||
{
|
||||
/* If you intend to serialize your type
|
||||
* as a whole at any point in your custom
|
||||
* SyncType and would like the automatic
|
||||
* serializers to include it then use
|
||||
* GetSerializedType() to return the type.
|
||||
* In this case, the type is MyContainer.
|
||||
* If you do not need a serializer generated
|
||||
* you may return null. */
|
||||
public object GetSerializedType() => typeof(MyContainer);
|
||||
}
|
||||
|
||||
public class YourClass
|
||||
{
|
||||
private readonly SyncMyContainer _myContainer = new();
|
||||
}
|
||||
```
|
||||
|
||||
Given how flexible a custom SyncType may be there is not a one-size fits all example. You may view several custom examples within your FishNet import under `FishNet/Demos/CustomSyncType`.
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
---
|
||||
description: >-
|
||||
There are settings and attributes unique to SyncTypes which allow various ways
|
||||
of customizing your SyncType.
|
||||
---
|
||||
|
||||
# Customizing Behavior
|
||||
|
||||
### SyncTypeSettings
|
||||
|
||||
[SyncTypeSettings](https://fish-networking.com/FishNet/api/api/FishNet.Object.Synchronizing.SyncTypeSettings.html) can be initialized with any SyncType to define the default settings of your SyncType.
|
||||
|
||||
```csharp
|
||||
// Custom settings are optional.
|
||||
// This is an example of declaring a SyncVar without custom settings.
|
||||
private readonly SyncVar<int> _myInt = new();
|
||||
|
||||
// Each SyncType has a different constructor to take settings.
|
||||
// Here is an example for SyncVars. This will demonstrate how to use
|
||||
// the unreliable channel for SyncVars, and send the value upon any change.
|
||||
// There are many different ways to create SyncTypeSettings; you can even
|
||||
// make a const settings and initialize with that!
|
||||
private readonly SyncVar<int> _myInt = new(new SyncTypeSettings(0f, Channel.Unreliable));
|
||||
```
|
||||
|
||||
Settings can also be changed at runtime. This can be very useful to change behavior based on your game mechanics and needs, or to even slow down send rate as your player count grows.
|
||||
|
||||
```csharp
|
||||
// This example shows in Awake but this code
|
||||
// can be used literally anywhere.
|
||||
private void Awake()
|
||||
{
|
||||
// You can change all settings at once.
|
||||
_myInt.UpdateSettings(new SyncTypeSettings(....);
|
||||
|
||||
// Or update only specific things, such as SendRate.
|
||||
// Change send rate to once per second.
|
||||
_myInt.UpdateSendRate(1f);
|
||||
}
|
||||
```
|
||||
|
||||
### Showing in the inspector
|
||||
|
||||
SyncTypes can also be shown in the inspector.
|
||||
|
||||
You must first make sure your type is marked as serializable if a container; this is a Unity requirement.
|
||||
|
||||
```csharp
|
||||
// SyncTypes can be virtually any data type. This example
|
||||
// shows a container to demonstrate the serializable attribute.
|
||||
[System.Serializable]
|
||||
public struct MyType { }
|
||||
```
|
||||
|
||||
Next the SyncType must not use the 'readonly' indicator. We require the readonly indicator by default to emphasis you should not initialize your SyncType at runtime.
|
||||
|
||||
Below is an example of what **NOT** to do.
|
||||
|
||||
```csharp
|
||||
private SyncVar<int> _myInt = new();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// This would result in errors at runtime.
|
||||
|
||||
// Do not make a SyncType into a new instance
|
||||
_myInt = new();
|
||||
// Do not set a SyncType to another instance.
|
||||
_myInt = _someOtherDeclaredSyncVar.
|
||||
}
|
||||
```
|
||||
|
||||
The code above will actually prevent compilation in the editor as our code generators will detect you did not include the readonly indicator. To remove the readonly indicator you must also add the [AllowMutableSyncType](https://fish-networking.com/FishNet/api/api/FishNet.CodeGenerating.AllowMutableSyncTypeAttribute.html) above your SyncType.
|
||||
|
||||
```csharp
|
||||
// This will work and show your SyncType in the inspector!
|
||||
[AllowMutableSyncType]
|
||||
[SerializeField] // Be sure to use the SerializeField attribute if not public.
|
||||
private SyncVar<int> _myInt = new();
|
||||
```
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
---
|
||||
description: >-
|
||||
SyncDictionary is an easy way to keep a Dictionary collection automatically
|
||||
synchronized over the network.
|
||||
---
|
||||
|
||||
# SyncDictionary
|
||||
|
||||
SyncDictionary supports all the functionality a normal dictionary would, just as SyncList supports List abilities.
|
||||
|
||||
Callbacks for SyncDictionary are similar to SyncList. And like other synchronization types changes are set immediately before the callback occurs.
|
||||
|
||||
```csharp
|
||||
private readonly SyncDictionary<NetworkConnection, string> _playerNames = new();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_playerNames.OnChange += _playerNames_OnChange;
|
||||
}
|
||||
|
||||
// SyncDictionaries also include the asServer parameter.
|
||||
private void _playerNames_OnChange(SyncDictionaryOperation op,
|
||||
NetworkConnection key, string value, bool asServer)
|
||||
{
|
||||
/* Key will be provided for
|
||||
* Add, Remove, and Set. */
|
||||
switch (op)
|
||||
{
|
||||
// Adds key with value.
|
||||
case SyncDictionaryOperation.Add:
|
||||
break;
|
||||
// Removes key.
|
||||
case SyncDictionaryOperation.Remove:
|
||||
break;
|
||||
// Sets key to a new value.
|
||||
case SyncDictionaryOperation.Set:
|
||||
break;
|
||||
// Clears the dictionary.
|
||||
case SyncDictionaryOperation.Clear:
|
||||
break;
|
||||
// Like SyncList, indicates all operations are complete.
|
||||
case SyncDictionaryOperation.Complete:
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{% hint style="danger" %}
|
||||
If you are using this SyncType with a container, such as a class or structure, and want to modify values within that container, you must set the value dirty. See the example below.
|
||||
{% endhint %}
|
||||
|
||||
```csharp
|
||||
[System.Serializable]
|
||||
private struct MyContainer
|
||||
{
|
||||
public string PlayerName;
|
||||
public int Level;
|
||||
}
|
||||
|
||||
private readonly SyncDictionary<int, MyContainer> _containers = new();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
MyContainer mc = new MyContainer
|
||||
{
|
||||
Level = 5
|
||||
};
|
||||
_containers[2] = mc;
|
||||
}
|
||||
|
||||
[Server]
|
||||
private void ModifyContainer()
|
||||
{
|
||||
MyContainer mc = _containers[2];
|
||||
// This will change the value locally but it will not synchronize to clients.
|
||||
mc.Level = 10;
|
||||
// You may re-apply the value to the dictionary.
|
||||
_containers[2] = mc;
|
||||
// Or set dirty on the value or key. Using the key is often more performant.
|
||||
_containers.Dirty(2);
|
||||
}
|
||||
```
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
---
|
||||
description: >-
|
||||
SyncHashSet is an easy way to keep a HashSet collection automatically
|
||||
synchronized over the network.
|
||||
---
|
||||
|
||||
# SyncHashSet
|
||||
|
||||
Callbacks for SyncHashSet are similar to SyncList. And like other synchronization types changes are set immediately before the callback occurs. 
|
||||
|
||||
```csharp
|
||||
private readonly SyncHashSet<int> _myCollection = new SyncHashSet<int>();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_myCollection.OnChange += _myCollection_OnChange;
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
// You can modify a synchashset as you would any other hashset.
|
||||
_myCollection.Add(Time.frameCount);
|
||||
}
|
||||
|
||||
/* Like SyncVars the callback offers an asServer option
|
||||
* to indicate if the callback is occurring on the server
|
||||
* or the client. As SyncVars do, changes have already been
|
||||
* made to the collection before the callback occurs. */
|
||||
private void _myCollection_OnChange(SyncHashSetOperation op, int item, bool asServer)
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
/* An object was added to the hashset. Item is
|
||||
* is the added object. */
|
||||
case SyncHashSetOperation.Add:
|
||||
break;
|
||||
/* An object has been removed from the hashset. Item
|
||||
* is the removed object. */
|
||||
case SyncHashSetOperation.Remove:
|
||||
break;
|
||||
/* The hashset has been cleared.
|
||||
* Item will be default. */
|
||||
case SyncHashSetOperation.Clear:
|
||||
break;
|
||||
/* An entry in the hashset has been updated.
|
||||
* When this occurs the item is removed
|
||||
* and added. Item will be the new value.
|
||||
* Item will likely need a custom comparer
|
||||
* for this to function properly. */
|
||||
case SyncHashSetOperation.Update:
|
||||
break;
|
||||
/* When complete calls all changes have been
|
||||
* made to the collection. You may use this
|
||||
* to refresh information in relation to
|
||||
* the changes, rather than doing so
|
||||
* after every entry change. All values are
|
||||
* default for this operation. */
|
||||
case SyncHashSetOperation.Complete:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
{% hint style="danger" %}
|
||||
If you are using this SyncType with a container, such as a class or structure, and want to modify values within that container, you must set the value dirty. See the example below.
|
||||
{% endhint %}
|
||||
|
||||
```csharp
|
||||
[System.Serializable]
|
||||
private struct MyContainer
|
||||
{
|
||||
public string PlayerName;
|
||||
public int Level;
|
||||
}
|
||||
|
||||
private readonly SyncHashSet<MyContainer> _containers = new();
|
||||
private MyContainer _containerReference = new();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_containerReference.Level = 5;
|
||||
_containers.Add(_containerReference);
|
||||
}
|
||||
|
||||
[Server]
|
||||
private void ModifyPlayer()
|
||||
{
|
||||
|
||||
// This will change the value locally but it will not synchronize to clients.
|
||||
_containerReference.Level = 10;
|
||||
// The value must be set dirty to force a synchronization.
|
||||
_containers.Dirty(_containerReference);
|
||||
}
|
||||
```
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
---
|
||||
description: >-
|
||||
SyncList is an easy way to keep a List collection automatically synchronized
|
||||
over the network.
|
||||
---
|
||||
|
||||
# SyncList
|
||||
|
||||
Using a SyncList is done the same as with a normal List.
|
||||
|
||||
Network callbacks on SyncList do have a little more information than SyncVars. Other non-SyncVar SyncTypes also have their own unique callbacks. The example below demonstrates a SyncList callback. 
|
||||
|
||||
```csharp
|
||||
private readonly SyncList<int> _myCollection = new();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
/* Listening to SyncList callbacks are a
|
||||
* little different from SyncVars. */
|
||||
_myCollection.OnChange += _myCollection_OnChange;
|
||||
}
|
||||
private void Update()
|
||||
{
|
||||
// You can modify a synclist as you would any other list.
|
||||
_myCollection.Add(10);
|
||||
_myCollection.RemoveAt(0);
|
||||
// ect.
|
||||
}
|
||||
|
||||
/* Like SyncVars the callback offers an asServer option
|
||||
* to indicate if the callback is occurring on the server
|
||||
* or the client. As SyncVars do, changes have already been
|
||||
* made to the collection before the callback occurs. */
|
||||
private void _myCollection_OnChange(SyncListOperation op, int index,
|
||||
int oldItem, int newItem, bool asServer)
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
/* An object was added to the list. Index
|
||||
* will be where it was added, which will be the end
|
||||
* of the list, while newItem is the value added. */
|
||||
case SyncListOperation.Add:
|
||||
break;
|
||||
/* An object was removed from the list. Index
|
||||
* is from where the object was removed. oldItem
|
||||
* will contain the removed item. */
|
||||
case SyncListOperation.RemoveAt:
|
||||
break;
|
||||
/* An object was inserted into the list. Index
|
||||
* is where the obejct was inserted. newItem
|
||||
* contains the item inserted. */
|
||||
case SyncListOperation.Insert:
|
||||
break;
|
||||
/* An object replaced another. Index
|
||||
* is where the object was replaced. oldItem
|
||||
* is the item that was replaced, while
|
||||
* newItem is the item which now has it's place. */
|
||||
case SyncListOperation.Set:
|
||||
break;
|
||||
/* All objects have been cleared. Index, oldValue,
|
||||
* and newValue are default. */
|
||||
case SyncListOperation.Clear:
|
||||
break;
|
||||
/* When complete calls all changes have been
|
||||
* made to the collection. You may use this
|
||||
* to refresh information in relation to
|
||||
* the list changes, rather than doing so
|
||||
* after every entry change. Like Clear
|
||||
* Index, oldItem, and newItem are all default. */
|
||||
case SyncListOperation.Complete:
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{% hint style="danger" %}
|
||||
If you are using this SyncType with a container, such as a class, and want to modify values within that container, you must set the value dirty. See the example below.
|
||||
{% endhint %}
|
||||
|
||||
<pre class="language-csharp"><code class="lang-csharp">private class MyClass
|
||||
{
|
||||
public string PlayerName;
|
||||
public int Level;
|
||||
}
|
||||
|
||||
private readonly SyncList<MyClass> _players = new SyncList<MyClass>();
|
||||
|
||||
// Call dirty on an index after modifying an entries field to force a synchronize.
|
||||
<strong>[Server]
|
||||
</strong>private void ModifyPlayer()
|
||||
{
|
||||
_players[0].Level = 10;
|
||||
// Dirty the 0 index.
|
||||
_players.Dirty(0);
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
Structures cannot have their values modified when they reside within a collection. You must instead create a local variable for the collection index you wish to modify, change values on the local copy, then set the local copy back into the collection
|
||||
|
||||
```csharp
|
||||
/* . */
|
||||
[System.Serializable]
|
||||
private struct MyStruct
|
||||
{
|
||||
public string PlayerName;
|
||||
public int Level;
|
||||
}
|
||||
|
||||
private readonly SyncList<MyStruct> _players = new();
|
||||
|
||||
[Server]
|
||||
private void ModifyPlayer()
|
||||
{
|
||||
MyStruct ms = _players[0];
|
||||
ms.Level = 10;
|
||||
_players[0] = ms;
|
||||
}
|
||||
```
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
---
|
||||
description: >-
|
||||
SyncStopwatch provides an efficient way to synchronize a stopwatch between
|
||||
server and clients.
|
||||
---
|
||||
|
||||
# SyncStopwatch
|
||||
|
||||
Like SyncTimer, SyncStopwatch only updates state changes such as start or stopping, rather than each individual delta change.
|
||||
|
||||
```csharp
|
||||
private readonly SyncStopwatch _timePassed = new()
|
||||
```
|
||||
|
||||
Making changes to the timer is very simple, and like other SyncTypes must be done on the server.
|
||||
|
||||
```csharp
|
||||
// All of the actions below are automatically synchronized.
|
||||
|
||||
/* Starts the stopwatch while optionally sending a stop
|
||||
* message first if stopwatch was already running.
|
||||
* If the stopwatch was previously running the time passed
|
||||
* would be reset. */
|
||||
/* This would invoke callbacks indicating a stopwatch
|
||||
* had stopped, then started again. */
|
||||
_timePassed.StartStopwatch(true);
|
||||
/* Pauses the stopwatch and optionally sends the current
|
||||
* timer value as it is on the server. */
|
||||
_timePassed.PauseStopwatch(false);
|
||||
// Unpauses the current stopwatch.
|
||||
_timePassed.UnpauseStopwatch();
|
||||
/* Ends the Stopwatch while optionally sends the
|
||||
* current value to clients, as it was during the stop. */
|
||||
_timePassed.StopStopwatch(false);
|
||||
```
|
||||
|
||||
Updating and reading the timer value is much like you would a normal float value.
|
||||
|
||||
```csharp
|
||||
private void Update()
|
||||
{
|
||||
/* Like SyncTimer, SyncStopwatch must be updated
|
||||
* with a delta on both server and client. Do not
|
||||
* update the delta twice if clientHost. You can update
|
||||
* the delta anywhere in your code. */
|
||||
_timePassed.Update(Time.deltaTime);
|
||||
|
||||
// Access the current time passed.
|
||||
Debug.Log(_timePassed.Elapsed);
|
||||
/* You may also see if the stopwatch is paused before
|
||||
* accessing elapsed. */
|
||||
if (!_timePassedPaused)
|
||||
Debug.Log(_timePassed.Elapsed);
|
||||
}
|
||||
```
|
||||
|
||||
Like other SyncTypes, you can subscribe to change events for the SyncStopwatch.
|
||||
|
||||
```csharp
|
||||
private void Awake()
|
||||
{
|
||||
_timePassed.OnChange += _timePassed_OnChange;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
_timePassed.OnChange -= _timePassed_OnChange;
|
||||
}
|
||||
|
||||
private void _timePassed_OnChange(SyncStopwatchOperation op, float prev, bool asServer)
|
||||
{
|
||||
/* Like all SyncType callbacks, asServer is true if the callback
|
||||
* is occuring on the server side, false if on the client side. */
|
||||
|
||||
// Operations can be used to be notified of changes to the timer.
|
||||
// This is much like other SyncTypes.
|
||||
// Here is an example of performing logic only if starting or stopping.
|
||||
if (op == SyncStopwatchOperation.Start || op == SyncStopwatchOperation.Stop)
|
||||
{
|
||||
// Do logic.
|
||||
}
|
||||
|
||||
/* prev, our float, indicates the value of the stopwatch prior to
|
||||
* the operation. Remember that you can get current value using
|
||||
* _timePassed.Elapsed */
|
||||
}
|
||||
```
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
---
|
||||
description: >-
|
||||
SyncTimer provides an efficient way to synchronize a timer between server and
|
||||
clients.
|
||||
---
|
||||
|
||||
# SyncTimer
|
||||
|
||||
Unlike SyncVars, a SyncTimer only updates state changes such as start or stopping, rather than each individual delta change.
|
||||
|
||||
SyncTimer is a [Custom SyncType](custom-synctype.md), and is declared like any other SyncType.
|
||||
|
||||
```csharp
|
||||
private readonly SyncTimer _timeRemaining = new SyncTimer();
|
||||
```
|
||||
|
||||
Making changes to the timer is very simple, and like other SyncTypes must be done on the server.
|
||||
|
||||
```csharp
|
||||
// All of the actions below are automatically synchronized.
|
||||
|
||||
/* Starts the timer with 5 seconds on it.
|
||||
*
|
||||
* The optional boolean argument will also send
|
||||
* a stop event before starting a new timer, only if
|
||||
* the previous timer is still running.
|
||||
*
|
||||
* EG: if a timer was started with 5 seconds and
|
||||
* you start a new timer with 2 seconds remaining
|
||||
* a StopTimer will be sent with the remaining time
|
||||
* of 2 seconds before the timer is started again at
|
||||
* 5 seconds. */
|
||||
_timeRemaining.StartTimer(5f, true);
|
||||
/* Pauses the timer and optionally sends the current
|
||||
* timer value as it is on the server. */
|
||||
_timeRemaining.PauseTimer(false);
|
||||
// Unpauses the current timer.
|
||||
_timeRemaining.UnpauseTimer();
|
||||
/* Stops the timer early and optionally sends the
|
||||
* current timer value as it is on the server. */
|
||||
_timeRemaining.StopTimer(false);
|
||||
```
|
||||
|
||||
Updating and reading the timer value is much like you would a normal float value.
|
||||
|
||||
```csharp
|
||||
private void Update()
|
||||
{
|
||||
/* Timers must be updated both on the server
|
||||
* and clients. This only needs
|
||||
* to be done on either/or if clientHost. The timer
|
||||
* may be updated in any method. */
|
||||
_timeRemaining.Update();
|
||||
|
||||
/* Access the current time remaining. This is how
|
||||
* you can utilize the current timer value. When a timer
|
||||
* is complete the remaining value will be 0f. */
|
||||
Debug.Log(_timeRemaining.Remaining);
|
||||
/* You may also see if the timer is paused before
|
||||
* accessing time remaining. */
|
||||
if (!_timeRemaining.Paused)
|
||||
Debug.Log(_timeRemaining.Remaining);
|
||||
}
|
||||
```
|
||||
|
||||
Like other SyncTypes, you can subscribe to change events for the SyncTimer.
|
||||
|
||||
```csharp
|
||||
private void Awake()
|
||||
{
|
||||
_timeRemaining.OnChange += _timeRemaining_OnChange;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
_timeRemaining.OnChange -= _timeRemaining_OnChange;
|
||||
}
|
||||
|
||||
private void _timeRemaining_OnChange(SyncTimerOperation op, float prev, float next, bool asServer)
|
||||
{
|
||||
/* Like all SyncType callbacks, asServer is true if the callback
|
||||
* is occuring on the server side, false if on the client side. */
|
||||
|
||||
// Operations can be used to be notified of changes to the timer.
|
||||
|
||||
// Timer has been started with initial values.
|
||||
if (op == SyncTimerOperation.Start)
|
||||
Debug.Log($"The timer was started with {next} seconds.");
|
||||
// Timer has been paused.
|
||||
else if (op == SyncTimerOperation.Pause)
|
||||
Debug.Log($"The timer was paused.");
|
||||
// Timer has been paused and latest server values were sent.
|
||||
else if (op == SyncTimerOperation.PauseUpdated)
|
||||
Debug.Log($"The timer was paused and remaining time has been updated to {next} seconds.");
|
||||
// Timer was unpaused.
|
||||
else if (op == SyncTimerOperation.Unpause)
|
||||
Debug.Log($"The timer was unpaused.");
|
||||
// Timer has been manually stopped.
|
||||
else if (op == SyncTimerOperation.Stop)
|
||||
Debug.Log($"The timer has been stopped and is no longer running.");
|
||||
/* Timer has been manually stopped.
|
||||
*
|
||||
* When StopUpdated is called Previous will contain the remaining time
|
||||
* prior to being stopped as it is locally. Next will contain the remaining
|
||||
* time prior to being stopped as it was on the server. These values
|
||||
* often align but the information is provided for your potential needs.
|
||||
*
|
||||
* When the server starts a new timer while one is already active, and chooses
|
||||
* to also send a stop update using the StartTimer(float,bool) option, a
|
||||
* StopUpdated is also sent to know previous timer values before starting a new timer. */
|
||||
else if (op == SyncTimerOperation.StopUpdated)
|
||||
Debug.Log($"The timer has been stopped and is no longer running. The timer was stopped at value {next} before stopping, and the previous value was {prev}");
|
||||
// A timer has reached 0f.
|
||||
else if (op == SyncTimerOperation.Finished)
|
||||
Debug.Log($"The timer has completed!");
|
||||
// Complete occurs after all change events are processed.
|
||||
else if (op == SyncTimerOperation.Complete)
|
||||
Debug.Log("All timer callbacks have completed for this tick.");
|
||||
}
|
||||
```
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
---
|
||||
description: >-
|
||||
SyncVars are the most simple way to automatically synchronize a single
|
||||
variable over the network.
|
||||
---
|
||||
|
||||
# SyncVar
|
||||
|
||||
SyncVars are used to synchronize a single field. Your field can be virtually anything: a value type, struct, or class. To utilize a SyncVar you must implement your type as a SyncVar class
|
||||
|
||||
```csharp
|
||||
public class YourClass : NetworkBehaviour
|
||||
{
|
||||
private readonly SyncVar<float> _health = new SyncVar<float>();
|
||||
}
|
||||
/* Any time _health is changed on the server the
|
||||
* new value will be sent to clients. */
|
||||
```
|
||||
|
||||
SyncTypes can also be customized with additional options by using the UpdateSettings method within your declared SyncTypes, or using the initializer of your SyncType. These options include being notified when the value changes, changing how often the SyncType will synchronize, and more. You can view a full list of SyncVar properties which may be changed by viewing the [API](https://fish-networking.com/FishNet/api/api/FishNet.Object.Synchronizing.SyncVarAttribute.html#fields).
|
||||
|
||||
Below is a demonstration on sending SyncTypes at a longer interval of at most every 1f, and being notified of when the value changes.
|
||||
|
||||
```csharp
|
||||
private readonly SyncVar<float> _health = new SyncVar<float>(new SyncTypeSettings(1f));
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_health.OnChange += on_health;
|
||||
}
|
||||
|
||||
// This is called when _health changes, for server and clients.
|
||||
private void on_health(float prev, float next, bool asServer)
|
||||
{
|
||||
/* Each callback for SyncVars must contain a parameter
|
||||
* for the previous value, the next value, and asServer.
|
||||
* The previous value will contain the value before the
|
||||
* change, while next contains the value after the change.
|
||||
* By the time the callback occurs the next value had
|
||||
* already been set to the field, eg: _health.
|
||||
* asServer indicates if the callback is occurring on the
|
||||
* server or on the client. Sometimes you may want to run
|
||||
* logic only on the server, or client. The asServer
|
||||
* allows you to make this distinction. */
|
||||
}
|
||||
```
|
||||
|
||||
Another common request is achieving client-side SyncVar values. This may be achieved by using a ServerRpc.
|
||||
|
||||
{% hint style="warning" %}
|
||||
When modifying SyncVars using client-side, a RPC is sent with every set. If your SyncVar value will change frequently consider limiting how often you set the value client-side to reduce bandwidth usage.
|
||||
|
||||
In a future release we are planning to make all SyncTypes have client-authoritative properties where this work-around will not be needed.
|
||||
{% endhint %}
|
||||
|
||||
```csharp
|
||||
// A typical server-side SyncVar.
|
||||
public readonly SyncVar<string> Name = new SyncVar<string>();
|
||||
// Create a ServerRpc to allow owner to update the value on the server.
|
||||
[ServerRpc] private void SetName(string value) => Name.Value = value;
|
||||
```
|
||||
|
||||
When using client-side SyncVars you may want to consider ExcludeOwner in the SyncVar ReadPermissions to prevent owners from receiving their own updates. In addition, using WritePermission.ClientUnsynchronized will allow the client to set the value locally. Lastly, RunLocally as true in the ServerRpc will execute the RPC code on both the sender(client) and the server.
|
||||
|
||||
Using ExcludeOwner as the SyncVar ReadPermissions, and ClientUnsynchronized in the WritePermissions. . In the example below we also set RunLocally to true for the ServerRpc so that the calling client also sets the value locally.
|
||||
|
||||
```csharp
|
||||
// Attributes shown in previous examples can stack but they were removed
|
||||
// here for simplicity.
|
||||
private readonly SyncVar<string> Name = new SyncVar<string>(new SyncTypeSettings(WritePermission.ClientUnsynchronized, ReadPermission.ExcludeOwner));
|
||||
// Create a ServerRpc to allow owner to update the value on the server.
|
||||
[ServerRpc(RunLocally = true)] private void SetName(string value) => Name.Value = value;
|
||||
```
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# Networked GameObjects and Scripts
|
||||
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
---
|
||||
description: >-
|
||||
NetworkBehaviours are a fundamental part of networking which allow you to
|
||||
easily synchronize data and access network related information.
|
||||
---
|
||||
|
||||
# NetworkBehaviour
|
||||
|
||||
When inheriting from [NetworkBehaviours ](../../../fishnet-building-blocks/components/network-behaviour-components/)you are indicating that your script will utilize the network in some way. Once a NetworkBehaviour script is added to an object the [NetworkObject](../../../fishnet-building-blocks/components/network-object.md) component will automatically be attached.
|
||||
|
||||
NetworkBehaviours are essential for [Remote Procedure Calls](../network-communication/remote-procedure-calls.md), [Synchronizing](../network-communication/synchronizing/), and having access to vital network information.
|
||||
|
||||
***
|
||||
|
||||
## Properties <a href="#properties" id="properties"></a>
|
||||
|
||||
There are several public properties available within NetworkBehaviour, many of which you will use regularly. Most of the properties are only available after the object has been initialized. See the [callbacks](network-behaviour-guides.md#server-and-host-1) section for more information on initialization.
|
||||
|
||||
Commonly used properties are:
|
||||
|
||||
* **IsClientInitialized** will be true if acting as a client and object is network initialized.
|
||||
* **IsServerInitialized** will be true if acting as the server and object is network initialized.
|
||||
* **IsOwner** is true if you are a client, and the owner of the object.
|
||||
* **HasAuthority** is true if you are the owner, or if server and there is no owner.
|
||||
|
||||
To view all properties please visit the [NetworkBehaviour API](https://fish-networking.com/FishNet/api/api/FishNet.Object.NetworkBehaviour.html#properties).
|
||||
|
||||
{% hint style="info" %}
|
||||
When accessing a NetworkBehaviour property or method consider using the 'base' keyword to show your intentions.
|
||||
|
||||
For example, base.IsOwner
|
||||
{% endhint %}
|
||||
|
||||
***
|
||||
|
||||
## Callbacks <a href="#callbacks" id="callbacks"></a>
|
||||
|
||||
{% embed url="https://www.youtube.com/watch?v=goH4CX-aG-E" %}
|
||||
|
||||
{% hint style="success" %}
|
||||
You do not need to include the base.XYZ() calls when implementing callbacks.
|
||||
{% endhint %}
|
||||
|
||||
Like Properties, there are a large number of available methods. The methods you will be using commonly are known as callbacks. If you are interested in all available methods view the [methods section under API](https://fish-networking.com/FishNet/api/api/FishNet.Object.NetworkBehaviour.html#methods).
|
||||
|
||||
Most callbacks have a server and client version, and each has great potential. To use a callback you must inherit from NetworkBehaviour and override the callback you wish to use.
|
||||
|
||||
```csharp
|
||||
// Example of implementing an override.
|
||||
public override void OnStartServer()
|
||||
{
|
||||
// This is run when the server initializes the object.
|
||||
}
|
||||
```
|
||||
|
||||
The execution order of spawn related callbacks is constant, but some callbacks may occur at any time. Included is a chart to help you remember callback order.
|
||||
|
||||
{% hint style="warning" %}
|
||||
Unity Awake and OnEnable callbacks will always occur before any network activity.
|
||||
{% endhint %}
|
||||
|
||||
<figure><picture><source srcset="../../../.gitbook/assets/network-behaviour-callbacks-dark.svg" media="(prefers-color-scheme: dark)"><img src="../../../.gitbook/assets/network-behaviour-callbacks-light.svg" alt=""></picture><figcaption><p>Created by Winterbolt</p></figcaption></figure>
|
||||
|
||||
To begin we will cover the server side callbacks.
|
||||
|
||||
### **OnStartNetwork**
|
||||
|
||||
In some instances you will need to initialize for both server and client. You can save some code and time by using OnStartNetwork instead of the OnStart for Client and Server. It's important to remember that OnStartNetwork will only call once, even if you are clientHost.
|
||||
|
||||
```csharp
|
||||
public override void OnStartNetwork()
|
||||
{
|
||||
/* If you wish to check for ownership inside
|
||||
* this method do not use base.IsOwner, use
|
||||
* the code below instead. This difference exist
|
||||
* to support a clientHost condition. */
|
||||
if (base.Owner.IsLocalClient)
|
||||
SetupCamera();
|
||||
}
|
||||
```
|
||||
|
||||
### **OnStartServer**
|
||||
|
||||
```csharp
|
||||
public override void OnStartServer()
|
||||
{
|
||||
/* This callback is performed first. It occurs
|
||||
* when the object is initialized on the server.
|
||||
* Once called Owner, ObjectId, and much more are
|
||||
* already set.
|
||||
* OnStartServer is best used for initializing
|
||||
* server aspects of your script, such as getting
|
||||
* components only the server would need.
|
||||
* It can also be useful for setting values based on
|
||||
* the current state of your game. If you change synchronized
|
||||
* values within this method, such as SyncTypes,
|
||||
* those changes will be delivered to clients
|
||||
* when this object spawns for them.
|
||||
* Eg: perhaps you want to set a players name, which is
|
||||
* a SyncVar. You can do that here and it will be set
|
||||
* for clients when the object spawns on their side. */
|
||||
|
||||
/* When using OnStartServer keep in mind that observers have
|
||||
* not yet been built for this object. If you were to send an ObserversRpc
|
||||
* for example it would not be delivered to any clients. You can however
|
||||
* still use an ObserversRpc and set BufferLast to true if you wish
|
||||
* clients to get it when the object is spawned for them. Another option
|
||||
* is to use OnSpawnServer, displayed below, and send a TargetRpc to the
|
||||
* connection which the object is spawning. */
|
||||
}
|
||||
```
|
||||
|
||||
{% hint style="info" %}
|
||||
When clientHost base.IsOwner will not return true, even with the owner set. This is because IsOwner is a client-side check, and the client has not yet initialized the object.
|
||||
|
||||
When you need to check 'IsOwner' on OnStartServer use **base.Owner.IsLocalClient**.
|
||||
{% endhint %}
|
||||
|
||||
### **OnOwnershipServer**
|
||||
|
||||
```csharp
|
||||
public override void OnOwnershipServer(NetworkConnection prevOwner)
|
||||
{
|
||||
/* This is received when the server is giving ownership
|
||||
* to a client. If a client is gaining
|
||||
* ownership when the object is spawned OnOwnershipServer
|
||||
* will be called immediately after OnStartServer. prevOwner
|
||||
* is the connection which just lost ownership. If ownership is changed
|
||||
* at runtime this callback also occurs. */
|
||||
}
|
||||
```
|
||||
|
||||
### **OnSpawnServer**
|
||||
|
||||
```csharp
|
||||
public override void OnSpawnServer(NetworkConnection connection)
|
||||
{
|
||||
/* This callback occurs after a spawn message for this
|
||||
* object has been sent out to a client. For example: if
|
||||
* this object will be visible to five clients, then this
|
||||
* callback will occur five times, where the connection
|
||||
* parameter will be for each client.
|
||||
* Primarily you will use this callback to send
|
||||
* tailored communications to the client
|
||||
* the object is being spawned for. */
|
||||
}
|
||||
```
|
||||
|
||||
### **OnDespawnServer**
|
||||
|
||||
```csharp
|
||||
public override void OnDespawnServer(NetworkConnection connection)
|
||||
{
|
||||
/* OnDespawnServer is similar to OnSpawnServer, except this is
|
||||
* called right before an object is despawned for a client.
|
||||
* This method can be used to send information to the client
|
||||
* before they receive the despawn message. You may even
|
||||
* send object orientated communications for this object
|
||||
* such as SyncTypes, and RPCs. */
|
||||
}
|
||||
```
|
||||
|
||||
{% hint style="info" %}
|
||||
When the server despawns a NetworkObject any pending synchronize changes will be sent out with the despawn message. This ensures clients will get the latest data, even if timed, before the object is despawned for them.
|
||||
{% endhint %}
|
||||
|
||||
### **OnStopServer**
|
||||
|
||||
```csharp
|
||||
public override void OnStopServer()
|
||||
{
|
||||
/* This is the last callback for server side.
|
||||
* OnStopServer will be called immediately before
|
||||
* the object is deinitialized. At this time it is
|
||||
* too late to send communications specific to this
|
||||
* object for clients.
|
||||
* This callback may be used for any number of things but
|
||||
* could be useful for resetting synchronized values
|
||||
* such as SyncTypes when a scene object is despawned. */
|
||||
}
|
||||
```
|
||||
|
||||
Next are the client callbacks. These callbacks will always occur after the server callbacks, even when acting as host. For example, a callback sequence might look like this: OnStartServer, OnOwnershipServer, OnSpawnServer, OnStartClient, OnOwnershipClient.
|
||||
|
||||
For the most part client callbacks are the same as the server ones, except they occur only if the client connection is started. Only key differences will be included in the descriptions of each callback.
|
||||
|
||||
### **OnStartClient**
|
||||
|
||||
```csharp
|
||||
public override void OnStartClient()
|
||||
{
|
||||
/* This is called on each client when the object
|
||||
* becomes visible to them. Networked values such as
|
||||
* Owner, ObjectId, and SyncTypes will already be
|
||||
* synchronized prior to this callback. */
|
||||
}
|
||||
```
|
||||
|
||||
{% hint style="info" %}
|
||||
Any buffered remote procedure calls will occur immediately after OnStartClient.
|
||||
{% endhint %}
|
||||
|
||||
{% hint style="info" %}
|
||||
If you are coming from Mirror instead of using OnLocalPlayer use OnStartClient with a base.IsOwner check.
|
||||
{% endhint %}
|
||||
|
||||
### **OnOwnershipClient**
|
||||
|
||||
```csharp
|
||||
public override void OnOwnershipClient(NetworkConnection prevOwner)
|
||||
{
|
||||
/* Current owner can be found by using base.Owner. prevOwner
|
||||
* contains the connection which lost ownership. Value will be
|
||||
* -1 if there was no previous owner. */
|
||||
}
|
||||
```
|
||||
|
||||
### **OnStopClient**
|
||||
|
||||
```csharp
|
||||
public override void OnStopClient()
|
||||
{
|
||||
/* Like the server variant this is called right
|
||||
* before the object is deinitialized. It is too
|
||||
* late to send communications to the server
|
||||
* for this object after this callback has occurred. */
|
||||
}
|
||||
```
|
||||
|
||||
### **OnStopNetwork**
|
||||
|
||||
Like OnStartNetwork, this method can be used to save lines of code when needing to deinitialize for both server and client stop. OnStopNetwork will also only be called once even when clientHost.
|
||||
|
||||
```csharp
|
||||
public override void OnStopNetwork()
|
||||
{
|
||||
/* This will call after OnStopServer
|
||||
* and OnStopClient. */
|
||||
}
|
||||
```
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
---
|
||||
description: >-
|
||||
Details on the different types of NetworkObjects that will be referenced
|
||||
throughout the guides.
|
||||
---
|
||||
|
||||
# NetworkObjects
|
||||
|
||||
## NetworkObject
|
||||
|
||||
Any GameObject with a [**NetworkObject**](../../../../fishnet-building-blocks/components/network-object.md) component on it will be considered a "NetworkObject" in these guides going forward.\
|
||||
\
|
||||
Review the [**NetworkObject**](../../../../fishnet-building-blocks/components/network-object.md) component Page for details on the various settings for a **NetworkObject.**
|
||||
|
||||
When you are to add a [**NetworkBehaviour**](../../../../fishnet-building-blocks/components/network-behaviour-components/README.md) component to your prefabs or scene objects the NetworkBehaviour will search for a NetworkObject component on the same object, or within parent objects. If a NetworkObject is not found then one will be added automatically to the top-most object.
|
||||
|
||||
## Spawned NetworkObject
|
||||
|
||||
NetworkObjects that are Instantiated and Spawned using ther ServerManager.Spawn() method will be considered a "Spawned NetworkObject".\
|
||||
\
|
||||
"IsSpawned" Property will be marked True internally on the NetworkObject Component.
|
||||
|
||||
## Scene NetworkObject
|
||||
|
||||
Any **NetworkObject** that exists as part of the Scene aka - never instantiated/spawned into the scene, will be considered a "**Scene NetworkObject**" in these guides going forward.\
|
||||
\
|
||||
"IsSceneObject" property will be marked true on the attached [**NetworkObject**](../../networked-gameobjects-and-scripts/networkobjects/README.md/) component internally.
|
||||
|
||||
## Global NetworkObject
|
||||
|
||||
Any **NetworkObject** with their bool "IsGlobal" marked "true" either in the inspector or with code will be considered a "**GlobalNetworkObject**" in the guides going forward.
|
||||
|
||||
**GlobalNetworkObjects** will automatically be put into the ("DontDestroyOnLoad") scene when instantiated on the server, and when spawned on the clients.
|
||||
|
||||
{% hint style="info" %}
|
||||
Scene objects cannot be marked as global. All global objects must be instantiated and spawned.
|
||||
{% endhint %}
|
||||
|
||||
## Nested NetworkObject
|
||||
|
||||
Any [**NetworkObject**](../../networked-gameobjects-and-scripts/networkobjects/README.md) that are a child of another **NetworkObject** will considered a "**Nested NetworkObject**" in these guides going forward.\
|
||||
\
|
||||
"IsNested" property will be marked true on the attached **NetworkObject** component internally.
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
# Nested NetworkObjects
|
||||
|
||||
When a NetworkObject is beneath another NetworkObject it is considered nested. Fish-Networking allows you to nest NetworkObjects both in the scene and prefabs.
|
||||
|
||||
While spawning a root NetworkObject any active nested NetworkObjects will also be spawned. The nested NetworkObjects will experience the same callbacks as root, and share the same information such as IsOwner.
|
||||
|
||||
You may also have nested NetworkObjects deactivated in the scene or prefab and spawn them later.
|
||||
|
||||
{% hint style="warning" %}
|
||||
When a nested NetworkObject is spawned after the root has already been spawned, the owner information is not automatically assumed the same as the root. You must specify if a client should gain ownership while calling Spawn.
|
||||
{% endhint %}
|
||||
|
||||
Between instantiating the prefab and spawning the root NetworkObject you may make changes to nested NetworkObjects. This includes SyncTypes, and even the active state of the object. These changes are automatically synchronized when the root is spawned.
|
||||
|
||||
For example, if my prefab looks like this:
|
||||
|
||||

|
||||
|
||||
But I change the activate state of _NestedNob_ to disabled before server spawning as such:
|
||||
|
||||

|
||||
|
||||
RootNob will be spawned over the network with NestedNob disabled / not spawned.
|
||||
|
||||
```csharp
|
||||
// An example of changing the enabled state for NestedNob.
|
||||
public GameObject MyPrefab;
|
||||
|
||||
private void SpawnPrefab()
|
||||
{
|
||||
GameObject go = Instantiate(MyPrefab);
|
||||
GameObject nestedNob = go.transform.GetChild(0);
|
||||
nestedNob.SetActive(false);
|
||||
// MyPrefab will spawn with NestedNob disabled for server and all clients
|
||||
// until you spawn it at a later time.
|
||||
base.Spawn(go);
|
||||
}
|
||||
```
|
||||
|
||||
{% hint style="warning" %}
|
||||
Nested NetworkObjects may be spawned and despawned but they should not be detached. This feature is being examined for a future update.
|
||||
|
||||
There is an exception for nested scene NetworkObjects; they may be detached.
|
||||
{% endhint %}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
---
|
||||
description: Information about how to spawn and despawn objects on the network.
|
||||
---
|
||||
|
||||
# Spawning and Despawning
|
||||
|
||||
For objects to exist over the network they must have a NetworkObject component on them, and **must be spawned using the server**. To spawn an object it must first be instantiated, and then passed into a Spawn method. There are a variety of Spawn methods to access for your convenience, all of which provide the same outcome.
|
||||
|
||||
{% hint style="warning" %}
|
||||
Networked addressable prefabs must be registered with the NetworkManager differently. See [Addressables](../../addressables.md) guide for more information on this.
|
||||
{% endhint %}
|
||||
|
||||
**Spawning without an owner** is done by passing in _null_ for the owner, or simply leaving the argument out.
|
||||
|
||||
```csharp
|
||||
GameObject go = Instantiate(_yourPrefab);
|
||||
InstanceFinder.ServerManager.Spawn(go, null);
|
||||
```
|
||||
|
||||
```csharp
|
||||
GameObject go = Instantiate(_yourPrefab);
|
||||
InstanceFinder.ServerManager.Spawn(go);
|
||||
```
|
||||
|
||||
**Spawning with ownership**
|
||||
|
||||
```csharp
|
||||
GameObject go = Instantiate(_yourPrefab);
|
||||
InstanceFinder.ServerManager.Spawn(go, ownerConnection);
|
||||
```
|
||||
|
||||
You may also access the spawn method within any script that inherits NetworkBehaviour, or by accessing the NetworkObject.
|
||||
|
||||
```csharp
|
||||
GameObject go = Instantiate(_yourPrefab);
|
||||
base.Spawn(go, ownerConnection); // networkBehaviour.
|
||||
// or
|
||||
networkObject.Spawn(go, ownerConnection); // referencing a NetworkObject.
|
||||
```
|
||||
|
||||
**Despawning** can be accessed the same ways as spawning. Through a NetworkBehaviour script, a reference NetworkObject, or through the ServerManager directly.
|
||||
|
||||
```csharp
|
||||
base.Despawn(); // networkBehaviour. Despawns the NetworkObject.
|
||||
networkObject.Despawn(); // referencing a NetworkObject.
|
||||
InstanceFinder.ServerManager.Despawn(gameObject); // through ServerManager.
|
||||
```
|
||||
|
||||
When despawning you may also choose to pool the object rather than destroy it. There are optional parameters available to change this behavior.
|
||||
|
||||
```csharp
|
||||
base.Despawn(DespawnType.Pool); // pools the object instead of destroying it.
|
||||
```
|
||||
|
||||
You can check if an object is spawned at anytime using the _IsSpawned_ property within NetworkBehaviour, or NetworkObject.
|
||||
|
||||
**Scene Objects** are spawned and despawned like instantiated objects, except you pass in the reference of the already instantiated/placed scene object. A scene object becomes disabled rather than destroyed when despawned.
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
---
|
||||
description: Information about how to use object pooling for networked objects.
|
||||
---
|
||||
|
||||
# Object Pooling
|
||||
|
||||
Fish-Networking has built-in functionality for Object pooling that will allow the server and client to keep instances of loaded prefabs in memory for later use. This could potentially provide better spawning performance for clients and server.
|
||||
|
||||
## General
|
||||
|
||||
When Despawning [Spawned NetworkObjects](../networkobjects/#spawned-networkobject) using FishNet, instead of destroying the object you may just want to disable it and store it to be used again at a later time. This describes what Object Pooling is.\
|
||||
\
|
||||
FishNet has a default implementation that will allow the objects you have instantiated to be disabled and pooled, instead of being destroyed. This functionality works for clients and server.\
|
||||
\
|
||||
You can also Pre-warm assets for later use, which is discussed later in this guide.
|
||||
|
||||
{% hint style="warning" %}
|
||||
Reminder that [Scene NetworkObjects](../networkobjects/#scene-networkobject) do not get added to the Object Pool and are already disabled instead of destroyed when despawned.
|
||||
{% endhint %}
|
||||
|
||||
## Setup
|
||||
|
||||
As mentioned on the [NetworkManager](../../../../fishnet-building-blocks/components/managers/network-manager.md) component page, there is an assignable field labeled _ObjectPool**.**_ You may assign any script which inherits from the **ObjectPool** class.\
|
||||
\
|
||||
By Default when examining the NetworkManager in the Editor nothing will be assigned to this field, however when you enter play mode the NetworkManager will automatically populate it with the default implementation, and attach the script to the NetworkManager GameObject.
|
||||
|
||||
See below for screen captures of the NetworkManager.
|
||||
|
||||
<figure><img src="../../../../.gitbook/assets/object-pool-unassigned.png" alt="Editor Mode"><figcaption><p>Editor Mode - Nothing will be assigned, unless you implement your own Object Pool</p></figcaption></figure>
|
||||
|
||||
<figure><img src="../../../../.gitbook/assets/object-pool-automatically-assigned.png" alt=""><figcaption><p>Play Mode - Default Object Pool automatically assigned</p></figcaption></figure>
|
||||
|
||||
{% hint style="info" %}
|
||||
By default the object pool is enabled, but your network objects will only use the pool if the default despawn behavior is modified, or through the despawn call. See below for examples to both of these.
|
||||
{% endhint %}
|
||||
|
||||
### Default despawn behavior
|
||||
|
||||
On the [NetworkObject](../../../../fishnet-building-blocks/components/network-object.md) component you can set what the default despawn behavior is for the object where the script is placed.\
|
||||
\
|
||||
This setting is set to "Destroy" by default, so make sure to switch this over to "Pool" if you want Fish-Networking to automatically use the default object pool.
|
||||
|
||||
<figure><img src="../../../../.gitbook/assets/network-object-despawn-type-pool.png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
### Manual despawn behavior
|
||||
|
||||
You can manually change the despawn behavior through code for specific situations.
|
||||
|
||||
```csharp
|
||||
// When calling FishNet's Despawn method from any location, you can pass an enum
|
||||
// parameter to deviate from the default behavior.
|
||||
ServerManager.Despawn(nob, DespawnType.Pool);
|
||||
```
|
||||
|
||||
### Spawning NetworkObjects
|
||||
|
||||
When using the object pool you will want to retrieve NetworkObject from it prior to network spawning them. Doing so will pull from the pool rather than instantiate new objects.
|
||||
|
||||
```csharp
|
||||
// There are many overrides which allow for a variety of information.
|
||||
// You can use GameObjects, NetworkObjects, PrefabIds, CollectionIds,
|
||||
// spawn positions, and more.
|
||||
NetworkObject nob = NetworkManager.GetPooledInstantiated(...);
|
||||
// Spawn normally.
|
||||
ServerManager.Spawn(nob);
|
||||
```
|
||||
|
||||
If you are certain you do not wish to use the object pool on a specific object you can still use the code above and simply set the Default Despawn Type to Destroy on the NetworkObject, or Instantiate and spawn normally.
|
||||
|
||||
## Pre-Warming the ObjectPool
|
||||
|
||||
If you want to manually store Network Objects to the ObjectPool prior to needing them at run-time you may do so through the NetworkManager API.
|
||||
|
||||
Here is a very basic implementation of pre-warming the ObjectPool.
|
||||
|
||||
```csharp
|
||||
[SerializeField]
|
||||
private NetworkObject _nobPrefab;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
/// <summary>
|
||||
/// Instantiates a number of objects and adds them to the pool.
|
||||
/// </summary>
|
||||
/// <param name="prefab">Prefab to cache.</param>
|
||||
/// <param name="count">Quantity to spawn.</param>
|
||||
/// <param name="asServer">True if storing prefabs for the server collection.</param>
|
||||
InstanceFinder.NetworkManager.CacheObjects(_nobPrefab, 100, IsServer);
|
||||
}
|
||||
```
|
||||
|
||||
## Custom implementation
|
||||
|
||||
FishNet allows for the user to implement their own method of object pooling. First create your own class inheriting from the **ObjectPool** class. Place your new class component in your scene, typically directly on the NetworkManager object. Then assign your component to the _ObjectPool_ field on the NetworkManager.
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
---
|
||||
description: >-
|
||||
Predicted spawning an object allows a client to spawn an object locally and
|
||||
begin using networked features immediately, including RPCs.
|
||||
---
|
||||
|
||||
# Predicted Spawning
|
||||
|
||||
{% hint style="warning" %}
|
||||
Currently predicted spawns cannot be nested during the spawning process. We are looking to remove this limitation throughout the development of version 4.
|
||||
{% endhint %}
|
||||
|
||||
The term predicted spawning also means predicted despawning, where the client may despawn the object locally while awaiting for a server response.
|
||||
|
||||
### Global settings
|
||||
|
||||
By default predicted spawning is disabled. To enable predicted spawning you must adjust settings on your ServerManager. If you do not already have a ServerManager then you must add one to your NetworkManager object to change settings.
|
||||
|
||||
The enabled state of this feature can be set by checking **Allow Predicted Spawning** on the ServerManager. You may also adjust the **Reserved Object Ids** value. To learn more of these visit the [ServerManager component](../../../../fishnet-building-blocks/components/managers/server-manager.md) section.
|
||||
|
||||
### Settings per object
|
||||
|
||||
Even with predicted spawning enabled globally NetworkObjects must be set to also allow predicted spawning or despawning. This ensures that only the objects you specify may use this feature, allows you to limit predicted spawning the capabilities for the object, and lets you use custom code to further validate or control predicted spawning.
|
||||
|
||||
For prefabs or scene objects you wish to allow predicted spawning on add the [PredictedSpawn component](../../../../fishnet-building-blocks/components/prediction/predictedspawn.md) to them.
|
||||
|
||||
{% hint style="info" %}
|
||||
You can inherit the PredictedSpawn component for overrides to customize predicted spawning.
|
||||
{% endhint %}
|
||||
|
||||
### Using Predicted Spawning
|
||||
|
||||
Once enabled globally and per your desired objects predicted spawning and despawning may be used on clients as though they were the server. All of the same spawn and despawn methods work, including even specifying who may own the object when spawned.
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
---
|
||||
description: >-
|
||||
Spawn Payloads allow the sending additional information along with an object's
|
||||
Spawn call.
|
||||
---
|
||||
|
||||
# Spawn Payloads
|
||||
|
||||
[NetworkBehaviours](../network-behaviour-guides.md) have support for writing and reading extra data and sending it along with the spawn payloads for the network object. This can be quite useful for syncing an initial state of the object.
|
||||
|
||||
To use this, you need to override the NetworkBehaviour's `WritePayload` and `ReadPayload` methods.
|
||||
|
||||
{% hint style="warning" %}
|
||||
When writing and reading, the order and amount of data written and read is important and **MUST** be the same.
|
||||
{% endhint %}
|
||||
|
||||
## Write Payload
|
||||
|
||||
The first parameter of the **WritePayload** method needs to be a [NetworkConnection](../../server-and-client-identification/networkconnections.md), this represents which client the spawn is going to happen for and is provided by FishNet when it calls the method.
|
||||
|
||||
The second parameter should be of the type [Writer](https://fish-networking.com/FishNet/api/api/FishNet.Serializing.Writer.html), this will represent a reference to the Writer being used and you will use it to add data to the payload.
|
||||
|
||||
For example you can do the following:
|
||||
|
||||
```csharp
|
||||
public override void WritePayload(NetworkConnection connection, Writer writer)
|
||||
{
|
||||
// Writes the string stored in the class variable _playerName to the spawn payload.
|
||||
writer.WriteString(_playerName);
|
||||
// Writes the integer 42 to the payload.
|
||||
writer.WriteInt32(42);
|
||||
}
|
||||
```
|
||||
|
||||
{% hint style="success" %}
|
||||
You can view the API for this method [here](https://fish-networking.com/FishNet/api/api/FishNet.Object.NetworkBehaviour.html#FishNet_Object_NetworkBehaviour_WritePayload_FishNet_Connection_NetworkConnection_FishNet_Serializing_Writer_).
|
||||
{% endhint %}
|
||||
|
||||
## Read Payload
|
||||
|
||||
The first parameter of the **ReadPayload** method needs to be a [NetworkConnection](../../server-and-client-identification/networkconnections.md), this represents which client the spawn is happening for and is provided by FishNet when it calls the method.
|
||||
|
||||
The second parameter should be of the type [Reader](https://fish-networking.com/FishNet/api/api/FishNet.Serializing.Reader.html), this will represent a reference to the Reader being used and you will use it to get data out of the payload.
|
||||
|
||||
For example you can do the following:
|
||||
|
||||
```csharp
|
||||
public override void ReadPayload(NetworkConnection connection, Reader reader)
|
||||
{
|
||||
// Reads a string from the payload. Since a string was written first, it must be read first too.
|
||||
_playerName = reader.ReadStringAllocated();
|
||||
// Reads an integer from the payload.
|
||||
_playerScore = reader.ReadInt32();
|
||||
}
|
||||
```
|
||||
|
||||
{% hint style="success" %}
|
||||
You can view the API for this method [here](https://fish-networking.com/FishNet/api/api/FishNet.Object.NetworkBehaviour.html#FishNet_Object_NetworkBehaviour_ReadPayload_FishNet_Connection_NetworkConnection_FishNet_Serializing_Reader_).
|
||||
{% endhint %}
|
||||
|
||||
## More advanced example
|
||||
|
||||
{% code title="SpawnPayloadExample.cs" lineNumbers="true" %}
|
||||
```csharp
|
||||
public class SpawnPayloadExample : NetworkBehaviour
|
||||
{
|
||||
private ArraySegment<byte> _cachedPayload = ArraySegment<byte>.Empty;
|
||||
|
||||
public override void WritePayload(NetworkConnection connection, Writer writer)
|
||||
{
|
||||
/* If you are using predicted spawning and want to write the
|
||||
* payload if you are the spawning client, this might help! */
|
||||
bool isPredictedSpawner = NetworkObject.PredictedSpawner.IsLocalClient;
|
||||
|
||||
/* It is worth noting that the server will still call WritePayload when
|
||||
* spawning the object for clients, even if a predicted spawn.
|
||||
* Should a client send a predicted spawn the server will receive ReadPayload
|
||||
* for the predicted spawn, and the server will also be given an opportunity to
|
||||
* send a payload to other clients which are not the predicted spawner. If you wish to
|
||||
* forward a payload from a predicted spawner cache the values locally, and
|
||||
* send them here.
|
||||
*
|
||||
* Here's an example of what that might look like. */
|
||||
|
||||
if (isPredictedSpawner)
|
||||
{
|
||||
// As the predicted spawner let's write our own payload!
|
||||
}
|
||||
// If not the predicted spawner, we are the server sending a payload to other clients.
|
||||
else
|
||||
{
|
||||
// Write our cached payload, or don't, send whatever you want!
|
||||
|
||||
// Check if not empty first.
|
||||
if (_cachedPayload != ArraySegment<byte>.Empty)
|
||||
writer.WriteArraySegmentAndSize(_cachedPayload);
|
||||
}
|
||||
}
|
||||
|
||||
public override void ReadPayload(NetworkConnection connection, Reader reader)
|
||||
{
|
||||
/* You can also check if the payload is coming from the predicted spawner as well.
|
||||
*
|
||||
* ReadPayload would be first called on the server, and if the connection IsValid, we know
|
||||
* that the connection is a predicted spawner, as that is the only time connection would be
|
||||
* valid on the server. */
|
||||
bool isFromPredictedSpawner = IsServerStarted && connection.IsValid;
|
||||
|
||||
/* Another way using a similar workflow to WritePayload. */
|
||||
bool isFromPredictedSpawner = IsServerStarted && connection == NetworkObject.PredictedSpawner;
|
||||
|
||||
/* When the connection is not valid, as noted in the parameter comments, this means
|
||||
* the server is the sender. */
|
||||
bool isFromServer = !connection.IsValid;
|
||||
|
||||
/* If you're the server and wish to cache a payload received from a predicted spawner,
|
||||
* this is the place to do it. We are caching into an arraysegment named _cachedPayload --
|
||||
* see WritePayload on how to forward this to other clients. */
|
||||
if (isFromPredictedSpawner)
|
||||
{
|
||||
int readerStart = reader.Position;
|
||||
|
||||
// Do all your reads as you would.
|
||||
|
||||
int readAmount = reader.Position - readerStart;
|
||||
// Let's use a pooled array so we don't allocate.
|
||||
byte[] payload = ArrayPool<byte>.Shared.Rent(readAmount);
|
||||
/* Copy into payload array from where the reader started, up to to
|
||||
* the calculated readAmount. */
|
||||
Array.Copy(reader.GetBuffer(), sourceIndex: readerStart, payload, destinationIndex: 0, readAmount);
|
||||
|
||||
_cachedPayload = new(payload, 0, readAmount);
|
||||
|
||||
// /* You will probably want to return the rented array when OnStop callbacks occur. */
|
||||
// if (_cachedPayload != ArraySegment<byte>.Empty)
|
||||
// ArrayPool<byte>.Shared.Return(_cachedPayload.Array);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
{% endcode %}
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
description: >-
|
||||
Fish-Networking features an advanced area-of-interest system that controls
|
||||
which clients receive updates about specific objects.
|
||||
---
|
||||
|
||||
# Area of Interest (Observer System)
|
||||
|
||||
An **observer** is a client which can see an object and use communications for the object. You may control which clients can observe an object by using the [NetworkObserver](../../../fishnet-building-blocks/components/network-observer.md) and/or [ObserverManager](../../../fishnet-building-blocks/components/managers/observermanager/) components.
|
||||
|
||||
If a client is not an observer of an object, then the object will not activate, and the client will not receive network messages or callbacks for that object. Should the object be a [scene object](../../high-level-overview/terminology/miscellaneous.md#scene-object) then it will remain disabled on the client until they become an observer of it. If the object is [instantiated](../../high-level-overview/terminology/miscellaneous.md#instantiated-object), then the client will simply not instantiate the object until after becoming an observer.
|
||||
|
||||
The observer system is designed to work out of the box for new developers. When it comes time to customize how clients observe objects, the observer system additionally offers a large amount of flexibility, keeping in mind there are many condition types, and that you may also create your own.
|
||||
|
||||
FishNet comes with a NetworkManager prefab which contains the recommended minimum components to begin working on a new project. Within that prefab is the [ObserverManager](../../../fishnet-building-blocks/components/managers/observermanager/) with an included [Scene Condition](../../../fishnet-building-blocks/components/network-observer.md#component-settings). If you have not familiarized yourself with the ObserverManager and condition types, please do so now using the links above.
|
||||
|
||||
A common problem new developers encounter is scene network objects not being enabled for clients. This occurs when the client is not considered part of the scene where the object resides, and the scene condition is preventing that object from spawning for the client. The NetworkManager prefab contains a PlayerSpawner script which adds the player to the current scene; this makes the client an observer for objects in that scene. This requires a player object to be spawned. Should you have made your own NetworkManager object or removed the PlayerSpawner script you will need to add the client to the scene you wish the client to be an observer of.
|
||||
|
||||
<div data-full-width="false"><figure><img src="../../../.gitbook/assets/player-spawner-component.png" alt=""><figcaption><p>The "Add To Default Scene" option enabled on the PlayerSpawner</p></figcaption></figure></div>
|
||||
|
||||
When encountering such an issue you may of course also remove the ObserverManager or scene condition from the ObserverManager, but this is not recommended as objects in other scenes will attempt to spawn for clients which do not occupy such scenes. Alternatively, you may add the client to the scene where the objects reside; there's a variety of ways to accomplish this.
|
||||
|
||||
You can use a script such as the following on an object in the scene. But be careful not to place it on a potentially DDoL object such as the NetworkManager.
|
||||
|
||||
{% code expandable="true" %}
|
||||
```csharp
|
||||
using FishNet;
|
||||
using FishNet.Connection;
|
||||
using UnityEngine;
|
||||
|
||||
public class InitialSceneObserver : MonoBehaviour
|
||||
{
|
||||
private void Start()
|
||||
{
|
||||
NetworkManager networkManager = InstanceFinder.NetworkManager;
|
||||
if (networkManager != null)
|
||||
networkManager.SceneManager.OnClientLoadedStartScenes += OnClientLoadedStartScenes;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
NetworkManager networkManager = InstanceFinder.NetworkManager;
|
||||
if (networkManager != null)
|
||||
networkManager.SceneManager.OnClientLoadedStartScenes -= OnClientLoadedStartScenes;
|
||||
}
|
||||
|
||||
private void OnClientLoadedStartScenes(NetworkConnection conn, bool asServer)
|
||||
{
|
||||
if (!asServer)
|
||||
return;
|
||||
|
||||
if (!conn.Scenes.Contains(gameObject.scene))
|
||||
InstanceFinder.NetworkManager.SceneManager.AddConnectionToScene(conn, gameObject.scene);
|
||||
}
|
||||
}
|
||||
```
|
||||
{% endcode %}
|
||||
|
||||
Under the assumption you removed the PlayerSpawner and/or are not using _SceneManager.AddOwnerToDefaultScene_ or the method above, then you must load the client into the scene using the SceneManager. Clients are only considered networked into scenes when those scenes are loaded using the SceneManager. Clients may become part of a scene by loading a scene globally or by loading a scene for a specific client (connection). See the [SceneManager](../scene-management/) section for more information on how to manage networked client scenes as well understand the difference between global and connection scenes.
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
description: A simple guide on how to write a custom observer condition.
|
||||
---
|
||||
|
||||
# Custom Conditions
|
||||
|
||||
Sometimes you may have unique requirements for an observer condition. When this is the case you can easily create your own ObserverCondition. The code below comments on how to create your own condition.
|
||||
|
||||
```csharp
|
||||
// The example below does not have many practical uses
|
||||
// but it shows the bare minimum needed to create a custom condition.
|
||||
// This condition makes an object only visible if the connections
|
||||
// ClientId mathes the serialized value, _id.
|
||||
|
||||
// Make a new class which inherits from ObserverCondition.
|
||||
// ObserverCondition is a scriptable object, so also create an asset
|
||||
// menu to create a new scriptable object of your condition.
|
||||
[CreateAssetMenu(menuName = "FishNet/Observers/ClientId Condition", fileName = "New ClientId Condition")]
|
||||
public class ClientIdCondition : ObserverCondition
|
||||
{
|
||||
/// <summary>
|
||||
/// ClientId a connection must be to pass the condition.
|
||||
/// </summary>
|
||||
[Tooltip("ClientId a connection must be to pass the condition.")]
|
||||
[SerializeField]
|
||||
private int _id = 0;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Awake can be optionally used to initialize values based on serialized
|
||||
// data. The source file of DistanceCondition is a good example
|
||||
// of where Awake may be used.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if the object which this condition resides should be visible to connection.
|
||||
/// </summary>
|
||||
/// <param name="connection">Connection which the condition is being checked for.</param>
|
||||
/// <param name="currentlyAdded">True if the connection currently has visibility of this object.</param>
|
||||
/// <param name="notProcessed">True if the condition was not processed. This can be used to skip processing for performance. While output as true this condition result assumes the previous ConditionMet value.</param>
|
||||
public override bool ConditionMet(NetworkConnection connection, bool currentlyAdded, out bool notProcessed)
|
||||
{
|
||||
notProcessed = false;
|
||||
|
||||
// When true is returned it means the connection meets
|
||||
// the condition requirements. When false, the
|
||||
// connection does not and will not see the object.
|
||||
|
||||
// Will return true if connection Id matches _id.
|
||||
return (connection.ClientId == _id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type of condition this is. Certain types are handled different, such as Timed which are checked for changes at timed intervals.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/* Since clientId does not change a normal condition type will work.
|
||||
* See API on ObserverConditionType for more information on what each
|
||||
* type does. */
|
||||
public override ObserverConditionType GetConditionType() => ObserverConditionType.Normal;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
You can get an idea of the flexibility of a condition by exploring the source of other premade conditions.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Modifying Conditions
|
||||
|
||||
Several conditions may be modified at run-time. What can be modified for each condition may vary. I encourage you to view the [API](https://fish-networking.com/FishNet/api/api/FishNet.Component.Observing.html) to see what each condition exposes.
|
||||
|
||||
To change properties on a condition you must access the condition through the [NetworkObserver](../../../fishnet-building-blocks/components/network-observer.md) component.
|
||||
|
||||
```csharp
|
||||
// Below is an example of modifying the distance requirement
|
||||
// on a DistanceCondition. This line of code can be called from
|
||||
// any NetworkBehaviour. You may also use nbReference.NetworkObserver...
|
||||
base.NetworkObserver.GetObserverCondition<DistanceCondition>().MaximumDistance = 10f;
|
||||
```
|
||||
|
||||
All conditions can be enabled or disabled. When a condition is disabled it's requirements are ignored, as if the condition does not exist. This can be useful for temporarily disabling condition requirements on objects.
|
||||
|
||||
```csharp
|
||||
// The OwnerOnlyCondition is returned and disabled.
|
||||
// This allows all players to see the object, rather than just the owner.
|
||||
ObserverCondition c = base.NetworkObject.NetworkObserver.GetObserverCondition<OwnerOnlyCondition>();
|
||||
c.SetIsEnabled(false);
|
||||
// Even though we are returning ObserverCondition type, it could be casted to
|
||||
// OwnerOnlyCondition.
|
||||
```
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
description: >-
|
||||
Understanding how to use ownership, as well how it affects clients and the
|
||||
server is essential for any project.
|
||||
---
|
||||
|
||||
# Ownership
|
||||
|
||||
### What is ownership?
|
||||
|
||||
Ownership is a core concept in Fish-Net, determining which client has control over an object.
|
||||
|
||||
Every NetworkObject in Fish-Net can be assigned a client to own it, or the object can have no assigned owner. The server is exclusively responsible for changing ownership of objects, even though it cannot own an object itself.
|
||||
|
||||
When a client owns an object, they are considered its rightful user, capable of actions like moving a character or using a weapon. Ownership can also be temporarily granted for interactions with world objects, such as using a turret.
|
||||
|
||||
Ownership checks are essential for ensuring proper control over objects. Common scenarios include:
|
||||
|
||||
* Validating if a client can manipulate an object.
|
||||
* Checking ownership status for player-related values like name or score.
|
||||
* Ensuring only the owner executes specific network calls (e.g., ServerRpcs).
|
||||
|
||||
### Assigning ownership
|
||||
|
||||
Ownership can be granted in several ways:
|
||||
|
||||
* Spawning with Ownership: When creating an object, ownership can be assigned to a specific connection.
|
||||
|
||||
```csharp
|
||||
Gameobject go = Instantiate(_yourPrefab);
|
||||
InstanceFinder.ServerManager.Spawn(go, ownerConnection);
|
||||
```
|
||||
|
||||
* Changing or Adding Ownership: If an object is already spawned, ownership can be modified at any time. Previous owners are replaced with the new owner.
|
||||
|
||||
```csharp
|
||||
networkObject.GiveOwnership(newOwnerConnection);
|
||||
```
|
||||
|
||||
* Removing Ownership: Ownership can be revoked from a client when needed.
|
||||
|
||||
```csharp
|
||||
networkObject.RemoveOwnership();
|
||||
```
|
||||
|
||||
{% hint style="success" %}
|
||||
`ownerConnection` and `newOwnerConnection` above represent a NetworkConnection that will be set as the new owner.
|
||||
{% endhint %}
|
||||
|
||||
### Checking ownership
|
||||
|
||||
Ownership can be verified via the following NetworkObject or NetworkBehaviour properties:
|
||||
|
||||
```csharp
|
||||
base.IsOwner; // Returns true if the local client owns the object.
|
||||
base.Owner; // Retrieves the current owner NetworkConnection.
|
||||
base.IsController; // True if the local client owns the object or is the server with no assigned owner.
|
||||
```
|
||||
|
||||
#### Simple movement example
|
||||
|
||||
A client must own an object to execute certain actions. Below is an example of movement logic restricted to the object's owner:
|
||||
|
||||
```csharp
|
||||
void Update()
|
||||
{
|
||||
// The client's game instance will have several player objects in it if there
|
||||
// are multiple players playing, but he will only own one of them and should
|
||||
// should only move that one that he owns.
|
||||
if (base.IsOwner)
|
||||
{
|
||||
float hor = Input.GetAxisRaw("Horizontal");
|
||||
float ver = Input.GetAxisRaw("Vertical");
|
||||
transform.position += new Vector3(hor, 0f, ver);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When paired with a [Network Transform](../../../fishnet-building-blocks/components/network-transform.md) set to Client Authoritative, this movement will be relayed to the server and synchronized across all clients.
|
||||
|
||||
### Transferring ownership
|
||||
|
||||
Only the server can assign, transfer, or remove ownership. Typically, ownership is granted when spawning an object.
|
||||
|
||||
* Automatic Ownership Assignment: The PlayerSpawner script (found in the NetworkManager prefab) ensures the spawned player object is owned by its respective client.
|
||||
* Immediate Ownership: In cases where a client needs immediate ownership—such as controlling a turret without delay—the [PredictedOwner](../../../fishnet-building-blocks/components/prediction/predictedowner.md) component can be used. This component can be extended for customized logic.
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
---
|
||||
description: >-
|
||||
Learn how to store values for clients and read them on unlinked objects by
|
||||
reading owner information.
|
||||
---
|
||||
|
||||
# Using Ownership to Read Values
|
||||
|
||||
{% hint style="info" %}
|
||||
This guide requires Share Ids to be enabled on the ServerManager for clients to read values. Share Ids is enabled by default, and does not reveal any sensitive information to clients.
|
||||
{% endhint %}
|
||||
|
||||
We are going to demonstrate how to assign a display name to each client, and display that name on player objects.
|
||||
|
||||
{% hint style="warning" %}
|
||||
This guide assumes you already have a NetworkManager object in your scene.
|
||||
{% endhint %}
|
||||
|
||||
## PlayerNames
|
||||
|
||||
First make a new script on an empty scene object and name it PlayerNames; this script needs to inherit NetworkBehaviour.
|
||||
|
||||
After adding the script to your scene object a NetworkObject component will automatically be added to the same object. On the NetworkObject enable 'Is Global', make this object a prefab, then delete it from your scene.
|
||||
|
||||
<figure><img src="../../../.gitbook/assets/player-names-prefab.png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
{% hint style="success" %}
|
||||
Notice that on the NetworkObject we also set the Initialize Order to -128. Doing so ensures that this NetworkObject will initialize before any other object does, which promises the OnStart callbacks will execute before other scripts. This step is most likely not needed, but given this is more-or-less a managing script for player names giving it execution priority is good practice.
|
||||
{% endhint %}
|
||||
|
||||
We are now going to populate the PlayerNames script with the following code.
|
||||
|
||||
```csharp
|
||||
using System;
|
||||
using FishNet.Connection;
|
||||
using FishNet.Object;
|
||||
using FishNet.Object.Synchronizing;
|
||||
using FishNet.Transporting;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
public class PlayerNames : NetworkBehaviour
|
||||
{
|
||||
/* Since this is a syncType it will automatically be synchronized to clients
|
||||
* whenever it updates, and when clients spawn this object! */
|
||||
|
||||
/// <summary>
|
||||
/// Called when a player name is updated.
|
||||
/// </summary>
|
||||
public event Action<NetworkConnection, string> OnPlayerNameChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Names of all connected clients.
|
||||
/// </summary>
|
||||
public readonly SyncDictionary<NetworkConnection, string> Names = new();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Names.OnChange += NamesOnOnChange;
|
||||
}
|
||||
|
||||
public override void OnStartNetwork()
|
||||
{
|
||||
// Register this to the NetworkManager so it can be found easily by any script!
|
||||
NetworkManager.RegisterInstance(this);
|
||||
}
|
||||
|
||||
public override void OnStartServer()
|
||||
{
|
||||
ServerManager.OnRemoteConnectionState += ServerManagerOnOnRemoteConnectionState;
|
||||
}
|
||||
|
||||
public override void OnStopServer()
|
||||
{
|
||||
ServerManager.OnRemoteConnectionState -= ServerManagerOnOnRemoteConnectionState;
|
||||
}
|
||||
|
||||
public override void OnStopNetwork()
|
||||
{
|
||||
// Unregister to clean up.
|
||||
if (NetworkManager != null)
|
||||
NetworkManager.UnregisterInstance<PlayerNames>();
|
||||
}
|
||||
|
||||
private void ServerManagerOnOnRemoteConnectionState(NetworkConnection conn, RemoteConnectionStateArgs args)
|
||||
{
|
||||
// If disconnecting remove from the dictionary.
|
||||
if (args.ConnectionState == RemoteConnectionState.Stopped)
|
||||
{
|
||||
Names.Remove(conn);
|
||||
}
|
||||
// If connecting then add.
|
||||
else if (args.ConnectionState == RemoteConnectionState.Started)
|
||||
{
|
||||
/* When a player connects assign them a random number
|
||||
* as their name. */
|
||||
|
||||
// Another cog in the machine.
|
||||
string randomName = Random.Range(1000, 999999).ToString();
|
||||
Names.Add(conn, randomName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls whenever the _names collection updates.
|
||||
/// </summary>
|
||||
private void NamesOnOnChange(SyncDictionaryOperation op, NetworkConnection key, string value, bool asServer)
|
||||
{
|
||||
// If an add or modify then invoke.
|
||||
if (op == SyncDictionaryOperation.Add || op == SyncDictionaryOperation.Set)
|
||||
OnPlayerNameChanged?.Invoke(key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allows a client to call this RPC, updating their name.
|
||||
/// </summary>
|
||||
[ServerRpc(RequireOwnership = false)]
|
||||
public void ServerSetName(string newName, NetworkConnection caller = null)
|
||||
{
|
||||
// Caller will never be null; the server will assign it automatically when a client calls this since RequireOwnership is false.
|
||||
// ReSharper disable once AssignNullToNotNullAttribute
|
||||
Names[caller] = newName;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The above code snippet will give players a random name when they connect, and allow clients to change their name by calling the SetName RPC.
|
||||
|
||||
## Automatically spawning PlayerNames
|
||||
|
||||
After you have made the prefab select your scene NetworkManager, make a child object named ServerSpawner, and add the script ServerSpawner. You may place this script anywhere in your scene or game, but for simplicity sake we're going to nest it beneath the NetworkManager. After you add the script, insert your newly created PlayerNames prefab into the 'Network Objects' field and ensure Automatically Spawn is enabled.
|
||||
|
||||
<figure><img src="../../../.gitbook/assets/player-names-server-spawner.png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
## Displaying player names
|
||||
|
||||
Next we are going to make a very simple script which changes the text value on a TextMeshPro component. This is a very simple example which might go on the players character. You could use similar scripts for chat names, and more.
|
||||
|
||||
Make a new script named CharacterName. Add the following:
|
||||
|
||||
```csharp
|
||||
using FishNet.Connection;
|
||||
using FishNet.Object;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
public class CharacterName : NetworkBehaviour
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Text box used to display the name of this character.
|
||||
/// </summary>
|
||||
[Tooltip("Text box used to display the name of this character.")]
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _text;
|
||||
|
||||
// Cached value for unsubscribing to save a little perf.
|
||||
private PlayerNames _playerNames;
|
||||
|
||||
public override void OnStartClient()
|
||||
{
|
||||
// If owner is not set then do not get the name, as this does not belong to a client.
|
||||
if (!base.Owner.IsValid)
|
||||
return;
|
||||
|
||||
// Get the PlayerNames instance to read this characters name(the player name).
|
||||
_playerNames = base.NetworkManager.GetInstance<PlayerNames>();
|
||||
|
||||
// If cannot be found exit method; this shouldn't ever happen.
|
||||
if (!_playerNames.Names.TryGetValue(base.Owner, out string theName))
|
||||
return;
|
||||
|
||||
_text.text = theName;
|
||||
|
||||
// Also listen for updates for
|
||||
_playerNames.OnPlayerNameChanged += PlayerNamesOnOnPlayerNameChanged;
|
||||
}
|
||||
|
||||
public override void OnStopClient()
|
||||
{
|
||||
// Unsubscribe from events to clean up.
|
||||
if (_playerNames != null)
|
||||
_playerNames.OnPlayerNameChanged -= PlayerNamesOnOnPlayerNameChanged;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Called when a player name changes after initially being set, or when added for the first time.
|
||||
/// </summary>
|
||||
private void PlayerNamesOnOnPlayerNameChanged(NetworkConnection conn, string theName)
|
||||
{
|
||||
// If the name being changed is not for this owner then do not update anything.
|
||||
if (conn != base.Owner)
|
||||
return;
|
||||
|
||||
// Set new name.
|
||||
_text.text = theName;
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Now when an object spawns containing the script above, the \_text field will be updated to the owners player name. In addition, if the owner changes their name at any time, the text will be updated again.
|
||||
|
||||
{% hint style="success" %}
|
||||
Notice how we make use of **NetworkManager.Register**, **Unregister**, and **GetInstance** in this guide. This is a very useful feature for accessing global networked scripts.
|
||||
{% endhint %}
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: >-
|
||||
Prediction is the act of server-authoritative actions while allowing clients
|
||||
to move in real-time without delay.
|
||||
---
|
||||
|
||||
# Prediction
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
---
|
||||
description: >-
|
||||
Setting up the NetworkObject inspector for prediction is mandatory for using
|
||||
prediction methods in scripts belonging to the NetworkObject.
|
||||
---
|
||||
|
||||
# Configuring NetworkObject
|
||||
|
||||
All of the current options for the NetworkObject inspector are shown below. There are many but many will not be seen depending on your setup, and most are just there to provide you more fine-tuning for your game or setup.
|
||||
|
||||
<figure><img src="../../../.gitbook/assets/prediction-configuring-networkobject.png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
To begin you must first choose to Enable Prediction. Next you will set the Prediction Type. If you are using a rigidbody or rigidbody2D set the prediction type accordingly. Other is used for non-physics such as character controllers.
|
||||
|
||||
## State forwarding
|
||||
|
||||
State forwarding will allow the same inputs to run on all clients as they do on the server. This can be useful if you want all clients and server to run the same input based logic, similar to if the client or server owns the object. State forwarding is more CPU intensive as it means a state buffer must be kept, and the object must reconcile to make corrections as well re-run past states.
|
||||
|
||||
Even with the overhead state forwarding is often the most preferred approach because of the fact you do gain the ability to run code identical to how the client and server ran it, resulting in a more reliable simulation and potentially easier coding approach.
|
||||
|
||||
When state forwarding is disabled only the owner and server will run the inputs, and only the owner will keep the buffer for objects they own. This also means you must forward any information to clients that is essential to them displaying actions on non-owned objects, such as using a NetworkAnimator to now relay animations, or using RPCs to send gun fire audio. Movement is also not forwarded, so you may want to attach a NetworkTransform and specify it within the NetworkTransform field. Specifying the NetworkTransform will simply try to configure it to prediction based on your other NetworkObject inspector settings. If you are finding this is causing issues you can simply leave the field empty.
|
||||
|
||||
## Graphical object
|
||||
|
||||
This is the object which holds your graphics for the NetworkObject. By graphics, this means anything which holds visual representation that you would likely want smoothed between ticks, as well corrections from any potential de-synchronizations. It's very much worth mentioning you can leave this field empty and you will lose view additional options, such as Smoothing.
|
||||
|
||||
When using a graphical object it's very important to remember that you do not want any components on or beneath it that could negatively be affected by smoothing, such as a capsule to move your player. During a network tick prediction methods will run, and the TimeManager.TickDelta is used as the frame delta. Think of this as the same as if you were creating a client-authoritative physics controller while using FixedUpdate to move. Even as client-authoratitive, if you were to smooth the root the distance traveled during the FixedUpdate then your root would be in a different spot than where the physics engine left it. In result the movement and collision system wouldn't behave quite right. With all of that said, typically your colliders and triggers which affect gameplay or transforming the NetworkObject should be on the same GameObject as your NetworkObject. Or at the very least, not within the graphical object.
|
||||
|
||||
To understand more what this does read the component information [here](../../../fishnet-building-blocks/components/network-object.md). Detaching the graphical object is a supplemental feature which might be useful depending on your setup. The default is keeping the graphical a child, where it is. When this is true the graphical object rolls back to it's transform properties after the tick, then smooths to the transform properties it was after the tick. This is done so you can run lower tick rates and have smooth transform updates rather than everything stepping to whatever your tick rate delta is.
|
||||
|
||||
However, keeping the graphical object attached could be problematic for certain animation setups, or even cameras. Third party assets often do not expect transforms to teleport back and then be smooth to their destination over time, and in result you might see some funky outcomes of this action. Cameras as well can display what appears to be stuttering or jitter of your graphical object, even though it's completely fine. By detaching the graphical object it is no longer rolled back after the tick, and it simply exists in world space moving towards the proper goal over the duration of a tick. In most scenarios detaching would likely be better but there is always a chance it's not right for you, hence the default of staying attached.
|
||||
|
||||
## Smoothing
|
||||
|
||||
The smoothing options are only present when a graphical object is set. Most of these settings are explained well on the same NetworkObject component information page linked above.
|
||||
|
||||
There are certain cases where you may want to customize smoothing to control what is actually smoothed, versus what you want to control yourself. That's where the smoothed properties come in. You will notice there is a smoothed properties for if you are the owner of the object, or a spectator(not owner). In a number of cases we've seen developers wanting to self smooth things such as rotation while letting FishNet handle the position and scale. In such a scenario you would just untick rotation from the smoothed properties.
|
||||
|
||||
Changing the smoothing has absolutely no affect on the prediction itself, at least it shouldn't assuming you utilized the previous notes of graphical object.
|
||||
|
||||
Adaptive Interpolation under Spectator options determines the level of interpolation on spectated objects. Setting this to lower values ultimately means less interpolation, and in result a larger chance that a de-synchronization will be more apparent from a visual stand point. A higher value of course means more interpolation, and much less chance of seeing the effects of a de-synchronization. Whichever you use for this keep your game type in mind, or just play around with the settings under some latency until you see what you like. The adaptive interpolation API is also exposed letting you tweak it at runtime, and we have components that do just this to help you get that perfect feel for your game.
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
---
|
||||
description: >-
|
||||
The PredictionManager is responsible for global prediction settings, and other
|
||||
prediction related information.
|
||||
---
|
||||
|
||||
# Configuring PredictionManager
|
||||
|
||||
You do not necessarily even need to add it to your NetworkManager object, but doing so does offer you to alter default settings.
|
||||
|
||||
<figure><img src="../../../.gitbook/assets/prediction-manager-component.png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
## Interpolation
|
||||
|
||||
Depending on your game type you may want to adjust the default client and server interpolation. The tooltips provide helpful information, but in short more interpolation means more resilience against network instability at the cost of larger delays between running actions.
|
||||
|
||||
Having more interpolation also means reducing the chances of data having to predict data when it is expected to be known. There's a variety of ways to know if data is confirmed, predicted, or in the future; this topic is covered later.
|
||||
|
||||
Client interpolation indicates how many ticks inputs from the server(and other clients) are held before they are run.
|
||||
|
||||
For casual games an interpolation of 2-3 may be desired to drastically improve the likeliness inputs will always be available to run. This will of course add on a delay to when those inputs are run, so perhaps for fast paced games a value of 1 would be better.
|
||||
|
||||
Server interpolation is much the same but typically should be a lower value. This is how much of a buffer the server tries to hold for inputs, resulting in the server not running them after a number of ticks equal to the specified interpolation.
|
||||
|
||||
## Excessive replicate dropping
|
||||
|
||||
Typically speaking the server will never have more than it's Server Interpolation +/- 1 in queue. However, there is a chance if the client is having network issues and is sending inputs in burst the queue could for example go from 0 to 5, if 5 of clients inputs came through at once.
|
||||
|
||||
When the server queued inputs exceed the maximum it will begin to drop old values. This protects the server against an allocation attack and also prevents cheating by the client trying to send extra inputs.
|
||||
|
||||
You may disable dropping of excessive replicates but this opens your game up to cheating, as multiple replicates will be run per tick on the server to consume extras. There is still a generous hard-coded value of the maximum amount of replicates which may be queued up hidden to you; this is to protect from allocation attacks.
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
description: Very little of the TimeManager has to be configured for prediction.
|
||||
---
|
||||
|
||||
# Configuring TimeManager
|
||||
|
||||
When using prediction it is essential that Fish-Networkings timing system is used. By default Unity's timing is used, but this can be changed on the TimeManager component.
|
||||
|
||||
Add the TimeManager component to your NetworkManager if it does not already exist.
|
||||
|
||||
<figure><img src="../../../.gitbook/assets/prediction-configuring-timemanager.png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
Once added change the Physics Mode to **Time Manager**, and you are done.
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: >-
|
||||
This guide provides examples of creating prediction code, understanding
|
||||
important aspects of the code, and leveraging it to your game.
|
||||
---
|
||||
|
||||
# Creating Code
|
||||
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
---
|
||||
description: >-
|
||||
This guide supplements the basic prediction guide by showing how to introduce
|
||||
more complexities to your controls.
|
||||
---
|
||||
|
||||
# Advanced Controls
|
||||
|
||||
{% hint style="info" %}
|
||||
Be sure to review the previous guides in this section before reviewing this page.
|
||||
{% endhint %}
|
||||
|
||||
## Guide goal
|
||||
|
||||
Implementing additional features into your prediction is much like you would code in a single player game, only remembering to reconcile anything that could de-synchronize the prediction.
|
||||
|
||||
In this guide a psuedo ground check before a jump will be added, as well a sprint function.
|
||||
|
||||
## Sprinting and ground checks
|
||||
|
||||
First the ReplicateData needs to be updated to contain our sprint action, which will rely on a stamina mechanic. Not much changed other than we added the Sprint boolean and set it using the constructor.
|
||||
|
||||
```csharp
|
||||
public struct ReplicateData : IReplicateData
|
||||
{
|
||||
public bool Jump;
|
||||
public bool Sprint;
|
||||
public float Horizontal;
|
||||
public float Vertical;
|
||||
public ReplicateData(bool jump, bool sprint, float horizontal, float vertical) : this()
|
||||
{
|
||||
Jump = jump;
|
||||
Sprint = sprint;
|
||||
Horizontal = horizontal;
|
||||
Vertical = vertical;
|
||||
}
|
||||
|
||||
private uint _tick;
|
||||
public void Dispose() { }
|
||||
public uint GetTick() => _tick;
|
||||
public void SetTick(uint value) => _tick = value;
|
||||
}
|
||||
```
|
||||
|
||||
After updating the ReplicateData we need to poll for the sprint key when creating the data, like you would in most games.
|
||||
|
||||
We are re-using methods from our previous guides so much of this should be familiar.
|
||||
|
||||
```csharp
|
||||
private ReplicateData CreateReplicateData()
|
||||
{
|
||||
if (!base.IsOwner)
|
||||
return default;
|
||||
|
||||
// Build the replicate data with all inputs which affect the prediction.
|
||||
float horizontal = Input.GetAxisRaw("Horizontal");
|
||||
float vertical = Input.GetAxisRaw("Vertical");
|
||||
|
||||
/* Sprint if left shift is held.
|
||||
* You do not necessarily have to perform security checks here.
|
||||
* For example, it was mentioned sprint will rely on stamina, we
|
||||
* are not checking the stamina requirement here. You certainly could
|
||||
* as a precaution but this is only building the replicate data, not where
|
||||
* the data is actually executed, which is where we want
|
||||
* the check. */
|
||||
bool sprint = Input.GetKeyDown(KeyCode.LeftShift);
|
||||
|
||||
ReplicateData md = new ReplicateData(_jump, sprint, horizontal, vertical);
|
||||
_jump = false;
|
||||
|
||||
return md;
|
||||
}
|
||||
```
|
||||
|
||||
Declare a stamina float in your class.
|
||||
|
||||
```csharp
|
||||
// Current stamina for the player.
|
||||
private float _stamina;
|
||||
```
|
||||
|
||||
Now use our new Sprint bool and stamina field to apply sprinting within the replicate method.
|
||||
|
||||
```csharp
|
||||
[Replicate]
|
||||
private void RunInputs(ReplicateData data, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
|
||||
{
|
||||
float delta = (float)base.TimeManager.TickDelta;
|
||||
// Regenerate stamina at 3f per second.
|
||||
_stamina += (3f * delta);
|
||||
// How much it cost to use sprint per delta.
|
||||
// This causes sprint to use stamina twice as fast
|
||||
// as the stamina recharges.
|
||||
float sprintCost = (6f * delta);
|
||||
Vector3 forces = new Vector3(data.Horizontal, 0f, data.Vertical) * _moveRate;
|
||||
// If sprint is held and enough stamina exist then multiple forces.
|
||||
if (data.Sprint && _stamina >= sprintCost)
|
||||
{
|
||||
// Reduce stamina by cost.
|
||||
_stamina -= sprintCost;
|
||||
// Increase forces by 30%.
|
||||
forces *= 1.3f;
|
||||
}
|
||||
|
||||
|
||||
/* You should check for any changes in replicate like we do
|
||||
* with stamina. Recall how it was said checking stamina when
|
||||
* gathering the inputs is not so important, but doign so in the replicate
|
||||
* is what grants server authority, as well makes prediction function
|
||||
* properly with corrections and rollbacks. */
|
||||
|
||||
/* Now check if to jump. IsGrounded() does not exist, we're going to
|
||||
* pretend it uses a raycast or overlap to check. */
|
||||
if (data.Jump && IsGrounded())
|
||||
{
|
||||
Vector3 jmpFrc = (Vector3.up * _jumpForce);
|
||||
PredictionRigidbody.AddForce(jmpFrc, ForceMode.Impulse);
|
||||
}
|
||||
|
||||
// Rest of the code remains the same.
|
||||
}
|
||||
```
|
||||
|
||||
{% hint style="danger" %}
|
||||
If a value can affect your prediction do not store it outside the replicate method, unless you are also reconciling the value. An exception applies if you are setting the value inside your replicate method.
|
||||
|
||||
This is a very important detail to remember, and is discussed further below.
|
||||
{% endhint %}
|
||||
|
||||
Reconciling only a rigidbody state is very simple.
|
||||
|
||||
```csharp
|
||||
[Reconcile]
|
||||
private void ReconcileState(ReconcileData data, Channel channel = Channel.Unreliable)
|
||||
{
|
||||
// Call reconcile on your PredictionRigidbody field passing in
|
||||
// values from data.
|
||||
PredictionRigidbody.Reconcile(data.PredictionRigidbody);
|
||||
}
|
||||
```
|
||||
|
||||
{% hint style="warning" %}
|
||||
If you are using multiple rigidbodies you at the very least need to reconcile their states as well. You can do so quickly by adding a RigidbodyState for each rigidbody to your reconcile.
|
||||
|
||||
If you are also applying forces to these rigidbodies be sure to use PredictionRigidbody with them, and reconcile the PredictionRigidbody instead of RigidbodyState.
|
||||
{% endhint %}
|
||||
|
||||
## Changes to reconcile
|
||||
|
||||
Because objects can reconcile to previous states it's fundamental to also reconcile any values stored outside the replicate method. Imagine if you had 10f stamina, enough to sprint, and did so successfully on the server and owner. After your sprint you only had 1f stamina, not enough to sprint further.
|
||||
|
||||
If you were to reconcile without resetting stamina to it's previous values then you would still be at 1f stamina after reconciling. Your replayed inputs, which previously allowed the sprint, would not sprint because you now lacked the needed stamina. In result of this, you would have a de-synchronization which would most likely be seen as jitter.
|
||||
|
||||
Including more variables in your prediction is fortunately easy enough. All you have to do is update your reconcile to include states or your new values or variables.
|
||||
|
||||
Added to our ReconcileData structure is a Stamina float.
|
||||
|
||||
```csharp
|
||||
public struct ReconcileData : IReconcileData
|
||||
{
|
||||
public PredictionRigidbody PredictionRigidbody;
|
||||
public float Stamina;
|
||||
|
||||
public ReconcileData(PredictionRigidbody pr, float stamina) : this()
|
||||
{
|
||||
PredictionRigidbody = pr;
|
||||
Stamina = stamina;
|
||||
}
|
||||
|
||||
private uint _tick;
|
||||
public void Dispose() { }
|
||||
public uint GetTick() => _tick;
|
||||
public void SetTick(uint value) => _tick = value;
|
||||
}
|
||||
```
|
||||
|
||||
Then of course we must include the current value of stamina within our created reconcile data.
|
||||
|
||||
```csharp
|
||||
public override void CreateReconcile()
|
||||
{
|
||||
ReconcileData rd = new ReconcileData(PredictionRigidbody, _stamina);
|
||||
ReconcileState(rd);
|
||||
}
|
||||
```
|
||||
|
||||
Very last you must utilize the new reconcile data to reset the stamina state.
|
||||
|
||||
```csharp
|
||||
[Reconcile]
|
||||
private void ReconcileState(ReconcileData data, Channel channel = Channel.Unreliable)
|
||||
{
|
||||
PredictionRigidbody.Reconcile(data.PredictionRigidbody);
|
||||
_stamina = data.Stamina;
|
||||
}
|
||||
```
|
||||
|
||||
With minor additions to the code you now have an authoritative ground check as well a stamina driven sprint. Just like that, tic-tac-toe, a winner.
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
---
|
||||
description: Learn how to create a predicted object that the owner or server can control.
|
||||
---
|
||||
|
||||
# Controlling an Object
|
||||
|
||||
## Data structures
|
||||
|
||||
Implementing prediction is done by creating a replicate and reconcile method, and making calls to the methods accordingly.
|
||||
|
||||
Your replicate method will take inputs you wish to run on the owner, server, and other clients if using state forwarding. This would be any input needed for your controller such as jumping, sprinting, movement direction, and could even include other mechanics such as holding a fire button.
|
||||
|
||||
The reconcile method takes a state of the object after a replicate is performed. This state is used to make corrections in the chance of de-synchronizations. For example, you may send back health, velocity, transform position and rotation, so on.
|
||||
|
||||
It's also worth mentioning if you are going to allocate in your structures it could be beneficial to utilize the Dispose callback, which will run as the data is being discarded.
|
||||
|
||||
Here are the two structures containing basic mechanics for a rigidbody.
|
||||
|
||||
```csharp
|
||||
public struct ReplicateData : IReplicateData
|
||||
{
|
||||
public bool Jump;
|
||||
public float Horizontal;
|
||||
public float Vertical;
|
||||
public ReplicateData(bool jump, float horizontal, float vertical) : this()
|
||||
{
|
||||
Jump = jump;
|
||||
Horizontal = horizontal;
|
||||
Vertical = vertical;
|
||||
}
|
||||
|
||||
private uint _tick;
|
||||
public void Dispose() { }
|
||||
public uint GetTick() => _tick;
|
||||
public void SetTick(uint value) => _tick = value;
|
||||
}
|
||||
|
||||
public struct ReconcileData : IReconcileData
|
||||
{
|
||||
// PredictionRigidbody is used to synchronize rigidbody states
|
||||
// and forces. This could be done manually but the PredictionRigidbody
|
||||
// type makes this process considerably easier. Velocities, kinematic state,
|
||||
// transform properties, pending velocities and more are automatically
|
||||
// handled with PredictionRigidbody.
|
||||
public PredictionRigidbody PredictionRigidbody;
|
||||
|
||||
public ReconcileData(PredictionRigidbody pr) : this()
|
||||
{
|
||||
PredictionRigidbody = pr;
|
||||
}
|
||||
|
||||
private uint _tick;
|
||||
public void Dispose() { }
|
||||
public uint GetTick() => _tick;
|
||||
public void SetTick(uint value) => _tick = value;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
{% hint style="info" %}
|
||||
Learn more about using [PredictionRigidbody](../predictionrigidbody.md).
|
||||
{% endhint %}
|
||||
|
||||
## Preparing to call prediction methods
|
||||
|
||||
Typically speaking you would want to run your replicate(or inputs) during OnTick. When you send the reconcile depends on if you are using physics bodies or not.
|
||||
|
||||
When using physics bodies, such as a rigidbody, you would send the reconcile during OnPostTick because you want to send the state after the physics have simulated your replicate inputs. See the [TimeManager API ](https://fish-networking.com/FishNet/api/api/FishNet.Managing.Timing.TimeManager.html#FishNet_Managing_Timing_TimeManager_OnPostTick)for more details on tick and physics event callbacks.
|
||||
|
||||
Non-physics controllers can also send in OnTick, since they do not need to wait for a physics simulation to have the correct outcome after running inputs.
|
||||
|
||||
The code below shows which callbacks and API to use for a rigidbody setup.
|
||||
|
||||
{% hint style="info" %}
|
||||
You may need to modify move and jump forces depending on the shape, drag, and mass of your rigidbody.
|
||||
{% endhint %}
|
||||
|
||||
```csharp
|
||||
// How much force to add to the rigidbody for jumps.
|
||||
[SerializeField]
|
||||
private float _jumpForce = 8f;
|
||||
// How much force to add to the rigidbody for normal movements.
|
||||
[SerializeField]
|
||||
private float _moveForce = 15f;
|
||||
// PredictionRigidbody is set within OnStart/StopNetwork to use our
|
||||
// caching system. You could simply initialize a new instance in the field
|
||||
// but for increased performance using the cache is demonstrated.
|
||||
public PredictionRigidbody PredictionRigidbody;
|
||||
// True if to jump next replicate.
|
||||
private bool _jump;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
PredictionRigidbody = ObjectCaches<PredictionRigidbody>.Retrieve();
|
||||
PredictionRigidbody.Initialize(GetComponent<Rigidbody>());
|
||||
}
|
||||
private void OnDestroy()
|
||||
{
|
||||
ObjectCaches<PredictionRigidbody>.StoreAndDefault(ref PredictionRigidbody);
|
||||
}
|
||||
public override void OnStartNetwork()
|
||||
{
|
||||
base.TimeManager.OnTick += TimeManager_OnTick;
|
||||
base.TimeManager.OnPostTick += TimeManager_OnPostTick;
|
||||
}
|
||||
|
||||
public override void OnStopNetwork()
|
||||
{
|
||||
base.TimeManager.OnTick -= TimeManager_OnTick;
|
||||
base.TimeManager.OnPostTick -= TimeManager_OnPostTick;
|
||||
}
|
||||
```
|
||||
|
||||
{% hint style="info" %}
|
||||
Instead of needing to manually subscribe to the TimeManager events such as OnTick and OnPostTick you can have your class inherit from [TickNetworkBehaviour](../../../../fishnet-building-blocks/components/network-behaviour-components/ticknetworkbehaviour.md) and then use its override methods.
|
||||
{% endhint %}
|
||||
|
||||
## Calling prediction methods
|
||||
|
||||
For our described demo, below is how you would gather input for your replicate and reconcile methods.
|
||||
|
||||
Update is used to gather inputs which are only fired for a single frame. Ticks do not occur every frame, but rather at the interval of your TickDelta, much like FixedUpdate works. While the code below only uses Update for single frame inputs there is nothing stopping you from using it for held inputs as well.
|
||||
|
||||
```csharp
|
||||
private void Update()
|
||||
{
|
||||
if (base.IsOwner)
|
||||
{
|
||||
if (Input.GetKeyDown(KeyCode.Space))
|
||||
_jump = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
OnTick will now be used to build our replicate data. A separate method of 'CreateReplicateData' is not needed to create the data but is done to organize our code better.
|
||||
|
||||
When attempting to create the replicate data we return with default if not the owner of the object. Server receives and runs inputs from the owner so it does not need to create data, and when clients do not own an object they will get the input for it from the server, as forwarded by other clients if using state forwarding. When not using state forwarding default should still be used in this scenario, but clients will not run replicates on non-owned objects. You can also run inputs on the server if there is no owner; using base.HasAuthority would probably be best for this. See [Checking Ownership](../../ownership/#checking-ownership) for more information.
|
||||
|
||||
```csharp
|
||||
private void TimeManager_OnTick()
|
||||
{
|
||||
RunInputs(CreateReplicateData());
|
||||
}
|
||||
|
||||
private ReplicateData CreateReplicateData()
|
||||
{
|
||||
if (!base.IsOwner)
|
||||
return default;
|
||||
|
||||
// Build the replicate data with all inputs which affect the prediction.
|
||||
float horizontal = Input.GetAxisRaw("Horizontal");
|
||||
float vertical = Input.GetAxisRaw("Vertical");
|
||||
ReplicateData md = new ReplicateData(_jump, horizontal, vertical);
|
||||
_jump = false;
|
||||
|
||||
return md;
|
||||
}
|
||||
```
|
||||
|
||||
Now implement your replicate method. The name may be anything but the parameters shown are required. The first is what we pass in, the remainder are set at runtime. Although, you absolutely may change the default channel used in the parameter or even at runtime.
|
||||
|
||||
For example, it could be beneficial to send an input as reliable if you absolutely want to ensure it's not dropped due to network issues.
|
||||
|
||||
```csharp
|
||||
[Replicate]
|
||||
private void RunInputs(ReplicateData data, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
|
||||
{
|
||||
/* ReplicateState is set based on if the data is new, being replayed, ect.
|
||||
* Visit the ReplicationState enum for more information on what each value
|
||||
* indicates. At the end of this guide a more advanced use of state will
|
||||
* be demonstrated. */
|
||||
|
||||
// Be sure to always apply and set velocities using PredictionRigidbody
|
||||
// and never on the rigidbody itself; this includes if also accessing from
|
||||
// another script.
|
||||
Vector3 forces = new Vector3(data.Horizontal, 0f, data.Vertical) * _moveForce;
|
||||
PredictionRigidbody.AddForce(forces);
|
||||
|
||||
if (data.Jump)
|
||||
{
|
||||
Vector3 jmpFrc = new Vector3(0f, _jumpForce, 0f);
|
||||
PredictionRigidbody.AddForce(jmpFrc, ForceMode.Impulse);
|
||||
}
|
||||
// Add gravity to make the object fall faster. This is of course
|
||||
// entirely optional.
|
||||
PredictionRigidbody.AddForce(Physics.gravity * 3f);
|
||||
// Simulate the added forces.
|
||||
// Typically you call this at the end of your replicate. Calling
|
||||
// Simulate is ultimately telling the PredictionRigidbody to iterate
|
||||
// the forces we added above.
|
||||
PredictionRigidbody.Simulate();
|
||||
}
|
||||
```
|
||||
|
||||
{% hint style="info" %}
|
||||
On non-owned objects a number of replicates will arrive as ReplicateState Created, but will contain default values. This is our PredictionManager.RedundancyCount feature working.
|
||||
|
||||
This is normal and indicates that the client or server had gracefully stopped sending states as there is no new data to send. This can be useful if you are [Predicting States.](understanding-replicatestate/predicting-states-in-code.md)
|
||||
{% endhint %}
|
||||
|
||||
Now the reconcile must be sent to clients to perform corrections. Only the server will actually send the reconcile but be sure to call CreateReconcile no matter if client, server, owner or not; this is to future proof an upcoming feature. Unlike our CreateReplicateData method, using CreateReconcile is not optional.
|
||||
|
||||
```csharp
|
||||
private void TimeManager_OnPostTick()
|
||||
{
|
||||
CreateReconcile();
|
||||
}
|
||||
|
||||
// Create the reconcile data here and call your reconcile method.
|
||||
public override void CreateReconcile()
|
||||
{
|
||||
// We must send back the state of the rigidbody. Using your
|
||||
// PredictionRigidbody field in the reconcile data is an easy
|
||||
// way to accomplish this. More advanced states may require other
|
||||
// values to be sent; this will be covered later on.
|
||||
ReconcileData rd = new ReconcileData(PredictionRigidbody);
|
||||
// Like with the replicate you could specify a channel here, though
|
||||
// it's unlikely you ever would with a reconcile.
|
||||
ReconcileState(rd);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Reconciling only a rigidbody state is very simple.
|
||||
|
||||
```csharp
|
||||
[Reconcile]
|
||||
private void ReconcileState(ReconcileData data, Channel channel = Channel.Unreliable)
|
||||
{
|
||||
// Call reconcile on your PredictionRigidbody field passing in
|
||||
// values from data.
|
||||
PredictionRigidbody.Reconcile(data.PredictionRigidbody);
|
||||
}
|
||||
```
|
||||
|
||||
{% hint style="info" %}
|
||||
When reconciling an object with a CharacterController, ensure you disable the CharacterController component immediately before setting its position and re-enable it afterwards. Because this controller uses a custom physics engine, skipping this toggle will prevent the internal state from refreshing, resulting in a synchronization error between the physics and the transform.\
|
||||
As an example:<br>
|
||||
|
||||
```csharp
|
||||
_characterController.enabled = false;
|
||||
transform.localPosition = reconcileData.Position;
|
||||
_characterController.enabled = true;
|
||||
```
|
||||
{% endhint %}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
---
|
||||
description: >-
|
||||
A very simple script for keeping non-controlled objects in synchronization
|
||||
with the prediction system.
|
||||
---
|
||||
|
||||
# Non-Controlled Object
|
||||
|
||||
Many games will require physics bodies to be networked, even if not controlled by players or the server. These objects can also work along-side the new state system by adding a basic prediction script on them.
|
||||
|
||||
It's worth noting that you can also 'control' non-owned objects on the server by using base.HasAuthority. This was discussed previously [here](controlling-an-object.md).
|
||||
|
||||
## Sample script
|
||||
|
||||
Below is a full example script to synchronize a non-controlled rigidbody. Since the rigidbody is only reactive, input polling is not needed. Otherwise you'll find the data structures are near identical to the ones where we took input.
|
||||
|
||||
{% hint style="warning" %}
|
||||
It is strongly recommended to review the [controlling objects ](controlling-an-object.md)guide for additional notes in understanding the code below.
|
||||
{% endhint %}
|
||||
|
||||
```csharp
|
||||
public class RigidbodySync : NetworkBehaviour
|
||||
{
|
||||
// Replicate structure.
|
||||
public struct ReplicateData : IReplicateData
|
||||
{
|
||||
// The uint isn't used but Unity C# version does not
|
||||
// allow parameter-less constructors we something
|
||||
// must be set as a parameter.
|
||||
public ReplicateData(uint unused) : this() {}
|
||||
private uint _tick;
|
||||
public void Dispose() { }
|
||||
public uint GetTick() => _tick;
|
||||
public void SetTick(uint value) => _tick = value;
|
||||
}
|
||||
// Reconcile structure.
|
||||
public struct ReconcileData : IReconcileData
|
||||
{
|
||||
public PredictionRigidbody PredictionRigidbody;
|
||||
|
||||
public ReconcileData(PredictionRigidbody pr) : this()
|
||||
{
|
||||
PredictionRigidbody = pr;
|
||||
}
|
||||
|
||||
private uint _tick;
|
||||
public void Dispose() { }
|
||||
public uint GetTick() => _tick;
|
||||
public void SetTick(uint value) => _tick = value;
|
||||
}
|
||||
|
||||
// Forces are not applied in this example but you
|
||||
// could definitely still apply forces to the PredictionRigidbody
|
||||
// even with no controller, such as if you wanted to bump it
|
||||
// with a player.
|
||||
private PredictionRigidbody PredictionRigidbody;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
PredictionRigidbody = ResettableObjectCaches<PredictionRigidbody>.Retrieve();
|
||||
PredictionRigidbody.Initialize(GetComponent<Rigidbody>());
|
||||
}
|
||||
private void OnDestroy()
|
||||
{
|
||||
ResettableObjectCaches<PredictionRigidbody>.StoreAndDefault(ref PredictionRigidbody);
|
||||
}
|
||||
|
||||
// In this example we do not need to use OnTick, only OnPostTick.
|
||||
// Because input is not processed on this object you only
|
||||
// need to pass in default for RunInputs, which can safely
|
||||
// be done in OnPostTick.
|
||||
public override void OnStartNetwork()
|
||||
{
|
||||
base.TimeManager.OnPostTick += TimeManager_OnPostTick;
|
||||
}
|
||||
|
||||
public override void OnStopNetwork()
|
||||
{
|
||||
base.TimeManager.OnPostTick -= TimeManager_OnPostTick;
|
||||
}
|
||||
|
||||
private void TimeManager_OnPostTick()
|
||||
{
|
||||
RunInputs(default);
|
||||
CreateReconcile();
|
||||
}
|
||||
|
||||
[Replicate]
|
||||
private void RunInputs(ReplicateData md, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
|
||||
{
|
||||
// If this object is free-moving and uncontrolled then there is no logic.
|
||||
// Just let physics do it's thing.
|
||||
}
|
||||
|
||||
// Create the reconcile data here and call your reconcile method.
|
||||
public override void CreateReconcile()
|
||||
{
|
||||
ReconcileData rd = new ReconcileData(PredictionRigidbody);
|
||||
ReconcileState(rd);
|
||||
}
|
||||
|
||||
[Reconcile]
|
||||
private void ReconcileState(ReconcileData data, Channel channel = Channel.Unreliable)
|
||||
{
|
||||
// Call reconcile on your PredictionRigidbody field passing in
|
||||
// values from data.
|
||||
PredictionRigidbody.Reconcile(data.PredictionRigidbody);
|
||||
}
|
||||
}
|
||||
```
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
---
|
||||
description: >-
|
||||
Being familiar with what each state means will help you fine-tune your
|
||||
gameplay on spectated objects.
|
||||
---
|
||||
|
||||
# Understanding ReplicateState
|
||||
|
||||
{% hint style="info" %}
|
||||
If you need a refresher on what each state means see our [API](https://fish-networking.com/FishNet/api/api/FishNet.Object.ReplicateState.html) or simply navigate to the ReplicateState enum in your code editor.
|
||||
{% endhint %}
|
||||
|
||||
Each state is a flag value; a replicate state may contain multiple flags. There are numerous extensions you may use to check if a state contains certain values.
|
||||
|
||||
{% hint style="success" %}
|
||||
You can view all built-in state extensions within the ReplicateState file.
|
||||
{% endhint %}
|
||||
|
||||
## Invalid
|
||||
|
||||
An invalid ReplicateState should never occur. This would imply internally Fish-Networking failed to properly set the state.
|
||||
|
||||
## Ticked
|
||||
|
||||
Server and clients use this flag. This flag will be set if the data tick has run outside a reconcile, such as from user code within OnTick . Ticked can exist during a replay/reconcile, but only if the data had first run outside the replay/reconcile.
|
||||
|
||||
## Replayed
|
||||
|
||||
Only clients will use this flag. The replayed flag is set if data is being run during a reconcile.
|
||||
|
||||
{% hint style="info" %}
|
||||
The server is always considered right and never has to correct data, so it never reconciles or replays inputs
|
||||
{% endhint %}
|
||||
|
||||
## Created
|
||||
|
||||
Server and client use this flag. A created flag indicates that the data was created by controller, such as owner or the server if no owner. The created flag will not be present if the controller has not sent updates, such as if they are not taking any action.
|
||||
|
||||
## State examples
|
||||
|
||||
Below are examples of some possible states and what they mean.
|
||||
|
||||
```csharp
|
||||
// You will see this value if the data is being replayed, it previously ran outside the
|
||||
// reconcile, and data is created by controller.
|
||||
state = (ReplicateState.Replayed | ReplicateState.Ticked | ReplicateState.Created);
|
||||
// When the state is only Replayed then the data is not Created, and the tick on data
|
||||
// has not occurred outside the reconcile yet. This is what we often refer to as a
|
||||
// future state.
|
||||
state = ReplicateState.Replayed;
|
||||
// When a state is Ticked only it indicates that the data is being run outside a
|
||||
// reconcile, and that the controller has not sent data for this particular tick.
|
||||
state = ReplicateState.Ticked;
|
||||
```
|
||||
|
||||
{% hint style="success" %}
|
||||
For more possible flag examples see the state extensions (the ReplicateState file).
|
||||
{% endhint %}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
---
|
||||
description: >-
|
||||
Due to the unpredictability of the Internet inputs may drop or arrive late.
|
||||
Predicting states is a simple way to compensate for these events.
|
||||
---
|
||||
|
||||
# Predicting States in Code
|
||||
|
||||
If your game is fast-paced and reaction based, even if not using physics, predicting states can be useful to _predict_ the next inputs you'll get from the server before you actually get them.
|
||||
|
||||
{% hint style="info" %}
|
||||
Both the server and client can predict inputs.
|
||||
|
||||
The server may predict inputs to accommodate for clients with unreliable connections.
|
||||
|
||||
Clients would predict inputs on objects they do not own, or as we often call them "spectated objects".
|
||||
{% endhint %}
|
||||
|
||||
By predicting inputs you can place objects in the future of where you know them to be, and even make them entirely real-time with the client. Keep in mind however, when predicting the future, if you guess wrong there will be a de-synchronization which may be seen as jitter when it's corrected in a reconcile.
|
||||
|
||||
Below is an example of a simple replicate method.
|
||||
|
||||
```csharp
|
||||
// What data does is irrelevant in this example.
|
||||
// We're only interested in how to predict a future state.
|
||||
[Replicate]
|
||||
private void RunInputs(ReplicateData data, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
|
||||
{
|
||||
float delta = (float)base.TimeManager.TickDelta;
|
||||
transform.position += new Vector3(data.Horizontal, 0f, data.Vertical) * _moveRate * delta;
|
||||
}
|
||||
```
|
||||
|
||||
Before we go any further you must first understand what each ReplicateState is. These change based on if input is known, replaying inputs or not, and more. You can check out the [ReplicateState API](https://fish-networking.com/FishNet/api/api/FishNet.Object.ReplicateState.html) which will explain thoroughly. You can also find this information right in the source of FishNet.
|
||||
|
||||
If you've read through ReplicateState and do not fully understand them please continue reading as they become more clear as this guide progresses. You can also visit us on Discord for questions!
|
||||
|
||||
Covered in the ReplicateStates API: CurrentCreated will only be seen on clients if they own the object. When inputs are received on spectated objects clients run them in the reconcile, which will have the state ReplayedCreated. Clients will also see ReplayedFuture and CurrentFuture on spectated objects.
|
||||
|
||||
A state ending in 'Future' essentially means input has not been received yet, and these are the states you could predict.
|
||||
|
||||
Let's assume your game has a likeliness that players will move in the same direction regularly enough. If a player was holding forward for three ticks the input would look like this...
|
||||
|
||||
```csharp
|
||||
(data.Vertical == 1)
|
||||
(data.Vertical == 1)
|
||||
(data.Vertical == 1)
|
||||
```
|
||||
|
||||
But what if one of the inputs didn't arrive, or arrived late? The chances of inputs not arriving at all are pretty slim, but arriving late due to network variance is extremely common. If perhaps an input did arrive late the values may appear as something of this sort...
|
||||
|
||||
```csharp
|
||||
(data.Vertical == 1)
|
||||
(data.Vertical == 1)
|
||||
(data.Vertical == 0) // Didn't arrive here, but will arrive late next tick.
|
||||
(data.Vertical == 1) // This was meant to arrive the tick before, but arrived late.
|
||||
```
|
||||
|
||||
Because of this interruption the player may seem to move forward twice, pause, then forward again. Realistically to help cover this up you will have interpolation on your graphicalObject as shown under the prediction settings for [NetworkObject.](../../../../../fishnet-building-blocks/components/network-object.md) The [PredictionManager](../../../../../fishnet-building-blocks/components/prediction/) also offers QueuedInputs which can give you even more of a buffer. For the sake of this guide though we're going to pretend both of those didn't get the job done, and you need to account for the late input.
|
||||
|
||||
Below is a simple way to track and use known inputs to create predicted ones.
|
||||
|
||||
```csharp
|
||||
private ReplicateData _lastCreatedInput = default;
|
||||
|
||||
[Replicate]
|
||||
private void RunInputs(ReplicateData data, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
|
||||
{
|
||||
// If inputs are not known. You could predict
|
||||
// all the way into CurrentFuture, which would be
|
||||
// real-time with the client. Though the more you predict
|
||||
// in the future the more you are likely to miss predict.
|
||||
if (state.IsFuture())
|
||||
{
|
||||
uint lastCreatedTick = _lastCreatedInput.GetTick();
|
||||
// If it's only been 2 ticks since the last created
|
||||
// input then run the logic below.
|
||||
// This essentially means if the last created tick
|
||||
// was 100, this logic would run if the future tick was 102
|
||||
// or less. This is an example of a basic approach to only
|
||||
// predict a certain number of inputs.
|
||||
uint thisTick = data.GetTick();
|
||||
if ((data.GetTick() - lastCreatedTick) <= 2)
|
||||
{
|
||||
// We do not necessarily want to predict all states.
|
||||
// For example, it probably wouldn't make sense to predict
|
||||
// multiple jumps in a row. In this example only the movement
|
||||
// inputs are predicted.
|
||||
data.Vertical = _lastCreatedInput.Vertical;
|
||||
}
|
||||
}
|
||||
// If created data then set as lastCreatedInput.
|
||||
else if (state == ReplicateState.ReplayedCreated)
|
||||
{
|
||||
// If ReplicateData contains fields which could generate garbage you
|
||||
// probably want to dispose of the lastCreatedInput
|
||||
// before replacing it. This step is optional.
|
||||
_lastCreatedInput.Dispose();
|
||||
// Assign newest value as last.
|
||||
_lastCreatedInput = data;
|
||||
}
|
||||
|
||||
float delta = (float)base.TimeManager.TickDelta;
|
||||
transform.position += new Vector3(data.Horizontal, 0f, data.Vertical) * _moveRate * delta;
|
||||
}
|
||||
```
|
||||
|
||||
{% hint style="warning" %}
|
||||
If your ReplicateData allocates do not forget to dispose of the lastCreatedInput when the network is stopped.
|
||||
{% endhint %}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
---
|
||||
description: >-
|
||||
Understanding how to use states will greatly improve your experience when
|
||||
writing code for your replicate method.
|
||||
---
|
||||
|
||||
# Using States in Code
|
||||
|
||||
## Future states
|
||||
|
||||
You will see the term 'in the future' or 'future state' used frequently when working with prediction. When in the future it's not possible to know the data from the controller, which is why we call it the _future_. When using client-prediction, as a client you are always moving in real-time, before even knowing the servers current state. Due to this, you will not know other clients or server states until they are forwarded to you, and during that time of unknowing we consider the object to be in the future.
|
||||
|
||||
The future this is where you gain the opportunity to predict future input from the controller. Or, you can simply prevent future movement entirely; uses vary depending on your needs. We'll cover this more later on this page.
|
||||
|
||||
{% hint style="info" %}
|
||||
You will never be in the future on objects you controller, given you only replay inputs up to what you created and never beyond.
|
||||
{% endhint %}
|
||||
|
||||
## The created flag
|
||||
|
||||
When a state is not created the data will be default. This often catches a lot of developers off guard as they might expect to see continual input from the controller, such as if the controller is always holding a movement key.
|
||||
|
||||
A common use case is updating the objects animator only when data is known.
|
||||
|
||||
```csharp
|
||||
[Replicate]
|
||||
private void MovePlayer(ReplicateData data, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
|
||||
{
|
||||
// Left/right movement.
|
||||
float horizontal = data.Horizontal;
|
||||
|
||||
// Only update the animator if data is created. Do not update the animator
|
||||
// if not created as this will cause the animator to switch between having input
|
||||
// and a default value.
|
||||
if (state.ContainsCreated())
|
||||
_myAnimator.SetFloat("Horizontal", horizontal);
|
||||
}
|
||||
```
|
||||
|
||||
{% hint style="warning" %}
|
||||
If you are using State Order -> Inserted on the PredictionManager then Created will only ever be set on spectated objects during a reconcile. Since states on spectated obejcts are inserted into the replicate history they will run during reconciles rather than outside of reconciles. See the [PredictionManager guide](../../../../../fishnet-building-blocks/components/managers/predictionmanager.md) for more information on State Orders.
|
||||
{% endhint %}
|
||||
|
||||
## Ticked
|
||||
|
||||
As mentioned before Ticked indicates the data has run outside a reconcile. Also described, the state can be Ticked as well Replayed, which means it ran outside a reconcile previously but is currently running again during a replay/reconcile.
|
||||
|
||||
Ticked and not replayed can often be used to perform one-time actions, such as showing visual effects, such as jumping. You probably wouldn't want to play jump audio when the jump first occurs, as well every time the input replays during a reconcile.
|
||||
|
||||
```csharp
|
||||
[Replicate]
|
||||
private void MovePlayer(ReplicateData data, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
|
||||
{
|
||||
if (data.Jump)
|
||||
{
|
||||
DoJump();
|
||||
// If ticked and not replayed then also play jump audio.
|
||||
if (state.ContainsTicked() && !state.ContainsReplayed())
|
||||
PlayJumpAudio();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Preventing future state logic and movement
|
||||
|
||||
The replayed state is most commonly used to predict the future, or limit as opposition, limit the future.
|
||||
|
||||
Limiting future velocities is where the replayed flag is typically used the most. This keeps the object out of future prediction, which can limit real-time reflection of the object, but also prevents excessive corrections or movement snapping.
|
||||
|
||||
Below is a pretty basic example showing jumping and moving, without going too into depth of move rates. On a spectated object without any future checks, during a replay the object will jump and then continue to snap upward as you replay into the future (beyond data you could possibly know due to latency).
|
||||
|
||||
{% hint style="success" %}
|
||||
This behavior can be difficult to explain, but is easy to see. Try our character controller prediction demo with and without an IsFuture check.
|
||||
{% endhint %}
|
||||
|
||||
```csharp
|
||||
[Replicate]
|
||||
private void MovePlayer(ReplicateData data, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
|
||||
{
|
||||
// Exit the method early to prevent going into the future, which would
|
||||
// result in the controller snapping upward very fast when replaying a jump.
|
||||
if (state.IsFuture())
|
||||
return;
|
||||
|
||||
// Set vertical velocity to jump up.
|
||||
if (data.Jump)
|
||||
_verticalVelocity = 10f;
|
||||
|
||||
// Only add vertical movement for this example.
|
||||
// Realistically, you would have x and z movement as well.
|
||||
Vector3 movement = new(0f, _verticalVelocity, 0f);
|
||||
// Reduce vertical velocity to begin falling, but prevent it from going too low.
|
||||
_verticalVelocity -= (float)base.TimeManager.TickDelta;
|
||||
_verticalVelocity = Mathf.Max(_verticalVelocity, -5f);
|
||||
|
||||
_characterController.Move(movement);
|
||||
}
|
||||
```
|
||||
|
||||
The example above shows a very easy implementation of preventing future movement on a character controller, but rigidbodies are a little different. With rigidbodies, even if you exit the method early preventing additional added velocities, the rigidbody still carries existing velocities; this is because physics simulates with every tick, replayed or not, regardless of if replicate runs.
|
||||
|
||||
Here's an example **without** preventing future movement on a rigidbody.
|
||||
|
||||
{% hint style="info" %}
|
||||
The code below shows use of[ PredictionRigidbody](../../predictionrigidbody.md). You do not need to understand how the component works to understand this example.
|
||||
{% endhint %}
|
||||
|
||||
```csharp
|
||||
[Replicate]
|
||||
private void MovePlayer(ReplicateData data, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
|
||||
{
|
||||
float horizontal = data.Horizontal;
|
||||
|
||||
Vector3 movement = new(horizontal, 0f, 0f);
|
||||
|
||||
_predictionRigidbody.AddVelocity(movement);
|
||||
_predictionRigidbody.Simulate();
|
||||
|
||||
// Nothing in this code prevents the rigidbody from moving into the future.
|
||||
}
|
||||
```
|
||||
|
||||
Preventing future movement on rigidbodies is a few more lines of code, but still pretty easy. We're going to use the RigidbodyPauser reference on the NetworkObject to pause the rigidbody when in the future. The RigidbodyPauser is a lesser known part of our [NetworkObject API ](https://fish-networking.com/FishNet/api/api/FishNet.Object.NetworkObject.html#FishNet_Object_NetworkObject_RigidbodyPauser)and is used almost exclusively for prediction.
|
||||
|
||||
```csharp
|
||||
[Replicate]
|
||||
private void MovePlayer(ReplicateData data, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
|
||||
{
|
||||
// Only the client would need to pause to prevent future movement,
|
||||
// and only on objects they do not own (objects they are spectating).
|
||||
bool canChangePause = !base.IsOwner && !base.IsServerStarted;
|
||||
// If in the future do not process any logic and pause the rigidbody.
|
||||
// If not in the future, then unpause the rigidbody.
|
||||
// There are no negative side-effects of calling pause or unpause when
|
||||
// when the RigidbodyPauser is already in the same state.
|
||||
if (state.IsFuture()
|
||||
{
|
||||
// This will prevent the rigidbody from moving by
|
||||
// making it kinematic. Pausing a rigidbody does also mean
|
||||
// objects can potentially pass through it. Another approach
|
||||
// will be shown in the next example.
|
||||
if (canChangePause)
|
||||
base.NetworkObject.RigidbodyPauser.Pause();
|
||||
}
|
||||
else
|
||||
{
|
||||
// When not future, unpause. This allows the object to move
|
||||
// again. Unpausing will restore velocities as they were
|
||||
// prior to pausing.
|
||||
if (canChangePause)
|
||||
base.NetworkObject.RigidbodyPauser.Unpause();
|
||||
|
||||
float horizontal = data.Horizontal;
|
||||
|
||||
Vector3 movement = new(horizontal, 0f, 0f);
|
||||
|
||||
_predictionRigidbody.AddVelocity(movement);
|
||||
_predictionRigidbody.Simulate();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
As explained in the example above pausing a rigidbody can cause objects to pass-through it; this is due to the pauser making the rigidbody kinematic.
|
||||
|
||||
Here's a different technique which instead simply zeros out velocities when in the future. In most cases this new code snippet is most ideal, but it's good to have an understanding that both are available.
|
||||
|
||||
```csharp
|
||||
[Replicate]
|
||||
private void MovePlayer(ReplicateData data, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
|
||||
{
|
||||
// If in the future then simply zero out velocities
|
||||
// and exit the method to prevent checking any input logic.
|
||||
// Using this approach will allow objects to still collide with
|
||||
// the rigidbody.
|
||||
if (state.IsFuture())
|
||||
{
|
||||
_myPredictionRigidbody.Velocity(Vector3.Zero);
|
||||
_myPredictionRigidbody.AngularVelocity(Vector3.Zero);
|
||||
return;
|
||||
}
|
||||
|
||||
float horizontal = data.Horizontal;
|
||||
|
||||
Vector3 movement = new(horizontal, 0f, 0f);
|
||||
|
||||
_predictionRigidbody.AddVelocity(movement);
|
||||
_predictionRigidbody.Simulate();
|
||||
}
|
||||
|
||||
```
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
description: >-
|
||||
Fish-Networking generates comparers for prediction data to perform internal
|
||||
optimizations, but on occasion certain types cannot have comparers
|
||||
automatically generated.
|
||||
---
|
||||
|
||||
# Custom Comparers
|
||||
|
||||
You may see an error in the console about a type requiring a custom comparer. For example, generics and arrays specifically must have a custom comparer provided.
|
||||
|
||||
```csharp
|
||||
/* For example, this will create an error stating
|
||||
* byte[] needs a custom comparer. */
|
||||
public struct MoveData : IReplicateData
|
||||
{
|
||||
public Vector2 MoveDirection;
|
||||
public byte[] CustomData;
|
||||
// rest omitted..
|
||||
}
|
||||
```
|
||||
|
||||
While a comparer could be made automatically for the type byte\[], we still require you to create your own as we may not know exactly how you want to compare these types. The code below compares byte arrays by iterating every byte to check for mismatches. Given how often prediction data sends, this could potentially burden the processor.
|
||||
|
||||
```csharp
|
||||
[CustomComparer]
|
||||
public static bool CompareByteArray(byte[] a, byte[] b)
|
||||
{
|
||||
bool aNull = (a is null);
|
||||
bool bNull = (b is null);
|
||||
// Both are null.
|
||||
if (aNull && bNull)
|
||||
return true;
|
||||
// One is null, other is not.
|
||||
if (aNull != bNull)
|
||||
return false;
|
||||
// Not same lengths, cannot match.
|
||||
if (a.Length != b.Length)
|
||||
return false;
|
||||
|
||||
// Both not null and same length, compare bytes.
|
||||
int length = a.Length;
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
// Differs.
|
||||
if (a[i] != b[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fall through, if here everything matches.
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
The above code is a working example of how to create a custom comparer, but it may not be the most ideal comparer for your needs; this is why we require you to make your own comparer for such types.
|
||||
|
||||
Creating your own comparer is simple. Make a new static method with any name and boolean as the return type. Decorate the method with the **\[CustomComparer]** attribute. There must also be two parameters, each being the type you want to compare. The method logic can contain whichever code you like
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
description: >-
|
||||
Both the PredictionManager and NetworkObject have interpolation values, but
|
||||
with separate objectives
|
||||
---
|
||||
|
||||
# Interpolations
|
||||
|
||||
PredictionManager interpolation is not the same as NetworkObject interpolation.
|
||||
|
||||
Interpolation on the PredictionManager holds a number of states in queue as interpolation while NetworkObject interpolation handles graphical smoothing after running states.
|
||||
|
||||
A prediction interpolation of 0, be it client or server setting, means that states would be run soon as they are received. Most games will not use 0 interpolation as it's generally best to have at least 1 state in queue to compensate for latency. For every 1 interpolation added is like adding latency to when spectated states are run, equal to (interpolation \* TickDelta).
|
||||
|
||||
For smoothing graphical objects beneath your NetworkObject see [NetworkTickSmoother](../../../fishnet-building-blocks/components/tick-smoothers/networkticksmoother.md). This component allows you to configure how the object is interpolated for owner and spectators.
|
||||
|
||||
Under the scenario your TickSmoother graphical had a set spectator interpolation of 2 and your PredictionManager an interpolation of 1, the spectated graphical object would take 2 ticks to get to it's goal after the PredictionManager ran the state 1 interpolation later than receiving it.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
description: >-
|
||||
In some cases you will want the player to be able to interact with
|
||||
non-networked rigidbodies; these require a special component.
|
||||
---
|
||||
|
||||
# Offline Rigidbodies
|
||||
|
||||
While prediction is in use, if you have a rigidbody object in your game that is not synchronized with the network it must contain an OfflineRigidbody component. Please review the [OfflineRigidbody component page](../../../fishnet-building-blocks/components/prediction/offlinerigidbody.md) for more information on it's uses.
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
description: >-
|
||||
This class provides accurate simulations and re-simulations when applying
|
||||
outside forces, most commonly through collisions.
|
||||
---
|
||||
|
||||
# PredictionRigidbody
|
||||
|
||||
Using PredictionRigidbody is very straight forward. In short, you move from applying force changes to the rigidbody onto the PredictionRigidbody instance instead.
|
||||
|
||||
View the [Creating Code](creating-code/) guide for using PredictionRigidbody in your replicate and reconcile methods.
|
||||
|
||||
## Using outside the script
|
||||
|
||||
As mentioned above you should always apply forces to the PredictionRigidbody rather than the Rigidbody directly. Our first guides demonstrate how to do this within the replicate method, as well how to reconcile using the PredictionRigidbody, but do not show how to add forces from outside scripts, such as a bumper in your game.
|
||||
|
||||
There is virtually no complexity to adding outside forces other than remembering to add them again, to the PredictionRigidbody and not the Rigidbody.
|
||||
|
||||
The example below is what it might look like if using a trigger on a world object to repel the player.
|
||||
|
||||
{% hint style="warning" %}
|
||||
For triggers and collisions to work properly with prediction you must use our NetworkTrigger/NetworkCollision components. Otherwise, due to a Unity limitation, such interactions would not work. You can learn more about those components here.
|
||||
{% endhint %}
|
||||
|
||||
{% hint style="success" %}
|
||||
Fun fact: Fish-Networking is the only framework that has the ability to simulate Enter/Exit events with prediction; not even Fusion does this!
|
||||
{% endhint %}
|
||||
|
||||
```csharp
|
||||
private void NetworkTrigger_OnEnter(Collider other)
|
||||
{
|
||||
// Add upward impulse when hitting this trigger.
|
||||
if (other.TryGetComponent<RigidbodyPlayer>(out rbPlayer))
|
||||
rbPlayer.PredictionRigidbody.AddForce(Vector3.up, ForceMode.Impulse);
|
||||
// Do not call Simulate on the PredictionRigidbody here. That should only be done
|
||||
// within the replicate method.
|
||||
}
|
||||
```
|
||||
|
||||
There is a fair chance a number of controllers will want to set velocities directly as well. PredictionRigidbody does support this. We encourage you to review the [PredictionRigidbody API](https://fish-networking.com/FishNet/api/api/FishNet.Object.Prediction.PredictionRigidbody.html#methods) for all available functionality.
|
||||
|
||||
Our last example demonstrates setting velocities directly.
|
||||
|
||||
{% hint style="info" %}
|
||||
You would still call PredictionRigidbody.Simulate() regardless of how velocities are set.
|
||||
{% endhint %}
|
||||
|
||||
```csharp
|
||||
float horizontal = data.Horizontal;
|
||||
Vector3 velocity = new Vector3(data.Horizontal, 0f, 0f) * _moveRate;
|
||||
PredictionRigidbody.Velocity(velocity);
|
||||
```
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
description: >-
|
||||
Using each NetworkCollider component is the same, and can be used very similar
|
||||
to Unity callbacks.
|
||||
---
|
||||
|
||||
# Using NetworkColliders
|
||||
|
||||
When using client-side prediction the NetworkCollider components are designed to accurately dispatch Enter, Stay, and Exit callbacks for collisions and triggers.
|
||||
|
||||
All events have the same name across each component: NetworkCollider, NetworkCollider2D, NetworkTrigger, and NetworkTrigger2D. What is returned in each event naturally will vary, depending on which component you use. For example, the NetworkCollision component will return _Collider_ for what was intersected, while NetworkCollision2D will return _Collider2D._
|
||||
|
||||
{% hint style="warning" %}
|
||||
Our OnStay callback does not return _Collision_ as you would expect with the Unity version. Due to Unity limitations we are unable to provide a Collision callback without hotpath allocations.
|
||||
|
||||
If you require Collision to be returned please use the Unity OnStay callback instead; this will function properly with prediction.
|
||||
{% endhint %}
|
||||
|
||||
## Callbacks
|
||||
|
||||
**OnEnter** is invoked when this collider has entered the bounds of another collider.
|
||||
|
||||
**OnStay** is invoked when this collider remains in contact with another.
|
||||
|
||||
**OnExit** is invoked when this collider has exited the bounds of another collider.
|
||||
|
||||
{% hint style="info" %}
|
||||
Keep in mind each callback description varies if you are using Collsion, Trigger, or their 2D counterparts. None-the-less, they behave just like Unity callbacks.
|
||||
{% endhint %}
|
||||
|
||||
## Usage
|
||||
|
||||
This example script is placed on the player object. Our script demonstrates subscribing to all three events and using the `OnEnter` event to play a sound, as well applying forces to 'this' player.
|
||||
|
||||
```csharp
|
||||
// We are assuming this script inherits NetworkBehaviour.
|
||||
[SerializeField]
|
||||
private AudioSource _hitSound;
|
||||
|
||||
private NetworkCollision _networkCollision;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Get the NetworkCollision component placed on this object.
|
||||
// You can place the component anywhere you would normally
|
||||
// use Unity collider callbacks!
|
||||
_networkCollision = GetComponent<NetworkCollision>();
|
||||
// Subscribe to the desired collision event
|
||||
_networkCollision.OnEnter += NetworkCollisionEnter;
|
||||
_networkCollision.OnStay += NetworkCollisionStay;
|
||||
_networkCollision.OnExit += NetworkCollisionExit;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// Since the NetworkCollider is placed on the same object as
|
||||
// this script we do not need to unsubscribe; the callbacks
|
||||
// will be destroyed with the object.
|
||||
|
||||
// But if your NetworkCollider resides on another object you
|
||||
// likely will want to unsubscribe to your events as well as shown.
|
||||
if (_networkCollision != null)
|
||||
{
|
||||
_networkCollision.OnEnter -= NetworkCollisionEnter;
|
||||
_networkCollision.OnStay -= NetworkCollisionStay;
|
||||
_networkCollision.OnExit -= NetworkCollisionExit;
|
||||
}
|
||||
}
|
||||
|
||||
private void NetworkCollisionEnter(Collider other)
|
||||
{
|
||||
// Only play the sound effect when not currently reconciling.
|
||||
// If you were to play the sound also during reconciliations
|
||||
// the audio would execute each reconcile, each tick, until the
|
||||
// player was no longer reconciling into the collider.
|
||||
if (!base.PredictionManager.IsReconciling)
|
||||
_hitSound.Play();
|
||||
|
||||
// Always apply velocity to this player on enter, even if reconciling.
|
||||
PlayerMover pm = GetComponent<PlayerMover>();
|
||||
// For this example we are pushing away from the other object.
|
||||
Vector3 dir = (transform.position - other.gameObject.transform).normalized;
|
||||
pm.PredictionRigidbody.AddForce(dir * 50f, ForceMode.Impulse);
|
||||
}
|
||||
|
||||
private void NetworkCollisionStay(Collider other)
|
||||
{
|
||||
// Handle collision stay logic
|
||||
}
|
||||
|
||||
private void NetworkCollisionExit(Collider other)
|
||||
{
|
||||
// Handle collision exit logic (e.g., deactivate visual effects)
|
||||
}
|
||||
```
|
||||
|
||||
You may be wondering what is PredictionRigidbody and why did we apply forces to that instead of the Rigidbody directly. PredictionRigidbody(and 2D) is a mandatory but still simple way to apply forces to predicted rigidbodies. You can learn more about it's usage [here](predictionrigidbody.md).
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
---
|
||||
description: >-
|
||||
Client-Side Prediction allows clients to perform actions in real-time while
|
||||
maintaining server authority.
|
||||
---
|
||||
|
||||
# What Is Client-Side Prediction
|
||||
|
||||
Client-side prediction is a technique used to move in real-time on clients, providing responsiveness actions, while also ensuring such actions cannot be cheated. From here out, we will refer to client-side prediction as CSP.
|
||||
|
||||
During your development you may also hear the term 'server authoritative movement'. CSP is a form of server authoritative movement, but they are not the same.
|
||||
|
||||
As mentioned CSP allows the client to move in real-time while also ensuring they cannot cheat. Some server authoritative movements will ensure the client cannot cheat but does so by moving on the server only and relaying the results to clients. While both work, the latter of moving on the server and then relaying will result in the client to have a delay based on their latency.
|
||||
|
||||
Having such a delay would be unfair to those with higher latency and could ruin the experience for players if your game is expected to have responsive movement. This is why having CSP built into Fish-Networking is so great!
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
description: >-
|
||||
Fish-Networking comes with a powerful scene manager tool that enables you to
|
||||
synchronize networked scenes with minimal effort, while also exposing a lot of
|
||||
powerful options.
|
||||
---
|
||||
|
||||
# Scene Management
|
||||
|
||||
## General
|
||||
|
||||
The [**SceneManager**](../../../fishnet-building-blocks/components/managers/scenemanager.md) Component has many scene features to support your multiplayer needs. The links below will take you to the different guides for each feature that the [**SceneManager**](../../../fishnet-building-blocks/components/managers/scenemanager.md) has to offer.
|
||||
|
||||
{% hint style="success" %}
|
||||
Visit the [**API**](https://fish-networking.com/FishNet/api/api/FishNet.Managing.Scened.SceneManager.html) to see the public items exposed to the user in the SceneManager.
|
||||
{% endhint %}
|
||||
|
||||
## Sub pages
|
||||
|
||||
### [Automatic Online and Offline Scenes](loading-scenes/automatic-online-and-offline-scenes.md)
|
||||
|
||||
If you simply want an online scene to be loaded when the network starts and a different one to be loaded when it stops, then you can do that with a single component.
|
||||
|
||||
### [Scene Events](scene-events.md)
|
||||
|
||||
"Scene Events" are the Invoked Events that happen along the loading and unloading method.
|
||||
|
||||
### [Scene Data](scene-data/)
|
||||
|
||||
The Data Classes that are needed for the various features to function.
|
||||
|
||||
### [Loading Scenes](loading-scenes/)
|
||||
|
||||
Information on how to "load" scenes and the options available to the user while loading.
|
||||
|
||||
### [Unloading Scenes](unloading-scenes.md)
|
||||
|
||||
Information on how to "unload" scenes and the options available to the user while Unloading.
|
||||
|
||||
### [Scene Stacking](scene-stacking.md)
|
||||
|
||||
"Scene Stacking" is the ability for server or host to load multiple instances of a scene at once, usually with different observers in each scene.
|
||||
|
||||
### [Scene Caching](scene-caching.md)
|
||||
|
||||
"Scene Caching" is the ability for the Server to keep a scene loaded when either all clients have unloaded that scene, or stopped observing that scene.
|
||||
|
||||
### [Scene Visibility](scene-visibility.md)
|
||||
|
||||
"Scene Visibility" guide offers details of using the "Scene Condition" with the [**ObserverManager**](../../../fishnet-building-blocks/components/managers/observermanager/), and how to manage Observers in a Scene.
|
||||
|
||||
### [Persisting NetworkObjects](persisting-networkobjects.md)
|
||||
|
||||
"Persisting NetworkObjects" is the ability to keep a network object's state when loading and unloading scenes. As well as moving an object between scenes.
|
||||
|
||||
### [Custom Scene Processors](custom-scene-processors/)
|
||||
|
||||
Fishnet has the ability for users to create their own Custom Scene Processor for loading and unloading scenes. Using Addressables for example, would need a Custom Scene Processor created.
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
---
|
||||
description: >-
|
||||
You can create a custom scene processor to handle how a scene is
|
||||
loaded/unloaded.
|
||||
---
|
||||
|
||||
# Custom Scene Processors
|
||||
|
||||
FishNet's SceneManager uses Unity's methods under the hood, you can override its functionality to add or change how it functions. This can be useful when using addressables or for making custom loading screens.
|
||||
|
||||
To start, simply create a script that inherits from the [DefaultSceneProcessor](https://fish-networking.com/FishNet/api/api/FishNet.Managing.Scened.DefaultSceneProcessor.html), add it to your network manager object, and then in the [SceneManager](../../../../fishnet-building-blocks/components/managers/scenemanager.md) component on the network manager, assign it as the [Scene Processor](../../../../fishnet-building-blocks/components/managers/scenemanager.md#scene-processor).
|
||||
|
||||
{% hint style="success" %}
|
||||
You are encouraged to check out its full API [here](https://fish-networking.com/FishNet/api/api/FishNet.Managing.Scened.DefaultSceneProcessor.html).
|
||||
{% endhint %}
|
||||
|
||||
### Addressables
|
||||
|
||||
You will need to make a custom scene processor if you want your game to make use of addressable networked scenes. Follow [this guide](addressables.md) to learn a little more.
|
||||
|
||||
### Loading Screen
|
||||
|
||||
You can use a custom scene processor to easily hook up your loading screen into FishNet's networked scene loading. There is a complete tutorial for that [here](../../../../tutorials/simple/making-a-loading-screen.md).
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
# Addressables
|
||||
|
||||
This guide assumes you know how to load and unload addressable scenes without Fish-Networking. Below describes how to use your addressables scene logic with the Fish-Networking SceneManager.
|
||||
|
||||
There are a variety of ways to load and unload addressable scenes; because of this you are provided an easy way to create your own logic for accessing addressables. To begin using addressables with the SceneManager create a new script, and inherit from **DefaultSceneProcessor**.
|
||||
|
||||
{% hint style="info" %}
|
||||
LoadOptions and UnloadOptions have an addressables boolean you may utilize to know if your scene change is using addressables.
|
||||
{% endhint %}
|
||||
|
||||
The DefaultSceneProcessor script inherits from SceneProcessorBase to implement the default scene changing functionality. Your custom script inherits from DefaultSceneProcessor because often only some of the logic needs to be changed, and this allows you to only override the differences.
|
||||
|
||||
As mentioned what you need to modify may vary, but most developers find themselves overriding the following methods.
|
||||
|
||||
```csharp
|
||||
public abstract void BeginLoadAsync(string sceneName, LoadSceneParameters parameters);
|
||||
public abstract void BeginUnloadAsync(Scene scene);
|
||||
public abstract bool IsPercentComplete();
|
||||
public abstract float GetPercentComplete();
|
||||
public abstract IEnumerator AsyncsIsDone();
|
||||
```
|
||||
|
||||
Each method contains XML documentation to provide you a better description.
|
||||
|
||||
After you have completed your implementation of DefaultSceneProcessor add your newly created component to your NetworkManager object.
|
||||
|
||||
If the _SceneManager_ component is not added to the NetworkManager object yet, add that as well. Drag your DefaultSceneProcessor implementation into the _SceneProcessor_ field on the SceneManager.
|
||||
|
||||
You are all set now! Whenever Fish-Networking loads or unloads a scene your processor will be used and you can control the logic.
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
---
|
||||
description: >-
|
||||
Instructions for loading networked scenes with FishNet, both on the server and
|
||||
on clients.
|
||||
---
|
||||
|
||||
# Loading Scenes
|
||||
|
||||
{% embed url="https://www.youtube.com/watch?index=3&list=PLkx8oFug638qBthd3n_F98zAtOdI8o_Gb&v=PhWKZsL0Za4" %}
|
||||
Loading scenes video guide
|
||||
{% endembed %}
|
||||
|
||||
## General
|
||||
|
||||
This guide will go over how to setup and load new scenes, how to load into existing scenes, how to replace scenes, and advanced info on what happens behind the "scenes" during a load.
|
||||
|
||||
{% hint style="success" %}
|
||||
Having a simple online and offline scene is very to do through the built-in [DefaultScene ](../../../../fishnet-building-blocks/components/utilities/defaultscene.md)component. You can read more about using it [here](automatic-online-and-offline-scenes.md).
|
||||
{% endhint %}
|
||||
|
||||
{% hint style="info" %}
|
||||
Loading Scenes globally or by connection will add the specified clients connection as an [**Observer** ](../../observers/)to the scenes if utilizing the [**ObserverManagers**](../../../../fishnet-building-blocks/components/managers/observermanager/) Scene Condition, so globally will add all clients as Observers, and by connection will only add the connections specified. - See [**Scene Visibility**](../scene-visibility.md) for more info.
|
||||
{% endhint %}
|
||||
|
||||
{% hint style="warning" %}
|
||||
In the Examples below, all SceneManager calls are done inside a NetworkBehaviour class, that is why you can get reference to SceneManager by doing base.SceneManager.\
|
||||
If you would like a reference outside of a NetworkBehaviour consider using FishNet's [**InstanceFinder**](../../instancefinder-guides.md).SceneManager.
|
||||
{% endhint %}
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Setup</summary>
|
||||
|
||||
Before calling the SceneManager's Load Scene functions you will need to setup the load data to tell the [**SceneManager**](../../../../fishnet-building-blocks/components/managers/scenemanager.md) how you want it to handle the scene load.
|
||||
|
||||
**SceneLookupData**
|
||||
|
||||
[**SceneLookupData**](../scene-data/scenelookupdata.md) is the class used to specify what scene you want the [**SceneManager**](../../../../fishnet-building-blocks/components/managers/scenemanager.md) to load. You do not need to create the lookup data manually but can instead use the SceneLoadData constructors which will create the SceneLookupData automatically.
|
||||
|
||||
**SceneLoadData**
|
||||
|
||||
When loading a scene in any way, you must pass in an instance of a [**SceneLoadData**](../../../../fishnet-building-blocks/components/managers/scenemanager.md)[ ](../scene-data/sceneloaddata.md)class into the load methods. This class provides the scene manager all of the info it needs to load the scene or scenes properly.
|
||||
|
||||
The constructors available for [**SceneLoadData**](../scene-data/sceneloaddata.md) will automatically create the [**SceneLookupData**](../scene-data/scenelookupdata.md) needed for the SceneManager to handle if you are loading a new scene, or an existing instance of one.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Loading New Scenes</summary>
|
||||
|
||||
Scenes can be loaded globally or for any number of specified clients, including none (which will only load the scene on the server).
|
||||
|
||||
{% hint style="info" %}
|
||||
Loading new Scenes can only be done by Name, you **cannot** use Handles or Scene References.
|
||||
{% endhint %}
|
||||
|
||||
**Global Scenes**
|
||||
|
||||
* Global Scenes can be loaded by calling `LoadGlobalScenes()` in the SceneManager.
|
||||
* When loaded globally, scenes will be loaded for all current, and future clients.
|
||||
|
||||
```csharp
|
||||
SceneLoadData sld = new SceneLoadData("Town");
|
||||
base.SceneManager.LoadGlobalScenes(sld);
|
||||
```
|
||||
|
||||
**Connection Scenes**
|
||||
|
||||
Connection Scenes follow the same principle but have a few method overloads.
|
||||
|
||||
* You can load scenes for a single connection, multiple connections at once, or load scenes only on the server in preparation for connections.
|
||||
* When loading by connection only the connections specified will load the scenes.
|
||||
* You can add additional connections into a scene at any time.
|
||||
|
||||
```csharp
|
||||
SceneLoadData sld = new SceneLoadData("Main");
|
||||
|
||||
// Load scenes for a single connection.
|
||||
NetworkConnection conn = base.Owner;
|
||||
base.SceneManager.LoadConnectionScenes(conn, sld);
|
||||
|
||||
// Load scenes for several connections at once.
|
||||
NetworkConnection[] conns = new NetworkConnection[] { connA, connB };
|
||||
base.SceneManager.LoadConnectionScenes(conns, sld);
|
||||
|
||||
// Load scenes only on the server. This can be used to preload scenes
|
||||
// that you don't want all players in.
|
||||
base.SceneManager.LoadConnectionScenes(sld);
|
||||
```
|
||||
|
||||
**Loading Multiple Scenes**
|
||||
|
||||
* Whether loading globally or by connection, you can load more than one scene in a single method call.
|
||||
* When loading multiple scenes in one call, the NetworkObjects you put into [**Moved NetworkObjects**](../scene-data/sceneloaddata.md#movednetworkobjects) will be moved to the first valid scene in the list of scenes you tried to load. See Persisting NetworkObjects for more info about keeping NetworkObjects across scenes.
|
||||
|
||||
```csharp
|
||||
// Loading Multiple Connections into Multiple Scenes
|
||||
string[] scenesToLoad = new string[] {"Main", "Additive"};
|
||||
NetworkConnection[] conns = new NetworkConnection[] {connA, connB,connC}
|
||||
|
||||
SceneLoadData sld = new SceneLoadData(scenesToLoad);
|
||||
base.SceneManager.LoadConnectionScenes(conns, sld);
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Loading Existing Scenes</summary>
|
||||
|
||||
If the scene is already loaded on the server, and you want to load clients into that instance of the scene. Most likely you will want to lookup that scene by scene reference, or handle to make sure you are getting the exact scene you need.
|
||||
|
||||
If you load the scene by name, it will load the connections into the first scene found with that name. If you are utilizing [**Scene Stacking**](../scene-stacking.md), then there may be multiple scenes loaded with the same name. So be alert when loading into existing scenes by name.
|
||||
|
||||
You can load clients into scenes that have no other clients in them if you are utilizing [**Scene Caching**](../scene-caching.md) **-** the ability to keep a scene loaded with its current state on the server when all clients leave the scene.
|
||||
|
||||
**Getting References to a Loaded Scene**
|
||||
|
||||
Here are a few ways to get reference to the scenes that you already loaded using FishNet's **SceneManager**.
|
||||
|
||||
**By Event:**
|
||||
|
||||
```csharp
|
||||
// Manage your own collection of SceneReferences/Handles
|
||||
// Customize how you want to manage you scene references so it's easy
|
||||
// for you to find them later.
|
||||
List<Scene> ScenesLoaded = new();
|
||||
|
||||
public void OnEnable()
|
||||
{
|
||||
InstanceFinder.SceneManager.OnLoadEnd += RegisterScenes;
|
||||
}
|
||||
|
||||
public void RegisterScenes(SceneLoadEndEventArgs args)
|
||||
{
|
||||
// Only register on the server
|
||||
if (!args.QueueData.AsServer) return;
|
||||
|
||||
// if you know you only loaded one scene you could just grab index [0]
|
||||
foreach(var scene in args.loadedScenes)
|
||||
ScenesLoaded.Add(scene);
|
||||
}
|
||||
|
||||
public void OnDisable()
|
||||
{
|
||||
InstanceFinder.SceneManager.OnLoadEnd -= RegisterScene;
|
||||
}
|
||||
```
|
||||
|
||||
**By Connection:**
|
||||
|
||||
```csharp
|
||||
// NetworkConnections have a list of Scenes they are currently in.
|
||||
int clientToLookup;
|
||||
InstanceFinder.ServerManger.Clients[clientToLookup].Scenes;
|
||||
```
|
||||
|
||||
**By SceneManager.SceneConnnections:**
|
||||
|
||||
```csharp
|
||||
// SceneManager keeps a Dictionary of all connection scenes as the key
|
||||
// and the client connections that are in that scene as the value.
|
||||
NetworkConnection conn;
|
||||
Scene sceneNeeded;
|
||||
|
||||
// Get the scene you need with foreach or use LINQ to filter your conditions.
|
||||
foreach(var pair in SceneManager.SceneConnections)
|
||||
{
|
||||
if (pair.Value.Contains(conn))
|
||||
sceneNeeded = pair.Key;
|
||||
}
|
||||
```
|
||||
|
||||
**Using Reference to Load Into Existing Instance**
|
||||
|
||||
Use the methods above to get the reference or handle of a scene, and use that reference or handle to load a client into an existing scene.
|
||||
|
||||
```csharp
|
||||
scene sceneReference;
|
||||
NetworkConnection[] conns = new() {connA,connB};
|
||||
|
||||
// by reference
|
||||
SceneLoadData sld = new(sceneReference);
|
||||
base.SceneManager.LoadConnectionScenes(conns, sld);
|
||||
|
||||
// by handle
|
||||
SceneLoadData sld = new(sceneReference.handle);
|
||||
base.SceneManager.LoadConnectionScenes(conns, sld);
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Replacing Scenes</summary>
|
||||
|
||||
Fishnet gives the ability to replace scenes that are already loaded on the clients with the new requested scenes to load.
|
||||
|
||||
To Replace Scenes, you will set the ReplaceScene Option in the SceneLoadData
|
||||
|
||||
Replaced scenes will be unloaded before the new scenes are loaded.
|
||||
|
||||
Replacing Scenes by Default will replace scenes on both the server and clients. If you would like the server to keep the scene loaded and only replace the scene on the clients - see [**Scene Caching**](../scene-caching.md) for more details.
|
||||
|
||||
**Replace None:**
|
||||
|
||||
This is the default method when loading, it will ignore the replace options and load the scene in normally.
|
||||
|
||||
**Replace All:**
|
||||
|
||||
This will replace all scenes currently loaded in unity, even ones not managed by FishNet's SceneManager.
|
||||
|
||||
```csharp
|
||||
// Replace All Option.
|
||||
SceneLoadData sld = new SceneLoadData("DungeonScene");
|
||||
sld.ReplaceScenes = ReplaceOption.All;
|
||||
|
||||
// This will replace all Scenes loaded by FishNet or outside of FishNet like Unity,
|
||||
// and load "DungeonScene"
|
||||
SceneManager.LoadGlobalScenes(sld);
|
||||
```
|
||||
|
||||
**Replace Online Only:**
|
||||
|
||||
This will replace only scenes managed by the SceneManager in FishNet.
|
||||
|
||||
```csharp
|
||||
// Replace Online Only Option.
|
||||
SceneLoadData sld = new SceneLoadData("DungeonScene");
|
||||
sld.ReplaceScenes = ReplaceOption.OnlineOnly;
|
||||
|
||||
// This will replace only scenes managed by the SceneManager in FishNet.
|
||||
SceneManager.LoadGlobalScenes(sld);
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Advanced Info</summary>
|
||||
|
||||
**Behind the "Scenes"**
|
||||
|
||||
The [**SceneManager**](../../../../fishnet-building-blocks/components/managers/scenemanager.md) Class has very detailed XML comments on how the load process works in detail, if you need to troubleshoot the scene load process, these comments will help you understand the flow of how a scene loads.
|
||||
|
||||
**Events**
|
||||
|
||||
Make sure to check out the [**Scene Events**](../scene-events.md) that you can subscribe to in order to have better control over your game.
|
||||
|
||||
**More Control**
|
||||
|
||||
If you want more control over how FishNet loads and unloads scenes, you can create a custom scene processor to override FishNet's functionality. Learn more about that [here](../custom-scene-processors/).
|
||||
|
||||
</details>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
---
|
||||
description: Using the DefaultScene component to automatically manage simple scene setups.
|
||||
---
|
||||
|
||||
# Automatic Online and Offline Scenes
|
||||
|
||||
If you're looking for a simple way to manage scene transitions when your network starts or stops, FishNet has an easy way to manage it without any custom code. The [**DefaultScene**](../../../../fishnet-building-blocks/components/utilities/defaultscene.md) component is a ready-made solution that automatically loads your designated **online scene** when the network starts, and your **offline scene** when the network shuts down.
|
||||
|
||||
This is perfect for developers who want a plug-and-play setup without writing custom scene logic.
|
||||
|
||||
### Setup instructions
|
||||
|
||||
To get started:
|
||||
|
||||
1. Add the `DefaultScene` component to your **NetworkManager** GameObject.
|
||||
2. In the Inspector, assign your **Offline Scene** and **Online Scene**.
|
||||
3. Make sure the two scenes are different — using the same scene for both will prevent the component from functioning properly.
|
||||
|
||||
{% hint style="success" %}
|
||||
Read more about the component here: [defaultscene.md](../../../../fishnet-building-blocks/components/utilities/defaultscene.md "mention")
|
||||
{% endhint %}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
---
|
||||
description: >-
|
||||
This page will go over details on the options available to users for
|
||||
persisting NetworkObjects across scene loading and unloading
|
||||
---
|
||||
|
||||
# Persisting NetworkObjects
|
||||
|
||||
## General
|
||||
|
||||
The options available to users to keep [**NetworkObjects**](../networked-gameobjects-and-scripts/networkobjects/) persisting across scenes depends on the type of NetworkObject.
|
||||
|
||||
## Spawned NetworkObjects
|
||||
|
||||
[**Spawned NetworkObjects**](../networked-gameobjects-and-scripts/networkobjects/#spawnednetworkobject) that do not fall into the other categories below can persist between scenes by moving them while loading into the next scene or moving them manually on the server and then updating the observers.
|
||||
|
||||
#### Moving objects when loading the scene
|
||||
|
||||
The [**SceneLoadData**](scene-data/sceneloaddata.md) that is passed into the Load Method of the SceneManager has an array that you can populate with all of the Spawned NetworkObjects you want to send to the new loaded scene.
|
||||
|
||||
The SceneManager will handle the objects correctly for you and also flag a Debug if you try to send a GameObject that is not allowed.
|
||||
|
||||
{% hint style="warning" %}
|
||||
If you load Multiple Scenes in one method call, and you are moving network objects using SceneLoadData. The moved networkobjects will move into the first valid scene requested.
|
||||
{% endhint %}
|
||||
|
||||
#### Example
|
||||
|
||||
```csharp
|
||||
// Just create an array of NetworkObjects
|
||||
// that are not a Scene, Global or Nested NetworkObjects.
|
||||
NetworkObject[] objectsToMove = new NetworkObject[] { object1, object2, object3 }
|
||||
|
||||
// Assign this array to the SceneLoadData before you Load a Scene.
|
||||
SceneLoadData sld = new SceneLoadData("NewScene");
|
||||
sld.MovedNetworkObjects = objectsToMove;
|
||||
|
||||
// FishNet will handle the rest after loading!
|
||||
SceneManager.LoadGlobalScenes(sld);
|
||||
```
|
||||
|
||||
#### Moving objects manually on the server
|
||||
|
||||
You can also manually move such an object by using Unity's `SceneManager.MoveGameObjectToScene` method and then rebuilding FishNet's observers `ServerManager.Objects.RebuildObservers`. 
|
||||
|
||||
Both of these should only be done on the server.
|
||||
|
||||
#### Example
|
||||
|
||||
```csharp
|
||||
// Use Unity's SceneManager to move the specific object to a specifc scene.
|
||||
UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(theGameObject, theScene);
|
||||
// Then you can tell FishNet to rebuild the observers.
|
||||
InstanceFinder.ServerManager.Objects.RebuildObservers();
|
||||
```
|
||||
|
||||
## Scene NetworkObjects
|
||||
|
||||
[**Scene NetworkObjects**](../networked-gameobjects-and-scripts/networkobjects/#scenenetworkobject) currently cannot persist across scenes, it is a [**limitation**](../../troubleshooting/technical-limitations.md) with the way Unity and FishNet was designed. You cannot mark them as IsGlobal, or put them into the "DontDestroyOnLoad" scene. If you would like a Scene NetworkObject to persist across scenes it is recommended to remove them and use the other options available on this page.
|
||||
|
||||
## Global NetworkObjects
|
||||
|
||||
<mark style="color:blue;">**G**</mark>[<mark style="color:blue;">**lobal NetworkObjects**</mark>](../networked-gameobjects-and-scripts/networkobjects/#globalnetworkobject) work similar to how a normal GameObject would when put into the "DontDestroyOnLoad" (DDOL) scene.\
|
||||
\
|
||||
When loading and unloading scenes, **Global NetworkObjects** will stay in the (DDOL) scene on both the server and client persisting their state. No extra steps needed.
|
||||
|
||||
To make a NetworkObject global just mark the IsGlobal Boolean "true" on the NetworkObject component.
|
||||
|
||||
{% hint style="info" %}
|
||||
Clients typically are always an observer of the DDOL scene, so global objects may make more sense for Manager type GameObjects, instead of gameobjects that have meshes, but you are not limited to this.
|
||||
{% endhint %}
|
||||
|
||||
## Nested NetworkObjects
|
||||
|
||||
Unity will not allow [**Nested GameObjects**](../networked-gameobjects-and-scripts/networkobjects/nested-networkobjects.md) to be moved into other scenes.\
|
||||
\
|
||||
However! Fishnet will automatically detect if you are trying to send a NestedNetworkObject and send the root of the object instead!
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
description: >-
|
||||
Scene Caching is the ability for the Server to keep a scene loaded when either
|
||||
all clients have unloaded that scene, or stopped observing that scene.
|
||||
---
|
||||
|
||||
# Scene Caching
|
||||
|
||||
## General
|
||||
|
||||
When loading a scene you can specify in the SceneLoadData whether to AutomaticallyUnload the scene when all Clients unload or leave the scene as an observer.\
|
||||
\
|
||||
When Unloading the Scene you can override the AutomaticallyUnload options by setting the ServerUnloadMode.
|
||||
|
||||
## Examples
|
||||
|
||||
```csharp
|
||||
// When Loading Scenes.
|
||||
SceneLoadData sld = new SceneLoadData("MainScene");
|
||||
sld.Options.AutomaticallyUnload = false;
|
||||
|
||||
// When Manually Unloading Scenes.
|
||||
// Whatever you set the ServerUnloadMode to here will override the AutomaticallyUnload
|
||||
// setting you used when loading the scene earlier.
|
||||
SceneUnloadData sud = new SceneUnloadData("MainScene");
|
||||
sud.Options.Mode = ServerUnloadMode.KeepUnused;
|
||||
```
|
||||
|
||||
## Host behaviour
|
||||
|
||||
In situations where the Hosts Server needs to keep a scene loaded, but the Hosts Client was requested to unload that scene. Instead of unloading, the Hosts Client will be removed from the scene using the observer system updating the Clients [**Scene Visibility**](scene-visibility.md).\
|
||||
\
|
||||
As Host, the Server and Client share the same instance of loaded Scenes and Game Objects. If it were to actually unload a scene from the Host Client, it would also unload on the server.
|
||||
|
||||
{% hint style="danger" %}
|
||||
A **Scene Condition** must be set in the **ObserverManager** for you to utilize the ability to have the server keep a scene loaded, and the hosts client not see the objects in that scene.
|
||||
{% endhint %}
|
||||
|
||||
{% hint style="warning" %}
|
||||
This usually means that every GameObject with a mesh will have to have a NetworkObject attached if you want the Host Client not to visualize the scene.
|
||||
{% endhint %}
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
description: >-
|
||||
Scene Data is the data types used that the user will interface with when using
|
||||
the SceneManager.
|
||||
---
|
||||
|
||||
# Scene Data
|
||||
|
||||
{% embed url="https://www.youtube.com/watch?index=2&list=PLkx8oFug638qBthd3n_F98zAtOdI8o_Gb&v=T23OiTKy500" %}
|
||||
SceneLoadData video guide
|
||||
{% endembed %}
|
||||
|
||||
## General
|
||||
|
||||
See Sub-Pages below for the different Scene Data Classes.
|
||||
|
||||
* [**SceneLookupData**](scenelookupdata.md)
|
||||
* [**SceneLoadData**](sceneloaddata.md)
|
||||
* [**SceneUnloadData**](sceneunloaddata.md)
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
---
|
||||
description: >-
|
||||
The Data Class needed for the SceneManager to know how to handle loading a
|
||||
scene.
|
||||
---
|
||||
|
||||
# SceneLoadData
|
||||
|
||||
## General
|
||||
|
||||
Loading scenes of all types depend upon SceneLoadData. The SceneLoadData class contains information about what scenes to load, how to load them, which objects to move to new scenes, and more. You can view the SceneLoadData API [here](https://fish-networking.com/FishNet/api/api/FishNet.Managing.Scened.SceneLoadData.html).
|
||||
|
||||
## Default values
|
||||
|
||||
```csharp
|
||||
SceneLoadData sld = new SceneLoadData()
|
||||
{
|
||||
PreferredActiveScene = null,
|
||||
SceneLookupDatas = new SceneLookupData[0],
|
||||
MovedNetworkObjects = new NetworkObject[0],
|
||||
ReplaceScenes = ReplaceOption.None,
|
||||
Params = new LoadParams()
|
||||
{
|
||||
ServerParams = new object[0],
|
||||
ClientParams = new byte[0]
|
||||
},
|
||||
Options = new LoadOptions()
|
||||
{
|
||||
AutomaticallyUnload = true,
|
||||
AllowStacking = false,
|
||||
LocalPhysics = LocalPhysicsMode.None,
|
||||
ReloadScenes = false, // Obsolete, Coming Soon.
|
||||
Addressables = false
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
<summary>PreferredActiveScene</summary>
|
||||
|
||||
Preferred Active Scene will allow you to choose what scene will be active on the server and/or client. You can choose to have the chosen SceneLookupData apply to both the server and client or separate ones for each.
|
||||
|
||||
If left with the default value of null, the first valid scene loaded will become the ActiveScene.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>SceneLookupDatas</summary>
|
||||
|
||||
This Array is populated with scenes you would like to load, depending on the parameters you pass into the SceneLoadData when constructed.
|
||||
|
||||
See [**Loading Scenes**](../loading-scenes/) for examples.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>MovedNetworkObjects</summary>
|
||||
|
||||
Instantiated NetworkObjects can be moved when loading new scenes, such as if you want to move a player to a different scene as you load the new scene. You may include an array of NetworkObjects to move to the new scenes. NetworkObjects within this array will be moved to the first scene specified in SceneLookupData.\
|
||||
\
|
||||
See [**Persisting NetworkObjects**](../persisting-networkobjects.md) for more details on what type of NetworkObjects you are allowed to move.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>ReplaceScenes</summary>
|
||||
|
||||
Like the Unity SceneManager when loading a single scene, ReplaceScenes allows you to replace currently loaded scenes with new ones. There are a variety of options to use.\
|
||||
\
|
||||
See [**Replacing Scenes**](../loading-scenes/#replacing-scenes) Section of Loading Scenes guide for more details and examples.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Params</summary>
|
||||
|
||||
Params are an optional way to assign data to your scene loads/unloads. This data will be available within[ **Scene Events**](../scene-events.md), Information used in Params can be useful for storing information about the scene load/unload and referencing it later when the scene load/unload completes.
|
||||
|
||||
**ServerParams**
|
||||
|
||||
_ServerParams_ are only included on the server side, and are not networked. It is an array of objects, meaning you can send anything you want. However when accessing the Params through event args, you will have to cast the object to the data you want.
|
||||
|
||||
**ClientParams**
|
||||
|
||||
_ClientParams_ is a byte array which may contain anything, and will be sent to clients when they receive the load scene instructions. Clients can access the _ClientParams_ within the scene change events.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Options</summary>
|
||||
|
||||
You may further enhance how you load/unload scenes with Options.
|
||||
|
||||
**AutomaticallyUnload**
|
||||
|
||||
* When _set to_ true scenes will be unloaded automatically on the server when there are no more connections present. This is the default behaviour.
|
||||
* When set to false the scene will remain if connections leave the scene unexpected, such as being disconnected.
|
||||
* However, discussed in UnloadSceneData, this behavior can be overriden using the UnloadOptions of UnloadSceneData.
|
||||
* Only scenes loaded for connections will be automatically unloaded when emptied.
|
||||
* Global scenes can only be unloaded using ReplaceScenes or by calling unload on them.
|
||||
|
||||
**AllowStacking**
|
||||
|
||||
* When _AllowStacking_ remains false the SceneManager will not stack scenes in your SceneLoadDatas.
|
||||
* If true, then scenes can be stacked (loaded multiple times).
|
||||
* In the SceneLookupData section it was mentioned that if a Scene reference or handle is specified then the SceneManager will favor loading a scene using a scene handle. When you would like to load connections into the same stacked scene over multiple load calls, you will populate your SceneLookupDatas by Scene reference or handle.
|
||||
* See [**Scene Stacking**](../scene-stacking.md) for more detail and examples
|
||||
|
||||
**LocalPhysics**
|
||||
|
||||
* [_LocalPhysics_](https://docs.unity3d.com/ScriptReference/SceneManagement.LocalPhysicsMode.html) is a Unity property that lets you determine how physics are simulated in your scenes.
|
||||
* Generally, if you are stacking scenes you will want to set a LocalPhysics mode so that stacked scenes do not collide with each other.
|
||||
|
||||
**Addressables**
|
||||
|
||||
* _Addressables_ is only used as a reference and performs no additional functionality.
|
||||
* You may set this value to know if a scene is loading using addressables, without having to create Params.
|
||||
|
||||
</details>
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
---
|
||||
description: >-
|
||||
SceneLookupData is how the server determines to load clients into a new
|
||||
instance of a Scene, or load a client into a scene that the server already has
|
||||
loaded.
|
||||
---
|
||||
|
||||
# SceneLookupData
|
||||
|
||||
## General
|
||||
|
||||
{% hint style="warning" %}
|
||||
When Creating [**SceneLoadData**](sceneloaddata.md) or [**SceneUnloadData**](sceneunloaddata.md) there are provided constructors that automatically create SceneLookupData for you. Most likely you will be using these constructors and will not be creating a separate SceneLookupData.
|
||||
{% endhint %}
|
||||
|
||||
When you specify a scene by reference, or handle, the SceneManager will prefer to lookup that scene using the scene handle. This is important information when [**Scene Stacking**](../scene-stacking.md). Looking up a scene by handle or object reference will place connections in the scene specified, but when using scene names, the server will create a new scene instance for the specified connections and place them into that scene. The described behavior only applies when loading scenes over multiple load calls; such as if you call LoadConnectionScene twice, each with its own connection.
|
||||
|
||||
## Default values
|
||||
|
||||
{% hint style="info" %}
|
||||
A Scene Handle is an integer that Unity generates for a scene instance when it is loaded. You can use it to specify an instance of a scene that is already loaded, but not for loading a new instance of that scene. Read more about it here: [https://docs.unity3d.com/ScriptReference/SceneManagement.Scene-handle.html](https://docs.unity3d.com/ScriptReference/SceneManagement.Scene-handle.html)
|
||||
{% endhint %}
|
||||
|
||||
```csharp
|
||||
// SceneLookupData default values
|
||||
SceneLookupData slud = new SceneLookupData()
|
||||
{
|
||||
// If Handle is greater than 0 then it will ignore Name and use Handle
|
||||
// to look up the scene.
|
||||
Handle = 0,
|
||||
// If Handle is set to 0, then Name is used to lookup the scene instead.
|
||||
Name = null
|
||||
};
|
||||
```
|
||||
|
||||
See[ **Loading Scenes**](../loading-scenes/) and [**Unloading Scenes**](../unloading-scenes.md) for implementation.
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
---
|
||||
description: >-
|
||||
The Data Class needed for the SceneManager to know how to handle unloading a
|
||||
scene.
|
||||
---
|
||||
|
||||
# SceneUnloadData
|
||||
|
||||
## General
|
||||
|
||||
When unloading scenes information on what to unload is constructed within the SceneUnloadData class. SceneUnloadData is very similar to SceneLoadData. The API for SceneUnloadData can be found [here](https://fish-networking.com/FishNet/api/api/FishNet.Managing.Scened.SceneUnloadData.html).
|
||||
|
||||
## Default values
|
||||
|
||||
```csharp
|
||||
SceneUnloadData sud = new SceneUnloadData()
|
||||
{
|
||||
SceneLookupDatas = new SceneLookupData[0],
|
||||
Params = new UnloadParams()
|
||||
{
|
||||
ServerParams = new object[0],
|
||||
ClientParams = new byte[0]
|
||||
},
|
||||
Options = new UnloadOptions()
|
||||
{
|
||||
Mode = ServerUnloadMode.UnloadUnused,
|
||||
Addressables = false
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
<summary>SceneLookupDatas</summary>
|
||||
|
||||
This Array is populated with the scenes you want to unload, depending on the parameters you pass into the SceneUnloadData when constructed.
|
||||
|
||||
See [**Unloading Scenes**](../unloading-scenes.md) for examples.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Params</summary>
|
||||
|
||||
Params are an optional way to assign data to your scene loads/unloads. This data will be available within[ **Scene Events**](../scene-events.md), Information used in Params can be useful for storing information about the scene load/unload and referencing it later when the scene load/unload completes.
|
||||
|
||||
**ServerParams**
|
||||
|
||||
_ServerParams_ are only included on the server side and are not networked. It is an array of objects, meaning you can send anything you want. However when accessing the Params through event args, you will have to cast the object to the data you want.
|
||||
|
||||
**ClientParams**
|
||||
|
||||
_ClientParams_ is a byte array which may contain anything. It will be sent to clients when they receive the load scene instructions. Clients can access the _ClientParams_ within the scene change events.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Options</summary>
|
||||
|
||||
Like with Options in loading, the UnloadOptions offer additional settings when unloading.
|
||||
|
||||
**Mode**
|
||||
|
||||
These values will override the AutomaticallyUnload Option that was used when loaded the scene. If you set _AutomaticallyUnload_ to false but specified _ServerUnloadModes.UnloadUnused_ then the scene would be unloaded when emptied.
|
||||
|
||||
**ServerUnloadModes.UnloadUnused**
|
||||
|
||||
* This is the default setting which will only unload a scene which is no longer used.
|
||||
|
||||
**ServerUnloadModes.KeepUnused**
|
||||
|
||||
* This option will keep the scene loaded on the server if all clients have been removed. See [**Scene Caching**](../scene-caching.md) for more details
|
||||
|
||||
</details>
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
description: >-
|
||||
Information about FishNet's many available scene events, the order they are
|
||||
invoked in, and how to use them.
|
||||
---
|
||||
|
||||
# Scene Events
|
||||
|
||||
{% embed url="https://www.youtube.com/watch?index=1&list=PLkx8oFug638qBthd3n_F98zAtOdI8o_Gb&v=c_kdZELyJcQ" %}
|
||||
Scene events video guide
|
||||
{% endembed %}
|
||||
|
||||
## General
|
||||
|
||||
There are a variety of events within SceneManager to help with your development. The order in which events are invoked is reliable.
|
||||
|
||||
This diagram represents which order events can be expected to run. You may also find this diagram within your Fish-Networking import under Example/All/SceneManager.
|
||||
|
||||

|
||||
|
||||
{% hint style="info" %}
|
||||
Notice that client and server process queues and events exactly the same. OnClientPresence change is a special exception, as discussed below.
|
||||
{% endhint %}
|
||||
|
||||
## Events
|
||||
|
||||
### OnClientLoadedStartScenes
|
||||
|
||||
This event calls every time a client loads the starting scenes for the first time. The start scenes would be any global scenes you have set, or if no global scenes are set then the scene you are entering play into. The PlayerSpawner.cs example demonstrates usage of the **OnClientLoadedStartScenes** event. Included is the connecion, or client which loaded the scenes, and a boolean indicating true if the callback is on the server side or false if on the client side.
|
||||
|
||||
The NetworkConnection class also has a similar event **OnLoadedStartScenes** if you'd like to know when only a specific connection has loaded the start scenes.
|
||||
|
||||
### OnQueueStart, OnQueueEnd
|
||||
|
||||
**OnQueueStart** and **OnQueueEnd** are called when a scene change queue occurs. **Start** will only call if a scene has succesfully begun to load or unload. The queue may process any number of scene events. For example: if a scene is told to unload while a load is still in progress, then the unload will be placed in the queue. **OnQueueEnd** will invoke after both the load and unload have completed.
|
||||
|
||||
### OnLoadStart, OnUnloadStart
|
||||
|
||||
**OnLoadStart** and **OnUnloadStart** occur before a queue entry is processed. These will call for every entry in a queue. For example: if you call SceneManager.LoadGlobalScenes() twice in a row, **OnQueueStart** will invoke once, and OnLoadStart will invoke twice. **OnLoadStart** will be called when a scene load begins, and **OnUnloadStart** when an unload begins.
|
||||
|
||||
Both the Load and Unload events contain a structure which has a field labeled _QueueData._ _QueueData_ holds information about the queue entry. The provided data is exposed for your convenience.
|
||||
|
||||
### OnLoadPercentChange
|
||||
|
||||
Only available when loading scenes is the **OnLoadPercentChange** event. This event contains information about what percentage of your scenes have loaded for the queue entry. Like the Start events, _QueueData_ is provided. You will also be supplied a float _Percent_, indicating the total progress of your scene load. This can be useful to show loading screens, or perform a variety of initialization tasks.
|
||||
|
||||
### OnLoadEnd
|
||||
|
||||
The **OnLoadEnd** event is called after all scenes for the queue entry have been loaded. This event will only invoke after the scenes have fully loaded, and after the active scene has been set if applicable. Like the **OnLoadStart** event _QueueData_ is provided. Additionally, _LoadedScenes_ and _SkippedSceneNames. LoadedScenes_ provides Scene references to which scenes were loaded. _SkippedSceneNames_ contains strings of scenes which were not loaded; this generally occurs if the scene is already loaded.
|
||||
|
||||
{% hint style="warning" %}
|
||||
You may want to spawn objects for players once a scene is loaded, but it's best to do that once the specific client has started observing the scene to prevent race conditions. Thus it's better to use one of the OnClientPresenceChange events or OnSpawnServer from a NetworkBehaviour in the scene.
|
||||
{% endhint %}
|
||||
|
||||
### OnUnloadEnd
|
||||
|
||||
After scene unloading has occurred **OnUnloadEnd** is invoked. This event contains _QueueData_, and _UnloadedSceneHandles_. As before _QueueData_ contains information about the queue entry. _UnloadSceneHandles_ is a collection occupied with the handles of which scenes were unloaded.
|
||||
|
||||
### OnClientPresenceChangeStart, OnClientPresenceChangeEnd
|
||||
|
||||
These two events are only available on the server and indicate when a client is being added to a scene, or being removed from a scene. Both events are only invoked after the client has fully loaded or unloaded the scene. The **Start** variant is invoked before the [observer](../../../fishnet-building-blocks/components/network-observer.md) status has been updated for the client, and **End** after being updated. To clarify, if a client joins a scene **OnClientPresentStart** will call before the client has visibility of any networked content in the scene, and **OnClientPresentEnd** after the client has gained visibility.
|
||||
|
||||
Both events contain the same structure which has the following information: _Scene_, _Connection,_ and _Added._ _Scene_ is which scene the client is being added or removed from. _Connection_ is the NetworkConnection for the client. Lastly, _Added_ will be true if being added to the scene, or false is being removed.
|
||||
@@ -0,0 +1,140 @@
|
||||
---
|
||||
description: >-
|
||||
Scene Stacking is the ability for server or host to load multiple instances of
|
||||
the same scene at once, usually with different clients/observers in each
|
||||
scene.
|
||||
---
|
||||
|
||||
# Scene Stacking
|
||||
|
||||
## General
|
||||
|
||||
A good example of stacking scenes is having dungeon instances on a single server.\
|
||||
\
|
||||
If two clients went into the same dungeon, each client will load their own copy of that dungeon and have individual GameObjects, and state for those scenes. The Server however has two instances of that scene loaded at once. The Server having those two instances of the same scene loaded is called "Scene Stacking"\
|
||||
\
|
||||
Stacked scenes typically have different Clients observing each instance of the scene.
|
||||
|
||||
## Stacking scenes
|
||||
|
||||
### Loading into new stacked scene
|
||||
|
||||
* To stack scenes you must set _AllowStacking_ to true in your SceneLoadData's [**Options**](scene-data/sceneloaddata.md#options-1).
|
||||
* To create a new instance of a stacked scene the SceneLookupData must be populated by using the scene name. There is more information on this in the [SceneLookupData](scene-data/scenelookupdata.md) section.
|
||||
* Global Scenes cannot be stacked!
|
||||
|
||||
```csharp
|
||||
// Stacking a new Scene
|
||||
// Select Connections to load into new stacked scene.
|
||||
NetworkConnection[] conns = new NetworkConnection[]{connA,ConnB};
|
||||
|
||||
// You must use the scene name to stack scenes!
|
||||
SceneLoadData sld = new SceneLoadData("DungeonScene");
|
||||
|
||||
// Set AllowStacking Option to true.
|
||||
sld.Options.AllowStacking = true;
|
||||
|
||||
// Decide if you want seperate Physics for the scene.
|
||||
sld.Options.LocalPhysics = LocalPhysicsMode.Physics3D;
|
||||
|
||||
// load the Scene via connections, you cannot stack global scenes.
|
||||
base.SceneManager.LoadConnectionScene(conns,sld);
|
||||
```
|
||||
|
||||
### Loading into existing stacked scene
|
||||
|
||||
* If you were to load two connections into a scene by Scene reference or handle they will be added to the same scene, regardless if _AllowStacking_ is true or not.
|
||||
|
||||
This is identical to the examples given in the Loading Scenes Guide, on how to load into existing scenes. Review it [**here**](loading-scenes/#loading-existing-scenes).
|
||||
|
||||
## Separating physics
|
||||
|
||||
You may want to separate physics while stacking scenes. This ensures that the stacked scenes physics do not interact with each other. You will want to set the LocalPhysics option in the [**SceneLoadData**](scene-data/sceneloaddata.md)**.**\\
|
||||
|
||||
#### **LocalPhysicsMode.None**
|
||||
|
||||
This is the Default Option, Scene Physics will collide with other scenes in this state.
|
||||
|
||||
#### **LocalPhysicsMode.Physics2D**
|
||||
|
||||
A local 2D physics Scene will be created and owned by the Scene.
|
||||
|
||||
#### LocalPhysicsMode.Physics3D
|
||||
|
||||
A local 3D physics Scene will be created and owned by the Scene.
|
||||
|
||||
If you are to use separate physics scenes and you want to also simulate physics within them you must do so manually; this is intentional design.
|
||||
|
||||
Below is a script which you can place in your stacked physics scenes to also simulate physics along with the default physics scenes.
|
||||
|
||||
```csharp
|
||||
using FishNet.Object;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Synchronizes scene physics if not the default physics scene.
|
||||
/// </summary>
|
||||
public class PhysicsSceneSync : NetworkBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// True to synchronize physics 2d.
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
private bool _synchronizePhysics2D;
|
||||
/// <summary>
|
||||
/// True to synchronize physics 3d.
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
private bool _synchronizePhysics;
|
||||
/// <summary>
|
||||
/// Scenes which have physics handled by this script.
|
||||
/// </summary>
|
||||
private static HashSet<int> _synchronizedScenes = new HashSet<int>();
|
||||
|
||||
public override void OnStartNetwork()
|
||||
{
|
||||
/* If scene is already synchronized do not take action.
|
||||
* This means the script was added twice to the same scene. */
|
||||
int sceneHandle = gameObject.scene.handle;
|
||||
if (_synchronizedScenes.Contains(sceneHandle))
|
||||
return;
|
||||
|
||||
/* Set to synchronize the scene if either 2d or 3d
|
||||
* physics scene differ from the defaults. */
|
||||
_synchronizePhysics = (gameObject.scene.GetPhysicsScene() != Physics.defaultPhysicsScene);
|
||||
_synchronizePhysics2D = (gameObject.scene.GetPhysicsScene2D() != Physics2D.defaultPhysicsScene);
|
||||
|
||||
/* If to synchronize 2d or 3d manually then
|
||||
* register to pre physics simulation. */
|
||||
if (_synchronizePhysics || _synchronizePhysics2D)
|
||||
{
|
||||
_synchronizedScenes.Add(sceneHandle);
|
||||
base.TimeManager.OnPrePhysicsSimulation += TimeManager_OnPrePhysicsSimulation;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnStopNetwork()
|
||||
{
|
||||
// Check to unsubscribe.
|
||||
if (_synchronizePhysics || _synchronizePhysics2D)
|
||||
{
|
||||
_synchronizedScenes.Remove(gameObject.scene.handle);
|
||||
base.TimeManager.OnPrePhysicsSimulation -= TimeManager_OnPrePhysicsSimulation;
|
||||
}
|
||||
}
|
||||
|
||||
private void TimeManager_OnPrePhysicsSimulation(float delta)
|
||||
{
|
||||
/* If to simulate physics then do so on this objects
|
||||
* physics scene. If you know the object is not going to change
|
||||
* scenes you can cache the physics scenes
|
||||
* rather than look them up each time. */
|
||||
if (_synchronizePhysics)
|
||||
gameObject.scene.GetPhysicsScene().Simulate(delta);
|
||||
if (_synchronizePhysics2D)
|
||||
gameObject.scene.GetPhysicsScene2D().Simulate(delta);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
description: >-
|
||||
Scene Visibility offers details of using the "Scene Condition" with the
|
||||
ObserverManager, and how to manage Observers in a Scene.
|
||||
---
|
||||
|
||||
# Scene Visibility
|
||||
|
||||
## General
|
||||
|
||||
You can control if clients become an [**Observer**](../observers/) of gameobjects in a scene that they are in. This is possible if you set your [**ObserverManager**](../../../fishnet-building-blocks/components/managers/observermanager/) to include a **Scene Condition**.\
|
||||
\
|
||||
The **Scene Condition** ensures that[ **NetworkObjects**](../networked-gameobjects-and-scripts/networkobjects/) both scene and spawned are only visible to players in the same scene as the object. In most cases you will want to add a [NetworkObserver](../../../fishnet-building-blocks/components/network-observer.md) component, with a scene condition to your networked objects.
|
||||
|
||||
{% hint style="info" %}
|
||||
When encountering an error about being unable to find a NetworkObject or RPCLink during a scene change, you likely forgot to add the scene condition.
|
||||
{% endhint %}
|
||||
|
||||
## Managing visibility
|
||||
|
||||
### Initial scene load
|
||||
|
||||
When a client is loading into the game for the first time, the first scene/offline scene loaded is done so by the Unity Scene Manager. This means that clients were not automatically added to the scene when connected. Which also means they do not have visibility of that scene.
|
||||
|
||||
Once client connects to a host or server, If you use the **PlayerSpawner** component FishNet provided on the NetworkManager, there is logic in that component that will add the client to the default scene on the Player Objects spawn.
|
||||
|
||||
{% hint style="danger" %}
|
||||
If you decided not to use the **PlayerSpawner** Component provided on the NetworkManger in Fishnet, you will have to either have to load a client into a scene using FishNets SceneManager, or call SceneManger.AddOwnerToDefaultScene() on a Object that you gave that client ownership to.
|
||||
{% endhint %}
|
||||
|
||||
### Adding client connections to scenes
|
||||
|
||||
* When Loading a scene globally or by connection, the SceneManager will automatically place that connection into the scene.
|
||||
* Once added that client will be able to view all NetworkObjects that are apart of that scene.
|
||||
* You can add Client Connections to Scenes Manually if the scene is already loaded on the client, by using SceneManager.AddConnectionsToScene();
|
||||
|
||||
{% hint style="warning" %}
|
||||
Manually adding and removing client connections is recommended for Power Users only.
|
||||
{% endhint %}
|
||||
|
||||
### Removing client connections from scenes
|
||||
|
||||
* When Unloading a scene from a client, the server will automatically remove the client connection from the scene.
|
||||
* If you are the host, and you unload the hosts client from a scene, it will only remove the host from the scene and the hosts client will lose visibility. See Host Behaviour for [**Scene Caching**](scene-caching.md) for more details.
|
||||
* You can remove a client manually from a loaded scene, by using SceneManager.RemoveConnectionsFromScene();
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
description: Instructions for how to unload networked scenes in FishNet.
|
||||
---
|
||||
|
||||
# Unloading Scenes
|
||||
|
||||
## General
|
||||
|
||||
Unloading scenes is the same as loading scenes, except to call Unload rather than Load. Scenes can be unloaded by connection, or globally. When unloaded globally scenes will be unloaded for all players. When unloading by connection only the connections specified will unload the scenes.
|
||||
|
||||
## Unloading scenes
|
||||
|
||||
### Global scenes
|
||||
|
||||
* **Global Scenes** can be unloaded by calling UnloadGlobalScenes() in the SceneManager.
|
||||
* You cannot unload a global scene with UnloadConnectionScenes() method.
|
||||
|
||||
```csharp
|
||||
SceneUnloadData sud = new SceneUnloadData("Town");
|
||||
base.NetworkManager.SceneManager.UnloadGlobalScenes(sud);
|
||||
```
|
||||
|
||||
### Connection scenes
|
||||
|
||||
**Connection Scenes** follow the same principle, but has a few method overloads. You can unload scenes for a single connection, multiple connections at once, or unload scenes on the server.
|
||||
|
||||
* The Server will only Unload a connection scene on itself, if all connections have been unloaded from that scene.
|
||||
* If you wish to unload a scene and all connections, get all the connections in the scene and call unload with those connections. SceneManager.SceneConnections holds all online scenes and the connections in them.
|
||||
* If you wish to keep a scene loaded on server when unloading all Connections See [**Scene Caching**](scene-caching.md).
|
||||
|
||||
```csharp
|
||||
SceneUnloadData sud = new SceneUnloadData(new string[] { "Main", "Additive") });
|
||||
|
||||
// Unload scenes for a single connection.
|
||||
NetworkConnection conn = base.Owner;
|
||||
base.NetworkManager.SceneManager.UnloadConnectionScenes(conn, sud);
|
||||
|
||||
// Unload scenes for several connections at once.
|
||||
NetworkConnection[] conns = new NetworkConnection[] { connA, connB };
|
||||
base.NetworkManager.SceneManager.UnloadConnectionScenes(conns, sud);
|
||||
|
||||
// Unload scenes only on the server.
|
||||
// that you don't want all players in.
|
||||
base.NetworkManager.SceneManager.UnloadConnectionScenes(sud);
|
||||
```
|
||||
|
||||
## Advanced info
|
||||
|
||||
### Behind the "scenes"
|
||||
|
||||
The [**SceneManager**](../../../fishnet-building-blocks/components/managers/scenemanager.md) Class has very detailed XML comments on how the unload process works in detail, if you need to troubleshoot the scene unload process, these comments will help you understand the flow of how a scene loads.
|
||||
|
||||
### Events
|
||||
|
||||
Make sure to check out the [**Scene Events**](scene-events.md) that you can subscribe to to give better control over your game.
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: >-
|
||||
Find out how to know if your code is executing on the server or client, as
|
||||
well as how to identify clients from each other.
|
||||
---
|
||||
|
||||
# Server and Client Identification
|
||||
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
---
|
||||
description: >-
|
||||
How you can run certain code on only the server, or only a client, or only on
|
||||
a host.
|
||||
---
|
||||
|
||||
# Executing on Server or Client
|
||||
|
||||
## Overview
|
||||
|
||||
Very often your code will need to know if it's running on a server or a client, or [both at the same time](#user-content-fn-1)[^1]. There are several ways of knowing this, for example, code executed directly from client or server only events or methods, checking compiler directives to see the type of game build, marking methods to only allow running on a specific instance, or simply checking a Boolean value.
|
||||
|
||||
***
|
||||
|
||||
## Checking a Boolean
|
||||
|
||||
{% tabs %}
|
||||
{% tab title="Description" %}
|
||||
You can easily check if the current code is running on the server with a Boolean check. FishNet's [NetworkBehaviour](../networked-gameobjects-and-scripts/network-behaviour-guides.md) and [NetworkObject](../networked-gameobjects-and-scripts/networkobjects/) classes have easy to use properties for this exact reason. They are also accessible from the [NetworkManager](../../../fishnet-building-blocks/components/managers/network-manager.md) directly.
|
||||
|
||||
You can use `IsClientStarted` to detect if the instance running the code is a client (it could also be a server).
|
||||
|
||||
`IsClientOnlyStarted` can be used to detect if the instance running the code is a client only and not a server.
|
||||
|
||||
`IsServerStarted` detects if the instance running the code is the server (It could also be a client).
|
||||
|
||||
And finally, `IsServerOnlyStarted` returns whether the instance running the code is a server only and not a client.
|
||||
|
||||
When checking from a **NetworkBehaviour** or on a **NetworkObject** you will often want to know if the code is running on the client or server side AND the **NetworkObject** has already been initialized on the network and can be safely used for network functionality, such as RPCs or network properties. You can do that using the `IsServerInitialized`, `IsServerOnlyInitialized`, `IsClientInitialized`, and `IsClientOnlyInitialized` properties.
|
||||
|
||||
{% hint style="success" %}
|
||||
Instead of using something like `IsClientInitialized && IsServerInitialized` each time you want to check if the code is running on the host, you can simply check `IsHostInitialized`. There are _Started_, and _Only_ variations of this as well.
|
||||
{% endhint %}
|
||||
{% endtab %}
|
||||
|
||||
{% tab title="Example" %}
|
||||
Simple example showing how a NetworkBehaviour could prevent code from running on the server:
|
||||
|
||||
{% code title="Player.cs" %}
|
||||
```csharp
|
||||
using FishNet.Object;
|
||||
|
||||
public class Player : NetworkBehaviour
|
||||
{
|
||||
public void OnStruck()
|
||||
{
|
||||
if (!IsClientInitialized)
|
||||
return;
|
||||
|
||||
// Play visual effect and sounds only on a client.
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
{% endcode %}
|
||||
{% endtab %}
|
||||
{% endtabs %}
|
||||
|
||||
***
|
||||
|
||||
## Method attributes
|
||||
|
||||
There are a variety of attributes which can be used to ensure a method only runs on the appropriate game instance. Unlike properties, attributes are NetworkBehaviour-independent and can be used in any type of class or struct.
|
||||
|
||||
{% hint style="success" %}
|
||||
These methods are also used for Fish-Networking Pro's automatic code stripping. Importantly, methods marked with the `Server` attribute will have their method body stripped from non-server builds.
|
||||
{% endhint %}
|
||||
|
||||
### Client attribute
|
||||
|
||||
{% tabs %}
|
||||
{% tab title="Description" %}
|
||||
Placing a `Client` attribute above a method ensures that the method cannot be called unless the local client is active and the **NetworkObject** is initialized (the latter of which can be disabled by enabling the `UseIsStarted` option). There are additional properties which may be added to the attribute for additional functionality, such as for disabling the warning that would be logged if you try to call the method with this and it doesn't pass the check.
|
||||
{% endtab %}
|
||||
|
||||
{% tab title="Example" %}
|
||||
Simple example showing the `Client` attribute to prevent the method from running when not a client.
|
||||
|
||||
```csharp
|
||||
[Client]
|
||||
private void ShowUI()
|
||||
{
|
||||
// This code will only run on a client, otherwise it will print a warning.
|
||||
}
|
||||
```
|
||||
|
||||
And following is an example of the options you can use with it:
\
|
||||
If this method were called with no client active a warning would be printed and the method would not run. You can change the logging type using the `Logging` property to change whether it should log a warning at all or not. The default value is `LoggingType.Warning`.
|
||||
|
||||
It's also possible to allow only the owner of the object to call the method by setting `RequireOwnership` to true; this value is false by default.
|
||||
|
||||
Finally, you can use the `UseIsStarted` property to cause this to ignore the initialized state of the **NetworkObject** and check if the client alone is started.
|
||||
|
||||
```csharp
|
||||
// The server does not need to play VFX; only play VFX if the client is active.
|
||||
[Client(Logging = LoggingType.Off, RequireOwnership = true, UseIsStarted = true)]
|
||||
private void PlayVFX()
|
||||
{
|
||||
// Play VFX here...
|
||||
}
|
||||
```
|
||||
{% endtab %}
|
||||
{% endtabs %}
|
||||
|
||||
### Server attribute
|
||||
|
||||
{% tabs %}
|
||||
{% tab title="Description" %}
|
||||
The `Server` attribute provides similar features of the `Client` variant, except it will not allow the method to run if the **server** is not active and the **NetworkObject** is initialized (the latter of which can be disabled by enabling the `UseIsStarted` option). There are additional properties which may be added to the attribute for additional functionality, such as for disabling the warning that would be logged if you try to call the method with this and it doesn't pass the check.
|
||||
{% endtab %}
|
||||
|
||||
{% tab title="Example" %}
|
||||
Here is an example of the `Server` attribute being used to prevent code from running anywhere except the server.
|
||||
|
||||
Like the Client attribute, a warning will be thrown if this is called while the server is not active.
\
|
||||
The warning can be changed or disabled by adjusting the `Logging` property.
|
||||
|
||||
You can use the `UseIsStarted` property to cause this to ignore the initialized state of the **NetworkObject** and only check if the server is started.
|
||||
|
||||
```csharp
|
||||
// The server would validate hit results from a client.
|
||||
[Server(Logging = LoggingType.Off, UseIsStarted = false)]
|
||||
private void ValidateHit()
|
||||
{
|
||||
// Hit validation code here that will only run on the server.
|
||||
}
|
||||
```
|
||||
{% endtab %}
|
||||
{% endtabs %}
|
||||
|
||||
[^1]: This is what we refer to as a host.
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
---
|
||||
description: A look at how Fish-Networking distinguishes clients from one another.
|
||||
---
|
||||
|
||||
# NetworkConnections
|
||||
|
||||
## NetworkConnections
|
||||
|
||||
A **NetworkConnection** in FishNet is a core object that represents a single connected client within the networking system. As soon as a client connects to the server, a NetworkConnection is created to represent it, perform actions on it, and gather information about that specific client.
|
||||
|
||||
### Purposes
|
||||
|
||||
* **Client representation:**\
|
||||
Each connected player/client has a corresponding NetworkConnection instance. This object tracks the client's identity, state, and the objects it owns.
|
||||
* **Ownership and authority:**\
|
||||
NetworkConnection tracks which networked objects (such as player avatars) the client owns, enabling authority checks and state management.
|
||||
* **Authentication and state:**\
|
||||
It manages authentication state, and notes if it’s disconnecting.
|
||||
* **Scene tracking**\
|
||||
It is used to track which networked scenes the client has loaded into.
|
||||
* **Events:**\
|
||||
It provides events for actions like gaining or losing ownership of objects and loading start scenes.
|
||||
* **Custom data:**\
|
||||
Developers can associate custom data with a connection for gameplay or server logic. (This data is not automatically synchronized across the network.)
|
||||
* **Disconnection:**\
|
||||
Provides methods to disconnect the client, either immediately or after sending pending data.
|
||||
|
||||
***
|
||||
|
||||
### Important fields and properties
|
||||
|
||||
**ClientId** — One important field is the [`ClientId`](https://fish-networking.com/FishNet/api/api/FishNet.Connection.NetworkConnection.html#FishNet_Connection_NetworkConnection_ClientId). This is a unique ID for the NetworkConnection and it is used in a few other places in Fish-Networking. If a ClientId is not set, it defaults to -1, and generally ClientIds increment from 0 until the maximum integer value before reusing old ones from the beginning.
|
||||
|
||||
**FirstObject** — FishNet doesn't have a forced Player GameObject, but you can get the first object owned by a client with the **FirstObject** property. You can also set it with the `SetFirstObject` method, which can be useful if you have the object destroyed and re-instantiated.
|
||||
|
||||
**Objects** — This field is a HashSet of all the network objects owned by this connection. It is available to this connection and server.
|
||||
|
||||
**Scenes** — These are the networked scenes this connection is considered a part of.
|
||||
|
||||
**CustomData** — As mentioned above, this is a user-definable object for storing arbitrary data on the connection. It is not synced at all.
|
||||
|
||||
{% hint style="success" %}
|
||||
Many more useful fields, properties, and methods are available and can be found in the [API page](https://fish-networking.com/FishNet/api/api/FishNet.Connection.NetworkConnection.html).
|
||||
{% endhint %}
|
||||
|
||||
***
|
||||
|
||||
### Where to get them and where to use them?
|
||||
|
||||
#### How is it created/used?
|
||||
|
||||
* Each client has a NetworkConnection representing itself, this can be accessed with `ClientManager.Connection`, or `LocalConnecton` from within a NetworkBehaviour.
|
||||
* The server maintains a dictionary of NetworkConnection instances for all connected clients, this is available at `ServerManager.Clients`. It is also available on clients under the `ClientManager.Clients`. You should use the ServerManager version if the instance is running as a server. The dictionary key is the Client ID and the value is the NetworkConnection.
|
||||
* NetworkObjects can have an owner, which is a NetworkConnection that has authority over the object. From the NetworkObject or NetworkBehaviour you can get the NetworkConnection with the `Owner` property.
|
||||
* When calling [TargetRpcs](../network-communication/remote-procedure-calls.md) you will need to provide a NetworkConnection as the first argument of the method, this represents which client you will send the RPC to.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
description: >-
|
||||
Some transports offer features additional to what is configured within the
|
||||
inspector. Within this section are guides on using unique features of each
|
||||
official transport.
|
||||
---
|
||||
|
||||
# Transports
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Multipass
|
||||
|
||||
{% hint style="info" %}
|
||||
You may want to access specific transports directly. You can do so by getting a reference to Multipass and calling multipassReference.GetTransport\<Type>().
|
||||
|
||||
The Transports collection is also publicly accessible within Multipass.
|
||||
{% endhint %}
|
||||
|
||||
## Setup <a href="#guide-setup" id="guide-setup"></a>
|
||||
|
||||
Like other transports to use Multipass it must be added to a gameObject, generally your NetworkManager object. After adding Multipass you must assign it as the transport to use within [TransportManager](../../../fishnet-building-blocks/components/managers/transportmanager/). If TransportManager is not added to your NetworkManager automatically; you may need to add the component first. Once Multipass is added and specified in your TransportManager add whichever other transports you wish to support. Drag the added reference of each transport to **Transports** under Multipass.
|
||||
|
||||
{% hint style="warning" %}
|
||||
Be sure that each transport within Multipass is listening on a different port.
|
||||
{% endhint %}
|
||||
|
||||

|
||||
|
||||
## Client
|
||||
|
||||
A client may only use one transport within Multipass, while the server may listen on all the transports at once.
|
||||
|
||||
For client actions to work you must specify which transport to use for the client.
|
||||
|
||||
```csharp
|
||||
// This can be done easily using the TransportManager.
|
||||
Multipass mp = transportManager.GetTransport<Multipass>();
|
||||
|
||||
// In this example if the build is a webGL build
|
||||
// then use Bayou, otherwise use Tugboat.
|
||||
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
mp.SetClientTransport<Bayou>();
|
||||
#else
|
||||
mp.SetClientTransport<Tugboat>();
|
||||
#endif
|
||||
```
|
||||
|
||||
The example above shows using Bayou and Tugboat, but you may of course use your own logic for any number of transports.
|
||||
|
||||
Once a client transport is set you may perform functions normally as if you were using only one transport. An example of this would be: ClientManager.StartConnection().
|
||||
|
||||
## Server
|
||||
|
||||
As covered in the [component settings](../../../fishnet-building-blocks/transports/multipass.md#component-settings) _GlobalServerActions_ will execute server actions on all transports specified within Multipass. If you were to call ServerManager.StartConnection() while _GlobalServerActions_ is true then the server would start on all transports; otherwise an error will be thrown.
|
||||
|
||||
```csharp
|
||||
// Start all servers transports. This requires GlobalServerActions to be true.
|
||||
serverManagerReference.StartConnection();
|
||||
```
|
||||
|
||||
You may also access functions on a single transport by specifying the index of which transport to use.
|
||||
|
||||
```csharp
|
||||
// Start the connection on index 0.
|
||||
Multipass mp = TransportManager.GetTransport<Multipass>();
|
||||
// The true parameter is to indicate you are starting a server.
|
||||
mp.StartConnection(true, 0);
|
||||
```
|
||||
Reference in New Issue
Block a user