Skip to content

Commit

Permalink
feat: Pool Spawner for Multiple Additive Scenes
Browse files Browse the repository at this point in the history
  • Loading branch information
MrGadget1024 committed Dec 8, 2024
1 parent ca40d6a commit 7766ed2
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ GameObject:
- component: {fileID: 5119366209337690377}
- component: {fileID: 114048121767222990}
m_Layer: 0
m_Name: Prize
m_Name: Reward
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,7 @@ MonoBehaviour:
type: 3}
autoCreatePlayer: 1
playerSpawnMethod: 1
spawnPrefabs:
- {fileID: 1139254171913846, guid: 8cec47ed46e0eff45966a5173d3aa0d3, type: 3}
spawnPrefabs: []
exceptionsDisconnect: 1
snapshotSettings:
bufferTimeMultiplier: 2
Expand All @@ -291,6 +290,7 @@ MonoBehaviour:
timeInterpolationGui: 0
rewardPrefab: {fileID: 1139254171913846, guid: 8cec47ed46e0eff45966a5173d3aa0d3,
type: 3}
poolSize: 20
instances: 2
gameScene: Assets/Mirror/Examples/MultipleAdditiveScenes/Scenes/MirrorMultipleAdditiveScenesGame.unity
--- !u!4 &69965670
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public class MultiSceneNetManager : NetworkManager
[Header("Spawner Setup")]
[Tooltip("Reward Prefab for the Spawner")]
public GameObject rewardPrefab;
public byte poolSize = 20;

[Header("MultiScene Setup")]
public int instances = 3;
Expand Down Expand Up @@ -53,9 +54,16 @@ IEnumerator OnServerAddPlayerDelayed(NetworkConnectionToClient conn)
// Wait for end of frame before adding the player to ensure Scene Message goes first
yield return new WaitForEndOfFrame();

base.OnServerAddPlayer(conn);
Transform startPos = GetStartPosition();
GameObject player = startPos != null
? Instantiate(playerPrefab, startPos.position, startPos.rotation)
: Instantiate(playerPrefab);

PlayerScore playerScore = conn.identity.GetComponent<PlayerScore>();
// instantiating a "Player" prefab gives it the name "Player(clone)"
// => appending the connectionId is WAY more useful for debugging!
player.name = $"{playerPrefab.name} [connId={conn.connectionId}]";

PlayerScore playerScore = player.GetComponent<PlayerScore>();
playerScore.playerNumber = clientIndex;
playerScore.scoreIndex = clientIndex / subScenes.Count;
playerScore.matchIndex = clientIndex % subScenes.Count;
Expand All @@ -64,8 +72,9 @@ IEnumerator OnServerAddPlayerDelayed(NetworkConnectionToClient conn)
// This is what allows Scene Interest Management
// to isolate matches per scene instance on server.
if (subScenes.Count > 0)
SceneManager.MoveGameObjectToScene(conn.identity.gameObject, subScenes[clientIndex % subScenes.Count]);
SceneManager.MoveGameObjectToScene(player, subScenes[clientIndex % subScenes.Count]);

NetworkServer.AddPlayerForConnection(conn, player);
clientIndex++;
}

Expand Down Expand Up @@ -93,9 +102,13 @@ IEnumerator ServerLoadSubScenes()

Scene newScene = SceneManager.GetSceneAt(index);
subScenes.Add(newScene);
Spawner.InitialSpawn(newScene);
}

Spawner.InitializePool(rewardPrefab, poolSize);
for (int index = 0; index < subScenes.Count; index++)
if (subScenes[index].IsValid())
Spawner.InitialSpawn(subScenes[index]);

subscenesLoaded = true;
}

Expand All @@ -105,7 +118,8 @@ IEnumerator ServerLoadSubScenes()
public override void OnStopServer()
{
NetworkServer.SendToAll(new SceneMessage { sceneName = gameScene, sceneOperation = SceneOperation.UnloadAdditive });
StartCoroutine(ServerUnloadSubScenes());
if (gameObject.activeSelf) StartCoroutine(ServerUnloadSubScenes());
Spawner.ClearPool();
clientIndex = 0;
}

Expand All @@ -122,14 +136,14 @@ IEnumerator ServerUnloadSubScenes()
yield return Resources.UnloadUnusedAssets();
}

/// <summary>
/// This is called when a client is stopped.
/// </summary>
public override void OnStopClient()
public override void OnClientSceneChanged()
{
// Make sure we're not in ServerOnly mode now after stopping host client
if (mode == NetworkManagerMode.Offline)
StartCoroutine(ClientUnloadSubScenes());
// Don't initialize the pool for host client because it's
// already initialized in OnRoomServerSceneChanged
if (!NetworkServer.active && SceneManager.sceneCount > 1)
Spawner.InitializePool(rewardPrefab, 10);

base.OnClientSceneChanged();
}

// Unload all but the active scene, which is the "container" scene
Expand All @@ -140,6 +154,22 @@ IEnumerator ClientUnloadSubScenes()
yield return SceneManager.UnloadSceneAsync(SceneManager.GetSceneAt(index));
}

/// <summary>
/// This is called when a client is stopped.
/// </summary>
public override void OnStopClient()
{
// Clear the pool when stopping client
// Only do this if we're not the host client because
// pool needs to remain active for remote clients
if (!NetworkServer.active)
Spawner.ClearPool();

// Make sure we're not in ServerOnly mode now after stopping host client
if (mode == NetworkManagerMode.Offline)
if (gameObject.activeSelf) StartCoroutine(ClientUnloadSubScenes());
}

#endregion
}
}
45 changes: 23 additions & 22 deletions Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/Reward.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@

namespace Mirror.Examples.MultipleAdditiveScenes
{
[AddComponentMenu("")]
[RequireComponent(typeof(Common.RandomColor))]
public class Reward : NetworkBehaviour
{
public bool available = true;
[Header("Components")]
public Common.RandomColor randomColor;

[Header("Diagnostics")]
[ReadOnly, SerializeField]
bool available = true;

protected override void OnValidate()
{
base.OnValidate();
Expand All @@ -16,37 +21,33 @@ protected override void OnValidate()
randomColor = GetComponent<Common.RandomColor>();
}

[ServerCallback]
void OnTriggerEnter(Collider other)
public override void OnStartServer()
{
if (other.gameObject.CompareTag("Player"))
ClaimPrize(other.gameObject);
available = true;
}

[ServerCallback]
void ClaimPrize(GameObject player)
void OnTriggerEnter(Collider other)
{
if (available)
{
// This is a fast switch to prevent two players claiming the prize in a bang-bang close contest for it.
// First hit turns it off, pending the object being destroyed a few frames later.
available = false;
// Set up physics layers to prevent this from being called by non-players
// and eliminate the need for a tag check here.
if (!other.CompareTag("Player")) return;

Color32 color = randomColor.color;
// This is a fast switch to prevent two players claiming the reward in a bang-bang close contest for it.
// First to trigger turns it off, pending the object being destroyed a few frames later.
if (!available)
return;

// calculate the points from the color ... lighter scores higher as the average approaches 255
// UnityEngine.Color RGB values are float fractions of 255
uint points = (uint)(((color.r) + (color.g) + (color.b)) / 3);
available = false;

// award the points via SyncVar on the PlayerController
player.GetComponent<PlayerScore>().score += points;
// Calculate the points from the color...lighter scores higher as the average approaches 255
// UnityEngine.Color RGB values are byte 0 to 255
uint points = (uint)((randomColor.color.r + randomColor.color.g + randomColor.color.b) / 3);

// spawn a replacement
Spawner.SpawnReward(gameObject.scene);
// award the points via SyncVar on Player's PlayerScore
other.GetComponent<PlayerScore>().score += points;

// destroy this one
NetworkServer.Destroy(gameObject);
}
Spawner.RecycleReward(gameObject);
}
}
}
84 changes: 83 additions & 1 deletion Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/Spawner.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,78 @@
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace Mirror.Examples.MultipleAdditiveScenes
{
internal class Spawner
{
static GameObject prefab;
static byte poolSize = 10;
static Pool<GameObject> pool;
static ushort counter;

internal static void InitializePool(GameObject poolPrefab, byte count)
{
prefab = poolPrefab;
poolSize = count;

NetworkClient.RegisterPrefab(prefab, SpawnHandler, UnspawnHandler);
pool = new Pool<GameObject>(CreateNew, poolSize);
}

internal static void ClearPool()
{
if (prefab == null) return;

NetworkClient.UnregisterPrefab(prefab);

if (pool == null) return;

// destroy all objects in pool
while (pool.Count > 0)
Object.Destroy(pool.Get());

counter = 0;
pool = null;
}

static GameObject SpawnHandler(SpawnMessage msg) => Get(msg.position, msg.rotation);

static void UnspawnHandler(GameObject spawned) => Return(spawned);

static GameObject CreateNew()
{
// use this object as parent so that objects dont crowd hierarchy
GameObject next = Object.Instantiate(prefab);
counter++;
next.name = $"{prefab.name}_pooled_{counter:00}";
next.SetActive(false);
return next;
}

public static GameObject Get(Vector3 position, Quaternion rotation)
{
GameObject next = pool.Get();

// set position/rotation and set active
next.transform.position = position;
next.transform.rotation = rotation;
next.SetActive(true);
return next;
}

// Used to put object back into pool so they can b
// Should be used on server after unspawning an object
// Used on client by NetworkClient to unspawn objects
public static void Return(GameObject spawned)
{
// disable object
spawned.SetActive(false);

// add back to pool
pool.Return(spawned);
}

[ServerCallback]
internal static void InitialSpawn(Scene scene)
{
Expand All @@ -16,9 +84,23 @@ internal static void InitialSpawn(Scene scene)
internal static void SpawnReward(Scene scene)
{
Vector3 spawnPosition = new Vector3(Random.Range(-19, 20), 1, Random.Range(-19, 20));
GameObject reward = Object.Instantiate(((MultiSceneNetManager)NetworkManager.singleton).rewardPrefab, spawnPosition, Quaternion.identity);
GameObject reward = Get(spawnPosition, Quaternion.identity);
SceneManager.MoveGameObjectToScene(reward, scene);
NetworkServer.Spawn(reward);
}

[ServerCallback]
internal static async void RecycleReward(GameObject reward)
{
NetworkServer.UnSpawn(reward);
Return(reward);
await DelayedSpawn(reward.scene);
}

static async Task DelayedSpawn(Scene scene)
{
await Task.Delay(new System.TimeSpan(0, 0, 1));
SpawnReward(scene);
}
}
}

0 comments on commit 7766ed2

Please sign in to comment.