345 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
		
		
			
		
	
	
			345 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
|  | #if (UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR | ||
|  | namespace TPSBR | ||
|  | { | ||
|  | 	/// <summary> | ||
|  | 	/// Backfill works only on server | ||
|  | 	/// </summary> | ||
|  | 	public sealed class Backfill : SceneService | ||
|  | 	{ | ||
|  | 		public bool  BackfillEnabled { get; set; } | ||
|  | 		public float PlayerJoiningExpiration = 20; | ||
|  | 
 | ||
|  | 		public void PlayerJoined(Player player) {} | ||
|  | 		public void PlayerLeft(Player player)   {} | ||
|  | 	} | ||
|  | } | ||
|  | #else | ||
|  | using System.Collections.Generic; | ||
|  | using System.Linq; | ||
|  | using System.Threading.Tasks; | ||
|  | using Unity.Services.Matchmaker; | ||
|  | using Unity.Services.Matchmaker.Models; | ||
|  | using UnityEngine; | ||
|  | 
 | ||
|  | namespace TPSBR | ||
|  | { | ||
|  | 	/// <summary> | ||
|  | 	/// Uses Unity matchmaker to backfill more players into a Multiplay running dedicated game server | ||
|  | 	/// </summary> | ||
|  | 	public sealed class Backfill : SceneService | ||
|  | 	{ | ||
|  | 		// PUBLIC MEMBERS | ||
|  | 
 | ||
|  | 		public bool BackfillEnabled | ||
|  | 		{ | ||
|  | 			get => _backfillEnabled; | ||
|  | 			set | ||
|  | 			{ | ||
|  | 				if (_isBackfillingValid) _backfillEnabled = value; | ||
|  | 			} | ||
|  | 		} | ||
|  | 
 | ||
|  | 		public float PlayerJoiningExpiration = 20; | ||
|  | 
 | ||
|  | 		// PRIVATE MEMBERS | ||
|  | 
 | ||
|  | 		private bool _pendingBackfillChange = false; | ||
|  | 		private BackfillTicket _backfillTicket; | ||
|  | 		private bool _isBackfillingValid => Global.MultiplayManager != null && Global.MultiplayManager.Backfill; | ||
|  | 		private bool _backfillEnabled; | ||
|  | 
 | ||
|  | 		private string _connection; | ||
|  | 		private string _region; | ||
|  | 		private string _everyoneTeamName = "everyone"; | ||
|  | 		private string _everyoneTeamID; | ||
|  | 		private string _queueName; | ||
|  | 
 | ||
|  | 		/// <summary> | ||
|  | 		/// Keeps track of players that are expected to connect through matchmaking | ||
|  | 		/// along with when they connected so they can be removed if they don't arrive after a period of time | ||
|  | 		/// </summary> | ||
|  | 		private Dictionary<string, float> _playersMatchingIn = new(); | ||
|  | 
 | ||
|  | 		// Note: NetworkGame.Players is a NetworkArray of length = max players, filling from the last position | ||
|  | 		// This simplifies access to the current actively connected players | ||
|  | 		private IEnumerable<Player> _networkPlayers => Context.NetworkGame.ActivePlayers.Where(p => p != null); | ||
|  | 
 | ||
|  | 		private float _backfillIntervalMs = 3.0f; | ||
|  | 		private float _backfillTimerMs = 0f; | ||
|  | 
 | ||
|  | 		// PUBLIC METHODS | ||
|  | 
 | ||
|  | 		public void PlayerJoined(Player player) | ||
|  | 		{ | ||
|  | 			if (player.UnityID.HasValue() == false) | ||
|  | 			{ | ||
|  | 				Debug.Log($"Player [{player.UserID}] doesn't have UnityID, skipping..."); | ||
|  | 				return; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			Debug.Log($"Player [{player.UserID}] joined with UnityID [{player.UnityID}]"); | ||
|  | 
 | ||
|  | 			if (_playersMatchingIn.ContainsKey(player.UnityID)) | ||
|  | 			{ | ||
|  | 				// The player was expected, remove them from the "matching in" list | ||
|  | 				_playersMatchingIn.Remove(player.UnityID); | ||
|  | 			} | ||
|  | 			else if(_backfillTicket != null) | ||
|  | 			{ | ||
|  | 				// The player came from outside matchmaker, we need to update the backfill ticket | ||
|  | 				_pendingBackfillChange = true; | ||
|  | 			} | ||
|  | 		} | ||
|  | 
 | ||
|  | 		public void PlayerLeft(Player player) | ||
|  | 		{ | ||
|  | 			if (player.UnityID.HasValue() == false) | ||
|  | 				return; | ||
|  | 
 | ||
|  | 			Debug.Log($"Player [{player.UserID}] with UnityID [{player.UnityID}] left"); | ||
|  | 
 | ||
|  | 			if (_backfillTicket != null) | ||
|  | 			{ | ||
|  | 				// A player left, we need to update the backfill ticket | ||
|  | 				_pendingBackfillChange = true; | ||
|  | 			} | ||
|  | 		} | ||
|  | 
 | ||
|  | 		// SceneService INTERFACE | ||
|  | 
 | ||
|  | 		protected override async void OnInitialize() | ||
|  | 		{ | ||
|  | 			base.OnInitialize(); | ||
|  | 
 | ||
|  | 			_queueName = Global.Settings.Network.GetCustomOrDefaultQueueName(); | ||
|  | 
 | ||
|  | 			// If there are matchmaking hints, we'll take them | ||
|  | 			MatchmakingResults initMmResults = null; | ||
|  | 			if (Global.MultiplayManager != null && Global.MultiplayManager.MatchmakingResults != null) | ||
|  | 			{ | ||
|  | 				initMmResults = Global.MultiplayManager.MatchmakingResults; | ||
|  | 				_queueName = initMmResults.QueueName; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			if(_isBackfillingValid && !string.IsNullOrEmpty(initMmResults?.BackfillTicketId)){ | ||
|  | 				// Take note of which players we're expecting and enable backfill | ||
|  | 				Debug.Log("Initializing backfill"); | ||
|  | 				_backfillTicket = await MatchmakerService.Instance.ApproveBackfillTicketAsync(initMmResults.BackfillTicketId); | ||
|  | 				_playersMatchingIn = _backfillTicket.Properties.MatchProperties.Players.Select(p => p.Id).ToDictionary(k => k, _=> Time.realtimeSinceStartup); | ||
|  | 
 | ||
|  | 				_connection = _backfillTicket.Connection; | ||
|  | 				_region = initMmResults.MatchProperties.Region; | ||
|  | 				_everyoneTeamName = initMmResults.MatchProperties.Teams.First().TeamName; | ||
|  | 				_everyoneTeamID = initMmResults.MatchProperties.Teams.First().TeamId; | ||
|  | 
 | ||
|  | 				BackfillEnabled = true; | ||
|  | 			} | ||
|  | 			else if(initMmResults != null) | ||
|  | 			{ | ||
|  | 				// If backfilling isn't valid or turned on, just note which players are coming in via the initial matchmaker results | ||
|  | 				_playersMatchingIn = initMmResults?.MatchProperties.Players.Select(p => p.Id).ToDictionary(k => k, _=> Time.realtimeSinceStartup); | ||
|  | 			} | ||
|  | 
 | ||
|  | 			Debug.Log("Players matching in from Unity matchmaking " + string.Join(',', _playersMatchingIn)); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		protected override async void OnDeinitialize() | ||
|  | 		{ | ||
|  | 			await DeleteBackfillTicket(); | ||
|  | 			base.OnDeinitialize(); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		protected override async void OnTick() | ||
|  | 		{ | ||
|  | 			base.OnTick(); | ||
|  | 			await UpdateBackfill(); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		// PRIVATE METHODS | ||
|  | 
 | ||
|  | 		private async Task UpdateBackfill() | ||
|  | 		{ | ||
|  | 			_backfillTimerMs += Time.deltaTime; | ||
|  | 			if (_backfillTimerMs < _backfillIntervalMs) return; | ||
|  | 			_backfillTimerMs = 0f; | ||
|  | 
 | ||
|  | 			if (!BackfillEnabled) | ||
|  | 			{ | ||
|  | 				if (_backfillTicket != null) | ||
|  | 				{ | ||
|  | 					Debug.Log($"Backfill stopping, deleting ticket {_backfillTicket.Id}"); | ||
|  | 					await DeleteBackfillTicket(); | ||
|  | 				} | ||
|  | 
 | ||
|  | 				return; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			// Some of the players we were waiting for may have never connected, let's time them out | ||
|  | 			var toRemove = _playersMatchingIn.Where(p => Time.realtimeSinceStartup - p.Value > PlayerJoiningExpiration).Select(p => p.Key); | ||
|  | 			foreach (var key in toRemove) | ||
|  | 			{ | ||
|  | 				_playersMatchingIn.Remove(key); | ||
|  | 			} | ||
|  | 
 | ||
|  | 			// Wait till we're in a NetworkedGame to start roster-based backfilling | ||
|  | 			if(Context.NetworkGame == null || Context.NetworkGame.Object == null) | ||
|  | 			{ | ||
|  | 				Debug.LogWarning("Context.NetworkGame is no initialized"); | ||
|  | 				return; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			// Make a list of every player connected and currently expected to connect | ||
|  | 			HashSet<string> allKnownPlayers = new HashSet<string>(); | ||
|  | 			foreach (var ngp in _networkPlayers) | ||
|  | 			{ | ||
|  | 				if(string.IsNullOrEmpty(ngp.UnityID)) | ||
|  | 				{ | ||
|  | 					Debug.LogWarning("UnityID on Player was null or empty!"); | ||
|  | 					continue; | ||
|  | 				} | ||
|  | 				allKnownPlayers.Add(ngp.UnityID); | ||
|  | 			} | ||
|  | 
 | ||
|  | 			foreach (var unityId in _playersMatchingIn.Keys) | ||
|  | 			{ | ||
|  | 				allKnownPlayers.Add(unityId); | ||
|  | 			} | ||
|  | 
 | ||
|  | 			// Check to see if the game is full and subsequently backfilling needs to stop | ||
|  | 			int currentCount = allKnownPlayers.Count(); | ||
|  | 			int max = Global.MultiplayManager.MaxPlayers; | ||
|  | 			if (currentCount >= max) | ||
|  | 			{ | ||
|  | 				if (_backfillTicket != null) | ||
|  | 				{ | ||
|  | 					Debug.Log($"Game full with {currentCount} current and expected players, removing backfill ticket {_backfillTicket.Id}"); | ||
|  | 					await DeleteBackfillTicket(); | ||
|  | 				} | ||
|  | 				return; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			// If the game is not full but we don't currently have a backfill ticket in progress, let's make one | ||
|  | 			if (_backfillTicket == null) | ||
|  | 			{ | ||
|  | 				await CreateBackfillTicket(); | ||
|  | 				return; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			// If players have joined from outside the matchmaker or left the game, update the in-progress backfill ticket | ||
|  | 			if (_pendingBackfillChange) | ||
|  | 			{ | ||
|  | 				Debug.Log($"Roster changed, updating backfill {_backfillTicket.Id}"); | ||
|  | 				await UpdateBackfillTicket(); | ||
|  | 				_pendingBackfillChange = false; | ||
|  | 				return; | ||
|  | 			} | ||
|  | 
 | ||
|  | 			// There's no pending roster changes on the server, so we can approve the existing backfilling ticket and bring more players into the match | ||
|  | 			_backfillTicket = await MatchmakerService.Instance.ApproveBackfillTicketAsync(_backfillTicket.Id); | ||
|  | 			int backfillPlayerCount = _backfillTicket.Properties.MatchProperties.Players.Count(); | ||
|  | 			if (backfillPlayerCount > currentCount) | ||
|  | 			{ | ||
|  | 				// Matchmaking found new players that will now try to connect, note that we're expecting them | ||
|  | 				Debug.Log($"New players expected from backfilling: {backfillPlayerCount - currentCount}. Was {currentCount}/{max}"); | ||
|  | 				foreach (var matchedPlayer in _backfillTicket.Properties.MatchProperties.Players) | ||
|  | 				{ | ||
|  | 					if (!allKnownPlayers.Contains(matchedPlayer.Id)) | ||
|  | 					{ | ||
|  | 						// Add them to our set of expected players and mark the time | ||
|  | 						_playersMatchingIn.Add(matchedPlayer.Id, Time.realtimeSinceStartup); | ||
|  | 					} | ||
|  | 				} | ||
|  | 			} | ||
|  | 		} | ||
|  | 
 | ||
|  | 		/// <summary> | ||
|  | 		/// Create a new backfill ticket from the state of the game and expected players | ||
|  | 		/// A backfill ticket is automatically created by matchmaking if enabled on the pool rules. This method lets | ||
|  | 		/// the game server start backfilling on demand (eg the game is no longer full, is in-between rounds, etc) | ||
|  | 		/// </summary> | ||
|  | 		private async Task CreateBackfillTicket() | ||
|  | 		{ | ||
|  | 			var backfillPlayers = new List<Unity.Services.Matchmaker.Models.Player>(); | ||
|  | 			Team everyoneTeam = new Team(_everyoneTeamName, _everyoneTeamID, new List<string>()); | ||
|  | 
 | ||
|  | 			// Include current players | ||
|  | 			foreach (var connectedPlayer in _networkPlayers) | ||
|  | 			{ | ||
|  | 				backfillPlayers.Add(new Unity.Services.Matchmaker.Models.Player(connectedPlayer.UnityID)); | ||
|  | 				everyoneTeam.PlayerIds.Add(connectedPlayer.UnityID); | ||
|  | 			} | ||
|  | 
 | ||
|  | 			// Include players that we're still waiting to connect | ||
|  | 			foreach (var expectedPlayer in _playersMatchingIn) | ||
|  | 			{ | ||
|  | 				backfillPlayers.Add(new Unity.Services.Matchmaker.Models.Player(expectedPlayer.Key)); | ||
|  | 				everyoneTeam.PlayerIds.Add(expectedPlayer.Key); | ||
|  | 			} | ||
|  | 
 | ||
|  | 			// There's no backfill ticket, let's make one | ||
|  | 			MatchProperties props = new MatchProperties(new List<Team>(){everyoneTeam}, backfillPlayers, _region); | ||
|  | 
 | ||
|  | 			string backfillId = await MatchmakerService.Instance.CreateBackfillTicketAsync( | ||
|  | 				new CreateBackfillTicketOptions( | ||
|  | 					_queueName, | ||
|  | 					_connection, | ||
|  | 					attributes: null, | ||
|  | 					new BackfillTicketProperties(props))); | ||
|  | 			_backfillTicket = await MatchmakerService.Instance.ApproveBackfillTicketAsync(backfillId); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		/// <summary> | ||
|  | 		/// Rebuild the backfill ticket from the current state of the game and expected players | ||
|  | 		/// </summary> | ||
|  | 		private async Task UpdateBackfillTicket() | ||
|  | 		{ | ||
|  | 			var backfillPlayers = new List<Unity.Services.Matchmaker.Models.Player>(); | ||
|  | 
 | ||
|  | 			// Include current players | ||
|  | 			foreach (var connectedPlayer in _networkPlayers) | ||
|  | 			{ | ||
|  | 				backfillPlayers.Add(new Unity.Services.Matchmaker.Models.Player(connectedPlayer.UnityID)); | ||
|  | 			} | ||
|  | 
 | ||
|  | 			// Include players that we're still waiting to connect | ||
|  | 			foreach (var expectedPlayer in _playersMatchingIn) | ||
|  | 			{ | ||
|  | 				backfillPlayers.Add(new Unity.Services.Matchmaker.Models.Player(expectedPlayer.Key)); | ||
|  | 			} | ||
|  | 
 | ||
|  | 			_backfillTicket.Properties.MatchProperties.Players.Clear(); | ||
|  | 			_backfillTicket.Properties.MatchProperties.Players.AddRange(backfillPlayers); | ||
|  | 
 | ||
|  | 			Team team = _backfillTicket.Properties.MatchProperties.Teams.FirstOrDefault(); | ||
|  | 			if (team != null) | ||
|  | 			{ | ||
|  | 				team.PlayerIds.Clear(); | ||
|  | 				team.PlayerIds.AddRange(backfillPlayers.Select(p => p.Id)); | ||
|  | 			} | ||
|  | 
 | ||
|  | 			await MatchmakerService.Instance.UpdateBackfillTicketAsync(_backfillTicket.Id, _backfillTicket); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		/// <summary> | ||
|  | 		/// Deletes a backfill ticket. If a backfill ticket is not deleted, it can optimistically collect | ||
|  | 		/// "pending" matches and tie up players in matchmaking till the backfill ticket expires. Cleaning up | ||
|  | 		/// backfill tickets is important to a short player matchmaking experience | ||
|  | 		/// </summary> | ||
|  | 		private async Task DeleteBackfillTicket() | ||
|  | 		{ | ||
|  | 			if(_backfillTicket!=null) | ||
|  | 			{ | ||
|  | 				await MatchmakerService.Instance.DeleteBackfillTicketAsync(_backfillTicket.Id); | ||
|  | 				_backfillTicket = null; | ||
|  | 			} | ||
|  | 		} | ||
|  | 
 | ||
|  | 		private async void OnApplicationQuit() | ||
|  | 		{ | ||
|  | 			BackfillEnabled = false; | ||
|  | 			await DeleteBackfillTicket(); | ||
|  | 		} | ||
|  | 	} | ||
|  | } | ||
|  | #endif |