Dynamic Terrain Loading

I have a lot of Questions about Infinite Terrains and Terrain.SetNeighbors().

Here is a list of the questions:

1 If you use Terrain.SetNeighbors() do all the Neighboring Terrains have to be creared at the same resolution?

2 If I'm storing the extra Terrains as Assets Or separate scene files to Additve load, will the Terrain.SetNeighbors() function throw out a bunch of errors because the extra terrains are not in the Current scene?

3 Should the terrains be made into Prefabs?

4 Is there any major Benifit or drawback to making them prefabs?

5 Is there currently any way to Paint multiple terrains in Unity at the same time?

6 Is there already a system like UniLOD for terrains?

7 Is there any Hands on tutorials for infinite worlds?


What I imagine An Infinite Terrain system to be :

Like the old arcade games in the 80's if the player travled to far to the left of world/screen his location would be moved to the far right of the screen. And the same with the top or bottom.

Then the terrain would be a grid such as a chess board where the ring of terrains around the player would always be loaded.

Terrains could check "distance from player" and unload/destoy themselves when no longer needed.

Not sure how efficient this system would be with all the loading and unloading.

So I'm wondering if this is the right direction to go or if someone has already made a better system.

Check out this example. It will dynamically load und unload neighboring terrains as the player moves around the world, and should answer most of your questions.

using UnityEngine;
using System.Collections;
using System;

using Object = UnityEngine.Object;

public class ZoneLoader : MonoBehaviour
{
    const float   m_LoadDistance = 1500.0F;
    const float   m_UnloadDistance = 2500.0F;

    //EDDIE CHANGE TO FIT NEW TERRAIN SIZE
    public const float m_GridSize = 8046.0F;
    public const int m_ZoneCount = 12;

    static public ZoneLoader    ms_Singleton;

    static ZoneLoader singleton
    {
    get
    {   
        // @TODO: use FindObjectsOfType to implement improved hotloading of scripts
        if (ms_Singleton == null)   
        {
            GameObject go = new GameObject ("ZoneLoader", typeof(ZoneLoader));
            ms_Singleton = go.GetComponent(typeof(ZoneLoader)) as ZoneLoader;
        }
        return ms_Singleton;
    }
    }

    bool          m_IsLoading = false;
    bool          m_IsUnloading = false;
    bool          m_UseWWW = true;
    bool          m_UseWWWCaching = false;
    int           m_CacheVersion = 1;

    Vector3       m_PlayerPosition;
    Transform     m_PlayerTransform;

    static int    m_ZonesUnloadedSinceLastAssetUnload = 0;

    [System.Serializable]
    public class Zone
    {
        public bool m_Loaded = false;
        public bool m_IsLoadable = true;
        public GameObject m_Root;
        public Terrain m_Terrain;

        public AssetBundle  m_ZoneBundle;
        public WWW  m_ZoneDownload;
    }

