09-02-生态-小游戏平台适配

0 阅读8分钟

生态-小游戏平台适配

篇章:09-生态篇-集成与协同 状态:已完成 阅读时间:约 25 分钟


一、引言

1.1 本章在系列中的定位

小游戏(Mini Game)平台是 Unity 资源的最严峻考验:微信小游戏首包 4MB,抖音小游戏 10MB —— 比 Android / iOS 包体限制小了一个数量级。这要求 YooAsset 的资源管理体系必须做更激进的优化、更灵活的分包策略。

本章将深入 YooAsset 在微信/抖音/QQ 小游戏平台的适配方案,剖析其"虚拟文件系统"(VFS)的能力、小游戏特有的存储限制、首包极致优化技巧。

1.2 本章要解决的核心问题

  • 小游戏平台与原生平台的核心差异是什么?
  • YooAsset 的"虚拟文件系统"(VFS)如何实现?
  • 微信与抖音小游戏平台的差异如何处理?
  • 4MB 首包限制下如何极致优化?

1.3 阅读前置要求

  • 了解 YooAsset 的文件系统架构(参考 06-源码深度-文件系统与下载模块)
  • 了解微信/抖音小游戏的发布流程
  • 了解 Unity WebGL 构建配置

二、小游戏平台的特点与挑战

2.1 平台特性对比

平台首包限制总体积限制存储路径备注
Android (Google Play)150MB4GB/data/data//受限于 APK
iOS (App Store)200MB4GB/Library/蜂窝下载限制
微信小游戏4MB200MBwx.env.USER_DATA_PATH极严
抖音小游戏10MB500MBtt.env.USER_DATA_PATH较严
QQ 小游戏5MB200MBqq.env.USER_DATA_PATH较严
WebGL无浏览器缓存浏览器 Cache API首次加载慢

2.2 小游戏与原生应用的核心差异

2.2.1 文件系统

原生应用:

读写无限制
├── Application.persistentDataPath(可读写)
├── Application.streamingAssetsPath(只读)
└── /sd-card/(部分可写)

小游戏:

极受限
├── 主包(只读,不算"文件")
├── wx.env.USER_DATA_PATH(可读写,但有配额)
└── 内存(有限,需主动释放)
2.2.2 网络
  • 必须 HTTPS:小游戏禁止 HTTP 请求
  • 域名白名单:需要在平台后台配置合法域名
  • 请求并发限制:微信小游戏限制 5 个并发
  • 响应超时:通常 30 秒上限
2.2.3 内存

小游戏的内存限制非常严格:

平台内存限制后果
微信iOS 1.5GB / Android 1.0GB超出会被强制杀进程
抖音1.5GB同上
WebGL浏览器决定(通常 1-2GB)取决于设备
2.2.4 包体限制

小游戏的包体限制按包内文件大小计算:

微信小游戏包体 = 主包 + 初始资源 + 必要代码 ≤ 4MB
                  ↓
                  超出无法上传

2.3 YooAsset 的应对策略

YooAsset 通过以下策略应对小游戏的严苛限制:

  1. 虚拟文件系统(VFS):将所有资源"虚拟化",统一访问接口
  2. 极致分包:首包仅保留最核心的代码与资源
  3. 运行时按需下载:所有非核心资源都走 CDN
  4. 资源压缩:高压缩比算法(如 LZMA)
  5. 内存优化:主动释放、及时 GC

三、虚拟文件系统(VFS)

3.1 VFS 的概念

虚拟文件系统(Virtual File System)是 YooAsset 在小游戏平台上的核心能力。它将"物理存储"与"逻辑访问"解耦:

[应用层]
    │
    ├── LoadAssetAsync("Hero.prefab")  ← 统一接口
    │
[虚拟文件系统 VFS]
    │
    ├── 路由:Hero.prefab 属于"运行时包"
    │        实际存储在 .../yooasset/bundles/hero.bundle
    │
    ├── 加载:找到 bundle,调用对应物理 FS 读取
    │
[物理文件系统层]
    │
    ├── DefaultCacheFileSystem(写磁盘)
    ├── WechatFileSystem(写 wx env)
    └── MemoryFileSystem(纯内存)

3.2 VFS 的实现

3.2.1 路由规则
public class VirtualFileSystem
{
    private readonly Dictionary<string, IFileSystem> _routingTable = new();
    
    /// <summary>
    /// 注册物理文件系统
    /// </summary>
    public void RegisterFileSystem(string name, IFileSystem fileSystem, int priority = 0)
    {
        _routingTable[name] = fileSystem;
    }
    
    /// <summary>
    /// 根据文件名路由到具体物理文件系统
    /// </summary>
    public IFileSystem Route(string filePath)
    {
        // 例如:所有以"cache_"开头的文件走缓存文件系统
        if (filePath.StartsWith("cache_"))
            return _routingTable["CacheFS"];
        
        // 走运行时文件系统
        return _routingTable["RuntimeFS"];
    }
}
3.2.2 物理文件系统实现

小游戏平台需要实现自定义的物理文件系统:

public class WechatFileSystem : IFileSystem
{
    private string _userDataPath;  // wx.env.USER_DATA_PATH
    
    public WechatFileSystem()
    {
#if UNITY_WECHAT_GAME
        _userDataPath = WX.env.USER_DATA_PATH;
#endif
    }
    
    public bool Exists(string filePath)
    {
#if UNITY_WECHAT_GAME
        var fs = WX.GetFileSystemManager();
        return fs.AccessSync($"{_userDataPath}/{filePath}");
#else
        return File.Exists($"{_userDataPath}/{filePath}");
#endif
    }
    
    public byte[] ReadFile(string filePath)
    {
#if UNITY_WECHAT_GAME
        var fs = WX.GetFileSystemManager();
        return fs.ReadFileSync($"{_userDataPath}/{filePath}").AsBytes();
#else
        return File.ReadAllBytes($"{_userDataPath}/{filePath}");
#endif
    }
    
    public void WriteFile(string filePath, byte[] data)
    {
#if UNITY_WECHAT_GAME
        var fs = WX.GetFileSystemManager();
        fs.WriteFileSync($"{_userDataPath}/{filePath}", data);
#else
        File.WriteAllBytes($"{_userDataPath}/{filePath}", data);
#endif
    }
    
    public void DeleteFile(string filePath)
    {
#if UNITY_WECHAT_GAME
        var fs = WX.GetFileSystemManager();
        fs.UnlinkSync($"{_userDataPath}/{filePath}");
#else
        File.Delete($"{_userDataPath}/{filePath}");
#endif
    }
}

3.3 VFS 的配置

public class VFSConfiguration
{
    public List<FileSystemConfig> FileSystems = new();
    
    public void SetupForWechatGame()
    {
        // 1. 内置资源文件系统(小游戏包内)
        FileSystems.Add(new FileSystemConfig
        {
            Name = "BuiltinFS",
            Type = EFileSystemType.Builtin,
            ReadOnly = true,
            Priority = 100
        });
        
        // 2. 缓存文件系统(可读写)
        FileSystems.Add(new FileSystemConfig
        {
            Name = "CacheFS",
            Type = EFileSystemType.Cache,
            ReadOnly = false,
            Priority = 50,
            Location = "yooasset/cache"
        });
        
        // 3. 远程文件系统(HTTP)
        FileSystems.Add(new FileSystemConfig
        {
            Name = "WebFS",
            Type = EFileSystemType.Web,
            ReadOnly = true,
            Priority = 10,
            Location = "https://cdn.example.com/yooasset/"
        });
    }
}

四、微信小游戏适配

4.1 微信小游戏的特殊性

微信小游戏(WXGAME)是国内小游戏生态的标杆,也是最严格的:

维度限制
主包4MB
总体积200MB(CDN 资源)
启动时间5 秒内必须显示首屏
内存iOS 1.5GB / Android 1.0GB
网络必须 HTTPS + 白名单
文件 APIwx.getFileSystemManager()
存储路径wx.env.USER_DATA_PATH

4.2 微信小游戏的 YooAsset 配置

4.2.1 运行时设置
public class WechatGameConfig
{
    public static void Setup()
    {
        YooAssets.InitializeParameters initializeParameters = new();
        
        // 1. 配置构建报告(用于 CDN 拉取)
        initializeParameters.BuildinFileSystemParameters = 
            FileSystemParameters.CreateDefaultBuiltinFileSystemParameters();
        
        // 2. 配置缓存文件系统
        initializeParameters.CacheFileSystemParameters = 
            FileSystemParameters.CreateDefaultCacheFileSystemParameters(
                "yooasset_cache",
                remoteURL: "https://cdn.example.com/yooasset/v1/"
            );
        
        // 3. 初始化
        YooAssets.InitializeAsync(initializeParameters);
    }
}
4.2.2 文件 API 适配
public static class WechatFileAPI
{
    /// <summary>
    /// 读取文件
    /// </summary>
    public static byte[] ReadFile(string path)
    {
#if UNITY_WECHAT_GAME
        var fs = WX.GetFileSystemManager();
        if (fs.AccessSync(path))
        {
            return fs.ReadFileSync(path, "utf-8").AsBytes();
        }
        return null;
#else
        if (File.Exists(path))
            return File.ReadAllBytes(path);
        return null;
#endif
    }
    
    /// <summary>
    /// 写入文件
    /// </summary>
    public static void WriteFile(string path, byte[] data)
    {
#if UNITY_WECHAT_GAME
        var fs = WX.GetFileSystemManager();
        var dir = Path.GetDirectoryName(path);
        if (!string.IsNullOrEmpty(dir) && !fs.AccessSync(dir))
        {
            fs.MkdirSync(dir, true);
        }
        fs.WriteFileSync(path, data);
#else
        var dir = Path.GetDirectoryName(path);
        if (!string.IsNullOrEmpty(dir))
            Directory.CreateDirectory(dir);
        File.WriteAllBytes(path, data);
#endif
    }
}

4.3 性能优化

4.3.1 文件缓存
public class FileCache
{
    private readonly MemoryStream _cache = new();
    private readonly object _lock = new();
    
    public byte[] ReadWithCache(string filePath)
    {
        lock (_lock)
        {
            // 1. 先从内存缓存读
            if (_memoryCache.ContainsKey(filePath))
                return _memoryCache[filePath];
            
            // 2. 内存缓存未命中,读物理文件
            var data = WechatFileAPI.ReadFile(filePath);
            
            // 3. 放入内存缓存
            _memoryCache[filePath] = data;
            return data;
        }
    }
}
4.3.2 内存管理
public class MiniGameMemoryManager
{
    public async Task CheckMemoryPressure()
    {
        var memUsage = GetCurrentMemoryUsage();
        var threshold = GetMemoryThreshold();
        
        if (memUsage > threshold * 0.8)
        {
            // 主动清理内存
            Resources.UnloadUnusedAssets();
            GC.Collect();
            
            // 清理非关键缓存
            _cacheManager.ClearNonEssentialCache();
            
            // 降低后续资源加载
            YooAssets.ReduceLoadPriority();
        }
        
        await Task.Yield();
    }
}

五、抖音小游戏适配

5.1 抖音小游戏的特点

抖音小游戏(TTGAME)与微信小游戏类似但不同:

维度微信抖音
首包4MB10MB
总体积200MB500MB
文件 APIwx.getFileSystemManager()tt.getFileSystemManager()
存储路径wx.env.USER_DATA_PATHtt.env.USER_DATA_PATH
网络wx.requesttt.request

5.2 抖音小游戏的 YooAsset 适配

public class TiktokGameFileSystem : IFileSystem
{
    private string _userDataPath;
    
    public TiktokGameFileSystem()
    {
#if UNITY_TIKTOK_GAME
        _userDataPath = TT.env.USER_DATA_PATH;
#endif
    }
    
    public byte[] ReadFile(string filePath)
    {
#if UNITY_TIKTOK_GAME
        var fs = TT.GetFileSystemManager();
        if (fs.AccessSync($"{_userDataPath}/{filePath}"))
        {
            return fs.ReadFileSync($"{_userDataPath}/{filePath}").AsBytes();
        }
        return null;
#else
        return File.Exists($"{_userDataPath}/{filePath}") 
            ? File.ReadAllBytes($"{_userDataPath}/{filePath}") 
            : null;
#endif
    }
    
    public void WriteFile(string filePath, byte[] data)
    {
#if UNITY_TIKTOK_GAME
        var fs = TT.GetFileSystemManager();
        fs.WriteFileSync($"{_userDataPath}/{filePath}", data);
#else
        File.WriteAllBytes($"{_userDataPath}/{filePath}", data);
#endif
    }
}

5.3 平台检测

public static class PlatformDetector
{
    public static EPlatformType GetCurrentPlatform()
    {
#if UNITY_WECHAT_GAME
        return EPlatformType.Wechat;
#elif UNITY_TIKTOK_GAME
        return EPlatformType.TikTok;
#elif UNITY_QQ_GAME
        return EPlatformType.QQ;
#elif UNITY_BAIDU_GAME
        return EPlatformType.Baidu;
#elif UNITY_WEBGL
        return EPlatformType.WebGL;
#else
        return EPlatformType.Native;
#endif
    }
}

六、小游戏首包极致优化

6.1 首包限制

平台主包限制
微信4MB严格
抖音10MB适中
QQ5MB严格

注意:首包限制的是主包(包内)大小,不包括 CDN 资源。

6.2 优化策略

6.2.1 资源精简
public class ResourceMinimizer
{
    /// <summary>
    /// 移除未使用的资源
    /// </summary>
    public static void RemoveUnusedResources()
    {
        // 1. 检测未使用的 Texture
        var allTextures = AssetDatabase.FindAssets("t:Texture2D")
            .Select(AssetDatabase.GUIDToAssetPath);
        
        foreach (var texPath in allTextures)
        {
            if (!IsTextureUsed(texPath))
            {
                Debug.LogWarning($"移除未使用的纹理: {texPath}");
                AssetDatabase.DeleteAsset(texPath);
            }
        }
        
        // 2. 检测未使用的 AudioClip
        // 3. 检测未使用的 Material
        // 4. 检测未使用的 Shader
    }
}
6.2.2 压缩优化
public class CompressionOptimizer
{
    public static ECompressOption GetOptimalCompression()
    {
        return ECompressOption.LZMA;  // LZMA 压缩比最高
    }
    
    public static void OptimizeTextures()
    {
        var textureImportSettings = new TextureImporterPlatformSettings
        {
            // ASTC 格式:iOS / Android 通用
            format = TextureImporterFormat.ASTC_6x6,
            // 最大尺寸限制
            maxTextureSize = 1024,
            // 压缩质量
            compressionQuality = (int)TextureCompressionQuality.Normal,
            // 启用 crunch 压缩
            crunchedCompression = true,
        };
    }
}
6.2.3 启动优化
public class MiniGameStartupOptimizer
{
    public async Task OptimizedStartup()
    {
        // 1. 立即显示 Splash
        ShowSplash();
        
        // 2. 并行加载:网络请求 + 本地初始化
        var initTask = InitializeYooAsset();
        var networkTask = PrefetchManifest();
        await Task.WhenAll(initTask, networkTask);
        
        // 3. 关键资源同步加载(首屏所需)
        LoadCriticalResourcesSync();
        
        // 4. 显示首屏
        ShowFirstScreen();
        
        // 5. 后台异步加载其他资源
        _ = Task.Run(LoadNonCriticalResourcesAsync);
    }
}

6.3 优化效果对比

优化项优化前优化后节省
纹理压缩PNG 5MBASTC 1.5MB70%
音频压缩WAV 3MBOGG 800KB73%
代码裁剪2MB1.2MB40%
未使用资源800KB0100%
资源压缩LZ4 1.5MBLZMA 1MB33%
总包体12.3MB4.5MB63%

七、总结

7.1 本章要点回顾

  • 小游戏平台:首包 4-10MB、内存 1-1.5GB,是 Unity 资源最严苛的目标
  • VFS:虚拟文件系统将物理存储与逻辑访问解耦,是小游戏平台的核心能力
  • 平台适配:微信/抖音/QQ 小游戏的 API 差异通过 IFileSystem 屏蔽
  • 极致优化:纹理 ASTC + 音频 OGG + 代码裁剪 + LZMA 压缩,是 4MB 主包的必选项

7.2 与前后章节的关联

  • 前章:09-01 解决了代码热更问题
  • 本章:解决小游戏平台的资源管理问题
  • 后章:09-03 将介绍 YooAsset 与 GameFramework / xAsset 等其他框架的集成

7.3 实践建议

  1. VFS 优先:从一开始就用 VFS 抽象,便于后期多平台移植
  2. 首包严格控制:始终把"主包大小"作为最高优先级
  3. 资源按需加载:所有非启动必需资源都走 CDN
  4. 内存主动管理:小游戏内存敏感,需主动 GC + 卸载
  5. 平台 API 隔离:通过 #if UNITY_WECHAT_GAME 隔离平台代码

上一篇:YooAsset+HybridCLR 热更新方案 下一篇:与GameFramework/xAsset等框架集成