Syncing large amount of gameobject children between players?

So I’ve got a random level generator, and it works like this: the start prefab has three exits. The exits are looped through and each of them chooses a different segment to go off of itself, and then that segment loops through its exits and chooses a segment to go off them, and so on. This results in things like this:


With the start circled in red. A few times I even got levels like this:

Which generated a lot of lag. (and that was before I tried implementing multiplayer) But with a few things fixed, it doesn’t generate levels that big anymore. Anyway, I’ve got player movement synced up, but each player’s levels are different, as you can imagine due to the randomness. I tried to fix this by having each segment and its children (walls, floors, etc.) each have a NetworkObject and NetworkTransform component, but that had way too much lag, and probably wasn’t good for the game. So the way I think I could do it is only having the Host generate the level, and then just sync up the level for the Clients… I hope that makes sense. Please let me know if this is impossible or not, and/or if there’s a better way of doing it. Thanks!

Does each client need to know the whole level?, if not one possible solution is to set it up so you’re only spawning network objects for the client’s current segment and those adjacent to it, and handle the dynamic spawning and despawning as they move around. This could be a little tricky to implement, especially if each client is free to wander around the level. You’ll either want to disable scene management and handle the spawning and despawning yourself, or look into using networkObject.CheckObjectVisibility which could be feasible but I’ve only ever used the first option for interest management.

Thanks for the reply! I’m afraid that yes, every client will be free to move around the level. I’m planning for it to be a multiplayer FPS where the levels are randomly generated. And also I made a map feature that makes it so that when a player presses Tab they can look at an outside view of the entire map (rotate it around and zoom in) to see where they are and where the other players are. Does calling Spawn() on one gameobject with a networkobject and networktransform component sync all of its children regardless of whether the children have those two components?

Ah in that case, can’t help you. :smile:

You could look into generating the level with a random seed and passing that to the clients, or maybe sending the segments in the form of structs for the clients to construct the level. At that point I’m wondering how much you’ll be using game objects over network objects.

I’m not sure on your question as it’s not something I’ve had to worry about, should be easy to test though. Sorry I can’t be of more help. :slight_smile:

Ah well. Thanks for trying! Maybe I’ll use a string that keeps track of all of the paths of the parent-child segments and then send that to all of the other clients and then they use that to generate the same level…

@dcmrobin
Specific to the random level generation and how that gets synchronized with clients:

  • Having the host generate the levels is probably the best starting point for now

  • I wouldn’t make the level pieces NetworkObjects, but I would make a single in-scene NetworkObject that is the “generator” for all of your level’s pieces.

  • A NetworkBehaviour component (will call it “LevelGen”) attached to the root NetworkObject is what you can use to synchronize the level pieces.

  • As long as the level pieces don’t move around after the level has been generated, I would recommend:

  • Since the serialization cost (size) to describe “each piece” determines the number of unique pieces you are going to have, you need to come up with a total count and then stick with that… if you have less than 255 pieces then you would express each piece type as a byte… if you have more then you will have to double that and use a ushort.

  • You could define this in like a ScriptableObject that you would assign to your “LevelGen” NetworkBehavior. You could extend the pieces by making each unique “level” have unique ScriptableObject that defines the pieces for that “level/area” of your game.

  • This way you could limit the number of unique pieces per “level/area” to 255 and use a byte to describe “which piece” to use when re-creating the level on the client side.

  • As cerestorm mentioned, you will most likely want to create a struct that defines each piece.

  • This struct should implement INetworkSerializable.

  • Next, you need to map out the maximum and minimum size you want to allow each level piece to be and how you want to handle position and rotation for each piece.

  • As opposed to sending precise transform information per piece, you can greatly reduce your serialization by thinking of your scene as a grid of a minimum size.

  • As an example, you might use a 32x32x32 as your minimum level piece size. So your pieces would fit within some multiplier of that block/segment grid size. You can then express the full size of the piece by a multiplier (scale) of the block/segment grid size. So, if you have a piece with a size scale multiplier of say (1, 2, 3) then it would fit within a 32, 64, 96 region of the scene.

  • You could optimize this by creating scene size axis limiters

  • As an example, you could have a “level/area” that will fit no more than a (grid relative) 128, 2, 128 sized level.

  • Where the unity world space size would be multiplied by your predefined grid size. So if you used the 32 x 32 x 32 grid block/segment size then that level could span no more than 4096 x 96 x 4096 Unity world space units.

  • So, each piece’s position would be defined by a starting grid coordinate which, if you kept your maximum grid size to no larger than 255 x 255 x 255 (which is 8192 x 8192 x 8192), then each piece’s position would be no larger than 3 bytes to describe the “starting grid block” of the piece.

  • For rotation, you need to determine if you want to rotate each piece around all axis or limit your axial rotation to occur around the Y axis (I would recommend this at first).

  • If you are just rotating around the Y axis, then you could easily use a byte to describe this if you only allow 90 degree rotations (which I would recommend to reduce the complexity of aligning pieces).

