新增FishNet文档
This commit is contained in:
@@ -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;
|
||||
```
|
||||
Reference in New Issue
Block a user