In the last post we got our local character moving around using the server to set the actual movement. In this one, we will get other players spawning and de-spawning and also moving around. Can’t really call it multiplayer if there is only one player… Currently, the Server can see all the spawned players and deal with them and you can watch everyone moving around so now we need to make the Client do that too.
First, go into NetworkMessages (doesn’t matter which as long as you remember to copy over the changes). We need a new tag and message. Our tag will be SpawnPlayer. Create a new struct as follows:
public struct PlayerSpawnData : IDarkRiftSerializable
{
public ushort ID;
public Vector3 Position;
public PlayerSpawnData(ushort id, Vector3 position)
{
ID = id;
Position = position;
}
public void Deserialize(DeserializeEvent e)
{
ID = e.Reader.ReadUInt16();
Position = e.Reader.ReadVector3();
}
public void Serialize(SerializeEvent e)
{
e.Writer.Write(ID);
e.Writer.WriteVector3(Position);
}
}
We need the Position so people spawn in the correct location. If you don’t have this other players will spawn at origin and then get moved to the right place which is… not what we want.
Server: Sending New Players
Next, we want to use this to tell the other players to spawn the new player locally, and then send all the current players to the new one. Inside PlayerManager.cs on the Server inside SpawnPlayerOnServer() add the following:
ServerPlayerController controller = CurrentPlayers[clientID].GetComponent<ServerPlayerController>();
ServerManager.Instance.SendNewPlayerToOthers();
ServerManager.Instance.SendOthersToNewPlayer();
We want to send the new players position inside SendNewPlayerToOtherSpawn and pass the list of spawn players to SendOthersToNewPlayer so we can easily access them and do the same in reverse. SpawnPlayerOnServer() should now look like:
public void SpawnPlayerOnServer(ushort clientID)
{
if (!CurrentPlayers.ContainsKey(clientID))
{
GameObject go = Instantiate(ServerPlayerPrefab, ServerPlayerPrefab.transform.position, Quaternion.identity);
CurrentPlayers.Add(clientID, go);
ServerManager.Instance.ConnectedClients[clientID].SpawnLocalPlayerOnClient();
ServerPlayerController controller = CurrentPlayers[clientID].GetComponent<ServerPlayerController>();
ServerManager.Instance.SendNewPlayerToOthers(clientID, controller.transform.position);
ServerManager.Instance.SendOthersToNewPlayer(clientID, CurrentPlayers);
}
}
ASIDE: You might notice I sometimes use Client/Server and sometimes us client/server – Using the capital means I am referring to the *project* we are working in, the lower case being more general. “On the Client add this code” vs “The client doesn’t care if we do X” – I try to be good about this but if you spot an error ping me @Ace on the DR discord. Also I prefer PascalCase for lists and dictionaries but that is personal preference. moving on…
You probably have noticed those don’t exist so as per usual make those inside ServerManager. Move over to ServerManager.cs and lets fill those out. First, SendNewPlayerToOthers:
public void SendNewPlayerToOthers(ushort clientID, Vector3 position)
{
PlayerSpawnData spawnData = new PlayerSpawnData(clientID, position);
SendToAllExcept(clientID, Tags.SpawnPlayer, spawnData);
}
Hopefully you can see why things like SendToAllExcept can come in handy – without that we would have to write out the loop to do this every time we wanted this functionality. Ok, lets do SendOthersToNewPlayer():
public void SendOthersToNewPlayer(ushort clientID, Dictionary<ushort, GameObject> players)
{
foreach (KeyValuePair<ushort, GameObject> player in players)
{
if (player.Key != clientID)
{
PlayerSpawnData spawnData = new PlayerSpawnData(player.Key, player.Value.transform.position);
SendToClient(clientID, Tags.SpawnPlayer, spawnData);
}
}
}
Each time a new player connects/spawn, we will loop through every other spawned player and send that players info to the new one. Simple enough.
Client: Sending New Players
Lets move over to the Client – inside ConnectionManager.cs add the new case to OnMessage:
case Tags.SpawnPlayer:
OnSpawnPlayer(m.Deserialize<PlayerSpawnData>());
break;
Create that method, and like with other messages we will pass this on to GameManager:
private void OnSpawnPlayer(PlayerSpawnData data)
{
GameManager.Instance.SpawnPlayer(data);
}
Followed by:
public void SpawnPlayer(PlayerSpawnData data)
{
if (!ConnectedPlayers.ContainsKey(data.ID))
{
GameObject go = Instantiate(playerPrefab, data.Position, Quaternion.identity);
ConnectedPlayers.Add(data.ID, go);
}
}
inside GameManager.cs
REFACTOR: Hmm… this looks an AWFUL lot like SpawnLocalPlayer() – in-fact, we don’t actually need both, so lets refactor quickly. You can leave your SpawnLocalPlayerResponseData and tag if you want, you might want to do something different for the local player but right now EVERY player will have an ID and a position and if we need to tag the local player as isLocalPlayer later we can do that later in PlayerSpawnData. So, on the Client Inside ConnectionManager remove the case SpawnLocalPlayerResponse and the OnSpawnLocalPlayerResponse method. You can also remove SpawnLocalPlayer() inside GameManager.cs – Inside PlayerManger.cs on the Server inside SpawnPlayerOnServer where you have:
ServerManager.Instance.ConnectedClients[clientID].SpawnLocalPlayerOnClient();
Remove/change that to:
PlayerSpawnData data = new PlayerSpawnData(clientID, go.transform.position);
ServerManager.Instance.ConnectedClients[clientID].SpawnLocalPlayerOnClient(data);
And inside ConnectedClient.cs change SpawnLocalPlayerOnClient() to be:
public void SpawnLocalPlayerOnClient(PlayerSpawnData data)
{
ServerManager.Instance.SendToClient(data.ID, Tags.SpawnPlayer, data);
}
Much cleaner. While writing this series I run into situations like this and instead of editing every other article to match I think it is better (and let’s be honest, easier) to show you my process so you can start to get a feel for making your own edits (and to see how not planning ahead can lead to things like this). If you test now, everything should work like before /Refactor.
Lets test to see if other players are indeed spawning. I *highly* suggest you do update your project settings as follows:
By default, a built project will be fullscreen and is a pain to close – so for testing you want smaller windows you can move around easier (especially if you only have one monitor). Build the project (if this is your first build it will ask you where you want to build it. Right click and create a new folder called Build and use that). Move the player in the built version out of the way, and then start the game in the editor with both pulled up. Not only should both players show up on both, but because of the code we wrote before, synced movement *already* works. Pretty neat right? Now, we want to remove players when someone disconnects, so lets do that.
Open up NetworkMessages.cs (either one) and add a new tag:
DespawnPlayer
Create a PlayerDespawnData as follows:
public struct PlayerDespawnData : IDarkRiftSerializable
{
public ushort ID;
public PlayerSpawnData(ushort id, Vector3 position)
{
ID = id;
}
public void Deserialize(DeserializeEvent e)
{
ID = e.Reader.ReadUInt16();
}
public void Serialize(SerializeEvent e)
{
e.Writer.Write(ID);
}
}
Its the same as PlayerSpawnData but without the position (because.. we don’t care about that). Copy it over to the other NetworkMessages.cs (this WILL be the cause of frustration at some point, so always check they match if you have issues).
Server: De-spawning Players
Inside OnClientDisconnected.cs on the Server in OnClientDisconnected lets remove that player and tell others to do the same:
private void OnClientDisconnected(object sender, ClientDisconnectedEventArgs e)
{
ConnectedClients.Remove(e.Client.ID);
Destroy(PlayerManager.Instance.CurrentPlayers[e.Client.ID]);
PlayerManager.Instance.CurrentPlayers.Remove(e.Client.ID);
SendToAllExcept(e.Client.ID, Tags.DespawnPlayer, new PlayerDespawnData(e.Client.ID));
}
You will need to make CurrentPlayers public for this to work. We do this inside ServerManager instead of ConnectedClient because the server will still be listening for the disconnect even if it has unsubscribed from MessageReceived for that client.
ASIDE: PlayerDisconnected and PlayerDisconnectedData are probably better names but since all we are going to use this for is despawning client side I don’t think it is an issue here. Feel free to make that change though so you have a logical name for that event if you want to send more information.
Client: De-spawning Players
Back on the Client, in ConnectionManager add the case and the method:
case Tags.DespawnPlayer:
OnDespawnPlayer(m.Deserialize<PlayerDespawnData>());
break;
private void OnDespawnPlayer(PlayerDespawnData data)
{
GameManager.Instance.RemovePlayerFromGame(data);
}
And in GameManager:
public void RemovePlayerFromGame(PlayerDespawnData data)
{
if (ConnectedPlayers.ContainsKey(data.ID))
{
Destroy(ConnectedPlayers[data.ID]);
}
}
Alright! Build and test with a few clients, players should spawn, despawn, and move correctly. We edited several scripts so here is how each should look in case something broke on your end.
Client Code Changes
NetworkMessages.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DarkRift;
namespace DarkRiftRPG
{
public enum Tags
{
JoinGameRequest,
JoinGameResponse,
SpawnLocalPlayerRequest,
PlayerMovementRequest,
PlayerMovementUpdate,
SpawnPlayer,
DespawnPlayer
}
public struct JoinGameResponseData : IDarkRiftSerializable
{
public bool JoinGameRequestAccepted;
public JoinGameResponseData(bool accepted)
{
JoinGameRequestAccepted = accepted;
}
public void Deserialize(DeserializeEvent e)
{
JoinGameRequestAccepted = e.Reader.ReadBoolean();
}
public void Serialize(SerializeEvent e)
{
e.Writer.Write(JoinGameRequestAccepted);
}
}
public struct PlayerMovementRequestData : IDarkRiftSerializable
{
public Vector3 PlayerClickLocation;
public PlayerMovementRequestData(Vector3 clickPos)
{
PlayerClickLocation = clickPos;
}
public void Deserialize(DeserializeEvent e)
{
PlayerClickLocation = e.Reader.ReadVector3();
}
public void Serialize(SerializeEvent e)
{
e.Writer.WriteVector3(PlayerClickLocation);
}
}
public struct PlayerPositionInputData : IDarkRiftSerializable
{
public ushort ID;
public Vector3 Pos;
public PlayerPositionInputData(ushort id, Vector3 pos)
{
ID = id;
Pos = pos;
}
public void Deserialize(DeserializeEvent e)
{
ID = e.Reader.ReadUInt16();
Pos = e.Reader.ReadVector3();
}
public void Serialize(SerializeEvent e)
{
e.Writer.Write(ID);
e.Writer.WriteVector3(Pos);
}
}
public struct ProccessedPlayerMovementData : IDarkRiftSerializable
{
public PlayerPositionInputData[] ProccessedMovementUpdate;
public ProccessedPlayerMovementData(PlayerPositionInputData[] newPlayerPositions)
{
ProccessedMovementUpdate = newPlayerPositions;
}
public void Deserialize(DeserializeEvent e)
{
ProccessedMovementUpdate = e.Reader.ReadSerializables<PlayerPositionInputData>();
}
public void Serialize(SerializeEvent e)
{
e.Writer.Write(ProccessedMovementUpdate);
}
}
public struct PlayerSpawnData : IDarkRiftSerializable
{
public ushort ID;
public Vector3 Position;
public PlayerSpawnData(ushort id, Vector3 position)
{
ID = id;
Position = position;
}
public void Deserialize(DeserializeEvent e)
{
ID = e.Reader.ReadUInt16();
Position = e.Reader.ReadVector3();
}
public void Serialize(SerializeEvent e)
{
e.Writer.Write(ID);
e.Writer.WriteVector3(Position);
}
}
public struct PlayerDespawnData : IDarkRiftSerializable
{
public ushort ID;
public PlayerDespawnData(ushort id)
{
ID = id;
}
public void Deserialize(DeserializeEvent e)
{
ID = e.Reader.ReadUInt16();
}
public void Serialize(SerializeEvent e)
{
e.Writer.Write(ID);
}
}
}
ConnectionManager.cs
using System;
using System.Net;
using DarkRift;
using DarkRift.Client;
using DarkRift.Client.Unity;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace DarkRiftRPG
{
public class ConnectionManager : MonoBehaviour
{
//We want a static reference to ConnectionManager so it can be called directly from other scripts
public static ConnectionManager Instance;
//A reference to the Client component on this game object.
public UnityClient Client { get; private set; }
public ushort LocalClientID;
void Awake()
{
if (Instance != null)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(this);
}
void Start()
{
Client = GetComponent<UnityClient>();
Client.ConnectInBackground(IPAddress.Loopback, Client.Port, false, ConnectCallback);
Client.MessageReceived += OnMessage;
}
public void SendClickPosToServer(Vector3 point)
{
using (Message message = Message.Create((ushort)Tags.PlayerMovementRequest, new PlayerMovementRequestData(point)))
{
Client.SendMessage(message, SendMode.Reliable);
}
}
private void ConnectCallback(Exception e)
{
if (Client.ConnectionState == ConnectionState.Connected)
{
Debug.Log("Connected to server!");
OnConnectedToServer();
}
else
{
Debug.LogError($"Unable to connect to server. Reason: {e.Message} ");
}
}
private void OnConnectedToServer()
{
using (Message message = Message.CreateEmpty((ushort)Tags.JoinGameRequest))
{
Client.SendMessage(message, SendMode.Reliable);
}
}
private void OnMessage(object sender, MessageReceivedEventArgs e)
{
using (Message m = e.GetMessage())
{
switch ((Tags)m.Tag)
{
case Tags.JoinGameResponse:
OnPlayerJoinGameResponse(m.Deserialize<JoinGameResponseData>());
break;
case Tags.PlayerMovementUpdate:
OnPlayerMovementUpdate(m.Deserialize<ProccessedPlayerMovementData>());
break;
case Tags.SpawnPlayer:
OnSpawnPlayer(m.Deserialize<PlayerSpawnData>());
break;
case Tags.DespawnPlayer:
OnDespawnPlayer(m.Deserialize<PlayerDespawnData>());
break;
}
}
}
private void OnDespawnPlayer(PlayerDespawnData data)
{
GameManager.Instance.RemovePlayerFromGame(data);
}
private void OnSpawnPlayer(PlayerSpawnData data)
{
GameManager.Instance.SpawnPlayer(data);
}
private void OnPlayerMovementUpdate(ProccessedPlayerMovementData data)
{
if (GameManager.Instance != null)
GameManager.Instance.HandlePlayerMovementUpdate(data);
}
private void OnPlayerJoinGameResponse(JoinGameResponseData data)
{
if (!data.JoinGameRequestAccepted)
{
Debug.Log("houston we have a problem");
return;
}
LocalClientID = Client.Client.ID;
SceneManager.LoadScene("Game");
}
public void SpawnLocalPlayerRequest()
{
using (Message message = Message.CreateEmpty((ushort)Tags.SpawnLocalPlayerRequest))
{
Client.SendMessage(message, SendMode.Reliable);
}
}
private void OnDestroy()
{
Client.MessageReceived -= OnMessage;
}
}
}
GameManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DarkRift;
using DarkRift.Client;
using System;
using System.Linq;
namespace DarkRiftRPG
{
public class GameManager : MonoBehaviour
{
public static GameManager Instance;
public GameObject playerPrefab;
public Dictionary<ushort, GameObject> ConnectedPlayers = new Dictionary<ushort, GameObject>();
public ushort LocalPlayerID;
void Awake()
{
if (Instance != null)
{
Destroy(gameObject);
return;
}
Instance = this;
}
private void Start()
{
LocalPlayerID = ConnectionManager.Instance.LocalClientID;
Debug.Log("Local Player ID set to: " + LocalPlayerID);
ConnectionManager.Instance.SpawnLocalPlayerRequest();
}
public void SendClickPosToServer(Vector3 point)
{
ConnectionManager.Instance.SendClickPosToServer(point);
}
public void HandlePlayerMovementUpdate(ProccessedPlayerMovementData data)
{
List<PlayerPositionInputData> playerPositions = data.ProccessedMovementUpdate.ToList();
foreach (PlayerPositionInputData pos in playerPositions)
{
if (ConnectedPlayers.ContainsKey(pos.ID))
{
PlayerController controller = ConnectedPlayers[pos.ID].GetComponent<PlayerController>();
controller.SetNewDest(pos.Pos);
}
}
}
public void SpawnPlayer(PlayerSpawnData data)
{
if (!ConnectedPlayers.ContainsKey(data.ID))
{
GameObject go = Instantiate(playerPrefab, data.Position, Quaternion.identity);
ConnectedPlayers.Add(data.ID, go);
}
}
public void RemovePlayerFromGame(PlayerDespawnData data)
{
if (ConnectedPlayers.ContainsKey(data.ID))
{
Destroy(ConnectedPlayers[data.ID]);
}
}
}
}
Server Changes
ServerManager.cs
using System;
using System.Collections.Generic;
using DarkRift;
using DarkRift.Server;
using DarkRift.Server.Unity;
using UnityEngine;
namespace DarkRiftRPG
{
public class ServerManager : MonoBehaviour
{
public static ServerManager Instance;
public Dictionary<ushort, ConnectedClient> ConnectedClients = new Dictionary<ushort, ConnectedClient>();
private XmlUnityServer xmlServer;
private DarkRiftServer server;
void Awake()
{
if (Instance != null)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(this);
}
void Start()
{
xmlServer = GetComponent<XmlUnityServer>();
server = xmlServer.Server;
server.ClientManager.ClientConnected += OnClientConnected;
server.ClientManager.ClientDisconnected += OnClientDisconnected;
}
void OnDestroy()
{
server.ClientManager.ClientConnected -= OnClientConnected;
server.ClientManager.ClientDisconnected -= OnClientDisconnected;
}
private void OnClientDisconnected(object sender, ClientDisconnectedEventArgs e)
{
ConnectedClients.Remove(e.Client.ID);
Destroy(PlayerManager.Instance.CurrentPlayers[e.Client.ID]);
PlayerManager.Instance.CurrentPlayers.Remove(e.Client.ID);
SendToAllExcept(e.Client.ID, Tags.DespawnPlayer, new PlayerDespawnData(e.Client.ID));
}
public void SendNewPlayerToOthers(ushort clientID, Vector3 position)
{
PlayerSpawnData spawnData = new PlayerSpawnData(clientID, position);
SendToAllExcept(clientID, Tags.SpawnPlayer, spawnData);
}
public void SendOthersToNewPlayer(ushort clientID, Dictionary<ushort, GameObject> players)
{
foreach (KeyValuePair<ushort, GameObject> player in players)
{
if (player.Key != clientID)
{
PlayerSpawnData spawnData = new PlayerSpawnData(player.Key, player.Value.transform.position);
SendToClient(clientID, Tags.SpawnPlayer, spawnData);
}
}
}
private void OnClientConnected(object sender, ClientConnectedEventArgs e)
{
e.Client.MessageReceived += OnMessage;
}
private void OnMessage(object sender, MessageReceivedEventArgs e)
{
IClient client = (IClient)sender;
using (Message message = e.GetMessage())
{
switch ((Tags)message.Tag)
{
case Tags.JoinGameRequest:
OnPlayerJoinGameRequest(client);
break;
}
}
}
private void OnPlayerJoinGameRequest(IClient client)
{
JoinGameResponseData data = new JoinGameResponseData();
if (ConnectedClients.ContainsKey(client.ID))
{
data.JoinGameRequestAccepted = false;
using (Message message = Message.Create((ushort)Tags.JoinGameResponse, data))
{
client.SendMessage(message, SendMode.Reliable);
}
return;
} else
{
data.JoinGameRequestAccepted = true;
client.MessageReceived -= OnMessage;
ConnectedClient c = new ConnectedClient(client);
ConnectedClients.Add(c.ClientID, c);
using (Message message = Message.Create((ushort)Tags.JoinGameResponse, data))
{
client.SendMessage(message, SendMode.Reliable);
}
}
}
public void SendToClient(ushort id, Tags t, IDarkRiftSerializable obj, SendMode mode = SendMode.Reliable)
{
using (Message m = Message.Create<IDarkRiftSerializable>((ushort)t, obj))
{
ConnectedClients[id].Client.SendMessage(m, mode);
}
}
public void SendToAll(Tags t, IDarkRiftSerializable obj, SendMode mode = SendMode.Reliable)
{
foreach (KeyValuePair<ushort, ConnectedClient> connectedClient in ConnectedClients)
{
using (Message m = Message.Create<IDarkRiftSerializable>((ushort)t, obj))
{
connectedClient.Value.Client.SendMessage(m, mode);
}
}
}
public void SendToAllExcept(ushort id, Tags t, IDarkRiftSerializable obj, SendMode mode = SendMode.Reliable)
{
using (Message m = Message.Create<IDarkRiftSerializable>((ushort)t, obj))
{
foreach (KeyValuePair<ushort, ConnectedClient> connectedClient in ConnectedClients)
{
if (connectedClient.Key != id)
{
connectedClient.Value.Client.SendMessage(m, mode);
}
}
}
}
}
}
PlayerManager.cs
using System;
using System.Collections.Generic;
using UnityEngine;
using DarkRift;
namespace DarkRiftRPG
{
public class PlayerManager : MonoBehaviour
{
public static PlayerManager Instance;
public GameObject ServerPlayerPrefab;
public Dictionary<ushort, GameObject> CurrentPlayers = new Dictionary<ushort, GameObject>();
List<PlayerPositionInputData> UnprocessedPlayerMovementInput = new List<PlayerPositionInputData>();
List<PlayerPositionInputData> ProccessedPlayerMovementInput = new List<PlayerPositionInputData>();
void Awake()
{
if (Instance != null)
{
Destroy(gameObject);
return;
}
Instance = this;
}
public void SpawnPlayerOnServer(ushort clientID)
{
if (!CurrentPlayers.ContainsKey(clientID))
{
GameObject go = Instantiate(ServerPlayerPrefab, ServerPlayerPrefab.transform.position, Quaternion.identity);
CurrentPlayers.Add(clientID, go);
PlayerSpawnData data = new PlayerSpawnData(clientID, go.transform.position);
ServerManager.Instance.ConnectedClients[clientID].SpawnLocalPlayerOnClient(data);
ServerPlayerController controller = CurrentPlayers[clientID].GetComponent<ServerPlayerController>();
ServerManager.Instance.SendNewPlayerToOthers(clientID, controller.transform.position);
ServerManager.Instance.SendOthersToNewPlayer(clientID, CurrentPlayers);
}
}
public void HandlePlayerMovementRequest(ushort clientID, Vector3 playerClickLocation)
{
PlayerPositionInputData input = new PlayerPositionInputData(clientID, playerClickLocation);
UnprocessedPlayerMovementInput.Add(input);
}
private void FixedUpdate()
{
foreach (PlayerPositionInputData input in UnprocessedPlayerMovementInput)
{
ServerPlayerController controller = CurrentPlayers[input.ID].GetComponent<ServerPlayerController>();
controller.UpdateNavTarget(input.Pos);
ProccessedPlayerMovementInput.Add(input);
}
ProccessedPlayerMovementData proccessedMovement = new ProccessedPlayerMovementData(ProccessedPlayerMovementInput.ToArray());
ServerManager.Instance.SendToAll(Tags.PlayerMovementUpdate, proccessedMovement);
UnprocessedPlayerMovementInput.Clear();
ProccessedPlayerMovementInput.Clear();
}
}
}
In ConnectionManager.cs I removed the ref. to the old LocalPlayerSpawn code because we don’t need it.
If you made it this far, congratulations! Pat yourself on the back, at this point you should have at least a grasp on how DarkRift works. A few of these posts have been much longer then intended.
Now, this is great and all, but currently all we have is players spawning, de-spawning, and moving so to call this an “MMO” even with quotes is really not correct. However, we will use this as a base going forward. You will be able to find this code on Github and I will post about any changes made in Pt 7 (which will probably be the start of our login/account system). See you there 🙂