Once you have mapped out everything, then your associated “LevelGen” script could look something like:

    /// <summary>
    /// At 5 bytes per piece
    /// 100 pieces would be 500 bytes
    /// 250 pieces would be 1250 bytes
    /// 500 pieces would be 2500 bytes
    /// (etc)
    /// </summary>
    public struct LevelPiece : INetworkSerializable
    {
        public byte PieceType;
        public byte GridX;
        public byte GridY;
        public byte GridZ;
        public byte YRotation;

        public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
        {
            serializer.SerializeValue(ref PieceType);
            serializer.SerializeValue(ref GridX);
            serializer.SerializeValue(ref GridY);
            serializer.SerializeValue(ref GridZ);
            serializer.SerializeValue(ref YRotation);
        }
    }

    /// <summary>
    /// This would be on your in-scene placed NetworkObject/Network Prefab
    /// </summary>
    public class LevelGen : NetworkBehaviour
    {
        public List<LevelPiece> LevelPieces = new List<LevelPiece>();


        // You could add your ScriptableObject property here that the server would use
        // to generate the level.

        protected override void OnSynchronize<T>(ref BufferSerializer<T> serializer)
        {
            // The server is always the writer
            // Since the server spawns this locally, it doesn't serialize for itself.
            if (serializer.IsWriter)
            {
                var numberOfPieces = (ushort)LevelPieces.Count;
                serializer.SerializeValue(ref numberOfPieces);
                foreach(var levelPiece in  LevelPieces)
                {
                    levelPiece.NetworkSerialize(serializer);
                }
            }
            else // Clients will always be the readers
            {
                LevelPieces.Clear();
                var levelPiece = new LevelPiece();
                var pieceCount = (ushort)0;
                serializer.SerializeValue(ref pieceCount);
                for(int i = 0; i <  pieceCount; i++)
                {
                    serializer.SerializeValue(ref levelPiece);
                    LevelPieces.Add(levelPiece);
                }
            }
            base.OnSynchronize(ref serializer);
        }

        private IEnumerator GenerateLevelCoroutine()
        {
            // Your server-side level generation script here
            // This is also where you populate the LevelPieces
            // list that will be used when synchronizing clients.

            yield return null;
        }

        [ServerRpc(RequireOwnership = false)]
        private void ClientLevelCreationCompletedServerRpc(ServerRpcParams serverRpcParams = default)
        {
            // Use serverRpcParams.Receive.SenderClientId to determine which client finished generating the level.
            // Spawn the client's player at this time.
            // However you want game play to being (all clients or when a client finishes creating the level) would
            // happen here as well.
        }

        private IEnumerator CreateLevelCoroutine()
        {
            // You client-side level creation script here
            foreach(var levelPiece in LevelPieces)
            {
                // Instantiate and position level pieces
                // You might even update a progress bar depending upon the size
                // or time it takes.
            }
            // Send notification the client has finished to the server
            ClientLevelCreationCompletedServerRpc();
            yield return null;
        }

        public override void OnNetworkSpawn()
        {
            if (IsServer)
            {
                StartCoroutine(GenerateLevelCoroutine());
            }
            else
            {
                // At this point on the client-side, LevelPieces will be populated
                StartCoroutine(CreateLevelCoroutine());
            }
            base.OnNetworkSpawn();
        }
    }

This uses NetworkBehaviour.OnSynchronize to handle custom serialization of your NetworkBehaviour.
The idea being that the initial leg work of mapping out your format and pre-defining some limitations/boundaries for minimum piece size, how big your level can be, and the like… you can greatly reduce the bandwidth cost to synchronize clients. While you could further optimize this approach by limiting the number of unique pieces, reducing the maximum grid size to 128 x 128 while also keeping everything at a Y = 0 grid position… the best you could do (regarding reducing the 5 bytes per piece) would be to get it down to 4 bytes per piece… which (all things considered) is not that much of a savings to synchronize the dynamically generated level.

You could shave off 1 byte by just not having a Y axis position and starting everything at the same Y axis grid position of 0 (you would just remove the LevelPiece.GridY property). You also should note that your defined grid position should be multiplied by your grid size (i.e. if you pick 32 unity world space units then GridX, GridY (optional), and GridZ would be multiplied by 32 to get the actual Unity world space position).
This assumes a positive progression to reduce the complexity of +/- positions, so your levels would generate in the positive X, Y, and Z unity world space quadrant of the scene. So, you might have to assure your pieces aren’t centered perfectly but have a positive quadrant offset in the model’s local space.

Either case, the above approach should provide you with a high-level starting point.

Thanks, @NoelStephens_Unity I’m sure that’ll come in handy one way or another. That is unless the way I’m currently generating the levels makes any difference… Here’s my script and a screenshot of how it’s used:

using System.Collections;
using System.Collections.Generic;
using Unity.Netcode;
using Unity.Netcode.Components;
using UnityEngine;

public class Segment : MonoBehaviour
{
    [Header("Segment variables")]
    public int rarity;
    public GameObject wallPrefab;
    public GameObject[] segmentPrefabList;
    public Transform[] exits;

    [Header("Light-flicker variables")]
    public float maxWait = 2;
    public float maxFlicker = 0.2f;
    float timer;
    float interval;

    [HideInInspector] public bool isOverlapping;
    bool giveUp;
    int flickerNum;

    private void Start() {
        flickerNum = Random.Range(0, 10);

        if (name != "Start")
        {
            GenerateSegment();
        }
    }

    private void Update() {
        if (isOverlapping)
        {
            Destroy(gameObject);
        }

        if (transform.Find("Light") != null)
        {
            if (Camera.main != null)
            {
                if (Vector3.Distance(transform.position, Camera.main.transform.position) >= 300)
                {
                    transform.Find("Light").gameObject.SetActive(false);
                }
                else if (Vector3.Distance(transform.position, Camera.main.transform.position) < 300)
                {
                    transform.Find("Light").gameObject.SetActive(true);
                }
            }
            else//this is gonna get mixed up between the players
            {
                transform.Find("Light").gameObject.SetActive(false);
            }

            if (!transform.Find("Light").Find("Bulb").GetComponent<Light>().enabled)
            {
                transform.Find("Light").Find("Bulb").gameObject.SetActive(false);
            }
            else if (transform.Find("Light").Find("Bulb").GetComponent<Light>().enabled)
            {
                transform.Find("Light").Find("Bulb").gameObject.SetActive(true);
            }
        }

        if (giveUp)
        {
            for (int i = 0; i < exits.Length; i++)
            {
                if (exits[i].childCount == 0)
                {
                    Instantiate(wallPrefab, exits[i]);
                    giveUp = false;
                }
            }
        }

        if (transform.Find("Light") != null && flickerNum > 8)
        {
            timer += Time.deltaTime;
            if (timer > interval)
            {
                ToggleLight();
            }
        }
    }

