Search code examples
c#unity-game-enginemultiplayerprocedural-generationnetcode

A different generation of trees for every player. multiplayer (netcode for gameObjects)


When 2 or more players connect to each other, the trees are generated slightly differently for each player.

I tried to add Network transform to the prefab tree, but it didn't help and it's still different

Host's pov:

host's pov

Client's pov:

client's pov

This is script that generate terrain(mesh) and trees (DataStorage is a class that stores data between scenes):

using UnityEngine;
using System.Linq;
using System.Threading.Tasks;
using System.Net.NetworkInformation;
using Unity.Netcode;

public class MeshGenerator : MonoBehaviour
{
    Mesh mesh;

    Vector3[] vertices;
    int[] triangles;

    [Header("Map settings")]
    public int Size = 20;

    [Space]

    public float heightMultiplier = 20f;

    public AnimationCurve curve;  

    [Header("Noise map")]
    public float scale = 0.3f;

    public int octaves;
    public float presistance;
    public float lacunarity;

    [Space]

    public int seed;
    public Vector2 offset;

    float[,] heightMap;

    [Header("Trees")]
    public GameObject treePrefab;

    [Space]

    public int PosibleTrees;
    public int trees;

    float[,] treeHeightMap;



    [Header("Texture")]
    public Material mat;

    [Space]

    public Layer[] layers;

    const int textureSize = 512;
    const TextureFormat textureFormat = TextureFormat.RGB565;

    Vector2[] uvs;

    [Header("Player")]

    public GameObject playerPrefab;

    bool multiplayer;
    bool client;

    System.Random seedGen;

    async void Awake()
    {
        mesh = new Mesh();
        GetComponent<MeshFilter>().mesh = mesh;
        mesh.name = "Mesh";

        heightMap = GenerateNoiseMap(Size, scale, seed, octaves, presistance, lacunarity, offset);
        treeHeightMap = GenerateNoiseMap(Size, scale, seed + 3, octaves, presistance, lacunarity, offset);
        seed = DataStorage.seed;
        multiplayer = DataStorage.multiplayer;
        client = DataStorage.client;

        seedGen = new System.Random(seed);

        await Task.Delay(1000);

        CreateShape();
        UpdateMesh();
     
        LevelManager.instance._loaderConvas.SetActive(false);

    }

    void CreateShape()
    {

        vertices = new Vector3[(Size + 1) * (Size + 1)];


        for (int i = 0, z = 0; z <= Size; z++)
        {
            for (int x = 0; x <= Size; x++)
            {

                float currentHeight = (float)curve.Evaluate((float)heightMap[x, z]) * (float)heightMultiplier;

                GenerateTree(currentHeight, x, z, seedGen);

                vertices[i] = new Vector3(x, currentHeight, z);
                i++;
            }
        }

        triangles = new int[Size * Size * 6];

        int vert = 0;
        int tris = 0;

        for (int z = 0; z < Size; z++)
        {
            for (int x = 0; x < Size; x++)
            {
                triangles[tris + 0] = vert + 0;
                triangles[tris + 1] = vert + Size + 1;
                triangles[tris + 2] = vert + 1;
                triangles[tris + 3] = vert + 1;
                triangles[tris + 4] = vert + Size + 1;
                triangles[tris + 5] = vert + Size + 2;

                vert++;
                tris += 6;
            }
            vert++;
        }

        GenerateUVs();

        SetShaderVariable();
    }

    void GenerateUVs()
    {
        uvs = new Vector2[vertices.Length];
        for (int i = 0, z = 0; z <= Size; z++)
        {
            for (int x = 0; x <= Size; x++)
            {
                uvs[i] = new Vector2((float)x / Size, (float)z / Size);
                i++;
            }
        }
    }

    void SetShaderVariable()
    {
        mat.SetFloat("minHeight", heightMultiplier * curve.Evaluate(0));
        mat.SetFloat("maxHeight", heightMultiplier * curve.Evaluate(1));

        mat.SetInt("layerCount", layers.Length);
        mat.SetColorArray("baseColours", layers.Select(x => x.tint).ToArray());
        mat.SetFloatArray("baseStartHeights", layers.Select(x => x.startHeigth).ToArray());
        mat.SetFloatArray("baseBlends", layers.Select(x => x.blendStrength).ToArray());
        mat.SetFloatArray("baseColourStrength", layers.Select(x => x.tintStrength).ToArray());
        mat.SetFloatArray("baseTextureScales", layers.Select(x => x.textureScale).ToArray());

        Texture2DArray textureArray = GenerateTextureArray(layers.Select(x => x.texture).ToArray());

        mat.SetTexture("baseTextures", textureArray);
    }

    void SpawnPlayer(float currentHeight, Vector3 pos)
    {
        GameObject player = Instantiate(playerPrefab);

        player.transform.position = pos;
    }

    Texture2DArray GenerateTextureArray(Texture2D[] textures)
    {
        Texture2DArray textureArray = new Texture2DArray(textureSize, textureSize, textures.Length, textureFormat, true);

        for(int i = 0; i < textures.Length; i++)
        {
            textureArray.SetPixels(textures[i].GetPixels(), i);
        }

        textureArray.Apply();

        return textureArray;
    }
    void UpdateMesh()
    {
        mesh.Clear();

        mesh.vertices = vertices;
        mesh.triangles = triangles;
        mesh.uv = uvs;

        mesh.RecalculateNormals();

        GetComponent<MeshCollider>().sharedMesh = mesh;

        float currentHeight;
        Vector3 pos;

        while (true)
        {
            int x = seedGen.Next(0, Size);
            int z = seedGen.Next(0, Size);

            currentHeight = (float)curve.Evaluate((float)heightMap[x, z]) * (float)heightMultiplier;

            if (currentHeight > 1.8)
            {
                pos = new Vector3(x, (float)currentHeight + 2f, z);
                break;
            }
        }



     

        if (multiplayer && !client)
        {
            DataStorage.x = (float)pos.x;
            DataStorage.y = (float)pos.y;
            DataStorage.z = (float)pos.z;

            if (NetworkManager.Singleton.StartHost())
            {
                Debug.Log("host");
            }
            else
            {
                Debug.Log("host failed");
            }
        }
        else if (DataStorage.solo == true)
        {
            SpawnPlayer(currentHeight, pos);
            Debug.Log("player");
        }
    }

    float[,] GenerateNoiseMap(int size, float scale, int seed, int octaves, float presistance, float lacunarity, Vector2 offset)
    {
        float[,] noiseMap = new float[size + 1, size + 1];

        System.Random prng = new System.Random(seed);
        Vector2[] octaveOffsets = new Vector2[octaves + 1];

        for (int i = 0; i <= octaves; i++)
        {
            float offsetX = prng.Next(-100000, 100000) + offset.x;
            float offsetZ = prng.Next(-100000, 100000) + offset.y;

            octaveOffsets[i] = new Vector2(offsetX, offsetZ);
        }

        if (scale <= 0)
        {
            scale = 0.0001f;
        }

        float minNoiseHeight = float.MaxValue;
        float maxNoiseHeight = float.MinValue;

        float halfSize = size / 2;

        for (int z = 0; z <= size; z++)
        {
            for (int x = 0; x <= size; x++)
            {

                float amplitude = 1;
                float frequency = 1;
                float noiseHeight = 0;

                for (int i = 0; i <= octaves; i++)
                {
                    float sampleX = (x - halfSize) / scale * frequency + octaveOffsets[i].x;
                    float sampleY = (z - halfSize) / scale * frequency + octaveOffsets[i].y;

                    float perlinValue = Mathf.PerlinNoise(sampleX, sampleY) * 2 - 1;
                    noiseHeight += perlinValue * amplitude;

                    amplitude *= presistance;
                    frequency *= lacunarity;
                }

                if (noiseHeight < minNoiseHeight)
                {
                    minNoiseHeight = noiseHeight;
                }
                else if (noiseHeight > maxNoiseHeight)
                {
                    maxNoiseHeight = noiseHeight;
                }

                noiseMap[x, z] = noiseHeight;
            }
        }

        for (int z = 0; z <= size; z++)
        {
            for (int x = 0; x <= size; x++)
            {
                noiseMap[x, z] = Mathf.InverseLerp(minNoiseHeight, maxNoiseHeight, noiseMap[x, z]);

                float fallOffX = z / (float)size * 2 - 1;
                float fallOffZ = x / (float)size * 2 - 1;

                float value = Mathf.Max(Mathf.Abs(fallOffX), Mathf.Abs(fallOffZ));

                noiseMap[x, z] = Mathf.Clamp01(noiseMap[x, z] - Evaluate(value));
            }
        }

        return noiseMap;
    }

    void GenerateTree(float currentHeight, int x, int z, System.Random prng)
    {

        PosibleTrees++;

        int d = prng.Next(0, 10);
        int c = prng.Next(0, 10);

        double X = prng.NextDouble();
        double Z = prng.NextDouble();
        double Y = prng.NextDouble();

        if(Y < .5f)
        {
            Y = .5f;
        }

        if (treeHeightMap[x, z] >= Y && currentHeight > 1.5 && currentHeight < 10 && d >= 8)
        {
            GameObject tree = Instantiate(treePrefab);

            tree.transform.position = new Vector3((float)(x + X), currentHeight, (float)(z + Z));

            trees++;
        }
    }