    static string GetBaseUrl ()
    {
        if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.WindowsEditor)
            return "file:// + " + Application.dataPath + "/../builds/AssetBundles/";
        if (Application.platform == RuntimePlatform.OSXPlayer)
            return "file:// + " + Application.dataPath + "/../../AssetBundles/";
        else
            return "AssetBundles/";
    }

    Zone[] m_Zones = new Zone[m_ZoneCount * m_ZoneCount];

    static public string GetPrefix (string prefix, string postfix, int x, int y)
    {
        return string.Format("{0}{1}-{2}{3}", prefix, x, y, postfix);
    }

    static public string GetPrefix (int x, int y)
    {
        return GetPrefix("Map_", "", x, y); 
    }

    IEnumerator LoadZone (int x, int y)
    {
        ////@TODO: if (!Application.CanStreamedLevelBeLoaded (GetPrefix(x, y)))

        if (m_IsLoading)
        {
            Debug.LogError("Already loading zone");
            yield break;
        }

        Zone zone = m_Zones[m_ZoneCount*y + x];

        // Zone cant be loaded
        if (!zone.m_IsLoadable)
            yield break;

        m_IsLoading = true;

        string levelName = GetPrefix(x, y);

        if (m_UseWWWCaching || m_UseWWW)
        {
            string fullUrl = GetBaseUrl() + levelName + ".unity3d";
            if (m_UseWWWCaching)
                zone.m_ZoneDownload = WWW.LoadFromCacheOrDownload (fullUrl, m_CacheVersion);
            else
                zone.m_ZoneDownload = new WWW (fullUrl);
            yield return zone.m_ZoneDownload;

            // Make sure there were no errors in the download
            if (zone.m_ZoneDownload.error != null)
            {
                Debug.LogError(zone.m_ZoneDownload.error);
                zone.m_IsLoadable = false;
                m_IsLoading = false;
                zone.m_ZoneDownload.Dispose();
                zone.m_ZoneDownload = null;
                yield break;
            }

            // Load the scene so it becomes accessable from Application.LoadLevel
            zone.m_ZoneBundle = zone.m_ZoneDownload.assetBundle;
            zone.m_ZoneDownload.Dispose();
            zone.m_ZoneDownload = null;
            if (zone.m_ZoneBundle == null)
            {
                zone.m_IsLoadable = false;
                m_IsLoading = false;
                yield break;
            }
        }

        // Load Level
        AsyncOperation async = Application.LoadLevelAdditiveAsync(levelName);
        yield return async;

        // Necessary to prevent overwriting of another load level additive following immediately
        // yield return 0;

        // Find the root game object containing the level data
        zone.m_Root = GameObject.Find("/" + levelName);
        if (zone.m_Root != null)
        {
            Transform terain = zone.m_Root.transform.Find("Terrain");
            if (terain)
                zone.m_Terrain = terain.GetComponent(typeof(Terrain)) as Terrain;

            zone.m_Loaded = true;
        }
        else
        {
            Debug.LogError(levelName + " could not be found after loading level");  
        }

        // Hookup neighboring terrains so there are no seams in the LOD
        for (int yi=Mathf.Max(y-1, 0);yi<Mathf.Min(y+2, m_ZoneCount);yi++)
        {
            for (int xi=Mathf.Max(x-1, 0);xi<Mathf.Min(x+2, m_ZoneCount);xi++)
            {
                Terrain curTerrain = GetLoadedTerrain(xi, yi);
                if (curTerrain != null)
                {
                    Terrain left = GetLoadedTerrain(xi-1, yi);
                    Terrain right = GetLoadedTerrain(xi+1, yi);
                    Terrain top = GetLoadedTerrain(xi, yi+1);
                    Terrain bottom = GetLoadedTerrain(xi, yi-1);
                    curTerrain.SetNeighbors (left, top, right, bottom);
                }
            }
        }

        m_IsLoading = false;
    }

    IEnumerator UnloadZone (int x, int y)
    {
        m_IsUnloading = true;

        Zone zone = m_Zones[m_ZoneCount*y + x];
        zone.m_Loaded = false;

        if (zone.m_Root)
            Destroy(zone.m_Root);
        else
            Debug.LogError("Root for zone has already been unloaded:" + GetPrefix(x, y));

        zone.m_Terrain = null;
        zone.m_Root = null;

        yield return 0;

        if (m_UseWWWCaching || m_UseWWW)
        {
            zone.m_ZoneBundle.Unload(true);
            zone.m_ZoneBundle = null;
        }

        m_ZonesUnloadedSinceLastAssetUnload++;

        m_IsUnloading = false;
    }

    /// Unload assets at some points
    /// AsynchronousOperation op = Resources.GarbageCollectAssets(-1); 

    void OnEnable ()
    {
        ms_Singleton = this;    
    }

    void Awake ()
    {
        ms_Singleton = this;
        m_Zones = new Zone[m_ZoneCount * m_ZoneCount];
        for (int i=0;i<m_Zones.Length;i++)
        {
            m_Zones *= new Zone();*
 *}*
 *if (m_UseWWWCaching)*
 *{*
 *///////@TODO: REMOVE THIS*
 _Caching.Authorize ("test", "", 1024*1024*1000, "");_
 *Caching.CleanCache();*
 *}*
 *// Precompute which dongs are loadable*
 _/*_
 *for (int y=0;y<m_DongCount;y++)*
 *{*
 *for (int x=0;x<m_DongCount;x++)*
 *{*
 <em>Dong dong = m_Dongs[m_DongCount*y + x];</em>
 *dong.m_IsLoadable = CachingManifest.singleton.HasDongInZoneType(x, y, ms_ZoneType);*
 *}*
 *}*
 _*/_
 *ms_Singleton = this;*
 *}*
 *float GetSqrDistance (Vector3 position, int x, int y)*
 *{*
 <em>float minx = x * m_GridSize;</em>
 <em>float maxx = (x+1) * m_GridSize;</em>
 <em>float miny = y * m_GridSize;</em>
 <em>float maxy = (y+1) * m_GridSize;</em>
 *float xDistance = 0.0F;*
 *float yDistance = 0.0F;*
 *if (position.x < minx)*
 *xDistance = Mathf.Abs(minx - position.x);*
 *else if  (position.x > maxx)*
 *xDistance = Mathf.Abs(maxx - position.x);*
 *if (position.z < miny)*
 *yDistance = Mathf.Abs(miny - position.z);*
 *else if (position.z > maxy)*
 *yDistance = Mathf.Abs(maxy - position.z);*
 _return xDistance * xDistance + yDistance * yDistance;_ 
 *}*
 *public static void SetPosition (Vector3 position)*
 *{*
 *singleton.m_PlayerPosition = position;*
 *}*
 *public static void SetPlayerTransform (Transform player)*
 *{*
 *singleton.m_PlayerTransform = player;*
 *}*
 *void GetClosestZone (Vector3 position, float closestDistance, out int closestX, out int closestY)*
 *{*
 *closestX = -1;*
 *closestY = -1;*
 _closestDistance = closestDistance * closestDistance;_
 *for (int y=0;y<m_ZoneCount;y++)*
 *{*
 *for (int x=0;x<m_ZoneCount;x++)*
 *{*
 <em>Zone zone = m_Zones[m_ZoneCount*y + x];</em>
 *if (!zone.m_Loaded && zone.m_IsLoadable)*
 *{*
 *float sqrDistance = GetSqrDistance(position, x, y);*
 *if (sqrDistance < closestDistance)*
 *{*
 *closestDistance = sqrDistance;*
 *closestX = x;*
 *closestY = y;*
 *}*
 *}*
 *}*
 *}*
 *}*
 *void Update ()*
 *{*
 *if (m_PlayerTransform)*
 *m_PlayerPosition = m_PlayerTransform.position;*
 *if (m_IsLoading || m_IsUnloading)*
 *return;*
 *// Unload any zone that is far enough away*
 *for (int y=0;y<m_ZoneCount;y++)*
 *{*
 *for (int x=0;x<m_ZoneCount;x++)*
 *{*
 <em>Zone zone = m_Zones[m_ZoneCount*y + x];</em>
 *if (zone.m_Loaded)*
 *{*
 *float sqrDistance = GetSqrDistance(m_PlayerPosition, x, y);*
 <em>if (sqrDistance > m_UnloadDistance * m_UnloadDistance)</em>
 *{*
 *StartCoroutine(UnloadZone(x, y));*
 *return;*
 *}*
 *}*
 *}*
 *}*
 *// Try loading the closest zone*
 *int closestX, closestY;*
 *GetClosestZone(m_PlayerPosition, m_LoadDistance, out closestX, out closestY);*
 *if (closestX != -1)*
 *StartCoroutine(LoadZone (closestX, closestY));*
 *}*
 *Terrain GetLoadedTerrain (int x, int y)*
 *{*
 *if ((x >= 0 && x < m_ZoneCount) && (y >= 0 && y < m_ZoneCount))*
 *{*
 <em>Zone zone = m_Zones[m_ZoneCount*y + x];</em>
 *if (zone.m_Loaded)*
 *return zone.m_Terrain;*
 *}*
 *return null;*
 *}*
*}*
*```*

how did you fix the Assets/Plugins/ZoneLoader.cs(130,17): error CS0246: The type or namespace name `AsynchronousOperation' could not be found. Are you missing a using directive or an assembly reference? error?

I cant make it work... :(

Hi Christian,

thanks for the quick answer, but using your file I got new errors...

Assets/Editor/GenerateScenes.cs(53,52): error CS0103: The name `TerrainLighting' does not exist in the current context

and

Assets/Editor/GenerateScenes.cs(53,41): error CS1061: Type `UnityEngine.Terrain' does not contain a definition for`lighting' and no extension method `lighting' of type`UnityEngine.Terrain' could be found (are you missing a using directive or an assembly reference?)

I'm totally lost here...

Thanks in advance,

Toni

Christian, it appears that you have just commented out the line dealing with the "AsynchronousOperation"

I wanted to see this demo in Unity 3 so I figured it out. There are two/three compilation errors:

In GenerateScenes.cs, comment out lines 52 and 53:

//Terrain terrain = terrainGO.GetComponent(typeof(Terrain)) as Terrain;
//terrain.lighting = TerrainLighting.Lightmap;

In ZoneLoader.cs, change line 130 from:

AsynchronousOperation async = Application.LoadLevelAdditiveAsynchronous[...]

To:

AsyncOperation async = Application.LoadLevelAdditiveAsync(levelName);

This works in the free version of Unity also, though it throws errors at runtime due to the lack of LoadLevelAdditiveAsync (and falls back to LoadLevelAdditive I think).

Doesn’t work on latest version of unity . some error about rebuilding asset bundles shows up