You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 06:17:02