feat: Updated Chat Example

- Now uses Network Authenticator
- ChatUI is now a networked object with Cmd/Rpc
- Player is much simplified
- LoginUI is a separate canvas
- Login and server HUD combined into one panel
This commit is contained in:
MrGadget1024 2021-12-17 14:31:42 -05:00
parent de45f8d199
commit 279962579c
10 changed files with 689 additions and 609 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,212 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Mirror;
using UnityEngine;
/*
Documentation: https://mirror-networking.gitbook.io/docs/components/network-authenticators
API Reference: https://mirror-networking.com/docs/api/Mirror.NetworkAuthenticator.html
*/
namespace Mirror.Examples.Chat
{
[AddComponentMenu("")]
public class ChatAuthenticator : NetworkAuthenticator
{
readonly HashSet<NetworkConnection> connectionsPendingDisconnect = new HashSet<NetworkConnection>();
[Header("Client Username")]
public string playerName;
#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 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 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 OnServerAuthenticateInternal when a client needs to authenticate
/// </summary>
/// <param name="conn">Connection to client.</param>
public override void OnServerAuthenticate(NetworkConnection 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(NetworkConnection conn, AuthRequestMessage msg)
{
Debug.Log($"Authentication Request: {msg.authUsername}");
if (connectionsPendingDisconnect.Contains(conn)) return;
// check the credentials by calling your web server, database table, playfab api, or any method appropriate.
if (!Player.playerNames.Contains(msg.authUsername))
{
// Add the name to the HashSet
Player.playerNames.Add(msg.authUsername);
// Store username in authenticationData
// This will be read in Player.OnStartServer
// to set the playerName SyncVar.
conn.authenticationData = msg.authUsername;
// 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 = "Username already in use...try again"
};
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(NetworkConnection 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
// Called by UI element Username.OnValueChanged
public void SetPlayername(string username)
{
playerName = username;
LoginUI.instance.errorText.text = string.Empty;
LoginUI.instance.errorText.gameObject.SetActive(false);
}
/// <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>((Action<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 OnClientAuthenticateInternal when a client needs to authenticate
/// </summary>
public override void OnClientAuthenticate()
{
AuthRequestMessage authRequestMessage = new AuthRequestMessage
{
authUsername = playerName,
};
NetworkClient.connection.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
// StopHost works for both host client and remote clients
NetworkManager.singleton.StopHost();
// Do this AFTER StopHost so it doesn't get cleared / hidden by OnClientDisconnect
LoginUI.instance.errorText.text = msg.message;
LoginUI.instance.errorText.gameObject.SetActive(true);
}
}
#endregion
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6e2e6b40604520d408bef0a5243a58cd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -10,64 +10,27 @@ namespace Mirror.Examples.Chat
[AddComponentMenu("")]
public class ChatNetworkManager : NetworkManager
{
[Header("Chat GUI")]
public ChatWindow chatWindow;
public string PlayerName;
#region Messages
public struct CreatePlayerMessage : NetworkMessage
{
public string name;
}
#endregion
#region Server
public override void OnStartServer()
{
base.OnStartServer();
NetworkServer.RegisterHandler<CreatePlayerMessage>(OnCreatePlayer);
}
void OnCreatePlayer(NetworkConnection connection, CreatePlayerMessage createPlayerMessage)
{
// create a gameobject using the name supplied by client
GameObject playergo = Instantiate(playerPrefab);
playergo.GetComponent<Player>().playerName = createPlayerMessage.name;
// set it as the player
NetworkServer.AddPlayerForConnection(connection, playergo);
chatWindow.gameObject.SetActive(true);
}
#endregion
#region Client
// Called by UI element UsernameInput.OnValueChanged
public void SetPlayername(string playerName)
{
PlayerName = playerName;
}
// Called by UI element NetworkAddressInput.OnValueChanged
public void SetHostname(string hostname)
{
networkAddress = hostname;
}
public override void OnClientConnect()
public override void OnServerDisconnect(NetworkConnection conn)
{
base.OnClientConnect();
// remove player name from the HashSet
if (conn.authenticationData != null)
Player.playerNames.Remove((string)conn.authenticationData);
// tell the server to create a player with this name
NetworkClient.connection.Send(new CreatePlayerMessage { name = PlayerName });
base.OnServerDisconnect(conn);
}
#endregion
public override void OnClientDisconnect()
{
base.OnClientDisconnect();
LoginUI.instance.gameObject.SetActive(true);
LoginUI.instance.usernameInput.text = "";
LoginUI.instance.usernameInput.ActivateInputField();
}
}
}

View File

@ -0,0 +1,80 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace Mirror.Examples.Chat
{
public class ChatUI : NetworkBehaviour
{
[Header("UI Elements")]
public InputField chatMessage;
public Text chatHistory;
public Scrollbar scrollbar;
[Header("Diagnostic - Do Not Edit")]
public string localPlayerName;
Dictionary<NetworkConnectionToClient, string> connNames = new Dictionary<NetworkConnectionToClient, string>();
public static ChatUI instance;
void Awake()
{
instance = this;
}
[Command(requiresAuthority = false)]
public void CmdSend(string message, NetworkConnectionToClient sender = null)
{
if (!connNames.ContainsKey(sender))
connNames.Add(sender, sender.identity.GetComponent<Player>().playerName);
if (!string.IsNullOrWhiteSpace(message))
RpcReceive(connNames[sender], message.Trim());
}
[ClientRpc]
public void RpcReceive(string playerName, string message)
{
string prettyMessage = playerName == localPlayerName ?
$"<color=red>{playerName}:</color> {message}" :
$"<color=blue>{playerName}:</color> {message}";
AppendMessage(prettyMessage);
}
public void OnEndEdit(string input)
{
if (Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter) || Input.GetButtonDown("Submit"))
SendMessage();
}
// Called by OnEndEdit above and UI element SendButton.OnClick
public void SendMessage()
{
if (!string.IsNullOrWhiteSpace(chatMessage.text))
{
CmdSend(chatMessage.text.Trim());
chatMessage.text = string.Empty;
chatMessage.ActivateInputField();
}
}
internal void AppendMessage(string message)
{
StartCoroutine(AppendAndScroll(message));
}
IEnumerator AppendAndScroll(string message)
{
chatHistory.text += message + "\n";
// it takes 2 frames for the UI to update ?!?!
yield return null;
yield return null;
// slam the scrollbar down
scrollbar.value = 0;
}
}
}

View File

@ -1,72 +0,0 @@
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
namespace Mirror.Examples.Chat
{
public class ChatWindow : MonoBehaviour
{
public InputField chatMessage;
public Text chatHistory;
public Scrollbar scrollbar;
public void Awake()
{
Player.OnMessage += OnPlayerMessage;
chatMessage.onEndEdit.AddListener(OnEndEdit);
}
void OnEndEdit(string input)
{
if (Input.GetKeyDown(KeyCode.Return)
|| Input.GetKeyDown(KeyCode.KeypadEnter)
|| Input.GetButtonDown("Submit"))
{
//Debug.Log($"OnEndEdit {input}");
SendMessage();
chatMessage.text = string.Empty;
chatMessage.ActivateInputField();
}
}
void OnPlayerMessage(Player player, string message)
{
string prettyMessage = player.isLocalPlayer ?
$"<color=red>{player.playerName}:</color> {message}" :
$"<color=blue>{player.playerName}:</color> {message}";
AppendMessage(prettyMessage);
Debug.Log(message);
}
// Called by UI element SendButton.OnClick
public void SendMessage()
{
if (chatMessage.text.Trim() == string.Empty)
return;
// get our player
Player player = NetworkClient.connection.identity.GetComponent<Player>();
// send a message
player.CmdSend(chatMessage.text.Trim());
}
internal void AppendMessage(string message)
{
StartCoroutine(AppendAndScroll(message));
}
IEnumerator AppendAndScroll(string message)
{
chatHistory.text += message + "\n";
// it takes 2 frames for the UI to update ?!?!
yield return null;
yield return null;
// slam the scrollbar down
scrollbar.value = 0;
}
}
}

View File

@ -0,0 +1,29 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace Mirror.Examples.Chat
{
public class LoginUI : MonoBehaviour
{
[Header("UI Elements")]
public InputField usernameInput;
public Button hostButton;
public Button clientButton;
public Text errorText;
public static LoginUI instance;
void Awake()
{
instance = this;
}
public void ToggleButtons(string username)
{
hostButton.interactable = !string.IsNullOrWhiteSpace(username);
clientButton.interactable = !string.IsNullOrWhiteSpace(username);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7a77ca56c9d91af4b81b73a9907d6112
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,25 +1,23 @@
using System;
using System.Collections.Generic;
namespace Mirror.Examples.Chat
{
public class Player : NetworkBehaviour
{
[SyncVar]
public string playerName;
public static readonly HashSet<string> playerNames = new HashSet<string>();
public static event Action<Player, string> OnMessage;
[Command]
public void CmdSend(string message)
public override void OnStartServer()
{
if (message.Trim() != string.Empty)
RpcReceive(message.Trim());
playerName = (string)connectionToClient.authenticationData;
}
[ClientRpc]
public void RpcReceive(string message)
[SyncVar(hook = nameof(OnPlayerNameChanged))]
public string playerName;
void OnPlayerNameChanged(string _, string newName)
{
OnMessage?.Invoke(this, message);
ChatUI.instance.localPlayerName = playerName;
}
}
}