    public void GenerateSegment() {
        for (int j = 0; j < exits.Length; j++)
        {
            regen:
                if (exits[j].childCount == 0)
                {
                    int randomNum = Random.Range(0, segmentPrefabList.Length);
                    int randProb1 = Random.Range(0, segmentPrefabList[randomNum].GetComponent<Segment>().rarity);
                    int randProb2 = Random.Range(0, segmentPrefabList[randomNum].GetComponent<Segment>().rarity);
                    if (randProb1 == randProb2)
                    {
                        GameObject newSegment = Instantiate(segmentPrefabList[randomNum], exits[j]);
                    }
                    else
                    {
                        goto regen;
                    }
                }
        }

        giveUp = true;
    }

    void ToggleLight()
    {
        transform.Find("Light").Find("Bulb").GetComponent<Light>().enabled = !transform.Find("Light").Find("Bulb").GetComponent<Light>().enabled;
        if (transform.Find("Light").Find("Bulb").GetComponent<Light>().enabled)
        {
            interval = Random.Range(0, maxWait);
        }
        else
        {
            interval = Random.Range(0, maxFlicker);
        }
     
        timer = 0;
    }
}

Edit: I think I might be on to something here… I have this code line: ```
GameObject.Find(“Start”).GetComponent().seed += randomNum.ToString();

the seed script only has a string in it and is attached to the start prefab. Each time a segment is generated, it will add on the random num to the seed string. When everything's finished generating, that string could be sent to the clients, and then loop through the string and use that to assign which segment is being generated on the client's side instead of a random number! Maybe it'll work.

@cerestorm had mentioned using a seed value, and if that string is what generates the entire level then it could be a much easier implementation without having to go through the steps of the other suggested path.

I would just check to see how long the string is and if it turns out to be too large, then you could always compress it.

Yeah, the string has in it as many characters as the amount of segments that have been generated. I’m curious… what do you mean by compressing it? And also, how would I send the string containing the seed from the host to the clients? I’ve tried, but I don’t know how.

You could look at GZipStream.

Yes, that could work, I just have to figure out how to send the string to the client first.

Edit: Figured it out! I just used
NetworkVariable

That was in the player script and was assigned to the players from the host.

You can also just use a string…
Look at the custom serialization documentation on how you can do this (the example provided uses a string).
However, if you compress then you are going to be sending a byte array. So, you might just implement an INetworkSerializable that you then use as your NetworkVariable type.

This makes it easier to make adjustments (like whether it is compressed or not compressed and such).

Oh… Well I guess I’ll stick with what I’ve got before I try something new and possibly break something xD Anyway, the thing I’m trying to fix is something different now. The seed has been synced up between the players, and the start segment’s exits are also synced up. But then for some reason, it throws an IndexOutOfRangeException at this line of code: ```
GameObject newSegment = Instantiate(segmentPrefabList[int.Parse(seed[GameObject.Find(“Start”).GetComponent().counter].ToString())], exits[j]);

which is itself in this method:

```csharp
public void LateGenerateSegment(string seed)
    {
        for (int j = 0; j < exits.Length; j++)
        {
            if (exits[j].childCount == 0)
            {
                GameObject newSegment = Instantiate(segmentPrefabList[int.Parse(seed[GameObject.Find("Start").GetComponent<Seed>().counter].ToString())], exits[j]);
                GameObject.Find("Start").GetComponent<Seed>().counter += 1;
            }
        }
    }

which is called on the start segment prefab from the client. It is also called when the start segment on the client’s side generates the first three segments in the seed string. That’s when the error shows up: when the LateGenerateSegment method is called on the segments that are generated by the start segment. inhale okay I hope that makes sense. But just in case here are the scripts:

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using JetBrains.Annotations;
using Unity.Netcode;
using Unity.Netcode.Components;
using UnityEngine;

public class Segment : NetworkBehaviour
{
    [Header("Segment variables")]
    public bool isStart;
    public int rarity;
    public GameObject wallPrefab;
    public GameObject[] segmentPrefabList;
    public Transform[] exits;

    [HideInInspector] public bool isOverlapping;
    [HideInInspector] public bool keepGenerating;
    bool startedGenerating;
    bool finishedGenerating;
    int flickerNum;

    private void Awake() {
        NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;
    }

    private void Start() {
        flickerNum = UnityEngine.Random.Range(0, 10);

        if (!isStart)
        {
            GenerateSegment();
            if (GameObject.Find("Start").GetComponent<Segment>().keepGenerating)
            {
                LateGenerateSegment(GameObject.Find("Start").GetComponent<Seed>().seed);
            }
        }
    }

    void OnClientConnected(ulong clientId)
    {
        if (IsServer)
        {
            Debug.Log("Client " + clientId + " connected!");
            PlayerController connectedClient = NetworkManager.Singleton.ConnectedClients[clientId].PlayerObject.gameObject.GetComponent<PlayerController>();
            connectedClient.recievedString.Value = GameObject.Find("Start").GetComponent<Seed>().seed;
        }
    }

    private void Update() {
        if (isOverlapping)
        {
            Destroy(gameObject);
        }

        if (transform.Find("Light") != null)
        {
            if (Camera.main != null)
            {
                if (Vector3.Distance(transform.position, Camera.main.transform.position) >= 300)
                {
                    transform.Find("Light").gameObject.SetActive(false);
                }
                else if (Vector3.Distance(transform.position, Camera.main.transform.position) < 300)
                {
                    transform.Find("Light").gameObject.SetActive(true);
                }
            }
            else//this is gonna get mixed up between the players
            {
                transform.Find("Light").gameObject.SetActive(false);
            }

            if (!transform.Find("Light").Find("Bulb").GetComponent<Light>().enabled)
            {
                transform.Find("Light").Find("Bulb").gameObject.SetActive(false);
            }
            else if (transform.Find("Light").Find("Bulb").GetComponent<Light>().enabled)
            {
                transform.Find("Light").Find("Bulb").gameObject.SetActive(true);
            }
        }

        if (startedGenerating && GameObject.Find("Start").GetComponent<Seed>().worldSize < 1)
        {
            finishedGenerating = true;
        }

        if (finishedGenerating)
        {
            for (int i = 0; i < exits.Length; i++)
            {
                if (exits[i].childCount == 0)
                {
                    Instantiate(wallPrefab, exits[i]);
                    finishedGenerating = false;
                }
            }
        }
    }

    public void GenerateSegment() {
        startedGenerating = true;
        if (GameObject.Find("Start").GetComponent<Seed>().worldSize > -1)
        {
            for (int j = 0; j < exits.Length; j++)
            {
                regen:
                    if (exits[j].childCount == 0)
                    {
                        int randomNum = UnityEngine.Random.Range(0, segmentPrefabList.Length);
                        int randProb1 = UnityEngine.Random.Range(0, segmentPrefabList[randomNum].GetComponent<Segment>().rarity);
                        int randProb2 = UnityEngine.Random.Range(0, segmentPrefabList[randomNum].GetComponent<Segment>().rarity);
                        if (randProb1 == randProb2)
                        {
                            GameObject.Find("Start").GetComponent<Seed>().seed += randomNum.ToString();
                            GameObject.Find("Start").GetComponent<Seed>().worldSize -= 1;
                            GameObject newSegment = Instantiate(segmentPrefabList[randomNum], exits[j]);
                        }
                        else
                        {
                            goto regen;
                        }
                    }
            }
        }
    }

    public void LateGenerateSegment(string seed)
    {
        for (int j = 0; j < exits.Length; j++)
        {
            if (exits[j].childCount == 0)
            {
                GameObject newSegment = Instantiate(segmentPrefabList[int.Parse(seed[GameObject.Find("Start").GetComponent<Seed>().counter].ToString())], exits[j]);
                GameObject.Find("Start").GetComponent<Seed>().counter += 1;
            }
        }
    }
}

Player script:

using System.Collections;
using System.Collections.Generic;
using Unity.Mathematics;
using UnityEngine;
using Unity.Netcode;
using UnityEngine.InputSystem;
using Unity.Collections;

public class PlayerController : NetworkBehaviour
{
    public NetworkVariable<FixedString128Bytes> recievedString = new NetworkVariable<FixedString128Bytes>();
    [Header("Player Variables")]
    [Tooltip("How sensitive is the mouse look?")]
    public float mouseSensitivity = 2.0f;
    [Tooltip("How fast can the player move?")]
    public float moveSpeed = 5.0f;
    [Tooltip("How high can the player jump?")]
    public float jumpForce = 8.0f;
    [Tooltip("The player's flashlight")]
    public GameObject flashlight;
    [Tooltip("The map camera")]
    public GameObject mapCamera;
    [Tooltip("The main camera")]
    public GameObject mainCamera;

    [Header("Interaction variables")]
    [Tooltip("What layers can the player interact with?")]
    public LayerMask interactionMask;
    [Tooltip("How far can the player be away from the thing they're trying to interact with?")]
    public float maxDistance = 10.0f;

    private float verticalRotation = 0f;
    private Rigidbody rb;
    private bool isLookingAtMap;
    private GameObject[] playerCameras;

    RaycastHit hit;

    void Start()
    {
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;
        rb = GetComponent<Rigidbody>();
        rb.freezeRotation = true;
        if (GameObject.Find("Start").GetComponent<Segment>().exits[0].childCount == 0)
        {
            GameObject.Find("Start").GetComponent<Segment>().keepGenerating = true;
            GameObject.Find("Start").GetComponent<Segment>().LateGenerateSegment(recievedString.Value.ToString());
        }
    }

    void Update()
    {
        if (!IsOwner) return;

        if (!isLookingAtMap)
        {
            HandleMouseLook();
            HandleJump();
        }
        HandleInteractions();

        playerCameras = GameObject.FindGameObjectsWithTag("MainCamera");
        for (int i = 0; i < playerCameras.Length; i++)
        {
            if (playerCameras[i] != mainCamera)
            {
                playerCameras[i].SetActive(false);
            }
        }
    }

    void FixedUpdate()
    {
        if (!IsOwner) return;

        if (!isLookingAtMap)
        {
            HandlePlayerMovement();
        }
    }

    void HandleMouseLook()
    {
        float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity;
        float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity;

        verticalRotation -= mouseY;
        verticalRotation = Mathf.Clamp(verticalRotation, -90f, 90f);

        transform.Rotate(Vector3.up * mouseX);
        Camera.main.transform.localRotation = Quaternion.Euler(verticalRotation, 0f, 0f);
    }

    void HandlePlayerMovement()
    {
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");

        Vector3 moveDirection = new Vector3(horizontal, 0f, vertical).normalized;
        Vector3 moveVelocity = transform.TransformDirection(moveDirection) * moveSpeed;

        rb.velocity = new Vector3(moveVelocity.x, rb.velocity.y, moveVelocity.z);
    }

    void HandleJump()
    {
        if (Input.GetButtonDown("Jump") && IsGrounded())
        {
            rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
        }
    }

    bool IsGrounded()
    {
        return Physics.Raycast(transform.position, Vector3.down, 6f);
    }

    void HandleInteractions()
    {
        /*if (Input.GetKeyDown(KeyCode.F))
        {
            if (!flashlight.activeSelf)
            {
                flashlight.SetActive(true);
            }
            else
            {
                flashlight.SetActive(false);
            }
        }*/

        if (Input.GetKeyDown(KeyCode.Tab))
        {
            if (!isLookingAtMap)
            {
                isLookingAtMap = true;
                mapCamera.SetActive(true);
                transform.Find("Canvas").gameObject.SetActive(false);
                Cursor.lockState = CursorLockMode.None;
            }
            else
            {
                isLookingAtMap = false;
                mapCamera.SetActive(false);
                transform.Find("Canvas").gameObject.SetActive(true);
                Cursor.lockState = CursorLockMode.Locked;
            }
        }

        if (Physics.Raycast(Camera.main.transform.position, Camera.main.transform.forward, out hit, maxDistance, interactionMask))
        {
            GameObject currentObject = hit.collider.gameObject;
            if (Input.GetMouseButtonDown(0))
            {
                if (currentObject.name == "Door" && currentObject.transform.parent.localRotation != Quaternion.Euler(0, 90, 0))
                {
                    currentObject.transform.parent.localRotation = Quaternion.Euler(0, 90, 0);
                }
                else
                {
                    currentObject.transform.parent.localRotation = Quaternion.Euler(0, 0, 0);
                }
            }
        }
    }
}

Edit: I changed LateGenerateSegment(GameObject.Find("Start").GetComponent<Seed>().seed); to ```
LateGenerateSegment(GameObject.FindGameObjectWithTag(“Player”).GetComponent().recievedString.Value.ToString());

