Last active
December 29, 2021 00:48
-
-
Save FairlySadPanda/fdb0598238b962120f8459ad15ab09d1 to your computer and use it in GitHub Desktop.
A generic game lobby for Udon
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using UdonSharp; | |
using UnityEngine; | |
using VRC.SDKBase; | |
using VRC.Udon; | |
using VRC.SDK3.Components; | |
using UnityEditor; | |
namespace FSP.UsefulThings { | |
// Implemented ahead of U# 1.0 supporting custom property attributes. | |
public class ReadOnlyAttribute : PropertyAttribute | |
{ | |
} | |
[CustomPropertyDrawer(typeof(ReadOnlyAttribute))] | |
public class ReadOnlyDrawer : PropertyDrawer | |
{ | |
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) | |
{ | |
return EditorGUI.GetPropertyHeight(property, label, true); | |
} | |
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) | |
{ | |
GUI.enabled = false; | |
EditorGUI.PropertyField(position, property, label, true); | |
GUI.enabled = true; | |
} | |
} | |
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual), AddComponentMenu("FSP/Useful Things/Generic Game Lobby"), RequireComponent(typeof(VRCObjectPool))] | |
public class GenericGameLobby : UdonSharpBehaviour | |
{ | |
[Header("Lobby Configuration Data")] | |
public int minPlayersToPlayGame; | |
public int maxPlayersToPlayGame; | |
public bool onlyMasterCanStartGame; | |
public bool onlyLobbyMemberCanStartGame; | |
public bool onlyMasterCanEndGame; | |
public bool onlyLobbyMemberCanEndGame; | |
[Header("The UdonBehaviours that are informed when events occur.")] | |
public UdonBehaviour[] subscribers; | |
[SerializeField] | |
private VRCObjectPool playerPool; | |
[SerializeField] | |
private string logPrefix = "[FSP] [UsefulThings] [GenericGameLobby]"; | |
[Header("Properties that your subscribers must implement")] | |
[SerializeField, ReadOnly] | |
private string playerApiArrayOfCurrentLobbyPlayers = "GGL_currentLobbyPlayers"; | |
[SerializeField, ReadOnly] | |
private string playerApiOfLatestPlayerToChangeState = "GGL_latestPlayer"; | |
[Header("Events that your subscribers must implement")] | |
[SerializeField, ReadOnly] | |
private string errorOccured = "_GGL_LobbyErrored"; | |
[SerializeField, ReadOnly] | |
private string playerAdded = "_GGL_PlayerAdded"; | |
[SerializeField, ReadOnly] | |
private string playerRemoved = "_GGL_PlayerRemoved"; | |
[SerializeField, ReadOnly] | |
private string gameStartRequested = "_GGL_GameStartRequested"; | |
[SerializeField, ReadOnly] | |
private string gameEndRequested = "_GGL_GameEndRequested"; | |
[SerializeField, ReadOnly] | |
private string playerJoined = "_GGL_PlayerJoinedWorld"; | |
[SerializeField, ReadOnly] | |
private string playerLeft = "_GGL_PlayerLeftWorld"; | |
[Header("Properties that your player controllers must implement")] | |
[SerializeField, ReadOnly] | |
private string playerID = "_GGL_PlayerID"; | |
// Internal logic | |
private VRCPlayerApi[] lobbyPlayers; | |
private int playerCount; | |
public void Start() { | |
if (!playerPool) { | |
playerPool = GetComponent<VRCObjectPool>(); | |
} | |
if (!playerPool) { | |
Panic("No VRCObjectPool component to work with!"); | |
return; | |
} | |
lobbyPlayers = new VRCPlayerApi[playerPool.Pool.Length]; | |
playerCount = 0; | |
} | |
public override void OnPlayerJoined(VRCPlayerApi player) | |
{ | |
if (VRCPlayerApi.GetPlayerCount() > playerPool.Pool.Length) { | |
Panic($"Too many people ({VRCPlayerApi.GetPlayerCount()} vs expected max of {playerPool.Pool.Length}) have joined the world! Cannot safely allocate an object to newest player {player.displayName}. Shutting down."); | |
return; | |
} | |
GameObject spawn = playerPool.TryToSpawn(); | |
if (!spawn) { | |
Panic("Unable to spawn an object from the player pool! There needs to be enough objects in the pool to serve every player possible that joins the world (2 times soft cap plus 2"); | |
return; | |
} | |
UdonBehaviour behaviour = spawn.GetComponent<UdonBehaviour>(); | |
if (!behaviour) { | |
Panic("Player pool object {poolObject.name} has no UdonBehaviour! Pool objects must have an UdonBehaviour."); | |
return; | |
} | |
behaviour.SetProgramVariable<int>(playerID, player.playerId); | |
Networking.SetOwner(player, spawn); | |
foreach(UdonBehaviour sub in subscribers) { | |
sub.SetProgramVariable<VRCPlayerApi>(playerApiOfLatestPlayerToChangeState, player); | |
sub.SendCustomEvent(playerJoined); | |
} | |
} | |
public override void OnPlayerLeft(VRCPlayerApi player) | |
{ | |
foreach(GameObject poolObject in playerPool.Pool) { | |
if (poolObject) { | |
UdonBehaviour behaviour = poolObject.GetComponent<UdonBehaviour>(); | |
if (behaviour) { | |
string name = (string)behaviour.GetProgramVariable(playerID); | |
if (name == player.displayName) { | |
behaviour.SetProgramVariable<int>(playerID, 0); | |
playerPool.Return(poolObject); | |
} | |
} else { | |
Panic("Player pool object {poolObject.name} has no UdonBehaviour! Pool objects must have an UdonBehaviour."); | |
return; | |
} | |
} | |
} | |
RemovePlayer(player); | |
foreach(UdonBehaviour sub in subscribers) { | |
sub.SetProgramVariable<VRCPlayerApi>(playerApiOfLatestPlayerToChangeState, player); | |
sub.SendCustomEvent(playerLeft); | |
} | |
} | |
public override bool OnOwnershipRequest(VRCPlayerApi requestingPlayer, VRCPlayerApi requestedOwner) { | |
return requestingPlayer.isMaster && requestedOwner.isMaster; | |
} | |
public void _AddPlayer(UdonBehaviour playerBehaviour) { | |
if (playerCount >= maxPlayersToPlayGame) { | |
Debug.LogWarning($"{logPrefix} Cannot add player: too many players"); | |
return; | |
} | |
int newPlayerID = (int)playerBehaviour.GetProgramVariable(playerID); | |
if (newPlayerID < 1) { | |
Panic($"Player behaviour {playerBehaviour.name} does not have a playerID! PlayerID must be tracked so we know who is supposed to own each pooled behaviour"); | |
return; | |
} | |
VRCPlayerApi player = VRCPlayerApi.GetPlayerById(newPlayerID); | |
for (int i = 0; i < lobbyPlayers.Length; i++) { | |
if (lobbyPlayers[i] == null) { | |
lobbyPlayers[i] = player; | |
foreach(UdonBehaviour sub in subscribers) { | |
sub.SetProgramVariable<VRCPlayerApi>(playerApiOfLatestPlayerToChangeState, player); | |
sub.SendCustomEvent(playerAdded); | |
} | |
playerCount++; | |
return; | |
} | |
} | |
Panic($"Attempted to add player {player.displayName} to the lobby, but the lobby array is full!"); | |
} | |
public void _RemovePlayer(UdonBehaviour playerBehaviour) { | |
int newPlayerID = (int)playerBehaviour.GetProgramVariable(playerID); | |
if (newPlayerID < 1) { | |
Panic($"Player behaviour {playerBehaviour.name} does not have a playerID! PlayerID must be tracked so we know who is supposed to own each pooled behaviour"); | |
return; | |
} | |
RemovePlayer(VRCPlayerApi.GetPlayerById(newPlayerID)); | |
} | |
public void _StartGame(UdonBehaviour playerBehaviour) { | |
int newPlayerID = (int)playerBehaviour.GetProgramVariable(playerID); | |
if (newPlayerID < 1) { | |
Panic($"Player behaviour {playerBehaviour.name} does not have a playerID! PlayerID must be tracked so we know who is supposed to own each pooled behaviour"); | |
return; | |
} | |
if (playerCount < minPlayersToPlayGame) { | |
Debug.LogWarning($"{logPrefix} Cannot start game: not enough players"); | |
return; | |
} | |
VRCPlayerApi player = VRCPlayerApi.GetPlayerById(newPlayerID); | |
if (player == null) { | |
Panic($"Player with ID {newPlayerID} does not exist"); | |
return; | |
} | |
if (onlyMasterCanStartGame) { | |
if (!player.isMaster) { | |
Debug.LogWarning($"{logPrefix} cannot start game: requesting player isn't master"); | |
return; | |
} | |
} | |
if (onlyLobbyMemberCanStartGame) { | |
bool found = false; | |
foreach(VRCPlayerApi lobbyPlayer in lobbyPlayers) { | |
if (lobbyPlayer != null && lobbyPlayer.playerId == player.playerId) { | |
found = true; | |
break; | |
} | |
} | |
if (!found) { | |
Debug.LogWarning($"{logPrefix} cannot start game: requesting player isn't part of the game"); | |
return; | |
} | |
} | |
VRCPlayerApi[] players = new VRCPlayerApi[playerCount]; | |
int i = 0; | |
foreach(VRCPlayerApi lobbyPlayer in lobbyPlayers) { | |
if (lobbyPlayer != null) { | |
players[i++] = lobbyPlayer; | |
} | |
} | |
foreach(UdonBehaviour sub in subscribers) { | |
sub.SetProgramVariable<VRCPlayerApi[]>(playerApiArrayOfCurrentLobbyPlayers, players); | |
sub.SendCustomEvent(gameStartRequested); | |
} | |
} | |
public void _EndGame(UdonBehaviour playerBehaviour) { | |
int newPlayerID = (int)playerBehaviour.GetProgramVariable(playerID); | |
if (newPlayerID < 1) { | |
Panic($"Player behaviour {playerBehaviour.name} does not have a playerID! PlayerID must be tracked so we know who is supposed to own each pooled behaviour"); | |
return; | |
} | |
VRCPlayerApi player = VRCPlayerApi.GetPlayerById(newPlayerID); | |
if (player == null) { | |
Panic($"Player with ID {newPlayerID} does not exist"); | |
return; | |
} | |
if (onlyMasterCanEndGame) { | |
if (!player.isMaster) { | |
Debug.LogWarning($"{logPrefix} cannot end game: requesting player isn't master"); | |
return; | |
} | |
} | |
if (onlyLobbyMemberCanEndGame) { | |
bool found = false; | |
foreach(VRCPlayerApi lobbyPlayer in lobbyPlayers) { | |
if (lobbyPlayer != null && lobbyPlayer.playerId == player.playerId) { | |
found = true; | |
break; | |
} | |
} | |
if (!found) { | |
Debug.LogWarning($"{logPrefix} cannot end game: requesting player isn't part of the game"); | |
return; | |
} | |
} | |
Start(); | |
foreach(UdonBehaviour sub in subscribers) { | |
sub.SetProgramVariable<VRCPlayerApi>(playerApiOfLatestPlayerToChangeState, player); | |
sub.SendCustomEvent(gameEndRequested); | |
} | |
} | |
private void RemovePlayer(VRCPlayerApi player) { | |
for (int i = 0; i < lobbyPlayers.Length; i++) { | |
if (lobbyPlayers[i] != null && lobbyPlayers[i].playerId == player.playerId) { | |
lobbyPlayers[i] = null; | |
foreach(UdonBehaviour sub in subscribers) { | |
sub.SetProgramVariable<VRCPlayerApi>(playerApiOfLatestPlayerToChangeState, player); | |
sub.SendCustomEvent(playerRemoved); | |
} | |
playerCount--; | |
return; | |
} | |
} | |
} | |
// Log an error report, then report that this script has an error to subscribers, then shut down. | |
private void Panic(string message) { | |
Debug.LogError($"{logPrefix} {message}"); | |
foreach(UdonBehaviour sub in subscribers) { | |
sub.SendCustomEvent(errorOccured); | |
} | |
gameObject.SetActive(false); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment