Unity重构《塞尔达传说:黄昏公主》:优化二进制数据加载性能
《塞尔达传说:黄昏公主》GameCube文件解析与Unity重构加载优化
我正在逆向GameCube游戏《塞尔达传说:黄昏公主》,并在Unity中重构相关内容——具体是解析包含网格、纹理、材质数据的二进制文件,生成带网格和材质的Unity GameObject。
当前功能正常,但加载速度偏慢:单个简单场景加载约耗时100ms,加载所有NPC、物体等内容时耗时可达2秒。启动游戏加载这些二进制文件时,CPU使用率极低(约3%),我不确定该如何优化任务结构。
简化后的代码流程
- 加载二进制文件并解析头部
- 遍历文件中的每个“chunk(标签)”:
- 解码标签(如顶点、纹理、材质)
- 将解析后的数据存储到对应对象(如INF1、SHP1等)
- 完成所有chunk解析后,在Unity中创建网格并分配材质
核心实现代码(已精简)
public class BMD : MonoBehaviour { public string FullBMD = ""; public INF1 INF1Tag; public VTX1 VTX1Tag; public EVP1 EVP1Tag; public DRW1 DRW1Tag; public JNT1 JNT1Tag; public SHP1 SHP1Tag; public MAT3 MAT3Tag; public TEX1 TEX1Tag; public Material DefaultMaterial; // 此方法耗时约90ms,需要更快的加载流程 void Start() { using (EndianBinaryReader reader = new EndianBinaryReader(File.ReadAllBytes(FullBMD), Endian.Big)) { reader.Skip(8); int size = reader.ReadInt32(); int numChunks = reader.ReadInt32(); reader.Skip(16); for (int i = 0; i < numChunks; i++) { long tagStart = reader.BaseStream.Position; string tagName = reader.ReadString(4); int tagSize = reader.ReadInt32(); switch (tagName) { case "INF1": INF1Tag = new INF1(); INF1Tag.LoadINF1FromStream(reader, tagStart); break; case "VTX1": VTX1Tag = new VTX1(); VTX1Tag.LoadVTX1FromStream(reader, tagStart, tagSize); break; case "EVP1": EVP1Tag = new EVP1(); EVP1Tag.LoadEVP1FromStream(reader, tagStart); break; case "DRW1": DRW1Tag = new DRW1(); DRW1Tag.LoadDRW1FromStream(reader, tagStart); break; case "JNT1": JNT1Tag = new JNT1(); JNT1Tag.LoadJNT1FromStream(reader, tagStart); JNT1Tag.CalculateParentJointsForSkeleton(INF1Tag.HierarchyRoot); break; case "SHP1": SHP1Tag = new SHP1(); SHP1Tag.ReadSHP1FromStream(reader, tagStart, VTX1Tag.VertexData); break; case "MAT3": MAT3Tag = new MAT3(); MAT3Tag.LoadMAT3FromStream(reader, tagStart); break; case "TEX1": TEX1Tag = new TEX1(); TEX1Tag.LoadTEX1FromStream(this, reader, tagStart, new List<BTI>()); break; case "MDL3": break; } reader.BaseStream.Position = tagStart + tagSize; } // 加载数据完成后创建网格 CreateMeshes(); } } // 此方法耗时约10ms private void CreateMeshes() { List<MeshFilter> meshFilters = new List<MeshFilter>(); List<MeshRenderer> meshRenderers = new List<MeshRenderer>(); List<Material> meshMaterials = new List<Material>(); List<GameObject> childs = new List<GameObject>(); foreach (SHP1.Shape shape in SHP1Tag.Shapes) { GameObject go = new GameObject("shape" + SHP1Tag.Shapes.IndexOf(shape)); childs.Add(go); Material3 material = MAT3Tag.MaterialList[MAT3Tag.MaterialRemapTable[shape.MaterialIndex]]; BTI texture = TEX1Tag.BTIs[MAT3Tag.TextureRemapTable[baseTexture]]; List<Vector3> verts = shape.OverrideVertPos.Count > 0 ? shape.OverrideVertPos : shape.VertexData.Position; List<Vector3> normals = shape.OverrideNormals.Count > 0 ? shape.OverrideNormals : shape.VertexData.Normal; // 创建网格 Mesh mesh = new Mesh(); mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; mesh.vertices = verts.ToArray(); int numVertices = verts.Count; int numTriangles = numVertices / 3; int[] triangles = new int[numTriangles * 3]; // 单面 for (int i = 0; i < numTriangles; i++) { triangles[i * 3] = i * 3; triangles[i * 3 + 1] = i * 3 + 1; triangles[i * 3 + 2] = i * 3 + 2; } mesh.triangles = triangles; mesh.normals = normals.ToArray(); mesh.uv = shape.VertexData.Tex0.ToArray(); mesh.colors = shape.VertexData.Color0.ToArray(); // 给着色器分配材质 Material mat = new Material(DefaultMaterial); mat.mainTexture = texture.Texture; MeshFilter filter = go.AddComponent<MeshFilter>(); filter.sharedMesh = mesh; MeshRenderer renderer = go.AddComponent<MeshRenderer>(); renderer.sharedMaterial = mat; meshFilters.Add(filter); meshRenderers.Add(renderer); meshMaterials.Add(mat); } // 合并网格 CombineMeshesToASingle(); } }
优化建议
1. 异步化IO与解析
- 替换
File.ReadAllBytes为异步读取(如await File.ReadAllBytesAsync),避免阻塞主线程。 - 将二进制解析逻辑放到后台线程执行(注意Unity的API只能在主线程调用,所以解析数据部分后台处理,创建GameObject/网格的部分切回主线程),提升CPU利用率。
2. 减少内存分配与GC开销
- 解析数据时,提前预估容量并初始化固定长度数组,避免频繁创建
List导致的内存分配。 - 复用材质:相同材质的多个网格共享同一个
Material实例,不要在循环中每次都new Material(DefaultMaterial)。 - 优化三角形数组生成:可以直接用
Enumerable.Range或预计算填充,替代循环赋值,减少CPU开销。
3. 并行解析独立Chunk
如果各个Chunk之间没有依赖关系(比如TEX1和VTX1可独立解析),可以用多线程并行处理不同Chunk,充分利用多核CPU。注意线程安全,每个线程使用独立的读取器或锁定共享资源。
4. 预加载与缓存机制
- 缓存常用的材质、纹理资源,避免重复创建和加载。
- 将解析后的资源序列化为Unity AssetBundle,后续直接加载AssetBundle,跳过二进制解析步骤。
5. 优化二进制读取效率
- 检查
EndianBinaryReader的内部实现,减少不必要的内存拷贝,可直接操作字节数组提升读取速度。 - 尽量顺序读取文件,减少
Skip和Position跳转的操作,降低IO开销。
内容的提问来源于stack exchange,提问作者Finn
相关产品推荐
相关产品推荐