which fixed it. I mean, everything's flipped in the client's world, but I'll probably fix it xD

Generally if you’re trying to send large amounts of data or sync large amounts of objects over the network then the approach is wrong, with netcode you always want to minimise the data being sent. As you’ve probably discovered, the best way to do this would be to generate your map locally on each client and ensure that everyone generates the same exact map.

  • First your server would pick a random seed and then send that to all clients via a ClientRpc.
  • Next, both the server and the clients use the same algorithm and seed to generate identical maps.
  • When each client finishes generating its map, it sends the server a message telling it that it’s done. You can use [ServerRpc(RequireOwnership = false)] to create a method any client can execute on the server and then just keep a running total of how many clients report in until it reaches the ConnectedClients total. The server waits until all connected clients are done generating before continuing.
  • Finally, the server would generate and place all of the live objects like enemies and interactable items, then it would call Spawn() on them to make them spawn on all clients’ games. It doesn’t have to do this all at once, you could do it room by room as the players move around or by proximity or by some other strategy.
  • Note that all the things you spawn must be Prefabs with a NetworkObject on their root objects, and they must all be added to the NetworkManager’s Network Prefabs list.

I see you used NetworkVariable to transmit your seed, I would suggest using a ClientRpc instead because you can’t guarantee that the networkvariable is synced before your client goes to generate the level (due to possible lag). Using a ClientRpc you can send the seed and guarantee that the code is executed only when it arrives, and you should be able to just send a regular String.

@Nyphur Thanks! That sounds pretty much like what I’m trying to do. I did try ClientRpc at first… but couldn’t get it to work. Do you have any suggestions as to how I could use it to transmit a string?

I got it to work! Thank you so much to everyone who helped! It was just GenerateSegment(); conflicting with LateGenerateSegment(GameObject.FindGameObjectWithTag("Player").GetComponent<PlayerController>().recievedString.Value.ToString()); in the start method, and with a try-catch, I’m able to stop there from being walls missing. Here’s the whole script for anyone interested:

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using JetBrains.Annotations;
using Unity.Netcode;
using Unity.Netcode.Components;
using UnityEngine;

public class Segment : NetworkBehaviour
{
    [Header("Segment variables")]
    public bool isStart;
    public int rarity;
    public GameObject wallPrefab;
    public GameObject[] segmentPrefabList;
    public Transform[] exits;

    [HideInInspector] public bool isOverlapping;
    [HideInInspector] public bool keepGenerating;
    bool startedGenerating;
    bool finishedGenerating;
    //public int segmentSeedNum;

    private void Awake() {
        NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;
    }

    private void Start() {
        if (!isStart)
        {
            if (!GameObject.Find("Start").GetComponent<Segment>().keepGenerating)
            {
                GenerateSegment();
            }
            if (GameObject.Find("Start").GetComponent<Segment>().keepGenerating)
            {
                LateGenerateSegment(GameObject.FindGameObjectWithTag("Player").GetComponent<PlayerController>().recievedString.Value.ToString());
            }
        }
    }

