alap
This commit is contained in:
74
.gitignore
vendored
Normal file
74
.gitignore
vendored
Normal file
@ -0,0 +1,74 @@
|
||||
# This .gitignore file should be placed at the root of your Unity project directory
|
||||
#
|
||||
# Get latest from https://github.com/github/gitignore/blob/main/Unity.gitignore
|
||||
#
|
||||
.utmp/
|
||||
/[Ll]ibrary/
|
||||
/[Tt]emp/
|
||||
/[Oo]bj/
|
||||
/[Bb]uild/
|
||||
/[Bb]uilds/
|
||||
/[Ll]ogs/
|
||||
/[Uu]ser[Ss]ettings/
|
||||
|
||||
# MemoryCaptures can get excessive in size.
|
||||
# They also could contain extremely sensitive data
|
||||
/[Mm]emoryCaptures/
|
||||
|
||||
# Recordings can get excessive in size
|
||||
/[Rr]ecordings/
|
||||
|
||||
# Uncomment this line if you wish to ignore the asset store tools plugin
|
||||
# /[Aa]ssets/AssetStoreTools*
|
||||
|
||||
# Autogenerated Jetbrains Rider plugin
|
||||
/[Aa]ssets/Plugins/Editor/JetBrains*
|
||||
|
||||
# Visual Studio cache directory
|
||||
.vs/
|
||||
|
||||
# Gradle cache directory
|
||||
.gradle/
|
||||
|
||||
# Autogenerated VS/MD/Consulo solution and project files
|
||||
ExportedObj/
|
||||
.consulo/
|
||||
*.csproj
|
||||
*.unityproj
|
||||
*.sln
|
||||
*.suo
|
||||
*.tmp
|
||||
*.user
|
||||
*.userprefs
|
||||
*.pidb
|
||||
*.booproj
|
||||
*.svd
|
||||
*.pdb
|
||||
*.mdb
|
||||
*.opendb
|
||||
*.VC.db
|
||||
|
||||
# Unity3D generated meta files
|
||||
*.pidb.meta
|
||||
*.pdb.meta
|
||||
*.mdb.meta
|
||||
|
||||
# Unity3D generated file on crash reports
|
||||
sysinfo.txt
|
||||
|
||||
# Builds
|
||||
*.apk
|
||||
*.aab
|
||||
*.unitypackage
|
||||
*.unitypackage.meta
|
||||
*.app
|
||||
|
||||
# Crashlytics generated file
|
||||
crashlytics-build.properties
|
||||
|
||||
# Packed Addressables
|
||||
/[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin*
|
||||
|
||||
# Temporary auto-generated Android Assets
|
||||
/[Aa]ssets/[Ss]treamingAssets/aa.meta
|
||||
/[Aa]ssets/[Ss]treamingAssets/aa/*
|
5
.vscode/extensions.json
vendored
Normal file
5
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"visualstudiotoolsforunity.vstuc"
|
||||
]
|
||||
}
|
10
.vscode/launch.json
vendored
Normal file
10
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Attach to Unity",
|
||||
"type": "vstuc",
|
||||
"request": "attach"
|
||||
}
|
||||
]
|
||||
}
|
55
.vscode/settings.json
vendored
Normal file
55
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"files.exclude": {
|
||||
"**/.DS_Store": true,
|
||||
"**/.git": true,
|
||||
"**/.gitmodules": true,
|
||||
"**/*.booproj": true,
|
||||
"**/*.pidb": true,
|
||||
"**/*.suo": true,
|
||||
"**/*.user": true,
|
||||
"**/*.userprefs": true,
|
||||
"**/*.unityproj": true,
|
||||
"**/*.dll": true,
|
||||
"**/*.exe": true,
|
||||
"**/*.pdf": true,
|
||||
"**/*.mid": true,
|
||||
"**/*.midi": true,
|
||||
"**/*.wav": true,
|
||||
"**/*.gif": true,
|
||||
"**/*.ico": true,
|
||||
"**/*.jpg": true,
|
||||
"**/*.jpeg": true,
|
||||
"**/*.png": true,
|
||||
"**/*.psd": true,
|
||||
"**/*.tga": true,
|
||||
"**/*.tif": true,
|
||||
"**/*.tiff": true,
|
||||
"**/*.3ds": true,
|
||||
"**/*.3DS": true,
|
||||
"**/*.fbx": true,
|
||||
"**/*.FBX": true,
|
||||
"**/*.lxo": true,
|
||||
"**/*.LXO": true,
|
||||
"**/*.ma": true,
|
||||
"**/*.MA": true,
|
||||
"**/*.obj": true,
|
||||
"**/*.OBJ": true,
|
||||
"**/*.asset": true,
|
||||
"**/*.cubemap": true,
|
||||
"**/*.flare": true,
|
||||
"**/*.mat": true,
|
||||
"**/*.meta": true,
|
||||
"**/*.prefab": true,
|
||||
"**/*.unity": true,
|
||||
"build/": true,
|
||||
"Build/": true,
|
||||
"Library/": true,
|
||||
"library/": true,
|
||||
"obj/": true,
|
||||
"Obj/": true,
|
||||
"ProjectSettings/": true,
|
||||
"temp/": true,
|
||||
"Temp/": true
|
||||
},
|
||||
"dotnet.defaultSolution": "multiplayer-game.sln"
|
||||
}
|
6
.vsconfig
Normal file
6
.vsconfig
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"components": [
|
||||
"Microsoft.VisualStudio.Workload.ManagedGame"
|
||||
]
|
||||
}
|
15
Assets/DefaultVolumeProfile.asset
Normal file
15
Assets/DefaultVolumeProfile.asset
Normal file
@ -0,0 +1,15 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d7fd9488000d3734a9e00ee676215985, type: 3}
|
||||
m_Name: DefaultVolumeProfile
|
||||
m_EditorClassIdentifier:
|
||||
components: []
|
8
Assets/DefaultVolumeProfile.asset.meta
Normal file
8
Assets/DefaultVolumeProfile.asset.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4d700d2576e8690408c7f734564d78f7
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/Materials.meta
Normal file
8
Assets/Materials.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 38871691704490b4f8b7f99ba454f9c1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
135
Assets/Materials/black.mat
Normal file
135
Assets/Materials/black.mat
Normal file
@ -0,0 +1,135 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &-5298968235196956465
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
version: 9
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: black
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 0
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _Smoothness: 0
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 0.122641504, g: 0.122641504, b: 0.122641504, a: 1}
|
||||
- _Color: {r: 0.12264148, g: 0.12264148, b: 0.12264148, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
8
Assets/Materials/black.mat.meta
Normal file
8
Assets/Materials/black.mat.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7ae0124944d0364984ad3872a6720f9
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/Mirror.meta
Normal file
8
Assets/Mirror.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5cf8eb36be0834b3da408c694a41cb88
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/Mirror/Authenticators.meta
Normal file
8
Assets/Mirror/Authenticators.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1b2f9d254154cd942ba40b06b869b8f3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
192
Assets/Mirror/Authenticators/BasicAuthenticator.cs
Normal file
192
Assets/Mirror/Authenticators/BasicAuthenticator.cs
Normal file
@ -0,0 +1,192 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Authenticators
|
||||
{
|
||||
[AddComponentMenu("Network/ Authenticators/Basic Authenticator")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-authenticators/basic-authenticator")]
|
||||
public class BasicAuthenticator : NetworkAuthenticator
|
||||
{
|
||||
[Header("Server Credentials")]
|
||||
public string serverUsername;
|
||||
public string serverPassword;
|
||||
|
||||
[Header("Client Credentials")]
|
||||
public string username;
|
||||
public string password;
|
||||
|
||||
readonly HashSet<NetworkConnection> connectionsPendingDisconnect = new HashSet<NetworkConnection>();
|
||||
|
||||
#region Messages
|
||||
|
||||
public struct AuthRequestMessage : NetworkMessage
|
||||
{
|
||||
// use whatever credentials make sense for your game
|
||||
// for example, you might want to pass the accessToken if using oauth
|
||||
public string authUsername;
|
||||
public string authPassword;
|
||||
}
|
||||
|
||||
public struct AuthResponseMessage : NetworkMessage
|
||||
{
|
||||
public byte code;
|
||||
public string message;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Server
|
||||
|
||||
/// <summary>
|
||||
/// Called on server from StartServer to initialize the Authenticator
|
||||
/// <para>Server message handlers should be registered in this method.</para>
|
||||
/// </summary>
|
||||
public override void OnStartServer()
|
||||
{
|
||||
// register a handler for the authentication request we expect from client
|
||||
NetworkServer.RegisterHandler<AuthRequestMessage>(OnAuthRequestMessage, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on server from StopServer to reset the Authenticator
|
||||
/// <para>Server message handlers should be unregistered in this method.</para>
|
||||
/// </summary>
|
||||
public override void OnStopServer()
|
||||
{
|
||||
// unregister the handler for the authentication request
|
||||
NetworkServer.UnregisterHandler<AuthRequestMessage>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on server from OnServerConnectInternal when a client needs to authenticate
|
||||
/// </summary>
|
||||
/// <param name="conn">Connection to client.</param>
|
||||
public override void OnServerAuthenticate(NetworkConnectionToClient conn)
|
||||
{
|
||||
// do nothing...wait for AuthRequestMessage from client
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on server when the client's AuthRequestMessage arrives
|
||||
/// </summary>
|
||||
/// <param name="conn">Connection to client.</param>
|
||||
/// <param name="msg">The message payload</param>
|
||||
public void OnAuthRequestMessage(NetworkConnectionToClient conn, AuthRequestMessage msg)
|
||||
{
|
||||
//Debug.Log($"Authentication Request: {msg.authUsername} {msg.authPassword}");
|
||||
|
||||
if (connectionsPendingDisconnect.Contains(conn)) return;
|
||||
|
||||
// check the credentials by calling your web server, database table, playfab api, or any method appropriate.
|
||||
if (msg.authUsername == serverUsername && msg.authPassword == serverPassword)
|
||||
{
|
||||
// create and send msg to client so it knows to proceed
|
||||
AuthResponseMessage authResponseMessage = new AuthResponseMessage
|
||||
{
|
||||
code = 100,
|
||||
message = "Success"
|
||||
};
|
||||
|
||||
conn.Send(authResponseMessage);
|
||||
|
||||
// Accept the successful authentication
|
||||
ServerAccept(conn);
|
||||
}
|
||||
else
|
||||
{
|
||||
connectionsPendingDisconnect.Add(conn);
|
||||
|
||||
// create and send msg to client so it knows to disconnect
|
||||
AuthResponseMessage authResponseMessage = new AuthResponseMessage
|
||||
{
|
||||
code = 200,
|
||||
message = "Invalid Credentials"
|
||||
};
|
||||
|
||||
conn.Send(authResponseMessage);
|
||||
|
||||
// must set NetworkConnection isAuthenticated = false
|
||||
conn.isAuthenticated = false;
|
||||
|
||||
// disconnect the client after 1 second so that response message gets delivered
|
||||
StartCoroutine(DelayedDisconnect(conn, 1f));
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator DelayedDisconnect(NetworkConnectionToClient conn, float waitTime)
|
||||
{
|
||||
yield return new WaitForSeconds(waitTime);
|
||||
|
||||
// Reject the unsuccessful authentication
|
||||
ServerReject(conn);
|
||||
|
||||
yield return null;
|
||||
|
||||
// remove conn from pending connections
|
||||
connectionsPendingDisconnect.Remove(conn);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Client
|
||||
|
||||
/// <summary>
|
||||
/// Called on client from StartClient to initialize the Authenticator
|
||||
/// <para>Client message handlers should be registered in this method.</para>
|
||||
/// </summary>
|
||||
public override void OnStartClient()
|
||||
{
|
||||
// register a handler for the authentication response we expect from server
|
||||
NetworkClient.RegisterHandler<AuthResponseMessage>(OnAuthResponseMessage, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on client from StopClient to reset the Authenticator
|
||||
/// <para>Client message handlers should be unregistered in this method.</para>
|
||||
/// </summary>
|
||||
public override void OnStopClient()
|
||||
{
|
||||
// unregister the handler for the authentication response
|
||||
NetworkClient.UnregisterHandler<AuthResponseMessage>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on client from OnClientConnectInternal when a client needs to authenticate
|
||||
/// </summary>
|
||||
public override void OnClientAuthenticate()
|
||||
{
|
||||
AuthRequestMessage authRequestMessage = new AuthRequestMessage
|
||||
{
|
||||
authUsername = username,
|
||||
authPassword = password
|
||||
};
|
||||
|
||||
NetworkClient.Send(authRequestMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on client when the server's AuthResponseMessage arrives
|
||||
/// </summary>
|
||||
/// <param name="msg">The message payload</param>
|
||||
public void OnAuthResponseMessage(AuthResponseMessage msg)
|
||||
{
|
||||
if (msg.code == 100)
|
||||
{
|
||||
//Debug.Log($"Authentication Response: {msg.message}");
|
||||
|
||||
// Authentication has been accepted
|
||||
ClientAccept();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"Authentication Response: {msg.message}");
|
||||
|
||||
// Authentication has been rejected
|
||||
ClientReject();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
11
Assets/Mirror/Authenticators/BasicAuthenticator.cs.meta
Normal file
11
Assets/Mirror/Authenticators/BasicAuthenticator.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 28496b776660156428f00cf78289c1ec
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
129
Assets/Mirror/Authenticators/DeviceAuthenticator.cs
Normal file
129
Assets/Mirror/Authenticators/DeviceAuthenticator.cs
Normal file
@ -0,0 +1,129 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Authenticators
|
||||
{
|
||||
/// <summary>
|
||||
/// An authenticator that identifies the user by their device.
|
||||
/// <para>A GUID is used as a fallback when the platform doesn't support SystemInfo.deviceUniqueIdentifier.</para>
|
||||
/// <para>Note: deviceUniqueIdentifier can be spoofed, so security is not guaranteed.</para>
|
||||
/// <para>See https://docs.unity3d.com/ScriptReference/SystemInfo-deviceUniqueIdentifier.html for details.</para>
|
||||
/// </summary>
|
||||
[AddComponentMenu("Network/ Authenticators/Device Authenticator")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-authenticators/device-authenticator")]
|
||||
public class DeviceAuthenticator : NetworkAuthenticator
|
||||
{
|
||||
#region Messages
|
||||
|
||||
public struct AuthRequestMessage : NetworkMessage
|
||||
{
|
||||
public string clientDeviceID;
|
||||
}
|
||||
|
||||
public struct AuthResponseMessage : NetworkMessage { }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Server
|
||||
|
||||
/// <summary>
|
||||
/// Called on server from StartServer to initialize the Authenticator
|
||||
/// <para>Server message handlers should be registered in this method.</para>
|
||||
/// </summary>
|
||||
public override void OnStartServer()
|
||||
{
|
||||
// register a handler for the authentication request we expect from client
|
||||
NetworkServer.RegisterHandler<AuthRequestMessage>(OnAuthRequestMessage, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on server from StopServer to reset the Authenticator
|
||||
/// <para>Server message handlers should be registered in this method.</para>
|
||||
/// </summary>
|
||||
public override void OnStopServer()
|
||||
{
|
||||
// unregister the handler for the authentication request
|
||||
NetworkServer.UnregisterHandler<AuthRequestMessage>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on server from OnServerConnectInternal when a client needs to authenticate
|
||||
/// </summary>
|
||||
/// <param name="conn">Connection to client.</param>
|
||||
public override void OnServerAuthenticate(NetworkConnectionToClient conn)
|
||||
{
|
||||
// do nothing, wait for client to send his id
|
||||
}
|
||||
|
||||
void OnAuthRequestMessage(NetworkConnectionToClient conn, AuthRequestMessage msg)
|
||||
{
|
||||
Debug.Log($"connection {conn.connectionId} authenticated with id {msg.clientDeviceID}");
|
||||
|
||||
// Store the device id for later reference, e.g. when spawning the player
|
||||
conn.authenticationData = msg.clientDeviceID;
|
||||
|
||||
// Send a response to client telling it to proceed as authenticated
|
||||
conn.Send(new AuthResponseMessage());
|
||||
|
||||
// Accept the successful authentication
|
||||
ServerAccept(conn);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Client
|
||||
|
||||
/// <summary>
|
||||
/// Called on client from StartClient to initialize the Authenticator
|
||||
/// <para>Client message handlers should be registered in this method.</para>
|
||||
/// </summary>
|
||||
public override void OnStartClient()
|
||||
{
|
||||
// register a handler for the authentication response we expect from server
|
||||
NetworkClient.RegisterHandler<AuthResponseMessage>(OnAuthResponseMessage, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on client from StopClient to reset the Authenticator
|
||||
/// <para>Client message handlers should be unregistered in this method.</para>
|
||||
/// </summary>
|
||||
public override void OnStopClient()
|
||||
{
|
||||
// unregister the handler for the authentication response
|
||||
NetworkClient.UnregisterHandler<AuthResponseMessage>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on client from OnClientConnectInternal when a client needs to authenticate
|
||||
/// </summary>
|
||||
public override void OnClientAuthenticate()
|
||||
{
|
||||
string deviceUniqueIdentifier = SystemInfo.deviceUniqueIdentifier;
|
||||
|
||||
// Not all platforms support this, so we use a GUID instead
|
||||
if (deviceUniqueIdentifier == SystemInfo.unsupportedIdentifier)
|
||||
{
|
||||
// Get the value from PlayerPrefs if it exists, new GUID if it doesn't
|
||||
deviceUniqueIdentifier = PlayerPrefs.GetString("deviceUniqueIdentifier", Guid.NewGuid().ToString());
|
||||
|
||||
// Store the deviceUniqueIdentifier to PlayerPrefs (in case we just made a new GUID)
|
||||
PlayerPrefs.SetString("deviceUniqueIdentifier", deviceUniqueIdentifier);
|
||||
}
|
||||
|
||||
// send the deviceUniqueIdentifier to the server
|
||||
NetworkClient.Send(new AuthRequestMessage { clientDeviceID = deviceUniqueIdentifier } );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on client when the server's AuthResponseMessage arrives
|
||||
/// </summary>
|
||||
/// <param name="msg">The message payload</param>
|
||||
public void OnAuthResponseMessage(AuthResponseMessage msg)
|
||||
{
|
||||
Debug.Log("Authentication Success");
|
||||
ClientAccept();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
11
Assets/Mirror/Authenticators/DeviceAuthenticator.cs.meta
Normal file
11
Assets/Mirror/Authenticators/DeviceAuthenticator.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 60960a6ba81a842deb2fdcdc93788242
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
16
Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef
Normal file
16
Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "Mirror.Authenticators",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"GUID:30817c1a0e6d646d99c048fc403f5979"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e720aa64e3f58fb4880566a322584340
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
70
Assets/Mirror/Authenticators/TimeoutAuthenticator.cs
Normal file
70
Assets/Mirror/Authenticators/TimeoutAuthenticator.cs
Normal file
@ -0,0 +1,70 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Authenticators
|
||||
{
|
||||
/// <summary>
|
||||
/// An authenticator that disconnects connections if they don't
|
||||
/// authenticate within a specified time limit.
|
||||
/// </summary>
|
||||
[AddComponentMenu("Network/ Authenticators/Timeout Authenticator")]
|
||||
public class TimeoutAuthenticator : NetworkAuthenticator
|
||||
{
|
||||
public NetworkAuthenticator authenticator;
|
||||
|
||||
[Range(0, 600), Tooltip("Timeout to auto-disconnect in seconds. Set to 0 for no timeout.")]
|
||||
public float timeout = 60;
|
||||
|
||||
public void Awake()
|
||||
{
|
||||
authenticator.OnServerAuthenticated.AddListener(connection => OnServerAuthenticated.Invoke(connection));
|
||||
authenticator.OnClientAuthenticated.AddListener(OnClientAuthenticated.Invoke);
|
||||
}
|
||||
|
||||
public override void OnStartServer()
|
||||
{
|
||||
authenticator.OnStartServer();
|
||||
}
|
||||
|
||||
public override void OnStopServer()
|
||||
{
|
||||
authenticator.OnStopServer();
|
||||
}
|
||||
|
||||
public override void OnStartClient()
|
||||
{
|
||||
authenticator.OnStartClient();
|
||||
}
|
||||
|
||||
public override void OnStopClient()
|
||||
{
|
||||
authenticator.OnStopClient();
|
||||
}
|
||||
|
||||
public override void OnServerAuthenticate(NetworkConnectionToClient conn)
|
||||
{
|
||||
authenticator.OnServerAuthenticate(conn);
|
||||
if (timeout > 0)
|
||||
StartCoroutine(BeginAuthentication(conn));
|
||||
}
|
||||
|
||||
public override void OnClientAuthenticate()
|
||||
{
|
||||
authenticator.OnClientAuthenticate();
|
||||
if (timeout > 0)
|
||||
StartCoroutine(BeginAuthentication(NetworkClient.connection));
|
||||
}
|
||||
|
||||
IEnumerator BeginAuthentication(NetworkConnection conn)
|
||||
{
|
||||
//Debug.Log($"Authentication countdown started {conn} {timeout}");
|
||||
yield return new WaitForSecondsRealtime(timeout);
|
||||
|
||||
if (!conn.isAuthenticated)
|
||||
{
|
||||
Debug.LogError($"Authentication Timeout - Disconnecting {conn}");
|
||||
conn.Disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Authenticators/TimeoutAuthenticator.cs.meta
Normal file
11
Assets/Mirror/Authenticators/TimeoutAuthenticator.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 24d8269a07b8e4edfa374753a91c946e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/Mirror/CompilerSymbols.meta
Normal file
8
Assets/Mirror/CompilerSymbols.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1f8b918bcd89f5c488b06f5574f34760
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
14
Assets/Mirror/CompilerSymbols/Mirror.CompilerSymbols.asmdef
Normal file
14
Assets/Mirror/CompilerSymbols/Mirror.CompilerSymbols.asmdef
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "Mirror.CompilerSymbols",
|
||||
"references": [],
|
||||
"optionalUnityReferences": [],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": []
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 325984b52e4128546bc7558552f8b1d2
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
49
Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs
Normal file
49
Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
static class PreprocessorDefine
|
||||
{
|
||||
/// <summary>
|
||||
/// Add define symbols as soon as Unity gets done compiling.
|
||||
/// </summary>
|
||||
[InitializeOnLoadMethod]
|
||||
public static void AddDefineSymbols()
|
||||
{
|
||||
#if UNITY_2021_2_OR_NEWER
|
||||
string currentDefines = PlayerSettings.GetScriptingDefineSymbols(UnityEditor.Build.NamedBuildTarget.FromBuildTargetGroup(EditorUserBuildSettings.selectedBuildTargetGroup));
|
||||
#else
|
||||
// Deprecated in Unity 2023.1
|
||||
string currentDefines = PlayerSettings.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup);
|
||||
#endif
|
||||
// Remove oldest when adding next month's symbol.
|
||||
// Keep a rolling 12 months of symbols.
|
||||
HashSet<string> defines = new HashSet<string>(currentDefines.Split(';'))
|
||||
{
|
||||
"MIRROR",
|
||||
"MIRROR_79_OR_NEWER",
|
||||
"MIRROR_81_OR_NEWER",
|
||||
"MIRROR_82_OR_NEWER",
|
||||
"MIRROR_83_OR_NEWER",
|
||||
"MIRROR_84_OR_NEWER",
|
||||
"MIRROR_85_OR_NEWER",
|
||||
"MIRROR_86_OR_NEWER",
|
||||
"MIRROR_89_OR_NEWER"
|
||||
};
|
||||
|
||||
// only touch PlayerSettings if we actually modified it,
|
||||
// otherwise it shows up as changed in git each time.
|
||||
string newDefines = string.Join(";", defines);
|
||||
if (newDefines != currentDefines)
|
||||
{
|
||||
#if UNITY_2021_2_OR_NEWER
|
||||
PlayerSettings.SetScriptingDefineSymbols(UnityEditor.Build.NamedBuildTarget.FromBuildTargetGroup(EditorUserBuildSettings.selectedBuildTargetGroup), newDefines);
|
||||
#else
|
||||
// Deprecated in Unity 2023.1
|
||||
PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup, newDefines);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs.meta
Normal file
11
Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f1d66fe74ec6f42dd974cba37d25d453
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/Mirror/Components.meta
Normal file
8
Assets/Mirror/Components.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9bee879fbc8ef4b1a9a9f7088bfbf726
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
12
Assets/Mirror/Components/AssemblyInfo.cs
Normal file
12
Assets/Mirror/Components/AssemblyInfo.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Common")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests")]
|
||||
// need to use Unity.*.CodeGen assembly name to import Unity.CompilationPipeline
|
||||
// for ILPostProcessor tests.
|
||||
[assembly: InternalsVisibleTo("Unity.Mirror.Tests.CodeGen")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Generated")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Runtime")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Performance.Editor")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Performance.Runtime")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Editor")]
|
11
Assets/Mirror/Components/AssemblyInfo.cs.meta
Normal file
11
Assets/Mirror/Components/AssemblyInfo.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a65b9283f7a724e70b8e17cb277f4c1e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/Mirror/Components/Discovery.meta
Normal file
8
Assets/Mirror/Components/Discovery.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5dcf9618f5e14a4eb60bff5480284a6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
93
Assets/Mirror/Components/Discovery/NetworkDiscovery.cs
Normal file
93
Assets/Mirror/Components/Discovery/NetworkDiscovery.cs
Normal file
@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace Mirror.Discovery
|
||||
{
|
||||
[Serializable]
|
||||
public class ServerFoundUnityEvent<TResponseType> : UnityEvent<TResponseType> {};
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/Network Discovery")]
|
||||
public class NetworkDiscovery : NetworkDiscoveryBase<ServerRequest, ServerResponse>
|
||||
{
|
||||
#region Server
|
||||
|
||||
/// <summary>
|
||||
/// Process the request from a client
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Override if you wish to provide more information to the clients
|
||||
/// such as the name of the host player
|
||||
/// </remarks>
|
||||
/// <param name="request">Request coming from client</param>
|
||||
/// <param name="endpoint">Address of the client that sent the request</param>
|
||||
/// <returns>The message to be sent back to the client or null</returns>
|
||||
protected override ServerResponse ProcessRequest(ServerRequest request, IPEndPoint endpoint)
|
||||
{
|
||||
// In this case we don't do anything with the request
|
||||
// but other discovery implementations might want to use the data
|
||||
// in there, This way the client can ask for
|
||||
// specific game mode or something
|
||||
|
||||
try
|
||||
{
|
||||
// this is an example reply message, return your own
|
||||
// to include whatever is relevant for your game
|
||||
return new ServerResponse
|
||||
{
|
||||
serverId = ServerId,
|
||||
uri = transport.ServerUri()
|
||||
};
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
Debug.LogError($"Transport {transport} does not support network discovery");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Client
|
||||
|
||||
/// <summary>
|
||||
/// Create a message that will be broadcasted on the network to discover servers
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Override if you wish to include additional data in the discovery message
|
||||
/// such as desired game mode, language, difficulty, etc... </remarks>
|
||||
/// <returns>An instance of ServerRequest with data to be broadcasted</returns>
|
||||
protected override ServerRequest GetRequest() => new ServerRequest();
|
||||
|
||||
/// <summary>
|
||||
/// Process the answer from a server
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A client receives a reply from a server, this method processes the
|
||||
/// reply and raises an event
|
||||
/// </remarks>
|
||||
/// <param name="response">Response that came from the server</param>
|
||||
/// <param name="endpoint">Address of the server that replied</param>
|
||||
protected override void ProcessResponse(ServerResponse response, IPEndPoint endpoint)
|
||||
{
|
||||
// we received a message from the remote endpoint
|
||||
response.EndPoint = endpoint;
|
||||
|
||||
// although we got a supposedly valid url, we may not be able to resolve
|
||||
// the provided host
|
||||
// However we know the real ip address of the server because we just
|
||||
// received a packet from it, so use that as host.
|
||||
UriBuilder realUri = new UriBuilder(response.uri)
|
||||
{
|
||||
Host = response.EndPoint.Address.ToString()
|
||||
};
|
||||
response.uri = realUri.Uri;
|
||||
|
||||
OnServerFound.Invoke(response);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
11
Assets/Mirror/Components/Discovery/NetworkDiscovery.cs.meta
Normal file
11
Assets/Mirror/Components/Discovery/NetworkDiscovery.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c761308e733c51245b2e8bb4201f46dc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
470
Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs
Normal file
470
Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs
Normal file
@ -0,0 +1,470 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
// Based on https://github.com/EnlightenedOne/MirrorNetworkDiscovery
|
||||
// forked from https://github.com/in0finite/MirrorNetworkDiscovery
|
||||
// Both are MIT Licensed
|
||||
|
||||
namespace Mirror.Discovery
|
||||
{
|
||||
/// <summary>
|
||||
/// Base implementation for Network Discovery. Extend this component
|
||||
/// to provide custom discovery with game specific data
|
||||
/// <see cref="NetworkDiscovery">NetworkDiscovery</see> for a sample implementation
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-discovery")]
|
||||
public abstract class NetworkDiscoveryBase<Request, Response> : MonoBehaviour
|
||||
where Request : NetworkMessage
|
||||
where Response : NetworkMessage
|
||||
{
|
||||
public static bool SupportedOnThisPlatform { get { return Application.platform != RuntimePlatform.WebGLPlayer; } }
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("If true, broadcasts a discovery request every ActiveDiscoveryInterval seconds")]
|
||||
public bool enableActiveDiscovery = true;
|
||||
|
||||
// broadcast address needs to be configurable on iOS:
|
||||
// https://github.com/vis2k/Mirror/pull/3255
|
||||
[Tooltip("iOS may require LAN IP address here (e.g. 192.168.x.x), otherwise leave blank.")]
|
||||
public string BroadcastAddress = "";
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The UDP port the server will listen for multi-cast messages")]
|
||||
protected int serverBroadcastListenPort = 47777;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Time in seconds between multi-cast messages")]
|
||||
[Range(1, 60)]
|
||||
float ActiveDiscoveryInterval = 3;
|
||||
|
||||
[Tooltip("Transport to be advertised during discovery")]
|
||||
public Transport transport;
|
||||
|
||||
[Tooltip("Invoked when a server is found")]
|
||||
public ServerFoundUnityEvent<Response> OnServerFound;
|
||||
|
||||
// Each game should have a random unique handshake,
|
||||
// this way you can tell if this is the same game or not
|
||||
[HideInInspector]
|
||||
public long secretHandshake;
|
||||
|
||||
public long ServerId { get; private set; }
|
||||
|
||||
protected UdpClient serverUdpClient;
|
||||
protected UdpClient clientUdpClient;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public virtual void OnValidate()
|
||||
{
|
||||
if (transport == null)
|
||||
transport = GetComponent<Transport>();
|
||||
|
||||
if (secretHandshake == 0)
|
||||
{
|
||||
secretHandshake = RandomLong();
|
||||
UnityEditor.Undo.RecordObject(this, "Set secret handshake");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// virtual so that inheriting classes' Start() can call base.Start() too
|
||||
/// </summary>
|
||||
public virtual void Start()
|
||||
{
|
||||
ServerId = RandomLong();
|
||||
|
||||
// active transport gets initialized in Awake
|
||||
// so make sure we set it here in Start() after Awake
|
||||
// Or just let the user assign it in the inspector
|
||||
if (transport == null)
|
||||
transport = Transport.active;
|
||||
|
||||
// Server mode? then start advertising
|
||||
if (Utils.IsHeadless())
|
||||
{
|
||||
AdvertiseServer();
|
||||
}
|
||||
}
|
||||
|
||||
public static long RandomLong()
|
||||
{
|
||||
int value1 = UnityEngine.Random.Range(int.MinValue, int.MaxValue);
|
||||
int value2 = UnityEngine.Random.Range(int.MinValue, int.MaxValue);
|
||||
return value1 + ((long)value2 << 32);
|
||||
}
|
||||
|
||||
// Ensure the ports are cleared no matter when Game/Unity UI exits
|
||||
void OnApplicationQuit()
|
||||
{
|
||||
//Debug.Log("NetworkDiscoveryBase OnApplicationQuit");
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
//Debug.Log("NetworkDiscoveryBase OnDisable");
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
//Debug.Log("NetworkDiscoveryBase OnDestroy");
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
void Shutdown()
|
||||
{
|
||||
EndpMulticastLock();
|
||||
if (serverUdpClient != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
serverUdpClient.Close();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// it is just close, swallow the error
|
||||
}
|
||||
|
||||
serverUdpClient = null;
|
||||
}
|
||||
|
||||
if (clientUdpClient != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
clientUdpClient.Close();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// it is just close, swallow the error
|
||||
}
|
||||
|
||||
clientUdpClient = null;
|
||||
}
|
||||
|
||||
CancelInvoke();
|
||||
}
|
||||
|
||||
#region Server
|
||||
|
||||
/// <summary>
|
||||
/// Advertise this server in the local network
|
||||
/// </summary>
|
||||
public void AdvertiseServer()
|
||||
{
|
||||
if (!SupportedOnThisPlatform)
|
||||
throw new PlatformNotSupportedException("Network discovery not supported in this platform");
|
||||
|
||||
StopDiscovery();
|
||||
|
||||
// Setup port -- may throw exception
|
||||
serverUdpClient = new UdpClient(serverBroadcastListenPort)
|
||||
{
|
||||
EnableBroadcast = true,
|
||||
MulticastLoopback = false
|
||||
};
|
||||
|
||||
// listen for client pings
|
||||
_ = ServerListenAsync();
|
||||
}
|
||||
|
||||
public async Task ServerListenAsync()
|
||||
{
|
||||
BeginMulticastLock();
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ReceiveRequestAsync(serverUdpClient);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// socket has been closed
|
||||
break;
|
||||
}
|
||||
catch (Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
async Task ReceiveRequestAsync(UdpClient udpClient)
|
||||
{
|
||||
// only proceed if there is available data in network buffer, or otherwise Receive() will block
|
||||
// average time for UdpClient.Available : 10 us
|
||||
|
||||
UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync();
|
||||
|
||||
using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(udpReceiveResult.Buffer))
|
||||
{
|
||||
long handshake = networkReader.ReadLong();
|
||||
if (handshake != secretHandshake)
|
||||
{
|
||||
// message is not for us
|
||||
throw new ProtocolViolationException("Invalid handshake");
|
||||
}
|
||||
|
||||
Request request = networkReader.Read<Request>();
|
||||
|
||||
ProcessClientRequest(request, udpReceiveResult.RemoteEndPoint);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reply to the client to inform it of this server
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Override if you wish to ignore server requests based on
|
||||
/// custom criteria such as language, full server game mode or difficulty
|
||||
/// </remarks>
|
||||
/// <param name="request">Request coming from client</param>
|
||||
/// <param name="endpoint">Address of the client that sent the request</param>
|
||||
protected virtual void ProcessClientRequest(Request request, IPEndPoint endpoint)
|
||||
{
|
||||
Response info = ProcessRequest(request, endpoint);
|
||||
|
||||
if (info == null)
|
||||
return;
|
||||
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
try
|
||||
{
|
||||
writer.WriteLong(secretHandshake);
|
||||
|
||||
writer.Write(info);
|
||||
|
||||
ArraySegment<byte> data = writer.ToArraySegment();
|
||||
// signature matches
|
||||
// send response
|
||||
serverUdpClient.Send(data.Array, data.Count, endpoint);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogException(ex, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the request from a client
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Override if you wish to provide more information to the clients
|
||||
/// such as the name of the host player
|
||||
/// </remarks>
|
||||
/// <param name="request">Request coming from client</param>
|
||||
/// <param name="endpoint">Address of the client that sent the request</param>
|
||||
/// <returns>The message to be sent back to the client or null</returns>
|
||||
protected abstract Response ProcessRequest(Request request, IPEndPoint endpoint);
|
||||
|
||||
// Android Multicast fix: https://github.com/vis2k/Mirror/pull/2887
|
||||
#if UNITY_ANDROID
|
||||
AndroidJavaObject multicastLock;
|
||||
bool hasMulticastLock;
|
||||
#endif
|
||||
|
||||
void BeginMulticastLock()
|
||||
{
|
||||
#if UNITY_ANDROID
|
||||
if (hasMulticastLock) return;
|
||||
|
||||
if (Application.platform == RuntimePlatform.Android)
|
||||
{
|
||||
using (AndroidJavaObject activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer").GetStatic<AndroidJavaObject>("currentActivity"))
|
||||
{
|
||||
using (var wifiManager = activity.Call<AndroidJavaObject>("getSystemService", "wifi"))
|
||||
{
|
||||
multicastLock = wifiManager.Call<AndroidJavaObject>("createMulticastLock", "lock");
|
||||
multicastLock.Call("acquire");
|
||||
hasMulticastLock = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void EndpMulticastLock()
|
||||
{
|
||||
#if UNITY_ANDROID
|
||||
if (!hasMulticastLock) return;
|
||||
|
||||
multicastLock?.Call("release");
|
||||
hasMulticastLock = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Client
|
||||
|
||||
/// <summary>
|
||||
/// Start Active Discovery
|
||||
/// </summary>
|
||||
public void StartDiscovery()
|
||||
{
|
||||
if (!SupportedOnThisPlatform)
|
||||
throw new PlatformNotSupportedException("Network discovery not supported in this platform");
|
||||
|
||||
StopDiscovery();
|
||||
|
||||
try
|
||||
{
|
||||
// Setup port
|
||||
clientUdpClient = new UdpClient(0)
|
||||
{
|
||||
EnableBroadcast = true,
|
||||
MulticastLoopback = false
|
||||
};
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Free the port if we took it
|
||||
//Debug.LogError("NetworkDiscoveryBase StartDiscovery Exception");
|
||||
Shutdown();
|
||||
throw;
|
||||
}
|
||||
|
||||
_ = ClientListenAsync();
|
||||
|
||||
if (enableActiveDiscovery) InvokeRepeating(nameof(BroadcastDiscoveryRequest), 0, ActiveDiscoveryInterval);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop Active Discovery
|
||||
/// </summary>
|
||||
public void StopDiscovery()
|
||||
{
|
||||
//Debug.Log("NetworkDiscoveryBase StopDiscovery");
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Awaits for server response
|
||||
/// </summary>
|
||||
/// <returns>ClientListenAsync Task</returns>
|
||||
public async Task ClientListenAsync()
|
||||
{
|
||||
// while clientUpdClient to fix:
|
||||
// https://github.com/vis2k/Mirror/pull/2908
|
||||
//
|
||||
// If, you cancel discovery the clientUdpClient is set to null.
|
||||
// However, nothing cancels ClientListenAsync. If we change the if(true)
|
||||
// to check if the client is null. You can properly cancel the discovery,
|
||||
// and kill the listen thread.
|
||||
//
|
||||
// Prior to this fix, if you cancel the discovery search. It crashes the
|
||||
// thread, and is super noisy in the output. As well as causes issues on
|
||||
// the quest.
|
||||
while (clientUdpClient != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ReceiveGameBroadcastAsync(clientUdpClient);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// socket was closed, no problem
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends discovery request from client
|
||||
/// </summary>
|
||||
public void BroadcastDiscoveryRequest()
|
||||
{
|
||||
if (clientUdpClient == null)
|
||||
return;
|
||||
|
||||
if (NetworkClient.isConnected)
|
||||
{
|
||||
StopDiscovery();
|
||||
return;
|
||||
}
|
||||
|
||||
IPEndPoint endPoint = new IPEndPoint(IPAddress.Broadcast, serverBroadcastListenPort);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(BroadcastAddress))
|
||||
{
|
||||
try
|
||||
{
|
||||
endPoint = new IPEndPoint(IPAddress.Parse(BroadcastAddress), serverBroadcastListenPort);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
writer.WriteLong(secretHandshake);
|
||||
|
||||
try
|
||||
{
|
||||
Request request = GetRequest();
|
||||
|
||||
writer.Write(request);
|
||||
|
||||
ArraySegment<byte> data = writer.ToArraySegment();
|
||||
|
||||
clientUdpClient.SendAsync(data.Array, data.Count, endPoint);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// It is ok if we can't broadcast to one of the addresses
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a message that will be broadcasted on the network to discover servers
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Override if you wish to include additional data in the discovery message
|
||||
/// such as desired game mode, language, difficulty, etc... </remarks>
|
||||
/// <returns>An instance of ServerRequest with data to be broadcasted</returns>
|
||||
protected virtual Request GetRequest() => default;
|
||||
|
||||
async Task ReceiveGameBroadcastAsync(UdpClient udpClient)
|
||||
{
|
||||
// only proceed if there is available data in network buffer, or otherwise Receive() will block
|
||||
// average time for UdpClient.Available : 10 us
|
||||
|
||||
UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync();
|
||||
|
||||
using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(udpReceiveResult.Buffer))
|
||||
{
|
||||
if (networkReader.ReadLong() != secretHandshake)
|
||||
return;
|
||||
|
||||
Response response = networkReader.Read<Response>();
|
||||
|
||||
ProcessResponse(response, udpReceiveResult.RemoteEndPoint);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the answer from a server
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A client receives a reply from a server, this method processes the
|
||||
/// reply and raises an event
|
||||
/// </remarks>
|
||||
/// <param name="response">Response that came from the server</param>
|
||||
/// <param name="endpoint">Address of the server that replied</param>
|
||||
protected abstract void ProcessResponse(Response response, IPEndPoint endpoint);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b9971d60ce61f4e39b07cd9e7e0c68fa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
132
Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs
Normal file
132
Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs
Normal file
@ -0,0 +1,132 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Discovery
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/Network Discovery HUD")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-discovery")]
|
||||
[RequireComponent(typeof(NetworkDiscovery))]
|
||||
public class NetworkDiscoveryHUD : MonoBehaviour
|
||||
{
|
||||
readonly Dictionary<long, ServerResponse> discoveredServers = new Dictionary<long, ServerResponse>();
|
||||
Vector2 scrollViewPos = Vector2.zero;
|
||||
|
||||
public NetworkDiscovery networkDiscovery;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
void OnValidate()
|
||||
{
|
||||
if (networkDiscovery == null)
|
||||
{
|
||||
networkDiscovery = GetComponent<NetworkDiscovery>();
|
||||
UnityEditor.Events.UnityEventTools.AddPersistentListener(networkDiscovery.OnServerFound, OnDiscoveredServer);
|
||||
UnityEditor.Undo.RecordObjects(new Object[] { this, networkDiscovery }, "Set NetworkDiscovery");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
if (NetworkManager.singleton == null)
|
||||
return;
|
||||
|
||||
if (!NetworkClient.isConnected && !NetworkServer.active && !NetworkClient.active)
|
||||
DrawGUI();
|
||||
|
||||
if (NetworkServer.active || NetworkClient.active)
|
||||
StopButtons();
|
||||
}
|
||||
|
||||
void DrawGUI()
|
||||
{
|
||||
GUILayout.BeginArea(new Rect(10, 10, 300, 500));
|
||||
GUILayout.BeginHorizontal();
|
||||
|
||||
if (GUILayout.Button("Find Servers"))
|
||||
{
|
||||
discoveredServers.Clear();
|
||||
networkDiscovery.StartDiscovery();
|
||||
}
|
||||
|
||||
// LAN Host
|
||||
if (GUILayout.Button("Start Host"))
|
||||
{
|
||||
discoveredServers.Clear();
|
||||
NetworkManager.singleton.StartHost();
|
||||
networkDiscovery.AdvertiseServer();
|
||||
}
|
||||
|
||||
// Dedicated server
|
||||
if (GUILayout.Button("Start Server"))
|
||||
{
|
||||
discoveredServers.Clear();
|
||||
NetworkManager.singleton.StartServer();
|
||||
networkDiscovery.AdvertiseServer();
|
||||
}
|
||||
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
// show list of found server
|
||||
|
||||
GUILayout.Label($"Discovered Servers [{discoveredServers.Count}]:");
|
||||
|
||||
// servers
|
||||
scrollViewPos = GUILayout.BeginScrollView(scrollViewPos);
|
||||
|
||||
foreach (ServerResponse info in discoveredServers.Values)
|
||||
if (GUILayout.Button(info.EndPoint.Address.ToString()))
|
||||
Connect(info);
|
||||
|
||||
GUILayout.EndScrollView();
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
|
||||
void StopButtons()
|
||||
{
|
||||
GUILayout.BeginArea(new Rect(10, 40, 100, 25));
|
||||
|
||||
// stop host if host mode
|
||||
if (NetworkServer.active && NetworkClient.isConnected)
|
||||
{
|
||||
if (GUILayout.Button("Stop Host"))
|
||||
{
|
||||
NetworkManager.singleton.StopHost();
|
||||
networkDiscovery.StopDiscovery();
|
||||
}
|
||||
}
|
||||
// stop client if client-only
|
||||
else if (NetworkClient.isConnected)
|
||||
{
|
||||
if (GUILayout.Button("Stop Client"))
|
||||
{
|
||||
NetworkManager.singleton.StopClient();
|
||||
networkDiscovery.StopDiscovery();
|
||||
}
|
||||
}
|
||||
// stop server if server-only
|
||||
else if (NetworkServer.active)
|
||||
{
|
||||
if (GUILayout.Button("Stop Server"))
|
||||
{
|
||||
NetworkManager.singleton.StopServer();
|
||||
networkDiscovery.StopDiscovery();
|
||||
}
|
||||
}
|
||||
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
|
||||
void Connect(ServerResponse info)
|
||||
{
|
||||
networkDiscovery.StopDiscovery();
|
||||
NetworkManager.singleton.StartClient(info.uri);
|
||||
}
|
||||
|
||||
public void OnDiscoveredServer(ServerResponse info)
|
||||
{
|
||||
// Note that you can check the versioning to decide if you can connect to the server or not using this method
|
||||
discoveredServers[info.serverId] = info;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 88c37d3deca7a834d80cfd8d3cfcc510
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
4
Assets/Mirror/Components/Discovery/ServerRequest.cs
Normal file
4
Assets/Mirror/Components/Discovery/ServerRequest.cs
Normal file
@ -0,0 +1,4 @@
|
||||
namespace Mirror.Discovery
|
||||
{
|
||||
public struct ServerRequest : NetworkMessage {}
|
||||
}
|
11
Assets/Mirror/Components/Discovery/ServerRequest.cs.meta
Normal file
11
Assets/Mirror/Components/Discovery/ServerRequest.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea7254bf7b9454da4adad881d94cd141
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
18
Assets/Mirror/Components/Discovery/ServerResponse.cs
Normal file
18
Assets/Mirror/Components/Discovery/ServerResponse.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace Mirror.Discovery
|
||||
{
|
||||
public struct ServerResponse : NetworkMessage
|
||||
{
|
||||
// The server that sent this
|
||||
// this is a property so that it is not serialized, but the
|
||||
// client fills this up after we receive it
|
||||
public IPEndPoint EndPoint { get; set; }
|
||||
|
||||
public Uri uri;
|
||||
|
||||
// Prevent duplicate server appearance when a connection can be made via LAN on multiple NICs
|
||||
public long serverId;
|
||||
}
|
||||
}
|
11
Assets/Mirror/Components/Discovery/ServerResponse.cs.meta
Normal file
11
Assets/Mirror/Components/Discovery/ServerResponse.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 36f97227fdf2d7a4e902db5bfc43039c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/Mirror/Components/Experimental.meta
Normal file
8
Assets/Mirror/Components/Experimental.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bfbf2a1f2b300c5489dcab219ef2846e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Experimental
|
||||
{
|
||||
[AddComponentMenu("Network/ Experimental/Network Lerp Rigidbody")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-lerp-rigidbody")]
|
||||
[Obsolete("Use the new NetworkRigidbodyReliable/Unreliable component with Snapshot Interpolation instead.")]
|
||||
public class NetworkLerpRigidbody : NetworkBehaviour
|
||||
{
|
||||
[Header("Settings")]
|
||||
[SerializeField] internal Rigidbody target = null;
|
||||
|
||||
[Tooltip("How quickly current velocity approaches target velocity")]
|
||||
[SerializeField] float lerpVelocityAmount = 0.5f;
|
||||
|
||||
[Tooltip("How quickly current position approaches target position")]
|
||||
[SerializeField] float lerpPositionAmount = 0.5f;
|
||||
|
||||
[Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
|
||||
[SerializeField] bool clientAuthority = false;
|
||||
|
||||
double nextSyncTime;
|
||||
|
||||
[SyncVar()]
|
||||
Vector3 targetVelocity;
|
||||
|
||||
[SyncVar()]
|
||||
Vector3 targetPosition;
|
||||
|
||||
/// <summary>
|
||||
/// Ignore value if is host or client with Authority
|
||||
/// </summary>
|
||||
bool IgnoreSync => isServer || ClientWithAuthority;
|
||||
|
||||
bool ClientWithAuthority => clientAuthority && isOwned;
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
Reset();
|
||||
}
|
||||
|
||||
public virtual void Reset()
|
||||
{
|
||||
if (target == null)
|
||||
target = GetComponent<Rigidbody>();
|
||||
|
||||
syncDirection = SyncDirection.ClientToServer;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (isServer)
|
||||
SyncToClients();
|
||||
else if (ClientWithAuthority)
|
||||
SendToServer();
|
||||
}
|
||||
|
||||
void SyncToClients()
|
||||
{
|
||||
targetVelocity = target.velocity;
|
||||
targetPosition = target.position;
|
||||
}
|
||||
|
||||
void SendToServer()
|
||||
{
|
||||
double now = NetworkTime.localTime; // Unity 2019 doesn't have Time.timeAsDouble yet
|
||||
if (now > nextSyncTime)
|
||||
{
|
||||
nextSyncTime = now + syncInterval;
|
||||
CmdSendState(target.velocity, target.position);
|
||||
}
|
||||
}
|
||||
|
||||
[Command]
|
||||
void CmdSendState(Vector3 velocity, Vector3 position)
|
||||
{
|
||||
target.velocity = velocity;
|
||||
target.position = position;
|
||||
targetVelocity = velocity;
|
||||
targetPosition = position;
|
||||
}
|
||||
|
||||
void FixedUpdate()
|
||||
{
|
||||
if (IgnoreSync) { return; }
|
||||
|
||||
target.velocity = Vector3.Lerp(target.velocity, targetVelocity, lerpVelocityAmount);
|
||||
target.position = Vector3.Lerp(target.position, targetPosition, lerpPositionAmount);
|
||||
// add velocity to position as position would have moved on server at that velocity
|
||||
target.position += target.velocity * Time.fixedDeltaTime;
|
||||
|
||||
// TODO does this also need to sync acceleration so and update velocity?
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7f032128052c95a46afb0ddd97d994cc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
354
Assets/Mirror/Components/Experimental/NetworkRigidbody.cs
Normal file
354
Assets/Mirror/Components/Experimental/NetworkRigidbody.cs
Normal file
@ -0,0 +1,354 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Experimental
|
||||
{
|
||||
[AddComponentMenu("Network/ Experimental/Network Rigidbody")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-rigidbody")]
|
||||
[Obsolete("Use the new NetworkRigidbodyReliable/Unreliable component with Snapshot Interpolation instead.")]
|
||||
public class NetworkRigidbody : NetworkBehaviour
|
||||
{
|
||||
[Header("Settings")]
|
||||
[SerializeField] internal Rigidbody target = null;
|
||||
|
||||
[Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
|
||||
public bool clientAuthority = false;
|
||||
|
||||
[Header("Velocity")]
|
||||
[Tooltip("Syncs Velocity every SyncInterval")]
|
||||
[SerializeField] bool syncVelocity = true;
|
||||
|
||||
[Tooltip("Set velocity to 0 each frame (only works if syncVelocity is false")]
|
||||
[SerializeField] bool clearVelocity = false;
|
||||
|
||||
[Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")]
|
||||
[SerializeField] float velocitySensitivity = 0.1f;
|
||||
|
||||
[Header("Angular Velocity")]
|
||||
[Tooltip("Syncs AngularVelocity every SyncInterval")]
|
||||
[SerializeField] bool syncAngularVelocity = true;
|
||||
|
||||
[Tooltip("Set angularVelocity to 0 each frame (only works if syncAngularVelocity is false")]
|
||||
[SerializeField] bool clearAngularVelocity = false;
|
||||
|
||||
[Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")]
|
||||
[SerializeField] float angularVelocitySensitivity = 0.1f;
|
||||
|
||||
/// <summary>
|
||||
/// Values sent on client with authority after they are sent to the server
|
||||
/// </summary>
|
||||
readonly ClientSyncState previousValue = new ClientSyncState();
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
Reset();
|
||||
}
|
||||
|
||||
public virtual void Reset()
|
||||
{
|
||||
if (target == null)
|
||||
target = GetComponent<Rigidbody>();
|
||||
|
||||
syncDirection = SyncDirection.ClientToServer;
|
||||
}
|
||||
|
||||
#region Sync vars
|
||||
|
||||
[SyncVar(hook = nameof(OnVelocityChanged))]
|
||||
Vector3 velocity;
|
||||
|
||||
[SyncVar(hook = nameof(OnAngularVelocityChanged))]
|
||||
Vector3 angularVelocity;
|
||||
|
||||
[SyncVar(hook = nameof(OnIsKinematicChanged))]
|
||||
bool isKinematic;
|
||||
|
||||
[SyncVar(hook = nameof(OnUseGravityChanged))]
|
||||
bool useGravity;
|
||||
|
||||
[SyncVar(hook = nameof(OnuDragChanged))]
|
||||
float drag;
|
||||
|
||||
[SyncVar(hook = nameof(OnAngularDragChanged))]
|
||||
float angularDrag;
|
||||
|
||||
/// <summary>
|
||||
/// Ignore value if is host or client with Authority
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
bool IgnoreSync => isServer || ClientWithAuthority;
|
||||
|
||||
bool ClientWithAuthority => clientAuthority && isOwned;
|
||||
|
||||
void OnVelocityChanged(Vector3 _, Vector3 newValue)
|
||||
{
|
||||
if (IgnoreSync)
|
||||
return;
|
||||
|
||||
target.velocity = newValue;
|
||||
}
|
||||
|
||||
void OnAngularVelocityChanged(Vector3 _, Vector3 newValue)
|
||||
{
|
||||
if (IgnoreSync)
|
||||
return;
|
||||
|
||||
target.angularVelocity = newValue;
|
||||
}
|
||||
|
||||
void OnIsKinematicChanged(bool _, bool newValue)
|
||||
{
|
||||
if (IgnoreSync)
|
||||
return;
|
||||
|
||||
target.isKinematic = newValue;
|
||||
}
|
||||
|
||||
void OnUseGravityChanged(bool _, bool newValue)
|
||||
{
|
||||
if (IgnoreSync)
|
||||
return;
|
||||
|
||||
target.useGravity = newValue;
|
||||
}
|
||||
|
||||
void OnuDragChanged(float _, float newValue)
|
||||
{
|
||||
if (IgnoreSync)
|
||||
return;
|
||||
|
||||
target.drag = newValue;
|
||||
}
|
||||
|
||||
void OnAngularDragChanged(float _, float newValue)
|
||||
{
|
||||
if (IgnoreSync)
|
||||
return;
|
||||
|
||||
target.angularDrag = newValue;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
internal void Update()
|
||||
{
|
||||
if (isServer)
|
||||
SyncToClients();
|
||||
else if (ClientWithAuthority)
|
||||
SendToServer();
|
||||
}
|
||||
|
||||
internal void FixedUpdate()
|
||||
{
|
||||
if (clearAngularVelocity && !syncAngularVelocity)
|
||||
target.angularVelocity = Vector3.zero;
|
||||
|
||||
if (clearVelocity && !syncVelocity)
|
||||
target.velocity = Vector3.zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates sync var values on server so that they sync to the client
|
||||
/// </summary>
|
||||
[Server]
|
||||
void SyncToClients()
|
||||
{
|
||||
// only update if they have changed more than Sensitivity
|
||||
|
||||
Vector3 currentVelocity = syncVelocity ? target.velocity : default;
|
||||
Vector3 currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default;
|
||||
|
||||
bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity);
|
||||
bool angularVelocityChanged = syncAngularVelocity && ((previousValue.angularVelocity - currentAngularVelocity).sqrMagnitude > angularVelocitySensitivity * angularVelocitySensitivity);
|
||||
|
||||
if (velocityChanged)
|
||||
{
|
||||
velocity = currentVelocity;
|
||||
previousValue.velocity = currentVelocity;
|
||||
}
|
||||
|
||||
if (angularVelocityChanged)
|
||||
{
|
||||
angularVelocity = currentAngularVelocity;
|
||||
previousValue.angularVelocity = currentAngularVelocity;
|
||||
}
|
||||
|
||||
// other rigidbody settings
|
||||
isKinematic = target.isKinematic;
|
||||
useGravity = target.useGravity;
|
||||
drag = target.drag;
|
||||
angularDrag = target.angularDrag;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses Command to send values to server
|
||||
/// </summary>
|
||||
[Client]
|
||||
void SendToServer()
|
||||
{
|
||||
if (!isOwned)
|
||||
{
|
||||
Debug.LogWarning("SendToServer called without authority");
|
||||
return;
|
||||
}
|
||||
|
||||
SendVelocity();
|
||||
SendRigidBodySettings();
|
||||
}
|
||||
|
||||
[Client]
|
||||
void SendVelocity()
|
||||
{
|
||||
double now = NetworkTime.localTime; // Unity 2019 doesn't have Time.timeAsDouble yet
|
||||
if (now < previousValue.nextSyncTime)
|
||||
return;
|
||||
|
||||
Vector3 currentVelocity = syncVelocity ? target.velocity : default;
|
||||
Vector3 currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default;
|
||||
|
||||
bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity);
|
||||
bool angularVelocityChanged = syncAngularVelocity && ((previousValue.angularVelocity - currentAngularVelocity).sqrMagnitude > angularVelocitySensitivity * angularVelocitySensitivity);
|
||||
|
||||
// if angularVelocity has changed it is likely that velocity has also changed so just sync both values
|
||||
// however if only velocity has changed just send velocity
|
||||
if (angularVelocityChanged)
|
||||
{
|
||||
CmdSendVelocityAndAngular(currentVelocity, currentAngularVelocity);
|
||||
previousValue.velocity = currentVelocity;
|
||||
previousValue.angularVelocity = currentAngularVelocity;
|
||||
}
|
||||
else if (velocityChanged)
|
||||
{
|
||||
CmdSendVelocity(currentVelocity);
|
||||
previousValue.velocity = currentVelocity;
|
||||
}
|
||||
|
||||
|
||||
// only update syncTime if either has changed
|
||||
if (angularVelocityChanged || velocityChanged)
|
||||
previousValue.nextSyncTime = now + syncInterval;
|
||||
}
|
||||
|
||||
[Client]
|
||||
void SendRigidBodySettings()
|
||||
{
|
||||
// These shouldn't change often so it is ok to send in their own Command
|
||||
if (previousValue.isKinematic != target.isKinematic)
|
||||
{
|
||||
CmdSendIsKinematic(target.isKinematic);
|
||||
previousValue.isKinematic = target.isKinematic;
|
||||
}
|
||||
if (previousValue.useGravity != target.useGravity)
|
||||
{
|
||||
CmdSendUseGravity(target.useGravity);
|
||||
previousValue.useGravity = target.useGravity;
|
||||
}
|
||||
if (previousValue.drag != target.drag)
|
||||
{
|
||||
CmdSendDrag(target.drag);
|
||||
previousValue.drag = target.drag;
|
||||
}
|
||||
if (previousValue.angularDrag != target.angularDrag)
|
||||
{
|
||||
CmdSendAngularDrag(target.angularDrag);
|
||||
previousValue.angularDrag = target.angularDrag;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when only Velocity has changed on the client
|
||||
/// </summary>
|
||||
[Command]
|
||||
void CmdSendVelocity(Vector3 velocity)
|
||||
{
|
||||
// Ignore messages from client if not in client authority mode
|
||||
if (!clientAuthority)
|
||||
return;
|
||||
|
||||
this.velocity = velocity;
|
||||
target.velocity = velocity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when angularVelocity has changed on the client
|
||||
/// </summary>
|
||||
[Command]
|
||||
void CmdSendVelocityAndAngular(Vector3 velocity, Vector3 angularVelocity)
|
||||
{
|
||||
// Ignore messages from client if not in client authority mode
|
||||
if (!clientAuthority)
|
||||
return;
|
||||
|
||||
if (syncVelocity)
|
||||
{
|
||||
this.velocity = velocity;
|
||||
target.velocity = velocity;
|
||||
}
|
||||
|
||||
this.angularVelocity = angularVelocity;
|
||||
target.angularVelocity = angularVelocity;
|
||||
}
|
||||
|
||||
[Command]
|
||||
void CmdSendIsKinematic(bool isKinematic)
|
||||
{
|
||||
// Ignore messages from client if not in client authority mode
|
||||
if (!clientAuthority)
|
||||
return;
|
||||
|
||||
this.isKinematic = isKinematic;
|
||||
target.isKinematic = isKinematic;
|
||||
}
|
||||
|
||||
[Command]
|
||||
void CmdSendUseGravity(bool useGravity)
|
||||
{
|
||||
// Ignore messages from client if not in client authority mode
|
||||
if (!clientAuthority)
|
||||
return;
|
||||
|
||||
this.useGravity = useGravity;
|
||||
target.useGravity = useGravity;
|
||||
}
|
||||
|
||||
[Command]
|
||||
void CmdSendDrag(float drag)
|
||||
{
|
||||
// Ignore messages from client if not in client authority mode
|
||||
if (!clientAuthority)
|
||||
return;
|
||||
|
||||
this.drag = drag;
|
||||
target.drag = drag;
|
||||
}
|
||||
|
||||
[Command]
|
||||
void CmdSendAngularDrag(float angularDrag)
|
||||
{
|
||||
// Ignore messages from client if not in client authority mode
|
||||
if (!clientAuthority)
|
||||
return;
|
||||
|
||||
this.angularDrag = angularDrag;
|
||||
target.angularDrag = angularDrag;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// holds previously synced values
|
||||
/// </summary>
|
||||
public class ClientSyncState
|
||||
{
|
||||
/// <summary>
|
||||
/// Next sync time that velocity will be synced, based on syncInterval.
|
||||
/// </summary>
|
||||
public double nextSyncTime;
|
||||
public Vector3 velocity;
|
||||
public Vector3 angularVelocity;
|
||||
public bool isKinematic;
|
||||
public bool useGravity;
|
||||
public float drag;
|
||||
public float angularDrag;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 83392ae5c1b731446909f252fd494ae4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
349
Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs
Normal file
349
Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs
Normal file
@ -0,0 +1,349 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Experimental
|
||||
{
|
||||
[AddComponentMenu("Network/ Experimental/Network Rigidbody 2D")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-rigidbody")]
|
||||
public class NetworkRigidbody2D : NetworkBehaviour
|
||||
{
|
||||
[Header("Settings")]
|
||||
[SerializeField] internal Rigidbody2D target = null;
|
||||
|
||||
[Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
|
||||
public bool clientAuthority = false;
|
||||
|
||||
[Header("Velocity")]
|
||||
[Tooltip("Syncs Velocity every SyncInterval")]
|
||||
[SerializeField] bool syncVelocity = true;
|
||||
|
||||
[Tooltip("Set velocity to 0 each frame (only works if syncVelocity is false")]
|
||||
[SerializeField] bool clearVelocity = false;
|
||||
|
||||
[Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")]
|
||||
[SerializeField] float velocitySensitivity = 0.1f;
|
||||
|
||||
[Header("Angular Velocity")]
|
||||
[Tooltip("Syncs AngularVelocity every SyncInterval")]
|
||||
[SerializeField] bool syncAngularVelocity = true;
|
||||
|
||||
[Tooltip("Set angularVelocity to 0 each frame (only works if syncAngularVelocity is false")]
|
||||
[SerializeField] bool clearAngularVelocity = false;
|
||||
|
||||
[Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")]
|
||||
[SerializeField] float angularVelocitySensitivity = 0.1f;
|
||||
|
||||
/// <summary>
|
||||
/// Values sent on client with authority after they are sent to the server
|
||||
/// </summary>
|
||||
readonly ClientSyncState previousValue = new ClientSyncState();
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
Reset();
|
||||
}
|
||||
|
||||
public virtual void Reset()
|
||||
{
|
||||
if (target == null)
|
||||
target = GetComponent<Rigidbody2D>();
|
||||
|
||||
syncDirection = SyncDirection.ClientToServer;
|
||||
}
|
||||
|
||||
#region Sync vars
|
||||
|
||||
[SyncVar(hook = nameof(OnVelocityChanged))]
|
||||
Vector2 velocity;
|
||||
|
||||
[SyncVar(hook = nameof(OnAngularVelocityChanged))]
|
||||
float angularVelocity;
|
||||
|
||||
[SyncVar(hook = nameof(OnIsKinematicChanged))]
|
||||
bool isKinematic;
|
||||
|
||||
[SyncVar(hook = nameof(OnGravityScaleChanged))]
|
||||
float gravityScale;
|
||||
|
||||
[SyncVar(hook = nameof(OnuDragChanged))]
|
||||
float drag;
|
||||
|
||||
[SyncVar(hook = nameof(OnAngularDragChanged))]
|
||||
float angularDrag;
|
||||
|
||||
/// <summary>
|
||||
/// Ignore value if is host or client with Authority
|
||||
/// </summary>
|
||||
bool IgnoreSync => isServer || ClientWithAuthority;
|
||||
|
||||
bool ClientWithAuthority => clientAuthority && isOwned;
|
||||
|
||||
void OnVelocityChanged(Vector2 _, Vector2 newValue)
|
||||
{
|
||||
if (IgnoreSync)
|
||||
return;
|
||||
|
||||
target.linearVelocity = newValue;
|
||||
}
|
||||
|
||||
void OnAngularVelocityChanged(float _, float newValue)
|
||||
{
|
||||
if (IgnoreSync)
|
||||
return;
|
||||
|
||||
target.angularVelocity = newValue;
|
||||
}
|
||||
|
||||
void OnIsKinematicChanged(bool _, bool newValue)
|
||||
{
|
||||
if (IgnoreSync)
|
||||
return;
|
||||
|
||||
target.isKinematic = newValue;
|
||||
}
|
||||
|
||||
void OnGravityScaleChanged(float _, float newValue)
|
||||
{
|
||||
if (IgnoreSync)
|
||||
return;
|
||||
|
||||
target.gravityScale = newValue;
|
||||
}
|
||||
|
||||
void OnuDragChanged(float _, float newValue)
|
||||
{
|
||||
if (IgnoreSync)
|
||||
return;
|
||||
|
||||
target.linearDamping = newValue;
|
||||
}
|
||||
|
||||
void OnAngularDragChanged(float _, float newValue)
|
||||
{
|
||||
if (IgnoreSync)
|
||||
return;
|
||||
|
||||
target.angularDamping = newValue;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
internal void Update()
|
||||
{
|
||||
if (isServer)
|
||||
SyncToClients();
|
||||
else if (ClientWithAuthority)
|
||||
SendToServer();
|
||||
}
|
||||
|
||||
internal void FixedUpdate()
|
||||
{
|
||||
if (clearAngularVelocity && !syncAngularVelocity)
|
||||
target.angularVelocity = 0f;
|
||||
|
||||
if (clearVelocity && !syncVelocity)
|
||||
target.linearVelocity = Vector2.zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates sync var values on server so that they sync to the client
|
||||
/// </summary>
|
||||
[Server]
|
||||
void SyncToClients()
|
||||
{
|
||||
// only update if they have changed more than Sensitivity
|
||||
|
||||
Vector2 currentVelocity = syncVelocity ? target.linearVelocity : default;
|
||||
float currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default;
|
||||
|
||||
bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity);
|
||||
bool angularVelocityChanged = syncAngularVelocity && ((previousValue.angularVelocity - currentAngularVelocity) > angularVelocitySensitivity);
|
||||
|
||||
if (velocityChanged)
|
||||
{
|
||||
velocity = currentVelocity;
|
||||
previousValue.velocity = currentVelocity;
|
||||
}
|
||||
|
||||
if (angularVelocityChanged)
|
||||
{
|
||||
angularVelocity = currentAngularVelocity;
|
||||
previousValue.angularVelocity = currentAngularVelocity;
|
||||
}
|
||||
|
||||
// other rigidbody settings
|
||||
isKinematic = target.isKinematic;
|
||||
gravityScale = target.gravityScale;
|
||||
drag = target.linearDamping;
|
||||
angularDrag = target.angularDamping;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses Command to send values to server
|
||||
/// </summary>
|
||||
[Client]
|
||||
void SendToServer()
|
||||
{
|
||||
if (!isOwned)
|
||||
{
|
||||
Debug.LogWarning("SendToServer called without authority");
|
||||
return;
|
||||
}
|
||||
|
||||
SendVelocity();
|
||||
SendRigidBodySettings();
|
||||
}
|
||||
|
||||
[Client]
|
||||
void SendVelocity()
|
||||
{
|
||||
float now = Time.time;
|
||||
if (now < previousValue.nextSyncTime)
|
||||
return;
|
||||
|
||||
Vector2 currentVelocity = syncVelocity ? target.linearVelocity : default;
|
||||
float currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default;
|
||||
|
||||
bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity);
|
||||
bool angularVelocityChanged = syncAngularVelocity && previousValue.angularVelocity != currentAngularVelocity;//((previousValue.angularVelocity - currentAngularVelocity).sqrMagnitude > angularVelocitySensitivity * angularVelocitySensitivity);
|
||||
|
||||
// if angularVelocity has changed it is likely that velocity has also changed so just sync both values
|
||||
// however if only velocity has changed just send velocity
|
||||
if (angularVelocityChanged)
|
||||
{
|
||||
CmdSendVelocityAndAngular(currentVelocity, currentAngularVelocity);
|
||||
previousValue.velocity = currentVelocity;
|
||||
previousValue.angularVelocity = currentAngularVelocity;
|
||||
}
|
||||
else if (velocityChanged)
|
||||
{
|
||||
CmdSendVelocity(currentVelocity);
|
||||
previousValue.velocity = currentVelocity;
|
||||
}
|
||||
|
||||
// only update syncTime if either has changed
|
||||
if (angularVelocityChanged || velocityChanged)
|
||||
previousValue.nextSyncTime = now + syncInterval;
|
||||
}
|
||||
|
||||
[Client]
|
||||
void SendRigidBodySettings()
|
||||
{
|
||||
// These shouldn't change often so it is ok to send in their own Command
|
||||
if (previousValue.isKinematic != target.isKinematic)
|
||||
{
|
||||
CmdSendIsKinematic(target.isKinematic);
|
||||
previousValue.isKinematic = target.isKinematic;
|
||||
}
|
||||
if (previousValue.gravityScale != target.gravityScale)
|
||||
{
|
||||
CmdChangeGravityScale(target.gravityScale);
|
||||
previousValue.gravityScale = target.gravityScale;
|
||||
}
|
||||
if (previousValue.drag != target.linearDamping)
|
||||
{
|
||||
CmdSendDrag(target.linearDamping);
|
||||
previousValue.drag = target.linearDamping;
|
||||
}
|
||||
if (previousValue.angularDrag != target.angularDamping)
|
||||
{
|
||||
CmdSendAngularDrag(target.angularDamping);
|
||||
previousValue.angularDrag = target.angularDamping;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when only Velocity has changed on the client
|
||||
/// </summary>
|
||||
[Command]
|
||||
void CmdSendVelocity(Vector2 velocity)
|
||||
{
|
||||
// Ignore messages from client if not in client authority mode
|
||||
if (!clientAuthority)
|
||||
return;
|
||||
|
||||
this.velocity = velocity;
|
||||
target.linearVelocity = velocity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when angularVelocity has changed on the client
|
||||
/// </summary>
|
||||
[Command]
|
||||
void CmdSendVelocityAndAngular(Vector2 velocity, float angularVelocity)
|
||||
{
|
||||
// Ignore messages from client if not in client authority mode
|
||||
if (!clientAuthority)
|
||||
return;
|
||||
|
||||
if (syncVelocity)
|
||||
{
|
||||
this.velocity = velocity;
|
||||
target.linearVelocity = velocity;
|
||||
}
|
||||
this.angularVelocity = angularVelocity;
|
||||
target.angularVelocity = angularVelocity;
|
||||
}
|
||||
|
||||
[Command]
|
||||
void CmdSendIsKinematic(bool isKinematic)
|
||||
{
|
||||
// Ignore messages from client if not in client authority mode
|
||||
if (!clientAuthority)
|
||||
return;
|
||||
|
||||
this.isKinematic = isKinematic;
|
||||
target.isKinematic = isKinematic;
|
||||
}
|
||||
|
||||
[Command]
|
||||
void CmdChangeGravityScale(float gravityScale)
|
||||
{
|
||||
// Ignore messages from client if not in client authority mode
|
||||
if (!clientAuthority)
|
||||
return;
|
||||
|
||||
this.gravityScale = gravityScale;
|
||||
target.gravityScale = gravityScale;
|
||||
}
|
||||
|
||||
[Command]
|
||||
void CmdSendDrag(float drag)
|
||||
{
|
||||
// Ignore messages from client if not in client authority mode
|
||||
if (!clientAuthority)
|
||||
return;
|
||||
|
||||
this.drag = drag;
|
||||
target.linearDamping = drag;
|
||||
}
|
||||
|
||||
[Command]
|
||||
void CmdSendAngularDrag(float angularDrag)
|
||||
{
|
||||
// Ignore messages from client if not in client authority mode
|
||||
if (!clientAuthority)
|
||||
return;
|
||||
|
||||
this.angularDrag = angularDrag;
|
||||
target.angularDamping = angularDrag;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// holds previously synced values
|
||||
/// </summary>
|
||||
public class ClientSyncState
|
||||
{
|
||||
/// <summary>
|
||||
/// Next sync time that velocity will be synced, based on syncInterval.
|
||||
/// </summary>
|
||||
public float nextSyncTime;
|
||||
public Vector2 velocity;
|
||||
public float angularVelocity;
|
||||
public bool isKinematic;
|
||||
public float gravityScale;
|
||||
public float drag;
|
||||
public float angularDrag;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ab2cbc52526ea384ba280d13cd1a57b9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
133
Assets/Mirror/Components/GUIConsole.cs
Normal file
133
Assets/Mirror/Components/GUIConsole.cs
Normal file
@ -0,0 +1,133 @@
|
||||
// People should be able to see and report errors to the developer very easily.
|
||||
//
|
||||
// Unity's Developer Console only works in development builds and it only shows
|
||||
// errors. This class provides a console that works in all builds and also shows
|
||||
// log and warnings in development builds.
|
||||
//
|
||||
// Note: we don't include the stack trace, because that can also be grabbed from
|
||||
// the log files if needed.
|
||||
//
|
||||
// Note: there is no 'hide' button because we DO want people to see those errors
|
||||
// and report them back to us.
|
||||
//
|
||||
// Note: normal Debug.Log messages can be shown by building in Debug/Development
|
||||
// mode.
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
struct LogEntry
|
||||
{
|
||||
public string message;
|
||||
public LogType type;
|
||||
|
||||
public LogEntry(string message, LogType type)
|
||||
{
|
||||
this.message = message;
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
public class GUIConsole : MonoBehaviour
|
||||
{
|
||||
public int height = 80;
|
||||
public int offsetY = 40;
|
||||
|
||||
// only keep the recent 'n' entries. otherwise memory would grow forever
|
||||
// and drawing would get slower and slower.
|
||||
public int maxLogCount = 50;
|
||||
|
||||
// Unity Editor has the Console window, we don't need to show it there.
|
||||
// unless for testing, so keep it as option.
|
||||
public bool showInEditor = false;
|
||||
|
||||
// log as queue so we can remove the first entry easily
|
||||
readonly Queue<LogEntry> log = new Queue<LogEntry>();
|
||||
|
||||
// hotkey to show/hide at runtime for easier debugging
|
||||
// (sometimes we need to temporarily hide/show it)
|
||||
// Default is BackQuote, because F keys are already assigned in browsers
|
||||
[Tooltip("Hotkey to show/hide the console at runtime\nBack Quote is usually on the left above Tab\nChange with caution - F keys are generally already taken in Browsers")]
|
||||
public KeyCode hotKey = KeyCode.BackQuote;
|
||||
|
||||
// GUI
|
||||
bool visible;
|
||||
Vector2 scroll = Vector2.zero;
|
||||
|
||||
// only show at runtime, or if showInEditor is enabled
|
||||
bool show => !Application.isEditor || showInEditor;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
// only show at runtime, or if showInEditor is enabled
|
||||
if (show)
|
||||
Application.logMessageReceived += OnLog;
|
||||
}
|
||||
|
||||
// OnLog logs everything, even Debug.Log messages in release builds
|
||||
// => this makes a lot of things easier. e.g. addon initialization logs.
|
||||
// => it's really better to have than not to have those
|
||||
void OnLog(string message, string stackTrace, LogType type)
|
||||
{
|
||||
// is this important?
|
||||
// => always show exceptions & errors
|
||||
// => usually a good idea to show warnings too, otherwise it's too
|
||||
// easy to miss OnDeserialize warnings etc. in builds
|
||||
bool isImportant = type == LogType.Error || type == LogType.Exception || type == LogType.Warning;
|
||||
|
||||
// use stack trace only if important
|
||||
// (otherwise users would have to find and search the log file.
|
||||
// seeing it in the console directly is way easier to deal with.)
|
||||
// => only add \n if stack trace is available (only in debug builds)
|
||||
if (isImportant && !string.IsNullOrWhiteSpace(stackTrace))
|
||||
message += $"\n{stackTrace}";
|
||||
|
||||
// add to queue
|
||||
log.Enqueue(new LogEntry(message, type));
|
||||
|
||||
// respect max entries
|
||||
if (log.Count > maxLogCount)
|
||||
log.Dequeue();
|
||||
|
||||
// become visible if it was important
|
||||
// (no need to become visible for regular log. let the user decide.)
|
||||
if (isImportant)
|
||||
visible = true;
|
||||
|
||||
// auto scroll
|
||||
scroll.y = float.MaxValue;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (show && Input.GetKeyDown(hotKey))
|
||||
visible = !visible;
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
if (!visible) return;
|
||||
|
||||
// If this offset is changed, also change width in NetworkManagerHUD::OnGUI
|
||||
int offsetX = 300 + 20;
|
||||
|
||||
GUILayout.BeginArea(new Rect(offsetX, offsetY, Screen.width - offsetX - 10, height));
|
||||
|
||||
scroll = GUILayout.BeginScrollView(scroll, "Box", GUILayout.Width(Screen.width - offsetX - 10), GUILayout.Height(height));
|
||||
foreach (LogEntry entry in log)
|
||||
{
|
||||
if (entry.type == LogType.Error || entry.type == LogType.Exception)
|
||||
GUI.color = Color.red;
|
||||
else if (entry.type == LogType.Warning)
|
||||
GUI.color = Color.yellow;
|
||||
|
||||
GUILayout.Label(entry.message);
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
GUILayout.EndScrollView();
|
||||
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Components/GUIConsole.cs.meta
Normal file
11
Assets/Mirror/Components/GUIConsole.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9021b6cc314944290986ab6feb48db79
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/Mirror/Components/InterestManagement.meta
Normal file
8
Assets/Mirror/Components/InterestManagement.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c66f27e006ab94253b39a55a3b213651
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa4cbc6b9c584db4971985cb9f369077
|
||||
timeCreated: 1613110605
|
@ -0,0 +1,90 @@
|
||||
// straight forward Vector3.Distance based interest management.
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/ Interest Management/ Distance/Distance Interest Management")]
|
||||
public class DistanceInterestManagement : InterestManagement
|
||||
{
|
||||
[Tooltip("The maximum range that objects will be visible at. Add DistanceInterestManagementCustomRange onto NetworkIdentities for custom ranges.")]
|
||||
public int visRange = 500;
|
||||
|
||||
[Tooltip("Rebuild all every 'rebuildInterval' seconds.")]
|
||||
public float rebuildInterval = 1;
|
||||
double lastRebuildTime;
|
||||
|
||||
// cache custom ranges to avoid runtime TryGetComponent lookups
|
||||
readonly Dictionary<NetworkIdentity, DistanceInterestManagementCustomRange> CustomRanges = new Dictionary<NetworkIdentity, DistanceInterestManagementCustomRange>();
|
||||
|
||||
// helper function to get vis range for a given object, or default.
|
||||
[ServerCallback]
|
||||
int GetVisRange(NetworkIdentity identity)
|
||||
{
|
||||
return CustomRanges.TryGetValue(identity, out DistanceInterestManagementCustomRange custom) ? custom.visRange : visRange;
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void ResetState()
|
||||
{
|
||||
lastRebuildTime = 0D;
|
||||
CustomRanges.Clear();
|
||||
}
|
||||
|
||||
public override void OnSpawned(NetworkIdentity identity)
|
||||
{
|
||||
if (identity.TryGetComponent(out DistanceInterestManagementCustomRange custom))
|
||||
CustomRanges[identity] = custom;
|
||||
}
|
||||
|
||||
public override void OnDestroyed(NetworkIdentity identity)
|
||||
{
|
||||
CustomRanges.Remove(identity);
|
||||
}
|
||||
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
|
||||
{
|
||||
int range = GetVisRange(identity);
|
||||
return Vector3.Distance(identity.transform.position, newObserver.identity.transform.position) < range;
|
||||
}
|
||||
|
||||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
// cache range and .transform because both call GetComponent.
|
||||
int range = GetVisRange(identity);
|
||||
Vector3 position = identity.transform.position;
|
||||
|
||||
// brute force distance check
|
||||
// -> only player connections can be observers, so it's enough if we
|
||||
// go through all connections instead of all spawned identities.
|
||||
// -> compared to UNET's sphere cast checking, this one is orders of
|
||||
// magnitude faster. if we have 10k monsters and run a sphere
|
||||
// cast 10k times, we will see a noticeable lag even with physics
|
||||
// layers. but checking to every connection is fast.
|
||||
foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values)
|
||||
{
|
||||
// authenticated and joined world with a player?
|
||||
if (conn != null && conn.isAuthenticated && conn.identity != null)
|
||||
{
|
||||
// check distance
|
||||
if (Vector3.Distance(conn.identity.transform.position, position) < range)
|
||||
{
|
||||
newObservers.Add(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// internal so we can update from tests
|
||||
[ServerCallback]
|
||||
internal void Update()
|
||||
{
|
||||
// rebuild all spawned NetworkIdentity's observers every interval
|
||||
if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval)
|
||||
{
|
||||
RebuildAll();
|
||||
lastRebuildTime = NetworkTime.localTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f60becab051427fbdd3c8ac9ab4712b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,15 @@
|
||||
// add this to NetworkIdentities for custom range if needed.
|
||||
// only works with DistanceInterestManagement.
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/ Interest Management/ Distance/Distance Custom Range")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")]
|
||||
public class DistanceInterestManagementCustomRange : NetworkBehaviour
|
||||
{
|
||||
[Tooltip("The maximum range that objects will be visible at.")]
|
||||
public int visRange = 100;
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b2e242ee38a14076a39934172a19079b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
3
Assets/Mirror/Components/InterestManagement/Match.meta
Normal file
3
Assets/Mirror/Components/InterestManagement/Match.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5eca5245ae6bb460e9a92f7e14d5493a
|
||||
timeCreated: 1622649517
|
@ -0,0 +1,164 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/ Interest Management/ Match/Match Interest Management")]
|
||||
public class MatchInterestManagement : InterestManagement
|
||||
{
|
||||
[Header("Diagnostics")]
|
||||
[ReadOnly, SerializeField]
|
||||
internal ushort matchCount;
|
||||
|
||||
readonly Dictionary<Guid, HashSet<NetworkMatch>> matchObjects =
|
||||
new Dictionary<Guid, HashSet<NetworkMatch>>();
|
||||
|
||||
readonly HashSet<Guid> dirtyMatches = new HashSet<Guid>();
|
||||
|
||||
// LateUpdate so that all spawns/despawns/changes are done
|
||||
[ServerCallback]
|
||||
void LateUpdate()
|
||||
{
|
||||
// Rebuild all dirty matches
|
||||
// dirtyMatches will be empty if no matches changed members
|
||||
// by spawning or destroying or changing matchId in this frame.
|
||||
foreach (Guid dirtyMatch in dirtyMatches)
|
||||
{
|
||||
// rebuild always, even if matchObjects[dirtyMatch] is empty.
|
||||
// Players might have left the match, but they may still be spawned.
|
||||
RebuildMatchObservers(dirtyMatch);
|
||||
|
||||
// clean up empty entries in the dict
|
||||
if (matchObjects[dirtyMatch].Count == 0)
|
||||
matchObjects.Remove(dirtyMatch);
|
||||
}
|
||||
|
||||
dirtyMatches.Clear();
|
||||
|
||||
matchCount = (ushort)matchObjects.Count;
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
void RebuildMatchObservers(Guid matchId)
|
||||
{
|
||||
foreach (NetworkMatch networkMatch in matchObjects[matchId])
|
||||
if (networkMatch.netIdentity != null)
|
||||
NetworkServer.RebuildObservers(networkMatch.netIdentity, false);
|
||||
}
|
||||
|
||||
// called by NetworkMatch.matchId setter
|
||||
[ServerCallback]
|
||||
internal void OnMatchChanged(NetworkMatch networkMatch, Guid oldMatch)
|
||||
{
|
||||
// This object is in a new match so observers in the prior match
|
||||
// and the new match need to rebuild their respective observers lists.
|
||||
|
||||
// Remove this object from the hashset of the match it just left
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (oldMatch != Guid.Empty)
|
||||
{
|
||||
dirtyMatches.Add(oldMatch);
|
||||
matchObjects[oldMatch].Remove(networkMatch);
|
||||
}
|
||||
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (networkMatch.matchId == Guid.Empty)
|
||||
return;
|
||||
|
||||
dirtyMatches.Add(networkMatch.matchId);
|
||||
|
||||
// Make sure this new match is in the dictionary
|
||||
if (!matchObjects.ContainsKey(networkMatch.matchId))
|
||||
matchObjects[networkMatch.matchId] = new HashSet<NetworkMatch>();
|
||||
|
||||
// Add this object to the hashset of the new match
|
||||
matchObjects[networkMatch.matchId].Add(networkMatch);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnSpawned(NetworkIdentity identity)
|
||||
{
|
||||
if (!identity.TryGetComponent(out NetworkMatch networkMatch))
|
||||
return;
|
||||
|
||||
Guid networkMatchId = networkMatch.matchId;
|
||||
|
||||
// Guid.Empty is never a valid matchId...do not add to matchObjects collection
|
||||
if (networkMatchId == Guid.Empty)
|
||||
return;
|
||||
|
||||
// Debug.Log($"MatchInterestManagement.OnSpawned({identity.name}) currentMatch: {currentMatch}");
|
||||
if (!matchObjects.TryGetValue(networkMatchId, out HashSet<NetworkMatch> objects))
|
||||
{
|
||||
objects = new HashSet<NetworkMatch>();
|
||||
matchObjects.Add(networkMatchId, objects);
|
||||
}
|
||||
|
||||
objects.Add(networkMatch);
|
||||
|
||||
// Match ID could have been set in NetworkBehaviour::OnStartServer on this object.
|
||||
// Since that's after OnCheckObserver is called it would be missed, so force Rebuild here.
|
||||
// Add the current match to dirtyMatches for LateUpdate to rebuild it.
|
||||
dirtyMatches.Add(networkMatchId);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnDestroyed(NetworkIdentity identity)
|
||||
{
|
||||
// Don't RebuildSceneObservers here - that will happen in LateUpdate.
|
||||
// Multiple objects could be destroyed in same frame and we don't
|
||||
// want to rebuild for each one...let LateUpdate do it once.
|
||||
// We must add the current match to dirtyMatches for LateUpdate to rebuild it.
|
||||
if (identity.TryGetComponent(out NetworkMatch currentMatch))
|
||||
{
|
||||
if (currentMatch.matchId != Guid.Empty &&
|
||||
matchObjects.TryGetValue(currentMatch.matchId, out HashSet<NetworkMatch> objects) &&
|
||||
objects.Remove(currentMatch))
|
||||
dirtyMatches.Add(currentMatch.matchId);
|
||||
}
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
|
||||
{
|
||||
// Never observed if no NetworkMatch component
|
||||
if (!identity.TryGetComponent(out NetworkMatch identityNetworkMatch))
|
||||
return false;
|
||||
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (identityNetworkMatch.matchId == Guid.Empty)
|
||||
return false;
|
||||
|
||||
// Never observed if no NetworkMatch component
|
||||
if (!newObserver.identity.TryGetComponent(out NetworkMatch newObserverNetworkMatch))
|
||||
return false;
|
||||
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (newObserverNetworkMatch.matchId == Guid.Empty)
|
||||
return false;
|
||||
|
||||
return identityNetworkMatch.matchId == newObserverNetworkMatch.matchId;
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
if (!identity.TryGetComponent(out NetworkMatch networkMatch))
|
||||
return;
|
||||
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (networkMatch.matchId == Guid.Empty)
|
||||
return;
|
||||
|
||||
// Abort if this match hasn't been created yet by OnSpawned or OnMatchChanged
|
||||
if (!matchObjects.TryGetValue(networkMatch.matchId, out HashSet<NetworkMatch> objects))
|
||||
return;
|
||||
|
||||
// Add everything in the hashset for this object's current match
|
||||
foreach (NetworkMatch netMatch in objects)
|
||||
if (netMatch.netIdentity != null && netMatch.netIdentity.connectionToClient != null)
|
||||
newObservers.Add(netMatch.netIdentity.connectionToClient);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d09f5c8bf2f4747b7a9284ef5d9ce2a7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,42 @@
|
||||
// simple component that holds match information
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/ Interest Management/ Match/Network Match")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")]
|
||||
public class NetworkMatch : NetworkBehaviour
|
||||
{
|
||||
Guid _matchId;
|
||||
|
||||
#pragma warning disable IDE0052 // Suppress warning for unused field...this is for debugging purposes
|
||||
[SerializeField, ReadOnly]
|
||||
[Tooltip("Match ID is shown here on server for debugging purposes.")]
|
||||
string MatchID = string.Empty;
|
||||
#pragma warning restore IDE0052
|
||||
|
||||
///<summary>Set this to the same value on all networked objects that belong to a given match</summary>
|
||||
public Guid matchId
|
||||
{
|
||||
get => _matchId;
|
||||
set
|
||||
{
|
||||
if (!NetworkServer.active)
|
||||
throw new InvalidOperationException("matchId can only be set at runtime on active server");
|
||||
|
||||
if (_matchId == value)
|
||||
return;
|
||||
|
||||
Guid oldMatch = _matchId;
|
||||
_matchId = value;
|
||||
MatchID = value.ToString();
|
||||
|
||||
// Only inform the AOI if this netIdentity has been spawned (isServer) and only if using a MatchInterestManagement
|
||||
if (isServer && NetworkServer.aoi is MatchInterestManagement matchInterestManagement)
|
||||
matchInterestManagement.OnMatchChanged(this, oldMatch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5d17e718851449a6879986e45c458fb7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
3
Assets/Mirror/Components/InterestManagement/Scene.meta
Normal file
3
Assets/Mirror/Components/InterestManagement/Scene.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7655d309a46a4bd4860edf964228b3f6
|
||||
timeCreated: 1622649517
|
@ -0,0 +1,118 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/ Interest Management/ Scene/Scene Interest Management")]
|
||||
public class SceneInterestManagement : InterestManagement
|
||||
{
|
||||
// Use Scene instead of string scene.name because when additively
|
||||
// loading multiples of a subscene the name won't be unique
|
||||
readonly Dictionary<Scene, HashSet<NetworkIdentity>> sceneObjects =
|
||||
new Dictionary<Scene, HashSet<NetworkIdentity>>();
|
||||
|
||||
readonly Dictionary<NetworkIdentity, Scene> lastObjectScene =
|
||||
new Dictionary<NetworkIdentity, Scene>();
|
||||
|
||||
HashSet<Scene> dirtyScenes = new HashSet<Scene>();
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnSpawned(NetworkIdentity identity)
|
||||
{
|
||||
Scene currentScene = identity.gameObject.scene;
|
||||
lastObjectScene[identity] = currentScene;
|
||||
// Debug.Log($"SceneInterestManagement.OnSpawned({identity.name}) currentScene: {currentScene}");
|
||||
if (!sceneObjects.TryGetValue(currentScene, out HashSet<NetworkIdentity> objects))
|
||||
{
|
||||
objects = new HashSet<NetworkIdentity>();
|
||||
sceneObjects.Add(currentScene, objects);
|
||||
}
|
||||
|
||||
objects.Add(identity);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnDestroyed(NetworkIdentity identity)
|
||||
{
|
||||
// Don't RebuildSceneObservers here - that will happen in Update.
|
||||
// Multiple objects could be destroyed in same frame and we don't
|
||||
// want to rebuild for each one...let Update do it once.
|
||||
// We must add the current scene to dirtyScenes for Update to rebuild it.
|
||||
if (lastObjectScene.TryGetValue(identity, out Scene currentScene))
|
||||
{
|
||||
lastObjectScene.Remove(identity);
|
||||
if (sceneObjects.TryGetValue(currentScene, out HashSet<NetworkIdentity> objects) && objects.Remove(identity))
|
||||
dirtyScenes.Add(currentScene);
|
||||
}
|
||||
}
|
||||
|
||||
// internal so we can update from tests
|
||||
[ServerCallback]
|
||||
internal void Update()
|
||||
{
|
||||
// for each spawned:
|
||||
// if scene changed:
|
||||
// add previous to dirty
|
||||
// add new to dirty
|
||||
foreach (NetworkIdentity identity in NetworkServer.spawned.Values)
|
||||
{
|
||||
if (!lastObjectScene.TryGetValue(identity, out Scene currentScene))
|
||||
continue;
|
||||
|
||||
Scene newScene = identity.gameObject.scene;
|
||||
if (newScene == currentScene)
|
||||
continue;
|
||||
|
||||
// Mark new/old scenes as dirty so they get rebuilt
|
||||
dirtyScenes.Add(currentScene);
|
||||
dirtyScenes.Add(newScene);
|
||||
|
||||
// This object is in a new scene so observers in the prior scene
|
||||
// and the new scene need to rebuild their respective observers lists.
|
||||
|
||||
// Remove this object from the hashset of the scene it just left
|
||||
sceneObjects[currentScene].Remove(identity);
|
||||
|
||||
// Set this to the new scene this object just entered
|
||||
lastObjectScene[identity] = newScene;
|
||||
|
||||
// Make sure this new scene is in the dictionary
|
||||
if (!sceneObjects.ContainsKey(newScene))
|
||||
sceneObjects.Add(newScene, new HashSet<NetworkIdentity>());
|
||||
|
||||
// Add this object to the hashset of the new scene
|
||||
sceneObjects[newScene].Add(identity);
|
||||
}
|
||||
|
||||
// rebuild all dirty scenes
|
||||
foreach (Scene dirtyScene in dirtyScenes)
|
||||
RebuildSceneObservers(dirtyScene);
|
||||
|
||||
dirtyScenes.Clear();
|
||||
}
|
||||
|
||||
void RebuildSceneObservers(Scene scene)
|
||||
{
|
||||
foreach (NetworkIdentity netIdentity in sceneObjects[scene])
|
||||
if (netIdentity != null)
|
||||
NetworkServer.RebuildObservers(netIdentity, false);
|
||||
}
|
||||
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
|
||||
{
|
||||
return identity.gameObject.scene == newObserver.identity.gameObject.scene;
|
||||
}
|
||||
|
||||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
if (!sceneObjects.TryGetValue(identity.gameObject.scene, out HashSet<NetworkIdentity> objects))
|
||||
return;
|
||||
|
||||
// Add everything in the hashset for this object's current scene
|
||||
foreach (NetworkIdentity networkIdentity in objects)
|
||||
if (networkIdentity != null && networkIdentity.connectionToClient != null)
|
||||
newObservers.Add(networkIdentity.connectionToClient);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b979f26c95d34324ba005bfacfa9c4fc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cfa12b73503344d49b398b01bcb07967
|
||||
timeCreated: 1613110634
|
@ -0,0 +1,104 @@
|
||||
// Grid2D from uMMORPG: get/set values of type T at any point
|
||||
// -> not named 'Grid' because Unity already has a Grid type. causes warnings.
|
||||
// -> struct to avoid memory indirection. it's accessed a lot.
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// struct to avoid memory indirection. it's accessed a lot.
|
||||
public struct Grid2D<T>
|
||||
{
|
||||
// the grid
|
||||
// note that we never remove old keys.
|
||||
// => over time, HashSet<T>s will be allocated for every possible
|
||||
// grid position in the world
|
||||
// => Clear() doesn't clear them so we don't constantly reallocate the
|
||||
// entries when populating the grid in every Update() call
|
||||
// => makes the code a lot easier too
|
||||
// => this is FINE because in the worst case, every grid position in the
|
||||
// game world is filled with a player anyway!
|
||||
readonly Dictionary<Vector2Int, HashSet<T>> grid;
|
||||
|
||||
// cache a 9 neighbor grid of vector2 offsets so we can use them more easily
|
||||
readonly Vector2Int[] neighbourOffsets;
|
||||
|
||||
public Grid2D(int initialCapacity)
|
||||
{
|
||||
grid = new Dictionary<Vector2Int, HashSet<T>>(initialCapacity);
|
||||
|
||||
neighbourOffsets = new[] {
|
||||
Vector2Int.up,
|
||||
Vector2Int.up + Vector2Int.left,
|
||||
Vector2Int.up + Vector2Int.right,
|
||||
Vector2Int.left,
|
||||
Vector2Int.zero,
|
||||
Vector2Int.right,
|
||||
Vector2Int.down,
|
||||
Vector2Int.down + Vector2Int.left,
|
||||
Vector2Int.down + Vector2Int.right
|
||||
};
|
||||
}
|
||||
|
||||
// helper function so we can add an entry without worrying
|
||||
public void Add(Vector2Int position, T value)
|
||||
{
|
||||
// initialize set in grid if it's not in there yet
|
||||
if (!grid.TryGetValue(position, out HashSet<T> hashSet))
|
||||
{
|
||||
// each grid entry may hold hundreds of entities.
|
||||
// let's create the HashSet with a large initial capacity
|
||||
// in order to avoid resizing & allocations.
|
||||
#if !UNITY_2021_3_OR_NEWER
|
||||
// Unity 2019 doesn't have "new HashSet(capacity)" yet
|
||||
hashSet = new HashSet<T>();
|
||||
#else
|
||||
hashSet = new HashSet<T>(128);
|
||||
#endif
|
||||
grid[position] = hashSet;
|
||||
}
|
||||
|
||||
// add to it
|
||||
hashSet.Add(value);
|
||||
}
|
||||
|
||||
// helper function to get set at position without worrying
|
||||
// -> result is passed as parameter to avoid allocations
|
||||
// -> result is not cleared before. this allows us to pass the HashSet from
|
||||
// GetWithNeighbours and avoid .UnionWith which is very expensive.
|
||||
void GetAt(Vector2Int position, HashSet<T> result)
|
||||
{
|
||||
// return the set at position
|
||||
if (grid.TryGetValue(position, out HashSet<T> hashSet))
|
||||
{
|
||||
foreach (T entry in hashSet)
|
||||
result.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// helper function to get at position and it's 8 neighbors without worrying
|
||||
// -> result is passed as parameter to avoid allocations
|
||||
public void GetWithNeighbours(Vector2Int position, HashSet<T> result)
|
||||
{
|
||||
// clear result first
|
||||
result.Clear();
|
||||
|
||||
// add neighbours
|
||||
foreach (Vector2Int offset in neighbourOffsets)
|
||||
GetAt(position + offset, result);
|
||||
}
|
||||
|
||||
// clear: clears the whole grid
|
||||
// IMPORTANT: we already allocated HashSet<T>s and don't want to do
|
||||
// reallocate every single update when we rebuild the grid.
|
||||
// => so simply remove each position's entries, but keep
|
||||
// every position in there
|
||||
// => see 'grid' comments above!
|
||||
// => named ClearNonAlloc to make it more obvious!
|
||||
public void ClearNonAlloc()
|
||||
{
|
||||
foreach (HashSet<T> hashSet in grid.Values)
|
||||
hashSet.Clear();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c5232a4d2854116a35d52b80ec07752
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,154 @@
|
||||
// extremely fast spatial hashing interest management based on uMMORPG GridChecker.
|
||||
// => 30x faster in initial tests
|
||||
// => scales way higher
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/ Interest Management/ Spatial Hash/Spatial Hashing Interest Management")]
|
||||
public class SpatialHashingInterestManagement : InterestManagement
|
||||
{
|
||||
[Tooltip("The maximum range that objects will be visible at.")]
|
||||
public int visRange = 30;
|
||||
|
||||
// we use a 9 neighbour grid.
|
||||
// so we always see in a distance of 2 grids.
|
||||
// for example, our own grid and then one on top / below / left / right.
|
||||
//
|
||||
// this means that grid resolution needs to be distance / 2.
|
||||
// so for example, for distance = 30 we see 2 cells = 15 * 2 distance.
|
||||
//
|
||||
// on first sight, it seems we need distance / 3 (we see left/us/right).
|
||||
// but that's not the case.
|
||||
// resolution would be 10, and we only see 1 cell far, so 10+10=20.
|
||||
public int resolution => visRange / 2;
|
||||
|
||||
[Tooltip("Rebuild all every 'rebuildInterval' seconds.")]
|
||||
public float rebuildInterval = 1;
|
||||
double lastRebuildTime;
|
||||
|
||||
public enum CheckMethod
|
||||
{
|
||||
XZ_FOR_3D,
|
||||
XY_FOR_2D
|
||||
}
|
||||
[Tooltip("Spatial Hashing supports 3D (XZ) and 2D (XY) games.")]
|
||||
public CheckMethod checkMethod = CheckMethod.XZ_FOR_3D;
|
||||
|
||||
// debugging
|
||||
public bool showSlider;
|
||||
|
||||
// the grid
|
||||
// begin with a large capacity to avoid resizing & allocations.
|
||||
Grid2D<NetworkConnectionToClient> grid = new Grid2D<NetworkConnectionToClient>(1024);
|
||||
|
||||
// project 3d world position to grid position
|
||||
Vector2Int ProjectToGrid(Vector3 position) =>
|
||||
checkMethod == CheckMethod.XZ_FOR_3D
|
||||
? Vector2Int.RoundToInt(new Vector2(position.x, position.z) / resolution)
|
||||
: Vector2Int.RoundToInt(new Vector2(position.x, position.y) / resolution);
|
||||
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
|
||||
{
|
||||
// calculate projected positions
|
||||
Vector2Int projected = ProjectToGrid(identity.transform.position);
|
||||
Vector2Int observerProjected = ProjectToGrid(newObserver.identity.transform.position);
|
||||
|
||||
// distance needs to be at max one of the 8 neighbors, which is
|
||||
// 1 for the direct neighbors
|
||||
// 1.41 for the diagonal neighbors (= sqrt(2))
|
||||
// => use sqrMagnitude and '2' to avoid computations. same result.
|
||||
return (projected - observerProjected).sqrMagnitude <= 2;
|
||||
}
|
||||
|
||||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
// add everyone in 9 neighbour grid
|
||||
// -> pass observers to GetWithNeighbours directly to avoid allocations
|
||||
// and expensive .UnionWith computations.
|
||||
Vector2Int current = ProjectToGrid(identity.transform.position);
|
||||
grid.GetWithNeighbours(current, newObservers);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void ResetState()
|
||||
{
|
||||
lastRebuildTime = 0D;
|
||||
}
|
||||
|
||||
// update everyone's position in the grid
|
||||
// (internal so we can update from tests)
|
||||
[ServerCallback]
|
||||
internal void Update()
|
||||
{
|
||||
// NOTE: unlike Scene/MatchInterestManagement, this rebuilds ALL
|
||||
// entities every INTERVAL. consider the other approach later.
|
||||
|
||||
// IMPORTANT: refresh grid every update!
|
||||
// => newly spawned entities get observers assigned via
|
||||
// OnCheckObservers. this can happen any time and we don't want
|
||||
// them broadcast to old (moved or destroyed) connections.
|
||||
// => players do move all the time. we want them to always be in the
|
||||
// correct grid position.
|
||||
// => note that the actual 'rebuildall' doesn't need to happen all
|
||||
// the time.
|
||||
// NOTE: consider refreshing grid only every 'interval' too. but not
|
||||
// for now. stability & correctness matter.
|
||||
|
||||
// clear old grid results before we update everyone's position.
|
||||
// (this way we get rid of destroyed connections automatically)
|
||||
//
|
||||
// NOTE: keeps allocated HashSets internally.
|
||||
// clearing & populating every frame works without allocations
|
||||
grid.ClearNonAlloc();
|
||||
|
||||
// put every connection into the grid at it's main player's position
|
||||
// NOTE: player sees in a radius around him. NOT around his pet too.
|
||||
foreach (NetworkConnectionToClient connection in NetworkServer.connections.Values)
|
||||
{
|
||||
// authenticated and joined world with a player?
|
||||
if (connection.isAuthenticated && connection.identity != null)
|
||||
{
|
||||
// calculate current grid position
|
||||
Vector2Int position = ProjectToGrid(connection.identity.transform.position);
|
||||
|
||||
// put into grid
|
||||
grid.Add(position, connection);
|
||||
}
|
||||
}
|
||||
|
||||
// rebuild all spawned entities' observers every 'interval'
|
||||
// this will call OnRebuildObservers which then returns the
|
||||
// observers at grid[position] for each entity.
|
||||
if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval)
|
||||
{
|
||||
RebuildAll();
|
||||
lastRebuildTime = NetworkTime.localTime;
|
||||
}
|
||||
}
|
||||
|
||||
// OnGUI allocates even if it does nothing. avoid in release.
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
// slider from dotsnet. it's nice to play around with in the benchmark
|
||||
// demo.
|
||||
void OnGUI()
|
||||
{
|
||||
if (!showSlider) return;
|
||||
|
||||
// only show while server is running. not on client, etc.
|
||||
if (!NetworkServer.active) return;
|
||||
|
||||
int height = 30;
|
||||
int width = 250;
|
||||
GUILayout.BeginArea(new Rect(Screen.width / 2 - width / 2, Screen.height - height, width, height));
|
||||
GUILayout.BeginHorizontal("Box");
|
||||
GUILayout.Label("Radius:");
|
||||
visRange = Mathf.RoundToInt(GUILayout.HorizontalSlider(visRange, 0, 200, GUILayout.Width(150)));
|
||||
GUILayout.Label(visRange.ToString());
|
||||
GUILayout.EndHorizontal();
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 39adc6e09d5544ed955a50ce8600355a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/Mirror/Components/InterestManagement/Team.meta
Normal file
8
Assets/Mirror/Components/InterestManagement/Team.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d418e60072433b4bbebbf5f3a7de1bb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,39 @@
|
||||
// simple component that holds team information
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/ Interest Management/ Team/Network Team")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")]
|
||||
public class NetworkTeam : NetworkBehaviour
|
||||
{
|
||||
[SerializeField]
|
||||
[Tooltip("Set teamId on Server at runtime to the same value on all networked objects that belong to a given team")]
|
||||
string _teamId;
|
||||
|
||||
public string teamId
|
||||
{
|
||||
get => _teamId;
|
||||
set
|
||||
{
|
||||
if (Application.IsPlaying(gameObject) && !NetworkServer.active)
|
||||
throw new InvalidOperationException("teamId can only be set at runtime on active server");
|
||||
|
||||
if (_teamId == value)
|
||||
return;
|
||||
|
||||
string oldTeam = _teamId;
|
||||
_teamId = value;
|
||||
|
||||
//Only inform the AOI if this netIdentity has been spawned(isServer) and only if using a TeamInterestManagement
|
||||
if (isServer && NetworkServer.aoi is TeamInterestManagement teamInterestManagement)
|
||||
teamInterestManagement.OnTeamChanged(this, oldTeam);
|
||||
}
|
||||
}
|
||||
|
||||
[Tooltip("When enabled this object is visible to all clients. Typically this would be true for player objects")]
|
||||
public bool forceShown;
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2576730625b1632468cbcbfe5e721f88
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,182 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/ Interest Management/ Team/Team Interest Management")]
|
||||
public class TeamInterestManagement : InterestManagement
|
||||
{
|
||||
readonly Dictionary<string, HashSet<NetworkTeam>> teamObjects =
|
||||
new Dictionary<string, HashSet<NetworkTeam>>();
|
||||
|
||||
readonly HashSet<string> dirtyTeams = new HashSet<string>();
|
||||
|
||||
// LateUpdate so that all spawns/despawns/changes are done
|
||||
[ServerCallback]
|
||||
void LateUpdate()
|
||||
{
|
||||
// Rebuild all dirty teams
|
||||
// dirtyTeams will be empty if no teams changed members
|
||||
// by spawning or destroying or changing teamId in this frame.
|
||||
foreach (string dirtyTeam in dirtyTeams)
|
||||
{
|
||||
// rebuild always, even if teamObjects[dirtyTeam] is empty.
|
||||
// Players might have left the team, but they may still be spawned.
|
||||
RebuildTeamObservers(dirtyTeam);
|
||||
|
||||
// clean up empty entries in the dict
|
||||
if (teamObjects[dirtyTeam].Count == 0)
|
||||
teamObjects.Remove(dirtyTeam);
|
||||
}
|
||||
|
||||
dirtyTeams.Clear();
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
void RebuildTeamObservers(string teamId)
|
||||
{
|
||||
foreach (NetworkTeam networkTeam in teamObjects[teamId])
|
||||
if (networkTeam.netIdentity != null)
|
||||
NetworkServer.RebuildObservers(networkTeam.netIdentity, false);
|
||||
}
|
||||
|
||||
// called by NetworkTeam.teamId setter
|
||||
[ServerCallback]
|
||||
internal void OnTeamChanged(NetworkTeam networkTeam, string oldTeam)
|
||||
{
|
||||
// This object is in a new team so observers in the prior team
|
||||
// and the new team need to rebuild their respective observers lists.
|
||||
|
||||
// Remove this object from the hashset of the team it just left
|
||||
// Null / Empty string is never a valid teamId
|
||||
if (!string.IsNullOrWhiteSpace(oldTeam))
|
||||
{
|
||||
dirtyTeams.Add(oldTeam);
|
||||
teamObjects[oldTeam].Remove(networkTeam);
|
||||
}
|
||||
|
||||
// Null / Empty string is never a valid teamId
|
||||
if (string.IsNullOrWhiteSpace(networkTeam.teamId))
|
||||
return;
|
||||
|
||||
dirtyTeams.Add(networkTeam.teamId);
|
||||
|
||||
// Make sure this new team is in the dictionary
|
||||
if (!teamObjects.ContainsKey(networkTeam.teamId))
|
||||
teamObjects[networkTeam.teamId] = new HashSet<NetworkTeam>();
|
||||
|
||||
// Add this object to the hashset of the new team
|
||||
teamObjects[networkTeam.teamId].Add(networkTeam);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnSpawned(NetworkIdentity identity)
|
||||
{
|
||||
if (!identity.TryGetComponent(out NetworkTeam networkTeam))
|
||||
return;
|
||||
|
||||
string networkTeamId = networkTeam.teamId;
|
||||
|
||||
// Null / Empty string is never a valid teamId...do not add to teamObjects collection
|
||||
if (string.IsNullOrWhiteSpace(networkTeamId))
|
||||
return;
|
||||
|
||||
// Debug.Log($"TeamInterestManagement.OnSpawned({identity.name}) currentTeam: {currentTeam}");
|
||||
if (!teamObjects.TryGetValue(networkTeamId, out HashSet<NetworkTeam> objects))
|
||||
{
|
||||
objects = new HashSet<NetworkTeam>();
|
||||
teamObjects.Add(networkTeamId, objects);
|
||||
}
|
||||
|
||||
objects.Add(networkTeam);
|
||||
|
||||
// Team ID could have been set in NetworkBehaviour::OnStartServer on this object.
|
||||
// Since that's after OnCheckObserver is called it would be missed, so force Rebuild here.
|
||||
// Add the current team to dirtyTeames for LateUpdate to rebuild it.
|
||||
dirtyTeams.Add(networkTeamId);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnDestroyed(NetworkIdentity identity)
|
||||
{
|
||||
// Don't RebuildSceneObservers here - that will happen in LateUpdate.
|
||||
// Multiple objects could be destroyed in same frame and we don't
|
||||
// want to rebuild for each one...let LateUpdate do it once.
|
||||
// We must add the current team to dirtyTeames for LateUpdate to rebuild it.
|
||||
if (identity.TryGetComponent(out NetworkTeam currentTeam))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(currentTeam.teamId) &&
|
||||
teamObjects.TryGetValue(currentTeam.teamId, out HashSet<NetworkTeam> objects) &&
|
||||
objects.Remove(currentTeam))
|
||||
dirtyTeams.Add(currentTeam.teamId);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
|
||||
{
|
||||
// Always observed if no NetworkTeam component
|
||||
if (!identity.TryGetComponent(out NetworkTeam identityNetworkTeam))
|
||||
return true;
|
||||
|
||||
if (identityNetworkTeam.forceShown)
|
||||
return true;
|
||||
|
||||
// Null / Empty string is never a valid teamId
|
||||
if (string.IsNullOrWhiteSpace(identityNetworkTeam.teamId))
|
||||
return false;
|
||||
|
||||
// Always observed if no NetworkTeam component
|
||||
if (!newObserver.identity.TryGetComponent(out NetworkTeam newObserverNetworkTeam))
|
||||
return true;
|
||||
|
||||
// Null / Empty string is never a valid teamId
|
||||
if (string.IsNullOrWhiteSpace(newObserverNetworkTeam.teamId))
|
||||
return false;
|
||||
|
||||
//Debug.Log($"TeamInterestManagement.OnCheckObserver {identity.name} {identityNetworkTeam.teamId} | {newObserver.identity.name} {newObserverNetworkTeam.teamId}");
|
||||
|
||||
// Observed only if teamId's team
|
||||
return identityNetworkTeam.teamId == newObserverNetworkTeam.teamId;
|
||||
}
|
||||
|
||||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
// If this object doesn't have a NetworkTeam then it's visible to all clients
|
||||
if (!identity.TryGetComponent(out NetworkTeam networkTeam))
|
||||
{
|
||||
AddAllConnections(newObservers);
|
||||
return;
|
||||
}
|
||||
|
||||
// If this object has NetworkTeam and forceShown == true then it's visible to all clients
|
||||
if (networkTeam.forceShown)
|
||||
{
|
||||
AddAllConnections(newObservers);
|
||||
return;
|
||||
}
|
||||
|
||||
// Null / Empty string is never a valid teamId
|
||||
if (string.IsNullOrWhiteSpace(networkTeam.teamId))
|
||||
return;
|
||||
|
||||
// Abort if this team hasn't been created yet by OnSpawned or OnTeamChanged
|
||||
if (!teamObjects.TryGetValue(networkTeam.teamId, out HashSet<NetworkTeam> objects))
|
||||
return;
|
||||
|
||||
// Add everything in the hashset for this object's current team
|
||||
foreach (NetworkTeam netTeam in objects)
|
||||
if (netTeam.netIdentity != null && netTeam.netIdentity.connectionToClient != null)
|
||||
newObservers.Add(netTeam.netIdentity.connectionToClient);
|
||||
}
|
||||
|
||||
void AddAllConnections(HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values)
|
||||
{
|
||||
// authenticated and joined world with a player?
|
||||
if (conn != null && conn.isAuthenticated && conn.identity != null)
|
||||
newObservers.Add(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dceb9a7085758fd4590419ff5b14b636
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/Mirror/Components/LagCompensation.meta
Normal file
8
Assets/Mirror/Components/LagCompensation.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 00ac1d0527f234939aba22b4d7cbf280
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
107
Assets/Mirror/Components/LagCompensation/HistoryCollider.cs
Normal file
107
Assets/Mirror/Components/LagCompensation/HistoryCollider.cs
Normal file
@ -0,0 +1,107 @@
|
||||
// Applies HistoryBounds to the physics world by projecting to a trigger Collider.
|
||||
// This way we can use Physics.Raycast on it.
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public class HistoryCollider : MonoBehaviour
|
||||
{
|
||||
[Header("Components")]
|
||||
[Tooltip("The object's actual collider. We need to know where it is, and how large it is.")]
|
||||
public Collider actualCollider;
|
||||
|
||||
[Tooltip("The helper collider that the history bounds are projected onto.\nNeeds to be added to a child GameObject to counter-rotate an axis aligned Bounding Box onto it.\nThis is only used by this component.")]
|
||||
public BoxCollider boundsCollider;
|
||||
|
||||
[Header("History")]
|
||||
[Tooltip("Keep this many past bounds in the buffer. The larger this is, the further we can raycast into the past.\nMaximum time := historyAmount * captureInterval")]
|
||||
public int boundsLimit = 8;
|
||||
|
||||
[Tooltip("Gather N bounds at a time into a bucket for faster encapsulation. A factor of 2 will be twice as fast, etc.")]
|
||||
public int boundsPerBucket = 2;
|
||||
|
||||
[Tooltip("Capture bounds every 'captureInterval' seconds. Larger values will require fewer computations, but may not capture every small move.")]
|
||||
public float captureInterval = 0.100f; // 100 ms
|
||||
double lastCaptureTime = 0;
|
||||
|
||||
[Header("Debug")]
|
||||
public Color historyColor = new Color(1.0f, 0.5f, 0.0f, 1.0f);
|
||||
public Color currentColor = Color.red;
|
||||
|
||||
protected HistoryBounds history = null;
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
history = new HistoryBounds(boundsLimit, boundsPerBucket);
|
||||
|
||||
// ensure colliders were set.
|
||||
// bounds collider should always be a trigger.
|
||||
if (actualCollider == null) Debug.LogError("HistoryCollider: actualCollider was not set.");
|
||||
if (boundsCollider == null) Debug.LogError("HistoryCollider: boundsCollider was not set.");
|
||||
if (boundsCollider.transform.parent != transform) Debug.LogError("HistoryCollider: boundsCollider must be a child of this GameObject.");
|
||||
if (!boundsCollider.isTrigger) Debug.LogError("HistoryCollider: boundsCollider must be a trigger.");
|
||||
}
|
||||
|
||||
// capturing and projecting onto colliders should use physics update
|
||||
protected virtual void FixedUpdate()
|
||||
{
|
||||
// capture current bounds every interval
|
||||
if (NetworkTime.localTime >= lastCaptureTime + captureInterval)
|
||||
{
|
||||
lastCaptureTime = NetworkTime.localTime;
|
||||
CaptureBounds();
|
||||
}
|
||||
|
||||
// project bounds onto helper collider
|
||||
ProjectBounds();
|
||||
}
|
||||
|
||||
protected virtual void CaptureBounds()
|
||||
{
|
||||
// grab current collider bounds
|
||||
// this is in world space coordinates, and axis aligned
|
||||
// TODO double check
|
||||
Bounds bounds = actualCollider.bounds;
|
||||
|
||||
// insert into history
|
||||
history.Insert(bounds);
|
||||
}
|
||||
|
||||
protected virtual void ProjectBounds()
|
||||
{
|
||||
// grab total collider encapsulating all of history
|
||||
Bounds total = history.total;
|
||||
|
||||
// don't assign empty bounds, this will throw a Unity warning
|
||||
if (history.boundsCount == 0) return;
|
||||
|
||||
// scale projection doesn't work yet.
|
||||
// for now, don't allow scale changes.
|
||||
if (transform.lossyScale != Vector3.one)
|
||||
{
|
||||
Debug.LogWarning($"HistoryCollider: {name}'s transform global scale must be (1,1,1).");
|
||||
return;
|
||||
}
|
||||
|
||||
// counter rotate the child collider against the gameobject's rotation.
|
||||
// we need this to always be axis aligned.
|
||||
boundsCollider.transform.localRotation = Quaternion.Inverse(transform.rotation);
|
||||
|
||||
// project world space bounds to collider's local space
|
||||
boundsCollider.center = boundsCollider.transform.InverseTransformPoint(total.center);
|
||||
boundsCollider.size = total.size; // TODO projection?
|
||||
}
|
||||
|
||||
// TODO runtime drawing for debugging?
|
||||
protected virtual void OnDrawGizmos()
|
||||
{
|
||||
// draw total bounds
|
||||
Gizmos.color = historyColor;
|
||||
Gizmos.DrawWireCube(history.total.center, history.total.size);
|
||||
|
||||
// draw current bounds
|
||||
Gizmos.color = currentColor;
|
||||
Gizmos.DrawWireCube(actualCollider.bounds.center, actualCollider.bounds.size);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f5f2158d9776d4b569858f793be4da60
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
196
Assets/Mirror/Components/LagCompensation/LagCompensator.cs
Normal file
196
Assets/Mirror/Components/LagCompensation/LagCompensator.cs
Normal file
@ -0,0 +1,196 @@
|
||||
// Add this component to a Player object with collider.
|
||||
// Automatically keeps a history for lag compensation.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public struct Capture3D : Capture
|
||||
{
|
||||
public double timestamp { get; set; }
|
||||
public Vector3 position;
|
||||
public Vector3 size;
|
||||
|
||||
public Capture3D(double timestamp, Vector3 position, Vector3 size)
|
||||
{
|
||||
this.timestamp = timestamp;
|
||||
this.position = position;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public void DrawGizmo()
|
||||
{
|
||||
Gizmos.DrawWireCube(position, size);
|
||||
}
|
||||
|
||||
public static Capture3D Interpolate(Capture3D from, Capture3D to, double t) =>
|
||||
new Capture3D(
|
||||
0, // interpolated snapshot is applied directly. don't need timestamps.
|
||||
Vector3.LerpUnclamped(from.position, to.position, (float)t),
|
||||
Vector3.LerpUnclamped(from.size, to.size, (float)t)
|
||||
);
|
||||
|
||||
public override string ToString() => $"(time={timestamp} pos={position} size={size})";
|
||||
}
|
||||
|
||||
[Obsolete("This is a preview version. Community feedback is welcome!")]
|
||||
public class LagCompensator : NetworkBehaviour
|
||||
{
|
||||
[Header("Components")]
|
||||
[Tooltip("The collider to keep a history of.")]
|
||||
public Collider trackedCollider; // assign this in inspector
|
||||
|
||||
[Header("Settings")]
|
||||
public LagCompensationSettings lagCompensationSettings = new LagCompensationSettings();
|
||||
double lastCaptureTime;
|
||||
|
||||
// lag compensation history of <timestamp, capture>
|
||||
readonly Queue<KeyValuePair<double, Capture3D>> history = new Queue<KeyValuePair<double, Capture3D>>();
|
||||
|
||||
[Header("Debugging")]
|
||||
public Color historyColor = Color.white;
|
||||
|
||||
protected virtual void Update()
|
||||
{
|
||||
// only capture on server
|
||||
if (!NetworkServer.active) return;
|
||||
|
||||
// capture lag compensation snapshots every interval.
|
||||
// NetworkTime.localTime because Unity 2019 doesn't have 'double' time yet.
|
||||
if (NetworkTime.localTime >= lastCaptureTime + lagCompensationSettings.captureInterval)
|
||||
{
|
||||
lastCaptureTime = NetworkTime.localTime;
|
||||
Capture();
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Capture()
|
||||
{
|
||||
// capture current state
|
||||
Capture3D capture = new Capture3D(
|
||||
NetworkTime.localTime,
|
||||
trackedCollider.bounds.center,
|
||||
trackedCollider.bounds.size
|
||||
);
|
||||
|
||||
// insert into history
|
||||
LagCompensation.Insert(history, lagCompensationSettings.historyLimit, NetworkTime.localTime, capture);
|
||||
}
|
||||
|
||||
protected virtual void OnDrawGizmos()
|
||||
{
|
||||
// draw history
|
||||
Gizmos.color = historyColor;
|
||||
LagCompensation.DrawGizmos(history);
|
||||
}
|
||||
|
||||
// sampling ////////////////////////////////////////////////////////////
|
||||
// sample the sub-tick (=interpolated) history of this object for a hit test.
|
||||
// 'viewer' needs to be the player who fired!
|
||||
// for example, if A fires at B, then call B.Sample(viewer, point, tolerance).
|
||||
[Server]
|
||||
public virtual bool Sample(NetworkConnectionToClient viewer, out Capture3D sample)
|
||||
{
|
||||
// never trust the client: estimate client time instead.
|
||||
// https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
|
||||
// the estimation is very good. the error is as low as ~6ms for the demo.
|
||||
// note that passing 'rtt' is fine: EstimateTime halves it to latency.
|
||||
double estimatedTime = LagCompensation.EstimateTime(NetworkTime.localTime, viewer.rtt, NetworkClient.bufferTime);
|
||||
|
||||
// sample the history to get the nearest snapshots around 'timestamp'
|
||||
if (LagCompensation.Sample(history, estimatedTime, lagCompensationSettings.captureInterval, out Capture3D resultBefore, out Capture3D resultAfter, out double t))
|
||||
{
|
||||
// interpolate to get a decent estimation at exactly 'timestamp'
|
||||
sample = Capture3D.Interpolate(resultBefore, resultAfter, t);
|
||||
return true;
|
||||
}
|
||||
else Debug.Log($"CmdClicked: history doesn't contain {estimatedTime:F3}");
|
||||
|
||||
sample = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
// convenience tests ///////////////////////////////////////////////////
|
||||
// there are multiple different ways to check a hit against the sample:
|
||||
// - raycasting
|
||||
// - bounds.contains
|
||||
// - increasing bounds by tolerance and checking contains
|
||||
// - threshold to bounds.closestpoint
|
||||
// let's offer a few solutions directly and see which users prefer.
|
||||
|
||||
// bounds check: checks distance to closest point on bounds in history @ -rtt.
|
||||
// 'viewer' needs to be the player who fired!
|
||||
// for example, if A fires at B, then call B.Sample(viewer, point, tolerance).
|
||||
// this is super simple and fast, but not 100% physically accurate since we don't raycast.
|
||||
[Server]
|
||||
public virtual bool BoundsCheck(
|
||||
NetworkConnectionToClient viewer,
|
||||
Vector3 hitPoint,
|
||||
float toleranceDistance,
|
||||
out float distance,
|
||||
out Vector3 nearest)
|
||||
{
|
||||
// first, sample the history at -rtt of the viewer.
|
||||
if (Sample(viewer, out Capture3D capture))
|
||||
{
|
||||
// now that we know where the other player was at that time,
|
||||
// we can see if the hit point was within tolerance of it.
|
||||
// TODO consider rotations???
|
||||
// TODO consider original collider shape??
|
||||
Bounds bounds = new Bounds(capture.position, capture.size);
|
||||
nearest = bounds.ClosestPoint(hitPoint);
|
||||
distance = Vector3.Distance(nearest, hitPoint);
|
||||
return distance <= toleranceDistance;
|
||||
}
|
||||
nearest = hitPoint;
|
||||
distance = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// raycast check: creates a collider the sampled position and raycasts to hitPoint.
|
||||
// 'viewer' needs to be the player who fired!
|
||||
// for example, if A fires at B, then call B.Sample(viewer, point, tolerance).
|
||||
// this is physically accurate (checks against walls etc.), with the cost
|
||||
// of a runtime instantiation.
|
||||
//
|
||||
// originPoint: where the player fired the weapon.
|
||||
// hitPoint: where the player's local raycast hit.
|
||||
// tolerance: scale up the sampled collider by % in order to have a bit of a tolerance.
|
||||
// 0 means no extra tolerance, 0.05 means 5% extra tolerance.
|
||||
// layerMask: the layer mask to use for the raycast.
|
||||
[Server]
|
||||
public virtual bool RaycastCheck(
|
||||
NetworkConnectionToClient viewer,
|
||||
Vector3 originPoint,
|
||||
Vector3 hitPoint,
|
||||
float tolerancePercent,
|
||||
int layerMask,
|
||||
out RaycastHit hit)
|
||||
{
|
||||
// first, sample the history at -rtt of the viewer.
|
||||
if (Sample(viewer, out Capture3D capture))
|
||||
{
|
||||
// instantiate a real physics collider on demand.
|
||||
// TODO rotation??
|
||||
// TODO different collier types??
|
||||
GameObject temp = new GameObject("LagCompensatorTest");
|
||||
temp.transform.position = capture.position;
|
||||
BoxCollider tempCollider = temp.AddComponent<BoxCollider>();
|
||||
tempCollider.size = capture.size * (1 + tolerancePercent);
|
||||
|
||||
// raycast
|
||||
Vector3 direction = hitPoint - originPoint;
|
||||
float maxDistance = direction.magnitude * 2;
|
||||
bool result = Physics.Raycast(originPoint, direction, out hit, maxDistance, layerMask);
|
||||
|
||||
// cleanup
|
||||
Destroy(temp);
|
||||
return result;
|
||||
}
|
||||
|
||||
hit = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a898831dd60c4cdfbfd9a6ea5702ed01
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
16
Assets/Mirror/Components/Mirror.Components.asmdef
Normal file
16
Assets/Mirror/Components/Mirror.Components.asmdef
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "Mirror.Components",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"GUID:30817c1a0e6d646d99c048fc403f5979"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": true,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
7
Assets/Mirror/Components/Mirror.Components.asmdef.meta
Normal file
7
Assets/Mirror/Components/Mirror.Components.asmdef.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 72872094b21c16e48b631b2224833d49
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
655
Assets/Mirror/Components/NetworkAnimator.cs
Normal file
655
Assets/Mirror/Components/NetworkAnimator.cs
Normal file
@ -0,0 +1,655 @@
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>
|
||||
/// A component to synchronize Mecanim animation states for networked objects.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>The animation of game objects can be networked by this component. There are two models of authority for networked movement:</para>
|
||||
/// <para>If the object has authority on the client, then it should be animated locally on the owning client. The animation state information will be sent from the owning client to the server, then broadcast to all of the other clients. This is common for player objects.</para>
|
||||
/// <para>If the object has authority on the server, then it should be animated on the server and state information will be sent to all clients. This is common for objects not related to a specific client, such as an enemy unit.</para>
|
||||
/// <para>The NetworkAnimator synchronizes all animation parameters of the selected Animator. It does not automatically synchronize triggers. The function SetTrigger can by used by an object with authority to fire an animation trigger on other clients.</para>
|
||||
/// </remarks>
|
||||
// [RequireComponent(typeof(NetworkIdentity))] disabled to allow child NetworkBehaviours
|
||||
[AddComponentMenu("Network/Network Animator")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-animator")]
|
||||
public class NetworkAnimator : NetworkBehaviour
|
||||
{
|
||||
[Header("Authority")]
|
||||
[Tooltip("Set to true if animations come from owner client, set to false if animations always come from server")]
|
||||
public bool clientAuthority;
|
||||
|
||||
/// <summary>
|
||||
/// The animator component to synchronize.
|
||||
/// </summary>
|
||||
[FormerlySerializedAs("m_Animator")]
|
||||
[Header("Animator")]
|
||||
[Tooltip("Animator that will have parameters synchronized")]
|
||||
public Animator animator;
|
||||
|
||||
/// <summary>
|
||||
/// Syncs animator.speed
|
||||
/// </summary>
|
||||
[SyncVar(hook = nameof(OnAnimatorSpeedChanged))]
|
||||
float animatorSpeed;
|
||||
float previousSpeed;
|
||||
|
||||
// Note: not an object[] array because otherwise initialization is real annoying
|
||||
int[] lastIntParameters;
|
||||
float[] lastFloatParameters;
|
||||
bool[] lastBoolParameters;
|
||||
AnimatorControllerParameter[] parameters;
|
||||
|
||||
// multiple layers
|
||||
int[] animationHash;
|
||||
int[] transitionHash;
|
||||
float[] layerWeight;
|
||||
double nextSendTime;
|
||||
|
||||
bool SendMessagesAllowed
|
||||
{
|
||||
get
|
||||
{
|
||||
if (isServer)
|
||||
{
|
||||
if (!clientAuthority)
|
||||
return true;
|
||||
|
||||
// This is a special case where we have client authority but we have not assigned the client who has
|
||||
// authority over it, no animator data will be sent over the network by the server.
|
||||
//
|
||||
// So we check here for a connectionToClient and if it is null we will
|
||||
// let the server send animation data until we receive an owner.
|
||||
if (netIdentity != null && netIdentity.connectionToClient == null)
|
||||
return true;
|
||||
}
|
||||
|
||||
return (isOwned && clientAuthority);
|
||||
}
|
||||
}
|
||||
|
||||
void Initialize()
|
||||
{
|
||||
// store the animator parameters in a variable - the "Animator.parameters" getter allocates
|
||||
// a new parameter array every time it is accessed so we should avoid doing it in a loop
|
||||
parameters = animator.parameters
|
||||
.Where(par => !animator.IsParameterControlledByCurve(par.nameHash))
|
||||
.ToArray();
|
||||
lastIntParameters = new int[parameters.Length];
|
||||
lastFloatParameters = new float[parameters.Length];
|
||||
lastBoolParameters = new bool[parameters.Length];
|
||||
|
||||
animationHash = new int[animator.layerCount];
|
||||
transitionHash = new int[animator.layerCount];
|
||||
layerWeight = new float[animator.layerCount];
|
||||
}
|
||||
|
||||
// fix https://github.com/MirrorNetworking/Mirror/issues/2810
|
||||
// both Awake and Enable need to initialize arrays.
|
||||
// in case users call SetActive(false) -> SetActive(true).
|
||||
void Awake() => Initialize();
|
||||
void OnEnable() => Initialize();
|
||||
|
||||
public virtual void Reset()
|
||||
{
|
||||
syncDirection = SyncDirection.ClientToServer;
|
||||
}
|
||||
|
||||
void FixedUpdate()
|
||||
{
|
||||
if (!SendMessagesAllowed)
|
||||
return;
|
||||
|
||||
if (!animator.enabled)
|
||||
return;
|
||||
|
||||
CheckSendRate();
|
||||
|
||||
for (int i = 0; i < animator.layerCount; i++)
|
||||
{
|
||||
int stateHash;
|
||||
float normalizedTime;
|
||||
if (!CheckAnimStateChanged(out stateHash, out normalizedTime, i))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
WriteParameters(writer);
|
||||
SendAnimationMessage(stateHash, normalizedTime, i, layerWeight[i], writer.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
CheckSpeed();
|
||||
}
|
||||
|
||||
void CheckSpeed()
|
||||
{
|
||||
float newSpeed = animator.speed;
|
||||
if (Mathf.Abs(previousSpeed - newSpeed) > 0.001f)
|
||||
{
|
||||
previousSpeed = newSpeed;
|
||||
if (isServer)
|
||||
{
|
||||
animatorSpeed = newSpeed;
|
||||
}
|
||||
else if (isClient)
|
||||
{
|
||||
CmdSetAnimatorSpeed(newSpeed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnAnimatorSpeedChanged(float _, float value)
|
||||
{
|
||||
// skip if host or client with authority
|
||||
// they will have already set the speed so don't set again
|
||||
if (isServer || (isOwned && clientAuthority))
|
||||
return;
|
||||
|
||||
animator.speed = value;
|
||||
}
|
||||
|
||||
bool CheckAnimStateChanged(out int stateHash, out float normalizedTime, int layerId)
|
||||
{
|
||||
bool change = false;
|
||||
stateHash = 0;
|
||||
normalizedTime = 0;
|
||||
|
||||
float lw = animator.GetLayerWeight(layerId);
|
||||
if (Mathf.Abs(lw - layerWeight[layerId]) > 0.001f)
|
||||
{
|
||||
layerWeight[layerId] = lw;
|
||||
change = true;
|
||||
}
|
||||
|
||||
if (animator.IsInTransition(layerId))
|
||||
{
|
||||
AnimatorTransitionInfo tt = animator.GetAnimatorTransitionInfo(layerId);
|
||||
if (tt.fullPathHash != transitionHash[layerId])
|
||||
{
|
||||
// first time in this transition
|
||||
transitionHash[layerId] = tt.fullPathHash;
|
||||
animationHash[layerId] = 0;
|
||||
return true;
|
||||
}
|
||||
return change;
|
||||
}
|
||||
|
||||
AnimatorStateInfo st = animator.GetCurrentAnimatorStateInfo(layerId);
|
||||
if (st.fullPathHash != animationHash[layerId])
|
||||
{
|
||||
// first time in this animation state
|
||||
if (animationHash[layerId] != 0)
|
||||
{
|
||||
// came from another animation directly - from Play()
|
||||
stateHash = st.fullPathHash;
|
||||
normalizedTime = st.normalizedTime;
|
||||
}
|
||||
transitionHash[layerId] = 0;
|
||||
animationHash[layerId] = st.fullPathHash;
|
||||
return true;
|
||||
}
|
||||
return change;
|
||||
}
|
||||
|
||||
void CheckSendRate()
|
||||
{
|
||||
double now = NetworkTime.localTime;
|
||||
if (SendMessagesAllowed && syncInterval >= 0 && now > nextSendTime)
|
||||
{
|
||||
nextSendTime = now + syncInterval;
|
||||
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
if (WriteParameters(writer))
|
||||
SendAnimationParametersMessage(writer.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SendAnimationMessage(int stateHash, float normalizedTime, int layerId, float weight, byte[] parameters)
|
||||
{
|
||||
if (isServer)
|
||||
{
|
||||
RpcOnAnimationClientMessage(stateHash, normalizedTime, layerId, weight, parameters);
|
||||
}
|
||||
else if (isClient)
|
||||
{
|
||||
CmdOnAnimationServerMessage(stateHash, normalizedTime, layerId, weight, parameters);
|
||||
}
|
||||
}
|
||||
|
||||
void SendAnimationParametersMessage(byte[] parameters)
|
||||
{
|
||||
if (isServer)
|
||||
{
|
||||
RpcOnAnimationParametersClientMessage(parameters);
|
||||
}
|
||||
else if (isClient)
|
||||
{
|
||||
CmdOnAnimationParametersServerMessage(parameters);
|
||||
}
|
||||
}
|
||||
|
||||
void HandleAnimMsg(int stateHash, float normalizedTime, int layerId, float weight, NetworkReader reader)
|
||||
{
|
||||
if (isOwned && clientAuthority)
|
||||
return;
|
||||
|
||||
// usually transitions will be triggered by parameters, if not, play anims directly.
|
||||
// NOTE: this plays "animations", not transitions, so any transitions will be skipped.
|
||||
// NOTE: there is no API to play a transition(?)
|
||||
if (stateHash != 0 && animator.enabled)
|
||||
{
|
||||
animator.Play(stateHash, layerId, normalizedTime);
|
||||
}
|
||||
|
||||
animator.SetLayerWeight(layerId, weight);
|
||||
|
||||
ReadParameters(reader);
|
||||
}
|
||||
|
||||
void HandleAnimParamsMsg(NetworkReader reader)
|
||||
{
|
||||
if (isOwned && clientAuthority)
|
||||
return;
|
||||
|
||||
ReadParameters(reader);
|
||||
}
|
||||
|
||||
void HandleAnimTriggerMsg(int hash)
|
||||
{
|
||||
if (animator.enabled)
|
||||
animator.SetTrigger(hash);
|
||||
}
|
||||
|
||||
void HandleAnimResetTriggerMsg(int hash)
|
||||
{
|
||||
if (animator.enabled)
|
||||
animator.ResetTrigger(hash);
|
||||
}
|
||||
|
||||
ulong NextDirtyBits()
|
||||
{
|
||||
ulong dirtyBits = 0;
|
||||
for (int i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
AnimatorControllerParameter par = parameters[i];
|
||||
bool changed = false;
|
||||
if (par.type == AnimatorControllerParameterType.Int)
|
||||
{
|
||||
int newIntValue = animator.GetInteger(par.nameHash);
|
||||
changed = newIntValue != lastIntParameters[i];
|
||||
if (changed)
|
||||
lastIntParameters[i] = newIntValue;
|
||||
}
|
||||
else if (par.type == AnimatorControllerParameterType.Float)
|
||||
{
|
||||
float newFloatValue = animator.GetFloat(par.nameHash);
|
||||
changed = Mathf.Abs(newFloatValue - lastFloatParameters[i]) > 0.001f;
|
||||
// only set lastValue if it was changed, otherwise value could slowly drift within the 0.001f limit each frame
|
||||
if (changed)
|
||||
lastFloatParameters[i] = newFloatValue;
|
||||
}
|
||||
else if (par.type == AnimatorControllerParameterType.Bool)
|
||||
{
|
||||
bool newBoolValue = animator.GetBool(par.nameHash);
|
||||
changed = newBoolValue != lastBoolParameters[i];
|
||||
if (changed)
|
||||
lastBoolParameters[i] = newBoolValue;
|
||||
}
|
||||
if (changed)
|
||||
{
|
||||
dirtyBits |= 1ul << i;
|
||||
}
|
||||
}
|
||||
return dirtyBits;
|
||||
}
|
||||
|
||||
bool WriteParameters(NetworkWriter writer, bool forceAll = false)
|
||||
{
|
||||
// fix: https://github.com/MirrorNetworking/Mirror/issues/2852
|
||||
// serialize parameterCount to be 100% sure we deserialize correct amount of bytes.
|
||||
// (255 parameters should be enough for everyone, write it as byte)
|
||||
byte parameterCount = (byte)parameters.Length;
|
||||
writer.WriteByte(parameterCount);
|
||||
|
||||
ulong dirtyBits = forceAll ? (~0ul) : NextDirtyBits();
|
||||
writer.WriteULong(dirtyBits);
|
||||
|
||||
// iterate on byte count. if it's >256, it won't break
|
||||
// serialization - just not serialize excess layers.
|
||||
for (int i = 0; i < parameterCount; i++)
|
||||
{
|
||||
if ((dirtyBits & (1ul << i)) == 0)
|
||||
continue;
|
||||
|
||||
AnimatorControllerParameter par = parameters[i];
|
||||
if (par.type == AnimatorControllerParameterType.Int)
|
||||
{
|
||||
int newIntValue = animator.GetInteger(par.nameHash);
|
||||
writer.WriteInt(newIntValue);
|
||||
}
|
||||
else if (par.type == AnimatorControllerParameterType.Float)
|
||||
{
|
||||
float newFloatValue = animator.GetFloat(par.nameHash);
|
||||
writer.WriteFloat(newFloatValue);
|
||||
}
|
||||
else if (par.type == AnimatorControllerParameterType.Bool)
|
||||
{
|
||||
bool newBoolValue = animator.GetBool(par.nameHash);
|
||||
writer.WriteBool(newBoolValue);
|
||||
}
|
||||
}
|
||||
return dirtyBits != 0;
|
||||
}
|
||||
|
||||
void ReadParameters(NetworkReader reader)
|
||||
{
|
||||
// fix: https://github.com/MirrorNetworking/Mirror/issues/2852
|
||||
// serialize parameterCount to be 100% sure we deserialize correct amount of bytes.
|
||||
// mismatch shows error to make this super easy to debug.
|
||||
byte parameterCount = reader.ReadByte();
|
||||
if (parameterCount != parameters.Length)
|
||||
{
|
||||
Debug.LogError($"NetworkAnimator: serialized parameter count={parameterCount} does not match expected parameter count={parameters.Length}. Are you changing animators at runtime?", gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
bool animatorEnabled = animator.enabled;
|
||||
// need to read values from NetworkReader even if animator is disabled
|
||||
ulong dirtyBits = reader.ReadULong();
|
||||
for (int i = 0; i < parameterCount; i++)
|
||||
{
|
||||
if ((dirtyBits & (1ul << i)) == 0)
|
||||
continue;
|
||||
|
||||
AnimatorControllerParameter par = parameters[i];
|
||||
if (par.type == AnimatorControllerParameterType.Int)
|
||||
{
|
||||
int newIntValue = reader.ReadInt();
|
||||
if (animatorEnabled)
|
||||
animator.SetInteger(par.nameHash, newIntValue);
|
||||
}
|
||||
else if (par.type == AnimatorControllerParameterType.Float)
|
||||
{
|
||||
float newFloatValue = reader.ReadFloat();
|
||||
if (animatorEnabled)
|
||||
animator.SetFloat(par.nameHash, newFloatValue);
|
||||
}
|
||||
else if (par.type == AnimatorControllerParameterType.Bool)
|
||||
{
|
||||
bool newBoolValue = reader.ReadBool();
|
||||
if (animatorEnabled)
|
||||
animator.SetBool(par.nameHash, newBoolValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
{
|
||||
base.OnSerialize(writer, initialState);
|
||||
if (initialState)
|
||||
{
|
||||
// fix: https://github.com/MirrorNetworking/Mirror/issues/2852
|
||||
// serialize layerCount to be 100% sure we deserialize correct amount of bytes.
|
||||
// (255 layers should be enough for everyone, write it as byte)
|
||||
byte layerCount = (byte)animator.layerCount;
|
||||
writer.WriteByte(layerCount);
|
||||
|
||||
// iterate on byte count. if it's >256, it won't break
|
||||
// serialization - just not serialize excess layers.
|
||||
for (int i = 0; i < layerCount; i++)
|
||||
{
|
||||
AnimatorStateInfo st = animator.IsInTransition(i)
|
||||
? animator.GetNextAnimatorStateInfo(i)
|
||||
: animator.GetCurrentAnimatorStateInfo(i);
|
||||
writer.WriteInt(st.fullPathHash);
|
||||
writer.WriteFloat(st.normalizedTime);
|
||||
writer.WriteFloat(animator.GetLayerWeight(i));
|
||||
}
|
||||
WriteParameters(writer, true);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
{
|
||||
base.OnDeserialize(reader, initialState);
|
||||
if (initialState)
|
||||
{
|
||||
// fix: https://github.com/MirrorNetworking/Mirror/issues/2852
|
||||
// serialize layerCount to be 100% sure we deserialize correct amount of bytes.
|
||||
// mismatch shows error to make this super easy to debug.
|
||||
byte layerCount = reader.ReadByte();
|
||||
if (layerCount != animator.layerCount)
|
||||
{
|
||||
Debug.LogError($"NetworkAnimator: serialized layer count={layerCount} does not match expected layer count={animator.layerCount}. Are you changing animators at runtime?", gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < layerCount; i++)
|
||||
{
|
||||
int stateHash = reader.ReadInt();
|
||||
float normalizedTime = reader.ReadFloat();
|
||||
float weight = reader.ReadFloat();
|
||||
|
||||
animator.SetLayerWeight(i, weight);
|
||||
animator.Play(stateHash, i, normalizedTime);
|
||||
}
|
||||
|
||||
ReadParameters(reader);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Causes an animation trigger to be invoked for a networked object.
|
||||
/// <para>If local authority is set, and this is called from the client, then the trigger will be invoked on the server and all clients. If not, then this is called on the server, and the trigger will be called on all clients.</para>
|
||||
/// </summary>
|
||||
/// <param name="triggerName">Name of trigger.</param>
|
||||
public void SetTrigger(string triggerName)
|
||||
{
|
||||
SetTrigger(Animator.StringToHash(triggerName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Causes an animation trigger to be invoked for a networked object.
|
||||
/// </summary>
|
||||
/// <param name="hash">Hash id of trigger (from the Animator).</param>
|
||||
public void SetTrigger(int hash)
|
||||
{
|
||||
if (clientAuthority)
|
||||
{
|
||||
if (!isClient)
|
||||
{
|
||||
Debug.LogWarning("Tried to set animation in the server for a client-controlled animator", gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isOwned)
|
||||
{
|
||||
Debug.LogWarning("Only the client with authority can set animations", gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isClient)
|
||||
CmdOnAnimationTriggerServerMessage(hash);
|
||||
|
||||
// call on client right away
|
||||
HandleAnimTriggerMsg(hash);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!isServer)
|
||||
{
|
||||
Debug.LogWarning("Tried to set animation in the client for a server-controlled animator", gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
HandleAnimTriggerMsg(hash);
|
||||
RpcOnAnimationTriggerClientMessage(hash);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Causes an animation trigger to be reset for a networked object.
|
||||
/// <para>If local authority is set, and this is called from the client, then the trigger will be reset on the server and all clients. If not, then this is called on the server, and the trigger will be reset on all clients.</para>
|
||||
/// </summary>
|
||||
/// <param name="triggerName">Name of trigger.</param>
|
||||
public void ResetTrigger(string triggerName)
|
||||
{
|
||||
ResetTrigger(Animator.StringToHash(triggerName));
|
||||
}
|
||||
|
||||
/// <summary>Causes an animation trigger to be reset for a networked object.</summary>
|
||||
/// <param name="hash">Hash id of trigger (from the Animator).</param>
|
||||
public void ResetTrigger(int hash)
|
||||
{
|
||||
if (clientAuthority)
|
||||
{
|
||||
if (!isClient)
|
||||
{
|
||||
Debug.LogWarning("Tried to reset animation in the server for a client-controlled animator", gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isOwned)
|
||||
{
|
||||
Debug.LogWarning("Only the client with authority can reset animations", gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isClient)
|
||||
CmdOnAnimationResetTriggerServerMessage(hash);
|
||||
|
||||
// call on client right away
|
||||
HandleAnimResetTriggerMsg(hash);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!isServer)
|
||||
{
|
||||
Debug.LogWarning("Tried to reset animation in the client for a server-controlled animator", gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
HandleAnimResetTriggerMsg(hash);
|
||||
RpcOnAnimationResetTriggerClientMessage(hash);
|
||||
}
|
||||
}
|
||||
|
||||
#region server message handlers
|
||||
|
||||
[Command]
|
||||
void CmdOnAnimationServerMessage(int stateHash, float normalizedTime, int layerId, float weight, byte[] parameters)
|
||||
{
|
||||
// Ignore messages from client if not in client authority mode
|
||||
if (!clientAuthority)
|
||||
return;
|
||||
|
||||
//Debug.Log($"OnAnimationMessage for netId {netId}");
|
||||
|
||||
// handle and broadcast
|
||||
using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(parameters))
|
||||
{
|
||||
HandleAnimMsg(stateHash, normalizedTime, layerId, weight, networkReader);
|
||||
RpcOnAnimationClientMessage(stateHash, normalizedTime, layerId, weight, parameters);
|
||||
}
|
||||
}
|
||||
|
||||
[Command]
|
||||
void CmdOnAnimationParametersServerMessage(byte[] parameters)
|
||||
{
|
||||
// Ignore messages from client if not in client authority mode
|
||||
if (!clientAuthority)
|
||||
return;
|
||||
|
||||
// handle and broadcast
|
||||
using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(parameters))
|
||||
{
|
||||
HandleAnimParamsMsg(networkReader);
|
||||
RpcOnAnimationParametersClientMessage(parameters);
|
||||
}
|
||||
}
|
||||
|
||||
[Command]
|
||||
void CmdOnAnimationTriggerServerMessage(int hash)
|
||||
{
|
||||
// Ignore messages from client if not in client authority mode
|
||||
if (!clientAuthority)
|
||||
return;
|
||||
|
||||
// handle and broadcast
|
||||
// host should have already the trigger
|
||||
bool isHostOwner = isClient && isOwned;
|
||||
if (!isHostOwner)
|
||||
{
|
||||
HandleAnimTriggerMsg(hash);
|
||||
}
|
||||
|
||||
RpcOnAnimationTriggerClientMessage(hash);
|
||||
}
|
||||
|
||||
[Command]
|
||||
void CmdOnAnimationResetTriggerServerMessage(int hash)
|
||||
{
|
||||
// Ignore messages from client if not in client authority mode
|
||||
if (!clientAuthority)
|
||||
return;
|
||||
|
||||
// handle and broadcast
|
||||
// host should have already the trigger
|
||||
bool isHostOwner = isClient && isOwned;
|
||||
if (!isHostOwner)
|
||||
{
|
||||
HandleAnimResetTriggerMsg(hash);
|
||||
}
|
||||
|
||||
RpcOnAnimationResetTriggerClientMessage(hash);
|
||||
}
|
||||
|
||||
[Command]
|
||||
void CmdSetAnimatorSpeed(float newSpeed)
|
||||
{
|
||||
// set animator
|
||||
animator.speed = newSpeed;
|
||||
animatorSpeed = newSpeed;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region client message handlers
|
||||
|
||||
[ClientRpc]
|
||||
void RpcOnAnimationClientMessage(int stateHash, float normalizedTime, int layerId, float weight, byte[] parameters)
|
||||
{
|
||||
using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(parameters))
|
||||
HandleAnimMsg(stateHash, normalizedTime, layerId, weight, networkReader);
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
void RpcOnAnimationParametersClientMessage(byte[] parameters)
|
||||
{
|
||||
using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(parameters))
|
||||
HandleAnimParamsMsg(networkReader);
|
||||
}
|
||||
|
||||
[ClientRpc(includeOwner = false)]
|
||||
void RpcOnAnimationTriggerClientMessage(int hash)
|
||||
{
|
||||
HandleAnimTriggerMsg(hash);
|
||||
}
|
||||
|
||||
[ClientRpc(includeOwner = false)]
|
||||
void RpcOnAnimationResetTriggerClientMessage(int hash)
|
||||
{
|
||||
HandleAnimResetTriggerMsg(hash);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
11
Assets/Mirror/Components/NetworkAnimator.cs.meta
Normal file
11
Assets/Mirror/Components/NetworkAnimator.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7f6f3bf89aa97405989c802ba270f815
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
30
Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs
Normal file
30
Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public class NetworkDiagnosticsDebugger : MonoBehaviour
|
||||
{
|
||||
public bool logInMessages = true;
|
||||
public bool logOutMessages = true;
|
||||
void OnInMessage(NetworkDiagnostics.MessageInfo msgInfo)
|
||||
{
|
||||
if (logInMessages)
|
||||
Debug.Log(msgInfo);
|
||||
}
|
||||
void OnOutMessage(NetworkDiagnostics.MessageInfo msgInfo)
|
||||
{
|
||||
if (logOutMessages)
|
||||
Debug.Log(msgInfo);
|
||||
}
|
||||
void OnEnable()
|
||||
{
|
||||
NetworkDiagnostics.InMessageEvent += OnInMessage;
|
||||
NetworkDiagnostics.OutMessageEvent += OnOutMessage;
|
||||
}
|
||||
void OnDisable()
|
||||
{
|
||||
NetworkDiagnostics.InMessageEvent -= OnInMessage;
|
||||
NetworkDiagnostics.OutMessageEvent -= OnOutMessage;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bc9f0a0fe4124424b8f9d4927795ee01
|
||||
timeCreated: 1700945893
|
18
Assets/Mirror/Components/NetworkLobbyManager.cs
Normal file
18
Assets/Mirror/Components/NetworkLobbyManager.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>
|
||||
/// This is a specialized NetworkManager that includes a networked lobby.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>The lobby has slots that track the joined players, and a maximum player count that is enforced. It requires that the NetworkLobbyPlayer component be on the lobby player objects.</para>
|
||||
/// <para>NetworkLobbyManager is derived from NetworkManager, and so it implements many of the virtual functions provided by the NetworkManager class. To avoid accidentally replacing functionality of the NetworkLobbyManager, there are new virtual functions on the NetworkLobbyManager that begin with "OnLobby". These should be used on classes derived from NetworkLobbyManager instead of the virtual functions on NetworkManager.</para>
|
||||
/// <para>The OnLobby*() functions have empty implementations on the NetworkLobbyManager base class, so the base class functions do not have to be called.</para>
|
||||
/// </remarks>
|
||||
[AddComponentMenu("Network/Network Lobby Manager")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-room-manager")]
|
||||
[Obsolete("Use / inherit from NetworkRoomManager instead")]
|
||||
public class NetworkLobbyManager : NetworkRoomManager {}
|
||||
}
|
11
Assets/Mirror/Components/NetworkLobbyManager.cs.meta
Normal file
11
Assets/Mirror/Components/NetworkLobbyManager.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4c96e6dd99826849ab1431f94547141
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
15
Assets/Mirror/Components/NetworkLobbyPlayer.cs
Normal file
15
Assets/Mirror/Components/NetworkLobbyPlayer.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>
|
||||
/// This component works in conjunction with the NetworkLobbyManager to make up the multiplayer lobby system.
|
||||
/// <para>The LobbyPrefab object of the NetworkLobbyManager must have this component on it. This component holds basic lobby player data required for the lobby to function. Game specific data for lobby players can be put in other components on the LobbyPrefab or in scripts derived from NetworkLobbyPlayer.</para>
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/Network Lobby Player")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-room-player")]
|
||||
[Obsolete("Use / inherit from NetworkRoomPlayer instead")]
|
||||
public class NetworkLobbyPlayer : NetworkRoomPlayer {}
|
||||
}
|
11
Assets/Mirror/Components/NetworkLobbyPlayer.cs.meta
Normal file
11
Assets/Mirror/Components/NetworkLobbyPlayer.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 777a368af85f2e84da7ea5666581921b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
39
Assets/Mirror/Components/NetworkPingDisplay.cs
Normal file
39
Assets/Mirror/Components/NetworkPingDisplay.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>
|
||||
/// Component that will display the clients ping in milliseconds
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/Network Ping Display")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-ping-display")]
|
||||
public class NetworkPingDisplay : MonoBehaviour
|
||||
{
|
||||
public Color color = Color.white;
|
||||
public int padding = 2;
|
||||
public int width = 150;
|
||||
public int height = 25;
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
// only while client is active
|
||||
if (!NetworkClient.active) return;
|
||||
|
||||
// show stats in bottom right corner, right aligned
|
||||
GUI.color = color;
|
||||
Rect rect = new Rect(Screen.width - width - padding, Screen.height - height - padding, width, height);
|
||||
GUILayout.BeginArea(rect);
|
||||
GUIStyle style = GUI.skin.GetStyle("Label");
|
||||
style.alignment = TextAnchor.MiddleRight;
|
||||
GUILayout.BeginHorizontal(style);
|
||||
GUILayout.Label($"RTT: {Math.Round(NetworkTime.rtt * 1000)}ms");
|
||||
GUI.color = NetworkClient.connectionQuality.ColorCode();
|
||||
GUILayout.Label($"Q: {new string('-', (int)NetworkClient.connectionQuality)}");
|
||||
GUILayout.EndHorizontal();
|
||||
GUILayout.EndArea();
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Components/NetworkPingDisplay.cs.meta
Normal file
11
Assets/Mirror/Components/NetworkPingDisplay.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bc654f29862fc2643b948f772ebb9e68
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
3
Assets/Mirror/Components/NetworkRigidbody.meta
Normal file
3
Assets/Mirror/Components/NetworkRigidbody.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 80106690aef541a5b8e2f8fb3d5949ad
|
||||
timeCreated: 1686733778
|
@ -0,0 +1,96 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
|
||||
public class NetworkRigidbodyReliable : NetworkTransformReliable
|
||||
{
|
||||
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
|
||||
|
||||
Rigidbody rb;
|
||||
bool wasKinematic;
|
||||
|
||||
// cach Rigidbody and original isKinematic setting
|
||||
protected override void Awake()
|
||||
{
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can use its Rigidbody component.
|
||||
rb = target.GetComponent<Rigidbody>();
|
||||
if (rb == null)
|
||||
{
|
||||
Debug.LogError($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this);
|
||||
return;
|
||||
}
|
||||
wasKinematic = rb.isKinematic;
|
||||
base.Awake();
|
||||
}
|
||||
|
||||
// reset forced isKinematic flag to original.
|
||||
// otherwise the overwritten value would remain between sessions forever.
|
||||
// for example, a game may run as client, set rigidbody.iskinematic=true,
|
||||
// then run as server, where .iskinematic isn't touched and remains at
|
||||
// the overwritten=true, even though the user set it to false originally.
|
||||
public override void OnStopServer() => rb.isKinematic = wasKinematic;
|
||||
public override void OnStopClient() => rb.isKinematic = wasKinematic;
|
||||
|
||||
// overwriting Construct() and Apply() to set Rigidbody.MovePosition
|
||||
// would give more jittery movement.
|
||||
|
||||
// FixedUpdate for physics
|
||||
void FixedUpdate()
|
||||
{
|
||||
// who ever has authority moves the Rigidbody with physics.
|
||||
// everyone else simply sets it to kinematic.
|
||||
// so that only the Transform component is synced.
|
||||
|
||||
// host mode
|
||||
if (isServer && isClient)
|
||||
{
|
||||
// in host mode, we own it it if:
|
||||
// clientAuthority is disabled (hence server / we own it)
|
||||
// clientAuthority is enabled and we have authority over this object.
|
||||
bool owned = !clientAuthority || IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// client only
|
||||
else if (isClient)
|
||||
{
|
||||
// on the client, we own it only if clientAuthority is enabled,
|
||||
// and we have authority over this object.
|
||||
bool owned = IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// server only
|
||||
else if (isServer)
|
||||
{
|
||||
// on the server, we always own it if clientAuthority is disabled.
|
||||
bool owned = !clientAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can ensure that .target has a Rigidbody, and use it.
|
||||
if (target.GetComponent<Rigidbody>() == null)
|
||||
{
|
||||
Debug.LogWarning($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cb803efbe62c34d7baece46c9ffebad9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,96 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
|
||||
public class NetworkRigidbodyReliable2D : NetworkTransformReliable
|
||||
{
|
||||
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
|
||||
|
||||
Rigidbody2D rb;
|
||||
bool wasKinematic;
|
||||
|
||||
// cach Rigidbody and original isKinematic setting
|
||||
protected override void Awake()
|
||||
{
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can use its Rigidbody component.
|
||||
rb = target.GetComponent<Rigidbody2D>();
|
||||
if (rb == null)
|
||||
{
|
||||
Debug.LogError($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this);
|
||||
return;
|
||||
}
|
||||
wasKinematic = rb.isKinematic;
|
||||
base.Awake();
|
||||
}
|
||||
|
||||
// reset forced isKinematic flag to original.
|
||||
// otherwise the overwritten value would remain between sessions forever.
|
||||
// for example, a game may run as client, set rigidbody.iskinematic=true,
|
||||
// then run as server, where .iskinematic isn't touched and remains at
|
||||
// the overwritten=true, even though the user set it to false originally.
|
||||
public override void OnStopServer() => rb.isKinematic = wasKinematic;
|
||||
public override void OnStopClient() => rb.isKinematic = wasKinematic;
|
||||
|
||||
// overwriting Construct() and Apply() to set Rigidbody.MovePosition
|
||||
// would give more jittery movement.
|
||||
|
||||
// FixedUpdate for physics
|
||||
void FixedUpdate()
|
||||
{
|
||||
// who ever has authority moves the Rigidbody with physics.
|
||||
// everyone else simply sets it to kinematic.
|
||||
// so that only the Transform component is synced.
|
||||
|
||||
// host mode
|
||||
if (isServer && isClient)
|
||||
{
|
||||
// in host mode, we own it it if:
|
||||
// clientAuthority is disabled (hence server / we own it)
|
||||
// clientAuthority is enabled and we have authority over this object.
|
||||
bool owned = !clientAuthority || IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// client only
|
||||
else if (isClient)
|
||||
{
|
||||
// on the client, we own it only if clientAuthority is enabled,
|
||||
// and we have authority over this object.
|
||||
bool owned = IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// server only
|
||||
else if (isServer)
|
||||
{
|
||||
// on the server, we always own it if clientAuthority is disabled.
|
||||
bool owned = !clientAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can ensure that .target has a Rigidbody, and use it.
|
||||
if (target.GetComponent<Rigidbody2D>() == null)
|
||||
{
|
||||
Debug.LogWarning($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ec4f7556ca1e4b55a3381fc6a02b1bc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,96 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
|
||||
public class NetworkRigidbodyUnreliable : NetworkTransformUnreliable
|
||||
{
|
||||
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
|
||||
|
||||
Rigidbody rb;
|
||||
bool wasKinematic;
|
||||
|
||||
// cach Rigidbody and original isKinematic setting
|
||||
protected override void Awake()
|
||||
{
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can use its Rigidbody component.
|
||||
rb = target.GetComponent<Rigidbody>();
|
||||
if (rb == null)
|
||||
{
|
||||
Debug.LogError($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this);
|
||||
return;
|
||||
}
|
||||
wasKinematic = rb.isKinematic;
|
||||
base.Awake();
|
||||
}
|
||||
|
||||
// reset forced isKinematic flag to original.
|
||||
// otherwise the overwritten value would remain between sessions forever.
|
||||
// for example, a game may run as client, set rigidbody.iskinematic=true,
|
||||
// then run as server, where .iskinematic isn't touched and remains at
|
||||
// the overwritten=true, even though the user set it to false originally.
|
||||
public override void OnStopServer() => rb.isKinematic = wasKinematic;
|
||||
public override void OnStopClient() => rb.isKinematic = wasKinematic;
|
||||
|
||||
// overwriting Construct() and Apply() to set Rigidbody.MovePosition
|
||||
// would give more jittery movement.
|
||||
|
||||
// FixedUpdate for physics
|
||||
void FixedUpdate()
|
||||
{
|
||||
// who ever has authority moves the Rigidbody with physics.
|
||||
// everyone else simply sets it to kinematic.
|
||||
// so that only the Transform component is synced.
|
||||
|
||||
// host mode
|
||||
if (isServer && isClient)
|
||||
{
|
||||
// in host mode, we own it it if:
|
||||
// clientAuthority is disabled (hence server / we own it)
|
||||
// clientAuthority is enabled and we have authority over this object.
|
||||
bool owned = !clientAuthority || IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// client only
|
||||
else if (isClient)
|
||||
{
|
||||
// on the client, we own it only if clientAuthority is enabled,
|
||||
// and we have authority over this object.
|
||||
bool owned = IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// server only
|
||||
else if (isServer)
|
||||
{
|
||||
// on the server, we always own it if clientAuthority is disabled.
|
||||
bool owned = !clientAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can ensure that .target has a Rigidbody, and use it.
|
||||
if (target.GetComponent<Rigidbody>() == null)
|
||||
{
|
||||
Debug.LogWarning($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b20dc110904e47f8a154cdcf6433eae
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,96 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
|
||||
public class NetworkRigidbodyUnreliable2D : NetworkTransformUnreliable
|
||||
{
|
||||
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
|
||||
|
||||
Rigidbody2D rb;
|
||||
bool wasKinematic;
|
||||
|
||||
// cach Rigidbody and original isKinematic setting
|
||||
protected override void Awake()
|
||||
{
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can use its Rigidbody component.
|
||||
rb = target.GetComponent<Rigidbody2D>();
|
||||
if (rb == null)
|
||||
{
|
||||
Debug.LogError($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this);
|
||||
return;
|
||||
}
|
||||
wasKinematic = rb.isKinematic;
|
||||
base.Awake();
|
||||
}
|
||||
|
||||
// reset forced isKinematic flag to original.
|
||||
// otherwise the overwritten value would remain between sessions forever.
|
||||
// for example, a game may run as client, set rigidbody.iskinematic=true,
|
||||
// then run as server, where .iskinematic isn't touched and remains at
|
||||
// the overwritten=true, even though the user set it to false originally.
|
||||
public override void OnStopServer() => rb.isKinematic = wasKinematic;
|
||||
public override void OnStopClient() => rb.isKinematic = wasKinematic;
|
||||
|
||||
// overwriting Construct() and Apply() to set Rigidbody.MovePosition
|
||||
// would give more jittery movement.
|
||||
|
||||
// FixedUpdate for physics
|
||||
void FixedUpdate()
|
||||
{
|
||||
// who ever has authority moves the Rigidbody with physics.
|
||||
// everyone else simply sets it to kinematic.
|
||||
// so that only the Transform component is synced.
|
||||
|
||||
// host mode
|
||||
if (isServer && isClient)
|
||||
{
|
||||
// in host mode, we own it it if:
|
||||
// clientAuthority is disabled (hence server / we own it)
|
||||
// clientAuthority is enabled and we have authority over this object.
|
||||
bool owned = !clientAuthority || IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// client only
|
||||
else if (isClient)
|
||||
{
|
||||
// on the client, we own it only if clientAuthority is enabled,
|
||||
// and we have authority over this object.
|
||||
bool owned = IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// server only
|
||||
else if (isServer)
|
||||
{
|
||||
// on the server, we always own it if clientAuthority is disabled.
|
||||
bool owned = !clientAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can ensure that .target has a Rigidbody, and use it.
|
||||
if (target.GetComponent<Rigidbody2D>() == null)
|
||||
{
|
||||
Debug.LogWarning($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1c7e12ad9b9ae443c9fdf37e9f5ecd36
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
685
Assets/Mirror/Components/NetworkRoomManager.cs
Normal file
685
Assets/Mirror/Components/NetworkRoomManager.cs
Normal file
@ -0,0 +1,685 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>
|
||||
/// This is a specialized NetworkManager that includes a networked room.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>The room has slots that track the joined players, and a maximum player count that is enforced. It requires that the NetworkRoomPlayer component be on the room player objects.</para>
|
||||
/// <para>NetworkRoomManager is derived from NetworkManager, and so it implements many of the virtual functions provided by the NetworkManager class. To avoid accidentally replacing functionality of the NetworkRoomManager, there are new virtual functions on the NetworkRoomManager that begin with "OnRoom". These should be used on classes derived from NetworkRoomManager instead of the virtual functions on NetworkManager.</para>
|
||||
/// <para>The OnRoom*() functions have empty implementations on the NetworkRoomManager base class, so the base class functions do not have to be called.</para>
|
||||
/// </remarks>
|
||||
[AddComponentMenu("Network/Network Room Manager")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-room-manager")]
|
||||
public class NetworkRoomManager : NetworkManager
|
||||
{
|
||||
public struct PendingPlayer
|
||||
{
|
||||
public NetworkConnectionToClient conn;
|
||||
public GameObject roomPlayer;
|
||||
}
|
||||
|
||||
[Header("Room Settings")]
|
||||
[FormerlySerializedAs("m_ShowRoomGUI")]
|
||||
[SerializeField]
|
||||
[Tooltip("This flag controls whether the default UI is shown for the room")]
|
||||
public bool showRoomGUI = true;
|
||||
|
||||
[FormerlySerializedAs("m_MinPlayers")]
|
||||
[SerializeField]
|
||||
[Tooltip("Minimum number of players to auto-start the game")]
|
||||
public int minPlayers = 1;
|
||||
|
||||
[FormerlySerializedAs("m_RoomPlayerPrefab")]
|
||||
[SerializeField]
|
||||
[Tooltip("Prefab to use for the Room Player")]
|
||||
public NetworkRoomPlayer roomPlayerPrefab;
|
||||
|
||||
/// <summary>
|
||||
/// The scene to use for the room. This is similar to the offlineScene of the NetworkManager.
|
||||
/// </summary>
|
||||
[Scene]
|
||||
public string RoomScene;
|
||||
|
||||
/// <summary>
|
||||
/// The scene to use for the playing the game from the room. This is similar to the onlineScene of the NetworkManager.
|
||||
/// </summary>
|
||||
[Scene]
|
||||
public string GameplayScene;
|
||||
|
||||
/// <summary>
|
||||
/// List of players that are in the Room
|
||||
/// </summary>
|
||||
[FormerlySerializedAs("m_PendingPlayers")]
|
||||
public List<PendingPlayer> pendingPlayers = new List<PendingPlayer>();
|
||||
|
||||
[Header("Diagnostics")]
|
||||
/// <summary>
|
||||
/// True when all players have submitted a Ready message
|
||||
/// </summary>
|
||||
[Tooltip("Diagnostic flag indicating all players are ready to play")]
|
||||
[FormerlySerializedAs("allPlayersReady")]
|
||||
[ReadOnly, SerializeField] bool _allPlayersReady;
|
||||
|
||||
/// <summary>
|
||||
/// These slots track players that enter the room.
|
||||
/// <para>The slotId on players is global to the game - across all players.</para>
|
||||
/// </summary>
|
||||
[ReadOnly, Tooltip("List of Room Player objects")]
|
||||
public List<NetworkRoomPlayer> roomSlots = new List<NetworkRoomPlayer>();
|
||||
|
||||
public bool allPlayersReady
|
||||
{
|
||||
get => _allPlayersReady;
|
||||
set
|
||||
{
|
||||
bool wasReady = _allPlayersReady;
|
||||
bool nowReady = value;
|
||||
|
||||
if (wasReady != nowReady)
|
||||
{
|
||||
_allPlayersReady = value;
|
||||
|
||||
if (nowReady)
|
||||
{
|
||||
OnRoomServerPlayersReady();
|
||||
}
|
||||
else
|
||||
{
|
||||
OnRoomServerPlayersNotReady();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
|
||||
// always <= maxConnections
|
||||
minPlayers = Mathf.Min(minPlayers, maxConnections);
|
||||
|
||||
// always >= 0
|
||||
minPlayers = Mathf.Max(minPlayers, 0);
|
||||
|
||||
if (roomPlayerPrefab != null)
|
||||
{
|
||||
NetworkIdentity identity = roomPlayerPrefab.GetComponent<NetworkIdentity>();
|
||||
if (identity == null)
|
||||
{
|
||||
roomPlayerPrefab = null;
|
||||
Debug.LogError("RoomPlayer prefab must have a NetworkIdentity component.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SceneLoadedForPlayer(NetworkConnectionToClient conn, GameObject roomPlayer)
|
||||
{
|
||||
//Debug.Log($"NetworkRoom SceneLoadedForPlayer scene: {SceneManager.GetActiveScene().path} {conn}");
|
||||
|
||||
if (Utils.IsSceneActive(RoomScene))
|
||||
{
|
||||
// cant be ready in room, add to ready list
|
||||
PendingPlayer pending;
|
||||
pending.conn = conn;
|
||||
pending.roomPlayer = roomPlayer;
|
||||
pendingPlayers.Add(pending);
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject gamePlayer = OnRoomServerCreateGamePlayer(conn, roomPlayer);
|
||||
if (gamePlayer == null)
|
||||
{
|
||||
// get start position from base class
|
||||
Transform startPos = GetStartPosition();
|
||||
gamePlayer = startPos != null
|
||||
? Instantiate(playerPrefab, startPos.position, startPos.rotation)
|
||||
: Instantiate(playerPrefab, Vector3.zero, Quaternion.identity);
|
||||
}
|
||||
|
||||
if (!OnRoomServerSceneLoadedForPlayer(conn, roomPlayer, gamePlayer))
|
||||
return;
|
||||
|
||||
// replace room player with game player
|
||||
NetworkServer.ReplacePlayerForConnection(conn, gamePlayer, true);
|
||||
}
|
||||
|
||||
internal void CallOnClientEnterRoom()
|
||||
{
|
||||
OnRoomClientEnter();
|
||||
foreach (NetworkRoomPlayer player in roomSlots)
|
||||
if (player != null)
|
||||
{
|
||||
player.OnClientEnterRoom();
|
||||
}
|
||||
}
|
||||
|
||||
internal void CallOnClientExitRoom()
|
||||
{
|
||||
OnRoomClientExit();
|
||||
foreach (NetworkRoomPlayer player in roomSlots)
|
||||
if (player != null)
|
||||
{
|
||||
player.OnClientExitRoom();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CheckReadyToBegin checks all of the players in the room to see if their readyToBegin flag is set.
|
||||
/// <para>If all of the players are ready, then the server switches from the RoomScene to the PlayScene, essentially starting the game. This is called automatically in response to NetworkRoomPlayer.CmdChangeReadyState.</para>
|
||||
/// </summary>
|
||||
public void CheckReadyToBegin()
|
||||
{
|
||||
if (!Utils.IsSceneActive(RoomScene))
|
||||
return;
|
||||
|
||||
int numberOfReadyPlayers = NetworkServer.connections.Count(conn =>
|
||||
conn.Value != null &&
|
||||
conn.Value.identity != null &&
|
||||
conn.Value.identity.TryGetComponent(out NetworkRoomPlayer nrp) &&
|
||||
nrp.readyToBegin);
|
||||
|
||||
bool enoughReadyPlayers = minPlayers <= 0 || numberOfReadyPlayers >= minPlayers;
|
||||
if (enoughReadyPlayers)
|
||||
{
|
||||
pendingPlayers.Clear();
|
||||
allPlayersReady = true;
|
||||
}
|
||||
else
|
||||
allPlayersReady = false;
|
||||
}
|
||||
|
||||
#region server handlers
|
||||
|
||||
/// <summary>
|
||||
/// Called on the server when a new client connects.
|
||||
/// <para>Unity calls this on the Server when a Client connects to the Server. Use an override to tell the NetworkManager what to do when a client connects to the server.</para>
|
||||
/// </summary>
|
||||
/// <param name="conn">Connection from client.</param>
|
||||
public override void OnServerConnect(NetworkConnectionToClient conn)
|
||||
{
|
||||
// cannot join game in progress
|
||||
if (!Utils.IsSceneActive(RoomScene))
|
||||
{
|
||||
Debug.Log($"Not in Room scene...disconnecting {conn}");
|
||||
conn.Disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
base.OnServerConnect(conn);
|
||||
OnRoomServerConnect(conn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on the server when a client disconnects.
|
||||
/// <para>This is called on the Server when a Client disconnects from the Server. Use an override to decide what should happen when a disconnection is detected.</para>
|
||||
/// </summary>
|
||||
/// <param name="conn">Connection from client.</param>
|
||||
public override void OnServerDisconnect(NetworkConnectionToClient conn)
|
||||
{
|
||||
if (conn.identity != null)
|
||||
{
|
||||
NetworkRoomPlayer roomPlayer = conn.identity.GetComponent<NetworkRoomPlayer>();
|
||||
|
||||
if (roomPlayer != null)
|
||||
roomSlots.Remove(roomPlayer);
|
||||
|
||||
foreach (NetworkIdentity clientOwnedObject in conn.owned)
|
||||
{
|
||||
roomPlayer = clientOwnedObject.GetComponent<NetworkRoomPlayer>();
|
||||
if (roomPlayer != null)
|
||||
roomSlots.Remove(roomPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
allPlayersReady = false;
|
||||
|
||||
foreach (NetworkRoomPlayer player in roomSlots)
|
||||
{
|
||||
if (player != null)
|
||||
player.GetComponent<NetworkRoomPlayer>().readyToBegin = false;
|
||||
}
|
||||
|
||||
if (Utils.IsSceneActive(RoomScene))
|
||||
RecalculateRoomPlayerIndices();
|
||||
|
||||
OnRoomServerDisconnect(conn);
|
||||
base.OnServerDisconnect(conn);
|
||||
|
||||
if (Utils.IsHeadless())
|
||||
{
|
||||
if (numPlayers < 1)
|
||||
StopServer();
|
||||
}
|
||||
}
|
||||
|
||||
// Sequential index used in round-robin deployment of players into instances and score positioning
|
||||
public int clientIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Called on the server when a client is ready.
|
||||
/// <para>The default implementation of this function calls NetworkServer.SetClientReady() to continue the network setup process.</para>
|
||||
/// </summary>
|
||||
/// <param name="conn">Connection from client.</param>
|
||||
public override void OnServerReady(NetworkConnectionToClient conn)
|
||||
{
|
||||
//Debug.Log($"NetworkRoomManager OnServerReady {conn}");
|
||||
base.OnServerReady(conn);
|
||||
|
||||
if (conn != null && conn.identity != null)
|
||||
{
|
||||
GameObject roomPlayer = conn.identity.gameObject;
|
||||
|
||||
// if null or not a room player, don't replace it
|
||||
if (roomPlayer != null && roomPlayer.GetComponent<NetworkRoomPlayer>() != null)
|
||||
SceneLoadedForPlayer(conn, roomPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on the server when a client adds a new player with NetworkClient.AddPlayer.
|
||||
/// <para>The default implementation for this function creates a new player object from the playerPrefab.</para>
|
||||
/// </summary>
|
||||
/// <param name="conn">Connection from client.</param>
|
||||
public override void OnServerAddPlayer(NetworkConnectionToClient conn)
|
||||
{
|
||||
// increment the index before adding the player, so first player starts at 1
|
||||
clientIndex++;
|
||||
|
||||
if (Utils.IsSceneActive(RoomScene))
|
||||
{
|
||||
allPlayersReady = false;
|
||||
|
||||
//Debug.Log("NetworkRoomManager.OnServerAddPlayer playerPrefab: {roomPlayerPrefab.name}");
|
||||
|
||||
GameObject newRoomGameObject = OnRoomServerCreateRoomPlayer(conn);
|
||||
if (newRoomGameObject == null)
|
||||
newRoomGameObject = Instantiate(roomPlayerPrefab.gameObject, Vector3.zero, Quaternion.identity);
|
||||
|
||||
NetworkServer.AddPlayerForConnection(conn, newRoomGameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Late joiners not supported...should've been kicked by OnServerDisconnect
|
||||
Debug.Log($"Not in Room scene...disconnecting {conn}");
|
||||
conn.Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
[Server]
|
||||
public void RecalculateRoomPlayerIndices()
|
||||
{
|
||||
if (roomSlots.Count > 0)
|
||||
{
|
||||
for (int i = 0; i < roomSlots.Count; i++)
|
||||
{
|
||||
roomSlots[i].index = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This causes the server to switch scenes and sets the networkSceneName.
|
||||
/// <para>Clients that connect to this server will automatically switch to this scene. This is called automatically if onlineScene or offlineScene are set, but it can be called from user code to switch scenes again while the game is in progress. This automatically sets clients to be not-ready. The clients must call NetworkClient.Ready() again to participate in the new scene.</para>
|
||||
/// </summary>
|
||||
/// <param name="newSceneName"></param>
|
||||
public override void ServerChangeScene(string newSceneName)
|
||||
{
|
||||
if (newSceneName == RoomScene)
|
||||
{
|
||||
foreach (NetworkRoomPlayer roomPlayer in roomSlots)
|
||||
{
|
||||
if (roomPlayer == null)
|
||||
continue;
|
||||
|
||||
// find the game-player object for this connection, and destroy it
|
||||
NetworkIdentity identity = roomPlayer.GetComponent<NetworkIdentity>();
|
||||
|
||||
if (NetworkServer.active)
|
||||
{
|
||||
// re-add the room object
|
||||
roomPlayer.GetComponent<NetworkRoomPlayer>().readyToBegin = false;
|
||||
NetworkServer.ReplacePlayerForConnection(identity.connectionToClient, roomPlayer.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
allPlayersReady = false;
|
||||
}
|
||||
|
||||
base.ServerChangeScene(newSceneName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on the server when a scene is completed loaded, when the scene load was initiated by the server with ServerChangeScene().
|
||||
/// </summary>
|
||||
/// <param name="sceneName">The name of the new scene.</param>
|
||||
public override void OnServerSceneChanged(string sceneName)
|
||||
{
|
||||
if (sceneName != RoomScene)
|
||||
{
|
||||
// call SceneLoadedForPlayer on any players that become ready while we were loading the scene.
|
||||
foreach (PendingPlayer pending in pendingPlayers)
|
||||
SceneLoadedForPlayer(pending.conn, pending.roomPlayer);
|
||||
|
||||
pendingPlayers.Clear();
|
||||
}
|
||||
|
||||
OnRoomServerSceneChanged(sceneName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is invoked when a server is started - including when a host is started.
|
||||
/// <para>StartServer has multiple signatures, but they all cause this hook to be called.</para>
|
||||
/// </summary>
|
||||
public override void OnStartServer()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(RoomScene))
|
||||
{
|
||||
Debug.LogError("NetworkRoomManager RoomScene is empty. Set the RoomScene in the inspector for the NetworkRoomManager");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(GameplayScene))
|
||||
{
|
||||
Debug.LogError("NetworkRoomManager PlayScene is empty. Set the PlayScene in the inspector for the NetworkRoomManager");
|
||||
return;
|
||||
}
|
||||
|
||||
OnRoomStartServer();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is invoked when a host is started.
|
||||
/// <para>StartHost has multiple signatures, but they all cause this hook to be called.</para>
|
||||
/// </summary>
|
||||
public override void OnStartHost()
|
||||
{
|
||||
OnRoomStartHost();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is called when a server is stopped - including when a host is stopped.
|
||||
/// </summary>
|
||||
public override void OnStopServer()
|
||||
{
|
||||
roomSlots.Clear();
|
||||
OnRoomStopServer();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is called when a host is stopped.
|
||||
/// </summary>
|
||||
public override void OnStopHost()
|
||||
{
|
||||
OnRoomStopHost();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region client handlers
|
||||
|
||||
/// <summary>
|
||||
/// This is invoked when the client is started.
|
||||
/// </summary>
|
||||
public override void OnStartClient()
|
||||
{
|
||||
if (roomPlayerPrefab == null || roomPlayerPrefab.gameObject == null)
|
||||
Debug.LogError("NetworkRoomManager no RoomPlayer prefab is registered. Please add a RoomPlayer prefab.");
|
||||
else
|
||||
NetworkClient.RegisterPrefab(roomPlayerPrefab.gameObject);
|
||||
|
||||
if (playerPrefab == null)
|
||||
Debug.LogError("NetworkRoomManager no GamePlayer prefab is registered. Please add a GamePlayer prefab.");
|
||||
|
||||
OnRoomStartClient();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on the client when connected to a server.
|
||||
/// <para>The default implementation of this function sets the client as ready and adds a player. Override the function to dictate what happens when the client connects.</para>
|
||||
/// </summary>
|
||||
public override void OnClientConnect()
|
||||
{
|
||||
OnRoomClientConnect();
|
||||
base.OnClientConnect();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on clients when disconnected from a server.
|
||||
/// <para>This is called on the client when it disconnects from the server. Override this function to decide what happens when the client disconnects.</para>
|
||||
/// </summary>
|
||||
public override void OnClientDisconnect()
|
||||
{
|
||||
OnRoomClientDisconnect();
|
||||
base.OnClientDisconnect();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is called when a client is stopped.
|
||||
/// </summary>
|
||||
public override void OnStopClient()
|
||||
{
|
||||
OnRoomStopClient();
|
||||
CallOnClientExitRoom();
|
||||
roomSlots.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on clients when a scene has completed loaded, when the scene load was initiated by the server.
|
||||
/// <para>Scene changes can cause player objects to be destroyed. The default implementation of OnClientSceneChanged in the NetworkManager is to add a player object for the connection if no player object exists.</para>
|
||||
/// </summary>
|
||||
public override void OnClientSceneChanged()
|
||||
{
|
||||
if (Utils.IsSceneActive(RoomScene))
|
||||
{
|
||||
if (NetworkClient.isConnected)
|
||||
CallOnClientEnterRoom();
|
||||
}
|
||||
else
|
||||
CallOnClientExitRoom();
|
||||
|
||||
base.OnClientSceneChanged();
|
||||
OnRoomClientSceneChanged();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region room server virtuals
|
||||
|
||||
/// <summary>
|
||||
/// This is called on the host when a host is started.
|
||||
/// </summary>
|
||||
public virtual void OnRoomStartHost() {}
|
||||
|
||||
/// <summary>
|
||||
/// This is called on the host when the host is stopped.
|
||||
/// </summary>
|
||||
public virtual void OnRoomStopHost() {}
|
||||
|
||||
/// <summary>
|
||||
/// This is called on the server when the server is started - including when a host is started.
|
||||
/// </summary>
|
||||
public virtual void OnRoomStartServer() {}
|
||||
|
||||
/// <summary>
|
||||
/// This is called on the server when the server is started - including when a host is stopped.
|
||||
/// </summary>
|
||||
public virtual void OnRoomStopServer() {}
|
||||
|
||||
/// <summary>
|
||||
/// This is called on the server when a new client connects to the server.
|
||||
/// </summary>
|
||||
/// <param name="conn">The new connection.</param>
|
||||
public virtual void OnRoomServerConnect(NetworkConnectionToClient conn) {}
|
||||
|
||||
/// <summary>
|
||||
/// This is called on the server when a client disconnects.
|
||||
/// </summary>
|
||||
/// <param name="conn">The connection that disconnected.</param>
|
||||
public virtual void OnRoomServerDisconnect(NetworkConnectionToClient conn) {}
|
||||
|
||||
/// <summary>
|
||||
/// This is called on the server when a networked scene finishes loading.
|
||||
/// </summary>
|
||||
/// <param name="sceneName">Name of the new scene.</param>
|
||||
public virtual void OnRoomServerSceneChanged(string sceneName) {}
|
||||
|
||||
/// <summary>
|
||||
/// This allows customization of the creation of the room-player object on the server.
|
||||
/// <para>By default the roomPlayerPrefab is used to create the room-player, but this function allows that behaviour to be customized.</para>
|
||||
/// </summary>
|
||||
/// <param name="conn">The connection the player object is for.</param>
|
||||
/// <returns>The new room-player object.</returns>
|
||||
public virtual GameObject OnRoomServerCreateRoomPlayer(NetworkConnectionToClient conn)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This allows customization of the creation of the GamePlayer object on the server.
|
||||
/// <para>By default the gamePlayerPrefab is used to create the game-player, but this function allows that behaviour to be customized. The object returned from the function will be used to replace the room-player on the connection.</para>
|
||||
/// </summary>
|
||||
/// <param name="conn">The connection the player object is for.</param>
|
||||
/// <param name="roomPlayer">The room player object for this connection.</param>
|
||||
/// <returns>A new GamePlayer object.</returns>
|
||||
public virtual GameObject OnRoomServerCreateGamePlayer(NetworkConnectionToClient conn, GameObject roomPlayer)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This allows customization of the creation of the GamePlayer object on the server.
|
||||
/// <para>This is only called for subsequent GamePlay scenes after the first one.</para>
|
||||
/// <para>See <see cref="OnRoomServerCreateGamePlayer(NetworkConnectionToClient, GameObject)">OnRoomServerCreateGamePlayer(NetworkConnection, GameObject)</see> to customize the player object for the initial GamePlay scene.</para>
|
||||
/// </summary>
|
||||
/// <param name="conn">The connection the player object is for.</param>
|
||||
public virtual void OnRoomServerAddPlayer(NetworkConnectionToClient conn)
|
||||
{
|
||||
base.OnServerAddPlayer(conn);
|
||||
}
|
||||
|
||||
// for users to apply settings from their room player object to their in-game player object
|
||||
/// <summary>
|
||||
/// This is called on the server when it is told that a client has finished switching from the room scene to a game player scene.
|
||||
/// <para>When switching from the room, the room-player is replaced with a game-player object. This callback function gives an opportunity to apply state from the room-player to the game-player object.</para>
|
||||
/// </summary>
|
||||
/// <param name="conn">The connection of the player</param>
|
||||
/// <param name="roomPlayer">The room player object.</param>
|
||||
/// <param name="gamePlayer">The game player object.</param>
|
||||
/// <returns>False to not allow this player to replace the room player.</returns>
|
||||
public virtual bool OnRoomServerSceneLoadedForPlayer(NetworkConnectionToClient conn, GameObject roomPlayer, GameObject gamePlayer)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is called on server from NetworkRoomPlayer.CmdChangeReadyState when client indicates change in Ready status.
|
||||
/// </summary>
|
||||
public virtual void ReadyStatusChanged()
|
||||
{
|
||||
int CurrentPlayers = 0;
|
||||
int ReadyPlayers = 0;
|
||||
|
||||
foreach (NetworkRoomPlayer item in roomSlots)
|
||||
{
|
||||
if (item != null)
|
||||
{
|
||||
CurrentPlayers++;
|
||||
if (item.readyToBegin)
|
||||
ReadyPlayers++;
|
||||
}
|
||||
}
|
||||
|
||||
if (CurrentPlayers == ReadyPlayers)
|
||||
CheckReadyToBegin();
|
||||
else
|
||||
allPlayersReady = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is called on the server when all the players in the room are ready.
|
||||
/// <para>The default implementation of this function uses ServerChangeScene() to switch to the game player scene. By implementing this callback you can customize what happens when all the players in the room are ready, such as adding a countdown or a confirmation for a group leader.</para>
|
||||
/// </summary>
|
||||
public virtual void OnRoomServerPlayersReady()
|
||||
{
|
||||
// all players are readyToBegin, start the game
|
||||
ServerChangeScene(GameplayScene);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is called on the server when CheckReadyToBegin finds that players are not ready
|
||||
/// <para>May be called multiple times while not ready players are joining</para>
|
||||
/// </summary>
|
||||
public virtual void OnRoomServerPlayersNotReady() {}
|
||||
|
||||
#endregion
|
||||
|
||||
#region room client virtuals
|
||||
|
||||
/// <summary>
|
||||
/// This is a hook to allow custom behaviour when the game client enters the room.
|
||||
/// </summary>
|
||||
public virtual void OnRoomClientEnter() {}
|
||||
|
||||
/// <summary>
|
||||
/// This is a hook to allow custom behaviour when the game client exits the room.
|
||||
/// </summary>
|
||||
public virtual void OnRoomClientExit() {}
|
||||
|
||||
/// <summary>
|
||||
/// This is called on the client when it connects to server.
|
||||
/// </summary>
|
||||
public virtual void OnRoomClientConnect() {}
|
||||
|
||||
/// <summary>
|
||||
/// This is called on the client when disconnected from a server.
|
||||
/// </summary>
|
||||
public virtual void OnRoomClientDisconnect() {}
|
||||
|
||||
/// <summary>
|
||||
/// This is called on the client when a client is started.
|
||||
/// </summary>
|
||||
public virtual void OnRoomStartClient() {}
|
||||
|
||||
/// <summary>
|
||||
/// This is called on the client when the client stops.
|
||||
/// </summary>
|
||||
public virtual void OnRoomStopClient() {}
|
||||
|
||||
/// <summary>
|
||||
/// This is called on the client when the client is finished loading a new networked scene.
|
||||
/// </summary>
|
||||
public virtual void OnRoomClientSceneChanged() {}
|
||||
|
||||
#endregion
|
||||
|
||||
#region optional UI
|
||||
|
||||
/// <summary>
|
||||
/// virtual so inheriting classes can roll their own
|
||||
/// </summary>
|
||||
public virtual void OnGUI()
|
||||
{
|
||||
if (!showRoomGUI)
|
||||
return;
|
||||
|
||||
if (NetworkServer.active && Utils.IsSceneActive(GameplayScene))
|
||||
{
|
||||
GUILayout.BeginArea(new Rect(Screen.width - 150f, 10f, 140f, 30f));
|
||||
if (GUILayout.Button("Return to Room"))
|
||||
ServerChangeScene(RoomScene);
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
|
||||
if (Utils.IsSceneActive(RoomScene))
|
||||
GUI.Box(new Rect(10f, 180f, 520f, 150f), "PLAYERS");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
11
Assets/Mirror/Components/NetworkRoomManager.cs.meta
Normal file
11
Assets/Mirror/Components/NetworkRoomManager.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 615e6c6589cf9e54cad646b5a11e0529
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user