Search Unity

procedural level

Discussion in 'Multiplayer' started by awund1, Oct 18, 2017.

  1. awund1

    awund1

    Joined:
    Sep 6, 2017
    Posts:
    7
    i am having a hard time to understand how i can bring my procedural generated level into the network.
    players and enemies do work, based on this tutorial (https://unity3d.com/de/learn/tutori...rking/introduction-simple-multiplayer-example)

    the level is mainly some cubes. i tried to add NetworkIdentity to the GameObject. the script is attached to an empty gameobject.

    Code (csharp):
    1.  
    2. public override void OnStartServer ()
    3. {
    4.             buildLevel();
    5. }
    6. void buildLevel()
    7. {
    8.         level = new GameObject ("Level");
    9.         level.AddComponent<NetworkIdentity> ();
    10.         NetworkServer.Spawn (theLab);
    11.  
    12.         GameObject ground = GameObject.CreatePrimitive (PrimitiveType.Cube);
    13.         ground.transform.localScale = new Vector3 (10.0f, 1.0f, 10.0f);
    14.         ground.transform.localPosition = new Vector3 (0.0f, -0.5f, 0.0f);
    15.         ground.transform.parent = level.transform;
    16.         ground.AddComponent<NetworkIdentity> ();
    17.         NetworkServer.Spawn (ground);
    18.         GameObject cube = GameObject.CreatePrimitive (PrimitiveType.Cube);
    19.         cube.transform.localPosition = new Vector3 (2.0f, 0.5f, 2.0f);
    20.         cube.transform.parent = level.transform;
    21.         cube.AddComponent<NetworkIdentity> ();
    22.         NetworkServer.Spawn (cube);
    23. }
    24.  
    i get the following errors in the editor (client):
    OnObjSpawn netId: 2 has invalid asset Id
    UnityEngine.Networking.NetworkIdentity:UNetStaticUpdate()

    i tried ClientScene.RegisterPrefab on the objects, too. still the assetID remain a bunch of zeroes....

    i believe there is something common which i am doing wrong because of not knowing the how to. it must be possible to create the level on the host and than pass it to all clients, right?

    thanks for any input on this.
     
    Last edited: Oct 18, 2017
  2. awund1

    awund1

    Joined:
    Sep 6, 2017
    Posts:
    7
    ok, i managed the problem using an approach i didn't want to use, because its not so procedural as before, but i created the objects as prefabs and put them to the NetworkManager component.

    however, i have a gate prefab which is supposed to open and close on an irregular base
    Code (csharp):
    1.  
    2.         counter++;
    3.         if (counter < 0) {
    4.             moving = !moving;
    5.             counter = Random.Range (minGateTime, maxGateTime);
    6.         }
    7.  
    this does not happen synchronously on the different clients. i guess i need to apply those only on the host and somehow spread the vertical position. how do i do this?

    the prefab has NetworkIdentity and NetworkTransform attached

    btw, the movement is transfered, so, when a gate closes, it closes on all clients, and same for opening. just that the counter has different values on the different clients

    here is the complete gate script, in case something's there to be seen regarding the problem:
    Code (csharp):
    1.  
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. using UnityEngine.Networking;
    6.  
    7. public class Gate : NetworkBehaviour
    8. {
    9.  
    10.     public GameObject Wall;
    11.     public GameObject LightSource;
    12.     public GameObject PointLight;
    13.     public int offset;
    14.     private bool moving = false;
    15.     public bool closed = true;
    16.     public int counter;
    17.  
    18.     void Start ()
    19.     {
    20.         changeCounter ();
    21.  
    22.         Wall.GetComponent<MeshRenderer> ().material.color = Color.green;
    23.  
    24.         offset = Random.Range (0, 360);
    25.         moving = false;
    26.         changeCounter ();
    27.     }
    28.  
    29.     void Update ()
    30.     {
    31.         if (Random.Range (0, 10) > 6 && !moving) {
    32.             counter--;
    33.         }
    34.         if (counter < 0) {
    35.             moving = !moving;
    36.             changeCounter ();
    37.         }
    38.  
    39.         Vector3 pos = transform.localPosition;
    40.         float y = pos.y;
    41.  
    42.         float blink = transform.position.y * (0.4f * Mathf.Sin (Time.time * 3.3f + offset) + 0.6f);
    43.         PointLight.GetComponent<Light> ().intensity = blink;
    44.         MeshRenderer mr = LightSource.GetComponent<MeshRenderer> ();
    45.         mr.material.SetColor ("_Emission", new Color (1, 1, 1) * blink);
    46.         mr.material.color = new Color (1, 1, 1) * (blink + 0.5f);
    47.  
    48.         if (moving) {
    49.             if (closed) {
    50.                 y += Time.deltaTime * 10.0f;
    51.             } else {
    52.                 y -= Time.deltaTime * 10.0f;
    53.             }
    54.         }
    55.  
    56.         if (y >= 5) {
    57.             moving = false;
    58.             closed = false;
    59.         }
    60.  
    61.         if (y <= 0) {
    62.             moving = false;
    63.             closed = true;
    64.             y = 0;
    65.         }
    66.         transform.localPosition = new Vector3 (pos.x, y, pos.z);
    67.     }
    68.  
    69.     void changeCounter ()
    70.     {
    71.         counter = Random.Range (33, 333);
    72.     }
    73. }
    74.  
    the gate itself is instantiated like this:
    Code (csharp):
    1.  
    2.  
    3.                     GameObject gate = (GameObject)Instantiate (Gate, pos, Quaternion.AngleAxis (0, Vector3.up));
    4.                     gate.name = "Gate " + x + "/" + y;
    5.                     gate.transform.parent = theLab.transform;
    6.                     gate.GetComponent<Gate> ().offset = Random.Range (0, 360);
    7.                     NetworkServer.Spawn (gate);
    8.  
     
    Last edited: Oct 18, 2017
  3. awund1

    awund1

    Joined:
    Sep 6, 2017
    Posts:
    7
    thanks to that other thread i got it working:
    Code (csharp):
    1.  
    2.     [SyncVar (hook = "RandomNumberSyncCallback")]
    3.     public int counter;
    4.  
    5.     void RandomNumberSyncCallback (int newNumber)
    6.     {
    7.         if (isServer)
    8.             return;
    9.  
    10.         counter = newNumber;
    11.     }
    12.  
    @admin: cannot edit the previous post
     
  4. enhawk

    enhawk

    Joined:
    Aug 22, 2013
    Posts:
    833
    the basic thinking is:

    1. do random.range, level generation on the server, then propagate this to the clients.
    2. don't do these kinds of things on the clients (if it affects gameplay)
     
  5. awund1

    awund1

    Joined:
    Sep 6, 2017
    Posts:
    7
    thank you hawken, thats what i'd really like to do, create the level only on the server and somehow send it to the clients. but that "somehow" is what i don't relly understand. when i tried to scale the prefabs on the server, they where spawning unscaled on the clients. generally the scripted cubes and stuff wheren't spread to the clients due to missing asset numbers. that's why i created assets for all the objects. which makes it less procedural of course, but it does its job right now.

    however, i'd like to understand how i could do it for the future.
     
  6. tobiass

    tobiass

    Joined:
    Apr 7, 2009
    Posts:
    3,070
    Depending on how big (complex) your level becomes, you get into trouble sending it fully via network.
    If you can: Make your level generation deterministic. Then you only have to share the seed for the level and any client can generate it.
     
    TwoTen likes this.
  7. awund1

    awund1

    Joined:
    Sep 6, 2017
    Posts:
    7
    ok, so i would need to just use 1 global seed to initialize random? and everything else will for sure have the same random numbers over time?
     
  8. tobiass

    tobiass

    Joined:
    Apr 7, 2009
    Posts:
    3,070
    Well, you need to make sure that each level generation will get the same values in the same order, independent from timing, race conditions and floating point imprecision. You might have to read up on "determinism" but it's not an impossible task!
     
    PhilippG likes this.
  9. PhilippG

    PhilippG

    Joined:
    Jan 7, 2014
    Posts:
    257
    This works, I use the same approach. The only issue I ran into was I got differing results on the server vs. on the clients. I had a hard time tracking it down, but I found that when I spawned network entities (it was the damn doors) during the generation cycle, Unitys Random gave me differing values on the server afterwards. So I suspect that at some point Random is used within their network code.

    I solved this and still have the same convenience by writing a new Random_Safe that is just wrapping Unitys Random and always keeps track of the previous Random.state. It is not really nice to look at, but it works. Use if you like:
    Code (CSharp):
    1.  
    2. using UnityEngine;
    3. public class Random_Safe
    4. {
    5.     public static Random.State state { get; set; }
    6.     public static void InitState(int seed) { Random.InitState(seed); state = Random.state; }
    7.     public static Vector2 insideUnitCircle{ get { Random.state = state; Vector2 result = Random.insideUnitCircle; state = Random.state; return result;} }
    8.     public static Vector3 insideUnitSphere { get { Random.state = state; Vector3 result = Random.insideUnitSphere; state = Random.state; return result; } }
    9.     public static Vector3 onUnitSphere { get { Random.state = state; Vector3 result = Random.onUnitSphere; state = Random.state; return result; } }
    10.     public static Quaternion rotation { get { Random.state = state; Quaternion result = Random.rotation; state = Random.state; return result; } }
    11.     public static Quaternion rotationUniform { get { Random.state = state; Quaternion result = Random.rotationUniform; state = Random.state; return result; } }
    12.     public static float value { get { Random.state = state; float result = Random.value; state = Random.state; return result; } }
    13.     public static Color ColorHSV() { Random.state = state; Color result = Random.ColorHSV(); state = Random.state; return result; }
    14.     public static Color ColorHSV(float hueMin, float hueMax) { Random.state = state; Color result = Random.ColorHSV(hueMin, hueMax); state = Random.state; return result; }
    15.     public static Color ColorHSV(float hueMin, float hueMax, float saturationMin, float saturationMax) { Random.state = state; Color result = Random.ColorHSV(hueMin, hueMax, saturationMin, saturationMax); state = Random.state; return result; }
    16.     public static Color ColorHSV(float hueMin, float hueMax, float saturationMin, float saturationMax, float valueMin, float valueMax) { Random.state = state; Color result = Random.ColorHSV(hueMin, hueMax, saturationMin, saturationMax, valueMin, valueMax); state = Random.state; return result; }
    17.     public static Color ColorHSV(float hueMin, float hueMax, float saturationMin, float saturationMax, float valueMin, float valueMax, float alphaMin, float alphaMax) { Random.state = state; Color result = Random.ColorHSV(hueMin, hueMax, saturationMin, saturationMax, valueMin, valueMax, alphaMin, alphaMax); state = Random.state; return result; }
    18.     public static int Range(int min, int max) { Random.state = state; int result = Random.Range(min, max); state = Random.state; return result; }
    19.     public static float Range(float min, float max) { Random.state = state; float result = Random.Range(min, max); state = Random.state; return result; }
    20. }
    You'll just need to replace all your Random calls with Random_Safe, then it'll work.
    You can now also use the standard Random everywhere without being worried about screwing up the generation cycle.
     
    Last edited: Oct 25, 2017
    tobiass and Munchy2007 like this.