    void OnClientConnected(ulong clientId)
    {
        if (IsServer)
        {
            Debug.Log("Client " + clientId + " connected!");
            PlayerController connectedClient = NetworkManager.Singleton.ConnectedClients[clientId].PlayerObject.gameObject.GetComponent<PlayerController>();
            connectedClient.recievedString.Value = GameObject.Find("Start").GetComponent<Seed>().seed;
        }
    }

    private void Update() {
        if (isOverlapping)
        {
            Destroy(gameObject);
        }

        if (transform.Find("Light") != null)
        {
            if (Camera.main != null)
            {
                if (Vector3.Distance(transform.position, Camera.main.transform.position) >= 300)
                {
                    transform.Find("Light").gameObject.SetActive(false);
                }
                else if (Vector3.Distance(transform.position, Camera.main.transform.position) < 300)
                {
                    transform.Find("Light").gameObject.SetActive(true);
                }
            }
            else//this is gonna get mixed up between the players
            {
                transform.Find("Light").gameObject.SetActive(false);
            }

            if (!transform.Find("Light").Find("Bulb").GetComponent<Light>().enabled)
            {
                transform.Find("Light").Find("Bulb").gameObject.SetActive(false);
            }
            else if (transform.Find("Light").Find("Bulb").GetComponent<Light>().enabled)
            {
                transform.Find("Light").Find("Bulb").gameObject.SetActive(true);
            }
        }

        if (startedGenerating && GameObject.Find("Start").GetComponent<Seed>().worldSize < 1)
        {
            finishedGenerating = true;
        }

        if (finishedGenerating)
        {
            for (int i = 0; i < exits.Length; i++)
            {
                if (exits[i].childCount == 0)
                {
                    Instantiate(wallPrefab, exits[i]);
                    finishedGenerating = false;
                }
            }
        }
    }

    public void GenerateSegment() {
        startedGenerating = true;
        if (GameObject.Find("Start").GetComponent<Seed>().worldSize > -1)
        {
            for (int j = 0; j < exits.Length; j++)
            {
                regen:
                    if (exits[j].childCount == 0)
                    {
                        int randomNum = UnityEngine.Random.Range(0, segmentPrefabList.Length);
                        int randProb1 = UnityEngine.Random.Range(0, segmentPrefabList[randomNum].GetComponent<Segment>().rarity);
                        int randProb2 = UnityEngine.Random.Range(0, segmentPrefabList[randomNum].GetComponent<Segment>().rarity);
                        if (randProb1 == randProb2)
                        {
                            GameObject.Find("Start").GetComponent<Seed>().seed += randomNum.ToString();
                            GameObject.Find("Start").GetComponent<Seed>().worldSize -= 1;
                            GameObject newSegment = Instantiate(segmentPrefabList[randomNum], exits[j]);
                            //newSegment.GetComponent<Segment>().segmentSeedNum = randomNum;
                        }
                        else
                        {
                            goto regen;
                        }
                    }
            }
        }
    }

    public void LateGenerateSegment(string seed)
    {
        for (int j = 0; j < exits.Length; j++)
        {
            if (exits[j].childCount == 0)
            {
                try
                {
                    GameObject newSegment = Instantiate(segmentPrefabList[int.Parse(seed[GameObject.Find("Start").GetComponent<Seed>().counter].ToString())], exits[j]);
                    //newSegment.GetComponent<Segment>().segmentSeedNum = int.Parse(seed[GameObject.Find("Start").GetComponent<Seed>().counter].ToString());
                    GameObject.Find("Start").GetComponent<Seed>().counter += 1;
                }
                catch (IndexOutOfRangeException e)
                {
                    Instantiate(wallPrefab, exits[j]);
                    Debug.Log("Guess what? " + e + " Yep, that's right.");
                }
            }
        }
    }
}