    float Evaluate(float value)
    {
        float a = 3;
        float b = 2.2f;

        return Mathf.Pow(value, a) / (Mathf.Pow(value, a) + Mathf.Pow(b - b * value, a));
    }


    [System.Serializable]
    public class Layer
    {
        public Texture2D texture;
        public Color tint;

        [Range(0,1)]
        public float tintStrength;

        [Range(0, 1)]
        public float startHeigth;

        [Range(0, 1)]
        public float blendStrength;

        public float textureScale;
    }

    /*private void OnDrawGizmos()
    {
        Gizmos.color = Color.black;
        for (int i = 0; i < vertices.Length; i++)
        {
            Gizmos.DrawSphere(vertices[i], 0.1f);
        }
    }*/
}

This is a script that is attached to a tree to prevent trees from flying in the air (There may be a collision problem, so they will be destroyed, but but if that's the case, I don't know how to fix it.):

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting.Antlr3.Runtime.Tree;
using UnityEngine;

public class TreeMnager : MonoBehaviour
{

    public GameObject groundCheck;
    public GameObject iTree;


    // Start is called before the first frame update
    void Start()
    {
        if(!Physics.CheckSphere(groundCheck.transform.position, .1f, LayerMask.GetMask("Mesh")))
        {
            Destroy(iTree);
        }

        Collider[] hitColliders = Physics.OverlapSphere(iTree.transform.position, 1);
        foreach (var hitCollider in hitColliders)
        {
            if (hitCollider.gameObject != iTree &&
                hitCollider.gameObject.CompareTag("Tree"))
            {
                Destroy(iTree);
            }
        }
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

This script controls what type of game will be played(Multyplayer/singleplayer):

using System.Threading.Tasks;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class LevelManager : MonoBehaviour
{
    public static LevelManager instance;

    public GameObject _loaderConvas;
    [SerializeField] private Slider _loaderSlider;
    [SerializeField] private InputField input;

    // Start is called before the first frame update
    void Awake()
    {
        if(instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }

    public async void LoadScene(string sceneName)
    {
        int seed = 0;
        int.TryParse(input.text, out seed);

        if(seed == 0)
        {
            seed = Random.Range(-2147483647, 2147483647);
        }

        DataStorage.seed = seed;
        DataStorage.multiplayer = false;
        DataStorage.client = false;
        DataStorage.solo = true;

        var scene = SceneManager.LoadSceneAsync(sceneName);
        scene.allowSceneActivation = false;

       _loaderConvas.SetActive(true);

        do
        {
            await Task.Delay(500);
            _loaderSlider.value = scene.progress;
        } while (scene.progress < 0.9f);

        scene.allowSceneActivation = true;
    }

    public async void LoadSceneHost(string sceneName)
    {
        int seed = 0;
        int.TryParse(input.text, out seed);

        if (seed == 0)
        {
            seed = Random.Range(-2147483647, 2147483647);
        }

        DataStorage.seed = seed;
        DataStorage.multiplayer = true;
        DataStorage.client = false;
        DataStorage.solo = false;

        var scene = SceneManager.LoadSceneAsync(sceneName);
        scene.allowSceneActivation = false;

        _loaderConvas.SetActive(true);

        do
        {
            await Task.Delay(500);
            _loaderSlider.value = scene.progress;
        } while (scene.progress < 0.9f);

        scene.allowSceneActivation = true;
    }

    public void JoinWorld()
    {

        DataStorage.multiplayer = true;
        DataStorage.client = true;
        DataStorage.solo = false;

        if (NetworkManager.Singleton.StartClient())
        {
            Debug.Log("succes");
        }
        else
        {
            Debug.Log("World not found");
        }
    }
}

Solution

  • You do

    heightMap = GenerateNoiseMap(Size, scale, seed, octaves, presistance, lacunarity, offset);
    treeHeightMap = GenerateNoiseMap(Size, scale, seed + 3, octaves, presistance, lacunarity, offset);
    

    which both already use seed right before you even assign it to

    seed = DataStorage.seed;
    

    So later the CreateShape and finally GenerateTree etc definitely use a different

    seedGen = new System.Random(seed);
    

    than the height map generations before.

    => You probably want to move this on top so everything actually uses the very same seed.


    I know: This still does not explain why they are different between host and client. But fixing this first issue might spread some more light on that