PhotonTeamsManager.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. // --------------------------------------------------------------------------------------------------------------------
  2. // <copyright file="PhotonTeamsManager.cs" company="Exit Games GmbH">
  3. // Part of: Photon Unity Utilities,
  4. // </copyright>
  5. // <summary>
  6. // Implements teams in a room/game with help of player properties.
  7. // </summary>
  8. // <remarks>
  9. // Teams are defined by name and code. Change this to get more / different teams.
  10. // There are no rules when / if you can join a team. You could add this in JoinTeam or something.
  11. // </remarks>
  12. // <author>developer@exitgames.com</author>
  13. // --------------------------------------------------------------------------------------------------------------------
  14. using System;
  15. using System.Collections.Generic;
  16. using UnityEngine;
  17. using Photon.Realtime;
  18. using Hashtable = ExitGames.Client.Photon.Hashtable;
  19. namespace Photon.Pun.UtilityScripts
  20. {
  21. [Serializable]
  22. public class PhotonTeam
  23. {
  24. public string Name;
  25. public byte Code;
  26. public override string ToString()
  27. {
  28. return string.Format("{0} [{1}]", this.Name, this.Code);
  29. }
  30. }
  31. /// <summary>
  32. /// Implements teams in a room/game with help of player properties. Access them by Player.GetTeam extension.
  33. /// </summary>
  34. /// <remarks>
  35. /// Teams are defined by enum Team. Change this to get more / different teams.
  36. /// There are no rules when / if you can join a team. You could add this in JoinTeam or something.
  37. /// </remarks>
  38. [DisallowMultipleComponent]
  39. public class PhotonTeamsManager : MonoBehaviour, IMatchmakingCallbacks, IInRoomCallbacks
  40. {
  41. #if UNITY_EDITOR
  42. #pragma warning disable 0414
  43. [SerializeField]
  44. private bool listFoldIsOpen = true;
  45. #pragma warning restore 0414
  46. #endif
  47. [SerializeField]
  48. private List<PhotonTeam> teamsList = new List<PhotonTeam>
  49. {
  50. new PhotonTeam { Name = "Blue", Code = 1 },
  51. new PhotonTeam { Name = "Red", Code = 2 }
  52. };
  53. private Dictionary<byte, PhotonTeam> teamsByCode;
  54. private Dictionary<string, PhotonTeam> teamsByName;
  55. /// <summary>The main list of teams with their player-lists. Automatically kept up to date.</summary>
  56. private Dictionary<byte, HashSet<Player>> playersPerTeam;
  57. /// <summary>Defines the player custom property name to use for team affinity of "this" player.</summary>
  58. public const string TeamPlayerProp = "_pt";
  59. public static event Action<Player, PhotonTeam> PlayerJoinedTeam;
  60. public static event Action<Player, PhotonTeam> PlayerLeftTeam;
  61. private static PhotonTeamsManager instance;
  62. public static PhotonTeamsManager Instance
  63. {
  64. get
  65. {
  66. if (instance == null)
  67. {
  68. instance = FindObjectOfType<PhotonTeamsManager>();
  69. if (instance == null)
  70. {
  71. GameObject obj = new GameObject();
  72. obj.name = "PhotonTeamsManager";
  73. instance = obj.AddComponent<PhotonTeamsManager>();
  74. }
  75. instance.Init();
  76. }
  77. return instance;
  78. }
  79. }
  80. #region MonoBehaviour
  81. private void Awake()
  82. {
  83. if (instance == null || ReferenceEquals(this, instance))
  84. {
  85. this.Init();
  86. instance = this;
  87. }
  88. else
  89. {
  90. Destroy(this);
  91. }
  92. }
  93. private void OnEnable()
  94. {
  95. PhotonNetwork.AddCallbackTarget(this);
  96. }
  97. private void OnDisable()
  98. {
  99. PhotonNetwork.RemoveCallbackTarget(this);
  100. this.ClearTeams();
  101. }
  102. private void Init()
  103. {
  104. teamsByCode = new Dictionary<byte, PhotonTeam>(teamsList.Count);
  105. teamsByName = new Dictionary<string, PhotonTeam>(teamsList.Count);
  106. playersPerTeam = new Dictionary<byte, HashSet<Player>>(teamsList.Count);
  107. for (int i = 0; i < teamsList.Count; i++)
  108. {
  109. teamsByCode[teamsList[i].Code] = teamsList[i];
  110. teamsByName[teamsList[i].Name] = teamsList[i];
  111. playersPerTeam[teamsList[i].Code] = new HashSet<Player>();
  112. }
  113. }
  114. #endregion
  115. #region IMatchmakingCallbacks
  116. void IMatchmakingCallbacks.OnJoinedRoom()
  117. {
  118. this.UpdateTeams();
  119. }
  120. void IMatchmakingCallbacks.OnLeftRoom()
  121. {
  122. this.ClearTeams();
  123. }
  124. void IInRoomCallbacks.OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProps)
  125. {
  126. object temp;
  127. if (changedProps.TryGetValue(TeamPlayerProp, out temp))
  128. {
  129. if (temp == null)
  130. {
  131. foreach (byte code in playersPerTeam.Keys)
  132. {
  133. if (playersPerTeam[code].Remove(targetPlayer))
  134. {
  135. if (PlayerLeftTeam != null)
  136. {
  137. PlayerLeftTeam(targetPlayer, teamsByCode[code]);
  138. }
  139. break;
  140. }
  141. }
  142. }
  143. else if (temp is byte)
  144. {
  145. byte teamCode = (byte) temp;
  146. // check if player switched teams, remove from previous team
  147. foreach (byte code in playersPerTeam.Keys)
  148. {
  149. if (code == teamCode)
  150. {
  151. continue;
  152. }
  153. if (playersPerTeam[code].Remove(targetPlayer))
  154. {
  155. if (PlayerLeftTeam != null)
  156. {
  157. PlayerLeftTeam(targetPlayer, teamsByCode[code]);
  158. }
  159. break;
  160. }
  161. }
  162. PhotonTeam team = teamsByCode[teamCode];
  163. if (!playersPerTeam[teamCode].Add(targetPlayer))
  164. {
  165. Debug.LogWarningFormat("Unexpected situation while setting team {0} for player {1}, updating teams for all", team, targetPlayer);
  166. this.UpdateTeams();
  167. }
  168. if (PlayerJoinedTeam != null)
  169. {
  170. PlayerJoinedTeam(targetPlayer, team);
  171. }
  172. }
  173. else
  174. {
  175. Debug.LogErrorFormat("Unexpected: custom property key {0} should have of type byte, instead we got {1} of type {2}. Player: {3}",
  176. TeamPlayerProp, temp, temp.GetType(), targetPlayer);
  177. }
  178. }
  179. }
  180. void IInRoomCallbacks.OnPlayerLeftRoom(Player otherPlayer)
  181. {
  182. if (otherPlayer.IsInactive)
  183. {
  184. return;
  185. }
  186. PhotonTeam team = otherPlayer.GetPhotonTeam();
  187. if (team != null && !playersPerTeam[team.Code].Remove(otherPlayer))
  188. {
  189. Debug.LogWarningFormat("Unexpected situation while removing player {0} who left from team {1}, updating teams for all", otherPlayer, team);
  190. // revert to 'brute force' in case of unexpected situation
  191. this.UpdateTeams();
  192. }
  193. }
  194. void IInRoomCallbacks.OnPlayerEnteredRoom(Player newPlayer)
  195. {
  196. PhotonTeam team = newPlayer.GetPhotonTeam();
  197. if (team == null)
  198. {
  199. return;
  200. }
  201. if (playersPerTeam[team.Code].Contains(newPlayer))
  202. {
  203. // player rejoined w/ same team
  204. return;
  205. }
  206. // check if player rejoined w/ different team, remove from previous team
  207. foreach (var key in teamsByCode.Keys)
  208. {
  209. if (playersPerTeam[key].Remove(newPlayer))
  210. {
  211. break;
  212. }
  213. }
  214. if (!playersPerTeam[team.Code].Add(newPlayer))
  215. {
  216. Debug.LogWarningFormat("Unexpected situation while adding player {0} who joined to team {1}, updating teams for all", newPlayer, team);
  217. // revert to 'brute force' in case of unexpected situation
  218. this.UpdateTeams();
  219. }
  220. }
  221. #endregion
  222. #region Private methods
  223. private void UpdateTeams()
  224. {
  225. this.ClearTeams();
  226. for (int i = 0; i < PhotonNetwork.PlayerList.Length; i++)
  227. {
  228. Player player = PhotonNetwork.PlayerList[i];
  229. PhotonTeam playerTeam = player.GetPhotonTeam();
  230. if (playerTeam != null)
  231. {
  232. playersPerTeam[playerTeam.Code].Add(player);
  233. }
  234. }
  235. }
  236. private void ClearTeams()
  237. {
  238. foreach (var key in playersPerTeam.Keys)
  239. {
  240. playersPerTeam[key].Clear();
  241. }
  242. }
  243. #endregion
  244. #region Public API
  245. /// <summary>
  246. /// Find a PhotonTeam using a team code.
  247. /// </summary>
  248. /// <param name="code">The team code.</param>
  249. /// <param name="team">The team to be assigned if found.</param>
  250. /// <returns>If successful or not.</returns>
  251. public bool TryGetTeamByCode(byte code, out PhotonTeam team)
  252. {
  253. return teamsByCode.TryGetValue(code, out team);
  254. }
  255. /// <summary>
  256. /// Find a PhotonTeam using a team name.
  257. /// </summary>
  258. /// <param name="teamName">The team name.</param>
  259. /// <param name="team">The team to be assigned if found.</param>
  260. /// <returns>If successful or not.</returns>
  261. public bool TryGetTeamByName(string teamName, out PhotonTeam team)
  262. {
  263. return teamsByName.TryGetValue(teamName, out team);
  264. }
  265. /// <summary>
  266. /// Gets all teams available.
  267. /// </summary>
  268. /// <returns>Returns all teams available.</returns>
  269. public PhotonTeam[] GetAvailableTeams()
  270. {
  271. if (teamsList != null)
  272. {
  273. return teamsList.ToArray();
  274. }
  275. return null;
  276. }
  277. /// <summary>
  278. /// Gets all players joined to a team using a team code.
  279. /// </summary>
  280. /// <param name="code">The code of the team.</param>
  281. /// <param name="members">The array of players to be filled.</param>
  282. /// <returns>If successful or not.</returns>
  283. public bool TryGetTeamMembers(byte code, out Player[] members)
  284. {
  285. members = null;
  286. HashSet<Player> players;
  287. if (this.playersPerTeam.TryGetValue(code, out players))
  288. {
  289. members = new Player[players.Count];
  290. int i = 0;
  291. foreach (var player in players)
  292. {
  293. members[i] = player;
  294. i++;
  295. }
  296. return true;
  297. }
  298. return false;
  299. }
  300. /// <summary>
  301. /// Gets all players joined to a team using a team name.
  302. /// </summary>
  303. /// <param name="teamName">The name of the team.</param>
  304. /// <param name="members">The array of players to be filled.</param>
  305. /// <returns>If successful or not.</returns>
  306. public bool TryGetTeamMembers(string teamName, out Player[] members)
  307. {
  308. members = null;
  309. PhotonTeam team;
  310. if (this.TryGetTeamByName(teamName, out team))
  311. {
  312. return this.TryGetTeamMembers(team.Code, out members);
  313. }
  314. return false;
  315. }
  316. /// <summary>
  317. /// Gets all players joined to a team.
  318. /// </summary>
  319. /// <param name="team">The team which will be used to find players.</param>
  320. /// <param name="members">The array of players to be filled.</param>
  321. /// <returns>If successful or not.</returns>
  322. public bool TryGetTeamMembers(PhotonTeam team, out Player[] members)
  323. {
  324. members = null;
  325. if (team != null)
  326. {
  327. return this.TryGetTeamMembers(team.Code, out members);
  328. }
  329. return false;
  330. }
  331. /// <summary>
  332. /// Gets all team mates of a player.
  333. /// </summary>
  334. /// <param name="player">The player whose team mates will be searched.</param>
  335. /// <param name="teamMates">The array of players to be filled.</param>
  336. /// <returns>If successful or not.</returns>
  337. public bool TryGetTeamMatesOfPlayer(Player player, out Player[] teamMates)
  338. {
  339. teamMates = null;
  340. if (player == null)
  341. {
  342. return false;
  343. }
  344. PhotonTeam team = player.GetPhotonTeam();
  345. if (team == null)
  346. {
  347. return false;
  348. }
  349. HashSet<Player> players;
  350. if (this.playersPerTeam.TryGetValue(team.Code, out players))
  351. {
  352. if (!players.Contains(player))
  353. {
  354. Debug.LogWarningFormat("Unexpected situation while getting team mates of player {0} who is joined to team {1}, updating teams for all", player, team);
  355. // revert to 'brute force' in case of unexpected situation
  356. this.UpdateTeams();
  357. }
  358. teamMates = new Player[players.Count - 1];
  359. int i = 0;
  360. foreach (var p in players)
  361. {
  362. if (p.Equals(player))
  363. {
  364. continue;
  365. }
  366. teamMates[i] = p;
  367. i++;
  368. }
  369. return true;
  370. }
  371. return false;
  372. }
  373. /// <summary>
  374. /// Gets the number of players in a team by team code.
  375. /// </summary>
  376. /// <param name="code">Unique code of the team</param>
  377. /// <returns>Number of players joined to the team.</returns>
  378. public int GetTeamMembersCount(byte code)
  379. {
  380. PhotonTeam team;
  381. if (this.TryGetTeamByCode(code, out team))
  382. {
  383. return this.GetTeamMembersCount(team);
  384. }
  385. return 0;
  386. }
  387. /// <summary>
  388. /// Gets the number of players in a team by team name.
  389. /// </summary>
  390. /// <param name="name">Unique name of the team</param>
  391. /// <returns>Number of players joined to the team.</returns>
  392. public int GetTeamMembersCount(string name)
  393. {
  394. PhotonTeam team;
  395. if (this.TryGetTeamByName(name, out team))
  396. {
  397. return this.GetTeamMembersCount(team);
  398. }
  399. return 0;
  400. }
  401. /// <summary>
  402. /// Gets the number of players in a team.
  403. /// </summary>
  404. /// <param name="team">The team you want to know the size of</param>
  405. /// <returns>Number of players joined to the team.</returns>
  406. public int GetTeamMembersCount(PhotonTeam team)
  407. {
  408. HashSet<Player> players;
  409. if (team != null && this.playersPerTeam.TryGetValue(team.Code, out players) && players != null)
  410. {
  411. return players.Count;
  412. }
  413. return 0;
  414. }
  415. #endregion
  416. #region Unused methods
  417. void IMatchmakingCallbacks.OnFriendListUpdate(List<FriendInfo> friendList)
  418. {
  419. }
  420. void IMatchmakingCallbacks.OnCreatedRoom()
  421. {
  422. }
  423. void IMatchmakingCallbacks.OnCreateRoomFailed(short returnCode, string message)
  424. {
  425. }
  426. void IMatchmakingCallbacks.OnJoinRoomFailed(short returnCode, string message)
  427. {
  428. }
  429. void IMatchmakingCallbacks.OnJoinRandomFailed(short returnCode, string message)
  430. {
  431. }
  432. void IInRoomCallbacks.OnRoomPropertiesUpdate(Hashtable propertiesThatChanged)
  433. {
  434. }
  435. void IInRoomCallbacks.OnMasterClientSwitched(Player newMasterClient)
  436. {
  437. }
  438. #endregion
  439. }
  440. /// <summary>Extension methods for the Player class that make use of PhotonTeamsManager.</summary>
  441. public static class PhotonTeamExtensions
  442. {
  443. /// <summary>Gets the team the player is currently joined to. Null if none.</summary>
  444. /// <returns>The team the player is currently joined to. Null if none.</returns>
  445. public static PhotonTeam GetPhotonTeam(this Player player)
  446. {
  447. object teamId;
  448. PhotonTeam team;
  449. if (player.CustomProperties.TryGetValue(PhotonTeamsManager.TeamPlayerProp, out teamId) && PhotonTeamsManager.Instance.TryGetTeamByCode((byte)teamId, out team))
  450. {
  451. return team;
  452. }
  453. return null;
  454. }
  455. /// <summary>
  456. /// Join a team.
  457. /// </summary>
  458. /// <param name="player">The player who will join a team.</param>
  459. /// <param name="team">The team to be joined.</param>
  460. /// <returns></returns>
  461. public static bool JoinTeam(this Player player, PhotonTeam team)
  462. {
  463. if (team == null)
  464. {
  465. Debug.LogWarning("JoinTeam failed: PhotonTeam provided is null");
  466. return false;
  467. }
  468. PhotonTeam currentTeam = player.GetPhotonTeam();
  469. if (currentTeam != null)
  470. {
  471. Debug.LogWarningFormat("JoinTeam failed: player ({0}) is already joined to a team ({1}), call SwitchTeam instead", player, team);
  472. return false;
  473. }
  474. return player.SetCustomProperties(new Hashtable { { PhotonTeamsManager.TeamPlayerProp, team.Code } });
  475. }
  476. /// <summary>
  477. /// Join a team using team code.
  478. /// </summary>
  479. /// <param name="player">The player who will join the team.</param>
  480. /// <param name="teamCode">The code fo the team to be joined.</param>
  481. /// <returns></returns>
  482. public static bool JoinTeam(this Player player, byte teamCode)
  483. {
  484. PhotonTeam team;
  485. return PhotonTeamsManager.Instance.TryGetTeamByCode(teamCode, out team) && player.JoinTeam(team);
  486. }
  487. /// <summary>
  488. /// Join a team using team name.
  489. /// </summary>
  490. /// <param name="player">The player who will join the team.</param>
  491. /// <param name="teamName">The name of the team to be joined.</param>
  492. /// <returns></returns>
  493. public static bool JoinTeam(this Player player, string teamName)
  494. {
  495. PhotonTeam team;
  496. return PhotonTeamsManager.Instance.TryGetTeamByName(teamName, out team) && player.JoinTeam(team);
  497. }
  498. /// <summary>Switch that player's team to the one you assign.</summary>
  499. /// <remarks>Internally checks if this player is in that team already or not. Only team switches are actually sent.</remarks>
  500. /// <param name="player"></param>
  501. /// <param name="team"></param>
  502. public static bool SwitchTeam(this Player player, PhotonTeam team)
  503. {
  504. if (team == null)
  505. {
  506. Debug.LogWarning("SwitchTeam failed: PhotonTeam provided is null");
  507. return false;
  508. }
  509. PhotonTeam currentTeam = player.GetPhotonTeam();
  510. if (currentTeam == null)
  511. {
  512. Debug.LogWarningFormat("SwitchTeam failed: player ({0}) was not joined to any team, call JoinTeam instead", player);
  513. return false;
  514. }
  515. if (currentTeam.Code == team.Code)
  516. {
  517. Debug.LogWarningFormat("SwitchTeam failed: player ({0}) is already joined to the same team {1}", player, team);
  518. return false;
  519. }
  520. return player.SetCustomProperties(new Hashtable { { PhotonTeamsManager.TeamPlayerProp, team.Code } },
  521. new Hashtable { { PhotonTeamsManager.TeamPlayerProp, currentTeam.Code }});
  522. }
  523. /// <summary>Switch the player's team using a team code.</summary>
  524. /// <remarks>Internally checks if this player is in that team already or not.</remarks>
  525. /// <param name="player">The player that will switch teams.</param>
  526. /// <param name="teamCode">The code of the team to switch to.</param>
  527. /// <returns>If the team switch request is queued to be sent to the server or done in case offline or not joined to a room yet.</returns>
  528. public static bool SwitchTeam(this Player player, byte teamCode)
  529. {
  530. PhotonTeam team;
  531. return PhotonTeamsManager.Instance.TryGetTeamByCode(teamCode, out team) && player.SwitchTeam(team);
  532. }
  533. /// <summary>Switch the player's team using a team name.</summary>
  534. /// <remarks>Internally checks if this player is in that team already or not.</remarks>
  535. /// <param name="player">The player that will switch teams.</param>
  536. /// <param name="teamName">The name of the team to switch to.</param>
  537. /// <returns>If the team switch request is queued to be sent to the server or done in case offline or not joined to a room yet.</returns>
  538. public static bool SwitchTeam(this Player player, string teamName)
  539. {
  540. PhotonTeam team;
  541. return PhotonTeamsManager.Instance.TryGetTeamByName(teamName, out team) && player.SwitchTeam(team);
  542. }
  543. /// <summary>
  544. /// Leave the current team if any.
  545. /// </summary>
  546. /// <param name="player"></param>
  547. /// <returns>If the leaving team request is queued to be sent to the server or done in case offline or not joined to a room yet.</returns>
  548. public static bool LeaveCurrentTeam(this Player player)
  549. {
  550. PhotonTeam currentTeam = player.GetPhotonTeam();
  551. if (currentTeam == null)
  552. {
  553. Debug.LogWarningFormat("LeaveCurrentTeam failed: player ({0}) was not joined to any team", player);
  554. return false;
  555. }
  556. return player.SetCustomProperties(new Hashtable {{PhotonTeamsManager.TeamPlayerProp, null}}, new Hashtable {{PhotonTeamsManager.TeamPlayerProp, currentTeam.Code}});
  557. }
  558. /// <summary>
  559. /// Try to get the team mates.
  560. /// </summary>
  561. /// <param name="player">The player to get the team mates of.</param>
  562. /// <param name="teamMates">The team mates array to fill.</param>
  563. /// <returns>If successful or not.</returns>
  564. public static bool TryGetTeamMates(this Player player, out Player[] teamMates)
  565. {
  566. return PhotonTeamsManager.Instance.TryGetTeamMatesOfPlayer(player, out teamMates);
  567. }
  568. }
  569. }