This commit is contained in:
2025-06-16 15:14:23 +02:00
committed by devbeni
parent 60fe4620ff
commit 4ff561284f
3174 changed files with 428263 additions and 0 deletions

View File

@ -0,0 +1,179 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Mirror.Examples.MultipleAdditiveScenes
{
[AddComponentMenu("")]
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;
[Scene]
public string gameScene;
// This is set true after server loads all subscene instances
bool subscenesLoaded;
// subscenes are added to this list as they're loaded
readonly List<Scene> subScenes = new List<Scene>();
// Sequential index used in round-robin deployment of players into instances and score positioning
int clientIndex;
#region Server System Callbacks
/// <summary>
/// Called on the server when a client adds a new player with NetworkClient.AddPlayer.
/// <para>The default implementation for this function creates a new player object from the playerPrefab.</para>
/// </summary>
/// <param name="conn">Connection from client.</param>
public override void OnServerAddPlayer(NetworkConnectionToClient conn)
{
StartCoroutine(OnServerAddPlayerDelayed(conn));
}
// This delay is mostly for the host player that loads too fast for the
// server to have subscenes async loaded from OnStartServer ahead of it.
IEnumerator OnServerAddPlayerDelayed(NetworkConnectionToClient conn)
{
// wait for server to async load all subscenes for game instances
while (!subscenesLoaded)
yield return null;
// Send Scene message to client to additively load the game scene
conn.Send(new SceneMessage { sceneName = gameScene, sceneOperation = SceneOperation.LoadAdditive });
// Wait for end of frame before adding the player to ensure Scene Message goes first
yield return new WaitForEndOfFrame();
Transform startPos = GetStartPosition();
GameObject player = startPos != null
? Instantiate(playerPrefab, startPos.position, startPos.rotation)
: Instantiate(playerPrefab);
// 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;
// Do this only on server, not on clients
// This is what allows Scene Interest Management
// to isolate matches per scene instance on server.
if (subScenes.Count > 0)
SceneManager.MoveGameObjectToScene(player, subScenes[clientIndex % subScenes.Count]);
NetworkServer.AddPlayerForConnection(conn, player);
clientIndex++;
}
#endregion
#region Start & Stop Callbacks
/// <summary>
/// This is invoked when a server is started - including when a host is started.
/// <para>StartServer has multiple signatures, but they all cause this hook to be called.</para>
/// </summary>
public override void OnStartServer()
{
StartCoroutine(ServerLoadSubScenes());
}
// We're additively loading scenes, so GetSceneAt(0) will return the main "container" scene,
// therefore we start the index at one and loop through instances value inclusively.
// If instances is zero, the loop is bypassed entirely.
IEnumerator ServerLoadSubScenes()
{
for (int index = 1; index <= instances; index++)
{
yield return SceneManager.LoadSceneAsync(gameScene, new LoadSceneParameters { loadSceneMode = LoadSceneMode.Additive, localPhysicsMode = LocalPhysicsMode.Physics3D });
Scene newScene = SceneManager.GetSceneAt(index);
subScenes.Add(newScene);
}
Spawner.InitializePool(rewardPrefab, poolSize);
foreach (Scene scene in subScenes)
if (scene.IsValid())
Spawner.InitialSpawn(scene);
subscenesLoaded = true;
}
/// <summary>
/// This is called when a server is stopped - including when a host is stopped.
/// </summary>
public override void OnStopServer()
{
NetworkServer.SendToAll(new SceneMessage { sceneName = gameScene, sceneOperation = SceneOperation.UnloadAdditive });
if (gameObject.activeSelf)
StartCoroutine(ServerUnloadSubScenes());
Spawner.ClearPool();
clientIndex = 0;
}
// Unload the subScenes and unused assets and clear the subScenes list.
IEnumerator ServerUnloadSubScenes()
{
for (int index = 0; index < subScenes.Count; index++)
if (subScenes[index].IsValid())
yield return SceneManager.UnloadSceneAsync(subScenes[index]);
subScenes.Clear();
subscenesLoaded = false;
yield return Resources.UnloadUnusedAssets();
}
public override void OnClientSceneChanged()
{
// Don't initialize the pool for host client because it's
// already initialized in OnRoomServerSceneChanged
if (!NetworkServer.active && SceneManager.sceneCount > 1)
Spawner.InitializePool(rewardPrefab, poolSize);
base.OnClientSceneChanged();
}
// Unload all but the active scene, which is the "container" scene
IEnumerator ClientUnloadSubScenes()
{
for (int index = 0; index < SceneManager.sceneCount; index++)
if (SceneManager.GetSceneAt(index) != SceneManager.GetActiveScene())
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
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: b982a1fd37427e64e8310a863d03d2c9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/MultiSceneNetManager.cs
uploadId: 736421

View File

@ -0,0 +1,46 @@
using UnityEngine;
namespace Mirror.Examples.MultipleAdditiveScenes
{
[RequireComponent(typeof(Rigidbody))]
public class PhysicsCollision : NetworkBehaviour
{
[Tooltip("how forcefully to push this object")]
public float force = 12;
public Rigidbody rigidbody3D;
protected override void OnValidate()
{
base.OnValidate();
if (rigidbody3D == null)
rigidbody3D = GetComponent<Rigidbody>();
}
void Start()
{
rigidbody3D.isKinematic = !isServer;
}
[ServerCallback]
void OnCollisionStay(Collision other)
{
if (other.gameObject.CompareTag("Player"))
{
// get direction from which player is contacting object
Vector3 direction = other.contacts[0].normal;
// zero the y and normalize so we don't shove this through the floor or launch this over the wall
direction.y = 0;
direction = direction.normalized;
// push this away from player...a bit less force for host player
if (other.gameObject.GetComponent<NetworkIdentity>().connectionToClient.connectionId == NetworkConnection.LocalConnectionId)
rigidbody3D.AddForce(direction * force * .5f);
else
rigidbody3D.AddForce(direction * force);
}
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: c709489168fec9348b7f8290ee2e8466
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/PhysicsCollision.cs
uploadId: 736421

View File

@ -0,0 +1,30 @@
using UnityEngine;
namespace Mirror.Examples.MultipleAdditiveScenes
{
public class PlayerScore : NetworkBehaviour
{
[SyncVar]
public int playerNumber;
[SyncVar]
public int scoreIndex;
[SyncVar]
public int matchIndex;
[SyncVar]
public uint score;
public int clientMatchIndex = -1;
void OnGUI()
{
if (!isServerOnly && !isLocalPlayer && clientMatchIndex < 0)
clientMatchIndex = NetworkClient.connection.identity.GetComponent<PlayerScore>().matchIndex;
if (isLocalPlayer || matchIndex == clientMatchIndex)
GUI.Box(new Rect(10f + (scoreIndex * 110), 10f, 100f, 25f), $"P{playerNumber}: {score}");
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 8be750efa9df50f47b65ae156053d149
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/PlayerScore.cs
uploadId: 736421

View File

@ -0,0 +1,65 @@
using UnityEngine;
namespace Mirror.Examples.MultipleAdditiveScenes
{
[AddComponentMenu("")]
[RequireComponent(typeof(Common.RandomColor))]
public class Reward : NetworkBehaviour
{
[Header("Components")]
public Common.RandomColor randomColor;
[Header("Diagnostics")]
[ReadOnly, SerializeField]
bool available = true;
protected override void OnValidate()
{
if (Application.isPlaying) return;
base.OnValidate();
Reset();
}
void Reset()
{
// Default position out of reach
transform.position = new Vector3(0, -1000, 0);
if (randomColor == null)
randomColor = GetComponent<Common.RandomColor>();
}
public override void OnStartServer()
{
available = true;
}
[ServerCallback]
void OnTriggerEnter(Collider other)
{
// Don't process collisions when it's in the pool
if (!gameObject.activeSelf) return;
// 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;
// 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;
available = false;
// 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);
// award the points via SyncVar on Player's PlayerScore
other.GetComponent<PlayerScore>().score += points;
Spawner.RecycleReward(gameObject);
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 10da7fdf8caa1eb4697658bf129457fa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/Reward.cs
uploadId: 736421

View File

@ -0,0 +1,107 @@
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Mirror.Examples.MultipleAdditiveScenes
{
internal static 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.SetPositionAndRotation(position, 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);
// move the object out of reach so OnTriggerEnter doesn't get called
spawned.transform.position = new Vector3(0, -1000, 0);
// add back to pool
pool.Return(spawned);
}
[ServerCallback]
internal static void InitialSpawn(Scene scene)
{
for (int i = 0; i < 10; i++)
SpawnReward(scene);
}
[ServerCallback]
internal static void SpawnReward(Scene scene)
{
Vector3 spawnPosition = new Vector3(Random.Range(-19, 20), 1, Random.Range(-19, 20));
GameObject reward = Get(spawnPosition, Quaternion.identity);
SceneManager.MoveGameObjectToScene(reward, scene);
NetworkServer.Spawn(reward);
}
[ServerCallback]
internal static async void RecycleReward(GameObject reward)
{
NetworkServer.UnSpawn(reward);
await DelayedSpawn(reward.scene);
}
static async Task DelayedSpawn(Scene scene)
{
await Task.Delay(new System.TimeSpan(0, 0, 1));
SpawnReward(scene);
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: f522bf510b49da44caa9f3ca0ac17f3b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/Spawner.cs
uploadId: 736421