關於 web service, unity, blogger 等軟體工程筆記

Unity 遊戲存檔機制淺談,關於壓縮 (Compression) 的三兩事

Edit icon 沒有留言
Unity 遊戲存檔壓縮機制

延續在前一篇文章中的議題,在遊戲存檔進行壓縮 (compress) 後儲存,可減少其儲存所需空間,之後讀取先經過解壓縮 (decompress) 後,再反序列化 (deserialize) 還原成遊戲狀態物件來使用。

遊戲資料存取架構,考慮加密與壓縮

注意:使用 Unity 2018.1.1f1 測試,Scripting Runtime Version 採用 .Net 4.x Equvalent,而非預設的 .Net 3.5 Equvalent

關於壓縮演算法

資料壓縮演算法分為兩大類,以遊戲存檔應用來說,肯定是 Lossless 較為適合:

  • Lossless:非破壞性資料壓縮,機於,壓縮資料可完整還原
  • Lossy:破壞性資料壓縮,其損失資料是可以被接受
    • 考慮人類感知系統的局限,通常在圖片音樂影像等資料壓縮使用,例如 JPEG、MP3、MPEG

至於在 Lossless 演算法中,是基於熵編碼(entropy coding)、字典編碼 (dictionary coder)、還是其他方式則不是在本篇的討論重點…。

在這篇文章中,僅展示 Gzip, LZMA, LZ4 三種壓縮演算法的使用方式與比較。

Gzip, LZMA, LZ4 使用程式碼範例

以下整理三種壓縮演算法 Gzip, LZMA, LZ4 在 C# 中的使用範例,Gzip 使用 .Net 已實作的函式庫,而 LZMA 以及 LZ4 則是分別使用 7Zip SDK 以及 MiloszKrajewski/lz4net 的實作。

using System.IO;
using System.IO.Compression;

public static byte[] Compress(byte[] src)
{
   using (var ms = new MemoryStream(src.Length))
   {
      using (var cs = new GZipStream(ms, CompressionMode.Compress))
      {
         cs.Write(src, 0, src.Length);
      }
      return ms.ToArray();
   }
}

public static void Compress(byte[] src, string file_path)
{
   using (var fs = new FileStream(file_path, FileMode.OpenOrCreate, FileAccess.Write))
   {
      using (var cs = new GZipStream(fs, CompressionMode.Compress))
      {
         cs.Write(src, 0, src.Length);
      }
   }
}

public static byte[] Decompress(byte[] src)
{
   using (var ms = new MemoryStream())
   {
      using (var srcms = new MemoryStream(src))
      {
         using (var cs = new GZipStream(srcms, CompressionMode.Decompress))
         {
            // 注意:CopyTo 為 .Net 4.0 以後才支援的函式
            cs.CopyTo(ms);
         }
      }
      return ms.ToArray();
   }
}

public static byte[] Decompress(string file_path)
{
   using (var ms = new MemoryStream())
   {
      using (var fs = new FileStream(file_path, FileMode.Open, FileAccess.Read))
      {
         using (var cs = new GZipStream(fs, CompressionMode.Decompress))
         {
            cs.CopyTo(ms);
         }
      }
      return ms.ToArray();
   }
}
using System.IO;
using LZMAEncoder = SevenZip.Compression.LZMA.Encoder;
using LZMADecoder = SevenZip.Compression.LZMA.Decoder;

public static byte[] Compress(byte[] src)
{
   using (var output = new MemoryStream())
   {
      Compress(src, output);
      return output.ToArray();
   }
}

public static void Compress(byte[] src, Stream output)
{
   var coder = new LZMAEncoder();
   var inSize = src.GetLongLength(0);
   using (var input = new MemoryStream(src))
   {
      using (var bw = new BinaryWriter(output))
      {
         coder.WriteCoderProperties(output);
         bw.Write(inSize);
         coder.Code(input, output, inSize, -1, null);
      }
   }
}

public static void Compress(byte[] src, string file_path)
{
   using (var output = new FileStream(file_path, FileMode.OpenOrCreate, FileAccess.Write))
   {
      Compress(src, output);
   }
}

public static byte[] Decompress(byte[] src)
{
   using (var input = new MemoryStream(src))
   {
      return Decompress(input);
   }
}

public static byte[] Decompress(Stream src)
{
   var headers = new byte[13];
   if (src.Read(headers, 0, headers.Length) != headers.Length)
   {
      throw (new System.Exception("Input stream is not valid"));
   }
   
   var outSize = System.BitConverter.ToInt64(headers, 5);
   var inSize = src.Length - headers.Length;
   using (var output = new MemoryStream())
   {
      var coder = new LZMADecoder();
      coder.SetDecoderProperties(headers);
      coder.Code(src, output, inSize, outSize, null);
      return output.ToArray();
   }
}

public static byte[] Decompress(string file_path)
{
   using (var input = new FileStream(file_path, FileMode.Open, FileAccess.Read))
   {
      return Decompress(input);
   }
}
using System.IO;
using LZ4;

public static byte[] Compress(byte[] src)
{
   using (var output = new MemoryStream())
   {
      using (var cs = new LZ4Stream(output, LZ4StreamMode.Compress))
      {
         cs.Write(src, 0, src.Length);
         return output.ToArray();
      }
   }
}

public static void Compress(byte[] src, string file_path)
{
   using (var output = new FileStream(file_path, FileMode.OpenOrCreate, FileAccess.Write))
   {
      using (var cs = new LZ4Stream(output, LZ4StreamMode.Compress))
      {
         cs.Write(src, 0, src.Length);
      }
   }
}

public static byte[] Decompress(byte[] src)
{
   using (var output = new MemoryStream())
   {
      using (var input = new MemoryStream(src))
      {
         using (var cs = new LZ4Stream(input, LZ4StreamMode.Decompress))
         {
            // 注意:CopyTo 為 .Net 4.0 以後才支援的函式
            cs.CopyTo(output);
         }
      }
      return output.ToArray();
   }
}

public static byte[] Decompress(string file_path)
{
   using (var output = new MemoryStream())
   {
      using (var input = new FileStream(file_path, FileMode.Open, FileAccess.Read))
      {
         using (var cs = new LZ4Stream(input, LZ4StreamMode.Decompress))
         {
            cs.CopyTo(output);
         }
      }
      return output.ToArray();
   }
}

Gzip, LZMA, LZ4 比較與使用建議

關於這三種 Gzip, LZMA, LZ4 壓縮演算法的比較,可參考這份 Benchmark,從中整理其簡易比較表格:

Gzip LZMA LZ4
壓縮率(節省儲存空間)
壓縮速度
解壓縮速度
壓縮記憶體需求
解壓縮記憶體需求

採用哪種壓縮演算法的使用建議:

  • 採用 LZMA
    • 最好的壓縮率(省下最多的儲存空間),不在意壓縮與解壓縮時間
  • 採用 LZ4
    • 如果要最快的壓縮與解壓縮時間,但不在意壓縮率
  • 採用 Gzip
    • 僅僅想要壓縮的通常選擇
  • 不壓縮
    • 遊戲存檔大小通常不會很大
      • 小檔案通常無法減少其空間,甚至可能因為壓縮格式的標頭資料,使得壓縮後反而檔案變大
    • 沒有儲存空間限制
    • 沒有網路傳輸速度限制(遠端伺服器存檔)

順帶一提,Unity Assetbundle 也支援 LZMA 以及 LZ4 這兩種壓縮方式,看使用情境來決定採用哪種壓縮演算法。

Reference

沒有留言: