Unity 遊戲存檔機制淺談,關於壓縮 (Compression) 的三兩事
延續在前一篇文章中的議題,在遊戲存檔進行壓縮 (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 這兩種壓縮方式,看使用情境來決定採用哪種壓縮演算法。
沒有留言: