Search code examples
unity-game-engineterrainunity3d-terrain

What's the best way to save terraindata to file in Runtime?


My game lets the user modify the terrain at runtime, but now I need to save said terrain. I've tried to directly save the terrain's heightmap to a file, but this takes almost up to two minutes to write for this 513x513 heightmap.

What would be a good way to approach this? Is there any way to optimize the writing speed, or am I approaching this the wrong way?

    public static void Save(string pathraw, TerrainData terrain)
    {
        //Get full directory to save to
        System.IO.FileInfo path = new System.IO.FileInfo(Application.persistentDataPath + "/" + pathraw);
        path.Directory.Create();
        System.IO.File.Delete(path.FullName);
        Debug.Log(path);

        //Get the width and height of the heightmap, and the heights of the terrain
        int w = terrain.heightmapWidth;
        int h = terrain.heightmapHeight;
        float[,] tData = terrain.GetHeights(0, 0, w, h);

        //Write the heights of the terrain to a file
        for (int y = 0; y < h; y++)
        {
            for (int x = 0; x < w; x++)
            {
                //Mathf.Round is to round up the floats to decrease file size, where something like 5.2362534 becomes 5.24
                System.IO.File.AppendAllText(path.FullName, (Mathf.Round(tData[x, y] * 100) / 100) + ";");
            }
        }
    }

As a sidenote, the Mathf.Round doesn't seem to influence the saving time too much, if at all.


Solution

  • You are making a lot of small individual File IO calls. File IO is always time consuming and expensive as it contains opening the file, writing to it, saving the file and closing the file.


    Instead I would rather generate the complete string using e.g. a StringBuilder which is also more efficient than using something like

    var someString for(...) { someString += "xyz" }

    because the latter always allocates a new string.

    Then use e.g. a FileStream and StringWriter.WriteAsync(string) for writing async.

    Also rather use Path.Combine instead of directly concatenating string via /. Path.Combine automatically uses the correct connectors according to the OS it is used on.

    And instead of FileInfo.Directory.Create rather use Directory.CreateDirectory which doesn't throw an exception if the directory already exists.

    Something like

    using System.IO;
    
    ...
    
    public static void Save(string pathraw, TerrainData terrain)
    {
        //Get full directory to save to
        var filePath = Path.Combine(Application.persistentDataPath, pathraw);
        var path = new FileInfo(filePath);
        Directory.CreateDirectory(path.DirectoryName);
    
        // makes no sense to delete 
        // ... rather simply overwrite the file if exists
        //File.Delete(path.FullName);
        Debug.Log(path);
    
        //Get the width and height of the heightmap, and the heights of the terrain
        var w = terrain.heightmapWidth;
        var h = terrain.heightmapHeight;
        var tData = terrain.GetHeights(0, 0, w, h);
    
        // put the string together
        // StringBuilder is more efficient then using
        // someString += "xyz" because latter always allocates a new string
        var stringBuilder = new StringBuilder();
        for (var y = 0; y < h; y++)
        {
            for (var x = 0; x < w; x++)
            {
                //                                                         also add the linebreak if needed
                stringBuilder.Append(Mathf.Round(tData[x, y] * 100) / 100).Append(';').Append('\n');
            }
        }
    
        using (var file = File.Open(filePath, FileMode.OpenOrCreate, FileAccess.Write))
        {
            using (var streamWriter = new StreamWriter(file, Encoding.UTF8))
            {
                streamWriter.WriteAsync(stringBuilder.ToString());
            }
        }
    }
    

    You might want to specify how exactly the numbers shall be printed with a certain precision like e.g.

    (Mathf.Round(tData[x, y] * 100) / 100).ToString("0.00000000");