Boss Room 是一款使用 Unity MLAPI 制作的全功能合作多人 RPG。 它旨在作为学习样本,展示类似游戏中经常出现的某些典型游戏模式。
您最多选择25个主题 主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

449 行
16 KiB

using ExitGames.Client.Photon;
using MLAPI.Serialization.Pooled;
using MLAPI.Transports.Tasks;
using Photon.Realtime;
using System;
using System.Collections.Generic;
using System.IO;
using MLAPI.Logging;
using UnityEngine;
namespace MLAPI.Transports
{
[DefaultExecutionOrder(-1000)]
public class PhotonRealtimeTransport : Transport
{
[Header("Photon Cloud Settings")] [SerializeField]
private string appId;
[SerializeField] private string gameVersion = "0.0.0";
[Tooltip("The region master server to connect to. See https://doc.photonengine.com/en-us/realtime/current/connection-and-authentication/regions#available_regions for a list of regions.")]
[SerializeField]
private string region = "EU";
[Tooltip("The nickname of the player in the photon room. This value is only relevant for other photon realtime features. Leaving it empty generates a random name.")]
[SerializeField]
private string nickName;
[Header("Server Settings")] [Tooltip("Unique name of the room for this session.")] [SerializeField]
private string roomName;
[Tooltip("The maximum amount of players allowed in the room.")] [SerializeField]
private byte maxPlayers = 16;
[Header("Advanced Settings")]
[Tooltip("The Photon event code which will be used to send data over MLAPI channels.")]
[SerializeField]
private byte batchedTransportEventCode = 129;
[Tooltip("The first byte of the range of photon event codes which this transport will reserve for unbatched messages. Should be set to a number lower then 128 to not interfere with photon internal events. Approximately 8 events will be reserved.")]
[SerializeField]
private byte channelIdCodesStartRange = 130;
[Tooltip("Attaches the photon support logger to the transport. Useful for debugging disconnects or other issues.")]
[SerializeField]
private bool attachSupportLogger = false;
[Tooltip("The maximum size of the send queue which batches MLAPI events into Photon events.")]
[SerializeField]
private int sendQueueBatchSize = 4096;
private SocketTask connectTask;
private LoadBalancingClient client;
private bool isHostOrServer;
private readonly Dictionary<string, byte> channelNameToId = new Dictionary<string, byte>();
private readonly Dictionary<byte, string> channelIdToName = new Dictionary<byte, string>();
private readonly Dictionary<ushort, RealtimeChannel> channels = new Dictionary<ushort, RealtimeChannel>();
private readonly Dictionary<ulong, SendQueue> sendQueue = new Dictionary<ulong, SendQueue>();
///<inheritdoc/>
public override ulong ServerClientId => GetMLAPIClientId(0, true);
///<inheritdoc/>
public override void Send(ulong clientId, ArraySegment<byte> data, string channelName)
{
RealtimeChannel channel = channels[channelNameToId[channelName]];
SendQueue queue;
if (!sendQueue.TryGetValue(clientId, out queue))
{
queue = new SendQueue(sendQueueBatchSize);
sendQueue.Add(clientId, queue);
}
if (!queue.AddEvent(channel.Id, data))
{
if (data.Count > queue.Size)
{
Debug.LogWarning($"Sent {data.Count} bytes on channel: {channelName}. Event size exceeds sendQueueBatchSize: ({sendQueueBatchSize}).");
RaisePhotonEvent(clientId, data, channel.Id);
}
else
{
var sendBuffer = queue.GetData();
RaisePhotonEvent(clientId, sendBuffer, batchedTransportEventCode);
queue.Clear();
queue.AddEvent(channel.Id, data);
}
}
}
private void FlushAllSendQueues()
{
foreach (var kvp in sendQueue)
{
if (kvp.Value.IsEmpty())continue;
var sendBuffer = kvp.Value.GetData();
RaisePhotonEvent(kvp.Key, sendBuffer, batchedTransportEventCode);
kvp.Value.Clear();
}
}
private void RaisePhotonEvent(ulong clientId, ArraySegment<byte> data, byte eventCode)
{
client.OpRaiseEvent(
eventCode,
data,
new RaiseEventOptions()
{
TargetActors = new int[] { GetPhotonRealtimeId(clientId) }
},
SendOptions.SendReliable);
}
///<inheritdoc/>
public override NetEventType PollEvent(out ulong clientId, out string channelName, out ArraySegment<byte> payload, out float receiveTime)
{
clientId = 0;
channelName = null;
receiveTime = Time.realtimeSinceStartup;
return NetEventType.Nothing;
}
///<inheritdoc/>
public override SocketTasks StartClient()
{
return ConnectPeer().AsTasks();
}
///<inheritdoc/>
public override SocketTasks StartServer()
{
var task = ConnectPeer();
isHostOrServer = true;
return task.AsTasks();
}
private SocketTask ConnectPeer()
{
connectTask = SocketTask.Working;
InitializeClient();
bool couldConnect = client.ConnectToRegionMaster(region);
if (!couldConnect)
{
connectTask = SocketTask.Fault;
connectTask.Message = $"Can't connect to region: {region}";
}
return connectTask;
}
///<inheritdoc/>
public override void DisconnectRemoteClient(ulong clientId)
{
client.CurrentRoom.RemovePlayer(client.CurrentRoom.Players[GetPhotonRealtimeId(clientId)]);
}
///<inheritdoc/>
public override void DisconnectLocalClient()
{
client.Disconnect();
}
///<inheritdoc/>
public override ulong GetCurrentRtt(ulong clientId)
{
// This is only an approximate value based on the own client's rtt to the server and could cause issues, maybe use a similar approach as the Steamworks transport.
return (ulong) (client.LoadBalancingPeer.RoundTripTime * 2);
}
///<inheritdoc/>
public override void Shutdown()
{
if (client != null)
{
client.EventReceived -= ClientOnEventReceived;
if (client.IsConnected)
{
client.Disconnect();
}
}
}
///<inheritdoc/>
public override void Init()
{
for (byte i = 0; i < MLAPI_CHANNELS.Length; i++)
{
channelIdToName.Add((byte) (i + channelIdCodesStartRange), MLAPI_CHANNELS[i].Name);
channelNameToId.Add(MLAPI_CHANNELS[i].Name, (byte)(i + channelIdCodesStartRange));
channels.Add((byte)(i + channelIdCodesStartRange), new RealtimeChannel()
{
Id = (byte)(i + channelIdCodesStartRange),
Name = MLAPI_CHANNELS[i].Name,
SendMode = MLAPIChannelTypeToSendOptions(MLAPI_CHANNELS[i].Type)
});
}
}
private void Update()
{
if (client != null)
{
do ;
while (client.LoadBalancingPeer.DispatchIncomingCommands());
}
}
private void LateUpdate()
{
// Send messages at least once per update to make sure to receive messages even if MLAPI is not polling.
FlushAllSendQueues();
if (client != null)
{
do ;
while (client.LoadBalancingPeer.SendOutgoingCommands());
}
}
private void InitializeClient()
{
if (client == null)
{
client = new LoadBalancingClient()
{
AppId = appId,
AppVersion = gameVersion,
};
client.LocalPlayer.NickName = string.IsNullOrEmpty(nickName)
? nickName
: "usr" + SupportClass.ThreadSafeRandom.Next() % 99;
}
client.EventReceived += ClientOnEventReceived;
client.StateChanged += ClientOnStateChanged;
}
private void ClientOnStateChanged(ClientState lastState, ClientState currentState)
{
switch (currentState)
{
case ClientState.ConnectedToMasterServer:
// Once the client does connect to the master immediately redirect to the room.
if (currentState == ClientState.ConnectedToMasterServer)
{
var enterRoomParams = new EnterRoomParams()
{
RoomName = roomName,
RoomOptions = new RoomOptions()
{
MaxPlayers = maxPlayers,
}
};
var success = isHostOrServer
? client.OpCreateRoom(enterRoomParams)
: client.OpJoinRoom(enterRoomParams);
if (!success)
{
connectTask.IsDone = true;
connectTask.Success = false;
connectTask.TransportException =
new InvalidOperationException("Unable to create or join room.");
}
}
break;
case ClientState.Joined:
if (attachSupportLogger)
{
var logger = gameObject.GetComponent<SupportLogger>() ?? gameObject.AddComponent<SupportLogger>();
logger.Client = client;
client.ConnectionCallbackTargets.Add(logger);
}
// Client connected to the room successfully, connection process is completed
connectTask.IsDone = true;
connectTask.Success = true;
break;
}
}
private void ClientOnEventReceived(EventData eventData)
{
var clientId = GetMLAPIClientId(eventData.Sender, false);
var localClientId = GetMLAPIClientId(client.LocalPlayer.ActorNumber, false);
var isRelevantConnectionUpdateMessage =
isHostOrServer ^ clientId == localClientId; // Clients should ignore connection events from other clients, server should ignore its own connection event.
NetEventType netEvent = NetEventType.Nothing;
ArraySegment<byte> payload = default;
string channelName = default;
float receiveTime = Time.realtimeSinceStartup;
switch (eventData.Code)
{
case EventCode.Leave:
if (isRelevantConnectionUpdateMessage)
{
netEvent = NetEventType.Disconnect;
}
break;
case EventCode.Join:
if (isRelevantConnectionUpdateMessage)
{
netEvent = NetEventType.Connect;
}
break;
default:
if (eventData.Code >= 200)
{
return;
}
byte[] array = (byte[])eventData.CustomData;
if (eventData.Code == batchedTransportEventCode)
{
using (MemoryStream stream = new MemoryStream(array))
{
using (PooledBitReader reader = PooledBitReader.Get(stream))
{
while (stream.Position < stream.Length)
{
byte channelId = reader.ReadByteDirect();
var length = reader.ReadInt32Packed();
var dataArray = reader.ReadByteArray(null, length);
InvokeOnTransportEvent(NetEventType.Data, clientId, channelIdToName[channelId], new ArraySegment<byte>(dataArray, 0, dataArray.Length), receiveTime);
}
}
}
return;
}
netEvent = NetEventType.Data;
payload = new ArraySegment<byte>(array);
channelName = channelIdToName[eventData.Code];
break;
}
if (netEvent == NetEventType.Nothing) return;
InvokeOnTransportEvent(netEvent, clientId, channelName, payload, receiveTime);
}
private SendOptions MLAPIChannelTypeToSendOptions(ChannelType type)
{
switch (type)
{
case ChannelType.Unreliable:
return SendOptions.SendUnreliable;
default:
return SendOptions.SendReliable;
}
}
private ulong GetMLAPIClientId(int photonId, bool isServer)
{
if (isServer)
{
return 0;
}
else
{
return (ulong) (photonId + 1);
}
}
private int GetPhotonRealtimeId(ulong clientId)
{
if (clientId == 0)
{
return client.CurrentRoom.masterClientId;
}
else
{
return (int) (clientId - 1);
}
}
private class SendQueue
{
private MemoryStream stream;
/// <summary>
/// The size of the send queue.
/// </summary>
public int Size { get; }
public SendQueue(int size)
{
Size = size;
byte[] buffer = new byte[size];
stream = new MemoryStream(buffer, 0, buffer.Length, true, true);
}
internal bool AddEvent(byte channelId, ArraySegment<byte> data)
{
if (stream.Position + data.Count + 4 > Size)
{
return false;
}
using (PooledBitWriter writer = PooledBitWriter.Get(stream))
{
writer.WriteByte(channelId);
writer.WriteInt32Packed(data.Count);
Array.Copy(data.Array, data.Offset, stream.GetBuffer(), stream.Position, data.Count);
stream.Position += data.Count;
}
return true;
}
internal void Clear()
{
stream.Position = 0;
}
internal bool IsEmpty()
{
return stream.Position == 0;
}
internal ArraySegment<byte> GetData()
{
return new ArraySegment<byte>(stream.GetBuffer(), 0, (int) stream.Position);
}
}
private struct RealtimeChannel
{
public byte Id;
public string Name;
public SendOptions SendMode;
}
}
}