Unity | 资源热更(YooAsset & AB)

目录

一、AssetBundle

[1. 插件AssetBundle Browser 打AB包](#1. 插件AssetBundle Browser 打AB包)

[(1)Unity(我用的版本是2020.3.8)导入AssetBundle Browser](#(1)Unity(我用的版本是2020.3.8)导入AssetBundle Browser)

(2)设置Prefab

(3)AssetBundleBrowser面板

[2. 代码打AB包](#2. 代码打AB包)

二、YooAsset

[1. 准备工作](#1. 准备工作)

(1)官方文档

[(2) 通过Packages清单安装YooAsset](#(2) 通过Packages清单安装YooAsset)

2.全局配置

3.资源配置

[(1)Enable Addressable](#(1)Enable Addressable)

[(2)Pack Rule打包规则](#(2)Pack Rule打包规则)

4.资源构建

[(1)Build Mode](#(1)Build Mode)

(2)Encryption

5.YooAsset运行时

(1)初始化

(2)资源系统的运行模式

(3)获取资源版本

(4)更新资源清单

(5)资源包下载

(6)资源加载

(7)完整TestLoad

6.YooAsset测试

三、参考资料


一、AssetBundle

AB包可以存储绝大部分Unity资源但无法直接存储C#脚本,所以代码的热更新需要使用Lua或者存储编译后的DLL文件。

AB包不能重复进行加载,当AB包已经加载进内存后必须卸载后才能重新加载。

多个资源分布在不同的AB包可能会出现一个预制体的贴图等部分资源不在同一个包下,直接加载会出现部分资源丢失的情况,即AB包之间是存在依赖关系的,在加载当前AB包时需要一并加载其所依赖的包。

打包完成后,会自动生成一个主包(主包名称随平台不同而不同),主包的manifest下会存储有版本号、校验码(CRC)、所有其它包的相关信息(名称、依赖关系)。

1. 插件AssetBundle Browser 打AB包

(1)Unity(我用的版本是2020.3.8)导入AssetBundle Browser

github下载插件,将下载后的安装包解压到Unity工程的Packages文件夹下,如果报错,删除Tests即可。

(2)设置Prefab

(3)AssetBundleBrowser面板

正确获取到并安装完插件后,通过 Windows/AssetBundle Browser下打开AB包管理面板 一共有三个面板。利用AssetBundleBrowser打包时,我们用的是Build面板,设置好之后点击[build]即可。

  • Configure面板 :能查看当前AB包及其内部资源的基本情况(大小,资源,依赖情况等)。

  • Build面板:负责AssetBundle打包的相关设置。

  • Inspect面板:用来查看已经打包后的AB包文件的一些详细情况(大小,资源路径等)。

2. 代码打AB包

通过代码打AB包,不需要设置prefab的AssetBundle名称。

cs 复制代码
private static void BuildAssetBundles(string tempDir, string outputDir, BuildTarget target)
 {
     Debug.Log("AB outputDir:"+ outputDir);
     Directory.CreateDirectory(tempDir);
     Directory.CreateDirectory(outputDir);
     
     List<AssetBundleBuild> abs = new List<AssetBundleBuild>();
     {
         var prefabAssets = new List<string>();
         string testPrefab = $"{Application.dataPath}/Prefabs/Cube.prefab";
         prefabAssets.Add(testPrefab);
         AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
         abs.Add(new AssetBundleBuild
         {
             assetBundleName = "prefabs",
             assetNames = prefabAssets.Select(s => ToRelativeAssetPath(s)).ToArray(),
         });
     }

     BuildPipeline.BuildAssetBundles(outputDir, abs.ToArray(), BuildAssetBundleOptions.None, target);
     AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
 }

二、YooAsset

AssetBundle 的管理和使用相对复杂,需要处理依赖关系、版本控制、加载策略等问题。因此,很多开发者会选择使用如 YooAsset 这样的资源管理框架来简化 AssetBundle 的使用,自动处理这些复杂的任务。

1. 准备工作

(1)官方文档

(2) 通过Packages清单安装YooAsset

直接修改Packages文件夹下的清单文件manifest.json:

{
  "dependencies": {
    "com.tuyoogame.yooasset": "2.1.0",
    ......
  },
  "scopedRegistries": [
    {
      "name": "package.openupm.cn",
      "url": "https://package.openupm.cn",
      "scopes": [
        "com.tuyoogame.yooasset"
      ]
    }
  ]
}

如果出现以下报错,说明Unity没有安装Windows Build Support(IL2CPP)模块,安装即可。

2.全局配置

Project窗体内右键 -> Create -> YooAsset -> Create YooAsset Setting可创建YooAssetSettings全局配置文件,该文件需放在Resources文件夹下:

  • Manifest File Name : 清单文件名称
  • DefaultYooFolderName:Yoo文件夹名称

3.资源配置

Project窗体内右键 -> Create -> YooAsset -> Create AssetBundle Collector Setting可创建该文件,需要注意的是一个工程只能有一个AssetBundleCollectorSetting。

重点关注Enable Addressable及Pack Rule打包规则:

(1)Enable Addressable

启用可寻址资源定位系统。启用后加载资源时可以不写全路径,只根据资源名称即可加载:

cs 复制代码
 YooAssets.LoadSceneAsync("scene_home");

(2)Pack Rule打包规则

规则可以自定义扩展。下面是内置规则:

  • PackSeparately 以文件路径作为资源包名,每个资源文件单独打包(比如场景文件)。
  • PackDirectory 以文件所在的文件夹路径作为资源包名,该文件夹下所有文件打进一个资源包。
  • PackTopDirectory 以收集器下顶级文件夹为资源包名,该文件夹下所有文件打进一个资源包。
  • PackCollector 以收集器路径作为资源包名,收集的所有文件打进一个资源包。
  • PackGroup 以分组名称作为资源包名,收集的所有文件打进一个资源包。
  • PackRawFile 目录下的资源文件会被处理为原生资源包。

4.资源构建

(1)Build Mode

  • ForceRebuild:强制构建模式,会删除指定构建平台下的所有构建记录,重新构建所有资源包。
  • IncrementalBuild:增量构建模式,以上一次构建结果为基础,对于发生变化的资源进行增量构建。

(2)Encryption

加密类列表。Build时选的加密类型要和加载资源时的解密类型一致,否则加载资源时会报错。比如Build时选择的加密方式为FileOffsetEncryption,则文件解密服务接口也需要为FileOffset类型:

cs 复制代码
    {
        ...
        var initParameters = new HostPlayModeParameters();//HostPlayModeParameters继承自    InitializeParameters
        initParameters.BuildinQueryServices = new GameQueryServices();//内置资源查询服务接口
        initParameters.DecryptionServices = new FileOffsetDecryption();//如果资源包在构建的时候有加密,需要提供实现IDecryptionServices接口的实例类。
        ...
    }
    /// <summary>
    /// 资源文件偏移加载解密类
    /// </summary>
    private class FileOffsetDecryption : IDecryptionServices
    {
        /// <summary>
        /// 同步方式获取解密的资源包对象
        /// 注意:加载流对象在资源包对象释放的时候会自动释放
        /// </summary>
        AssetBundle IDecryptionServices.LoadAssetBundle(DecryptFileInfo fileInfo, out Stream managedStream)
        {
            managedStream = null;
            return AssetBundle.LoadFromFile(fileInfo.FileLoadPath, fileInfo.ConentCRC, GetFileOffset());
        }

        /// <summary>
        /// 异步方式获取解密的资源包对象
        /// 注意:加载流对象在资源包对象释放的时候会自动释放
        /// </summary>
        AssetBundleCreateRequest IDecryptionServices.LoadAssetBundleAsync(DecryptFileInfo fileInfo, out Stream managedStream)
        {
            managedStream = null;
            return AssetBundle.LoadFromFileAsync(fileInfo.FileLoadPath, fileInfo.ConentCRC, GetFileOffset());
        }

        private static ulong GetFileOffset()
        {
            return 32;
        }

    }

5.YooAsset运行时

以官方提供的Space Shooter这个Demo为例,重新编写YooAsset相关的流程。先确保Demo能正常运行:

  • Space Shooter在导入完成后,打开YooAsset->AssetBundle Collector窗口。
  • 点击修复按钮,然后点击Save按钮保存配置,最后关闭窗口。
  • 找到Boot.scene场景启动游戏。

打开Boot.cs文件,注释掉 IEnumerator Start方法,在TestLoad方法中一步一步进行YooAsset资源的加载:

cs 复制代码
    private void Start()
    {
        StartCoroutine(TestLoad());
    }
    IEnumerator TestLoad()
    {
        ...
    }

(1)初始化

cs 复制代码
    IEnumerator TestLoad()
    {
        // 初始化资源系统
        YooAssets.Initialize();

        // 创建默认的资源包
        var package = YooAssets.CreatePackage("DefaultPackage");

        // 设置该资源包为默认的资源包,可以使用YooAssets相关加载接口加载该资源包内容。
        YooAssets.SetDefaultPackage(package);
        ...    
    }

(2)资源系统的运行模式

cs 复制代码
...
if (PlayMode == EPlayMode.EditorSimulateMode)
{
    var initParameters = new EditorSimulateModeParameters();//EditorSimulateModeParameters继承自InitializeParameters
    string simulateManifestFilePath = EditorSimulateModeHelper.SimulateBuild(EDefaultBuildPipeline.BuiltinBuildPipeline.ToString(), "DefaultPackage");
    initParameters.SimulateManifestFilePath = simulateManifestFilePath;
    yield return package.InitializeAsync(initParameters);
}
else if (PlayMode == EPlayMode.OfflinePlayMode)
{
    var initParameters = new OfflinePlayModeParameters();//OfflinePlayModeParameters继承自InitializeParameters
    initParameters.DecryptionServices = new FileOffsetDecryption();//需要补充这个
    yield return package.InitializeAsync(initParameters);
}
else if (PlayMode == EPlayMode.HostPlayMode)
{
    // 注意:GameQueryServices.cs 太空战机的脚本类,详细见StreamingAssetsHelper.cs
    string defaultHostServer = "https://static0.xesimg.com/project-yooasset/0130Offset";
    string fallbackHostServer = "https://static0.xesimg.com/project-yooasset/0130Offset";
    var initParameters = new HostPlayModeParameters();//HostPlayModeParameters继承自InitializeParameters
    initParameters.BuildinQueryServices = new GameQueryServices();//内置资源查询服务接口
    initParameters.DecryptionServices = new FileOffsetDecryption();//如果资源包在构建的时候有加密,需要提供实现IDecryptionServices接口的实例类。
    initParameters.RemoteServices = new RemoteServices(defaultHostServer, fallbackHostServer);//远端服务器查询服务接口
    var initOperation = package.InitializeAsync(initParameters);
    yield return initOperation;

    if (initOperation.Status == EOperationStatus.Succeed)
    {
        Debug.Log("资源包初始化成功!");
    }
    else
    {
        Debug.LogError($"资源包初始化失败:{initOperation.Error}");
    }
}
else if (PlayMode == EPlayMode.WebPlayMode)
{
    string defaultHostServer = "http://127.0.0.1/CDN/WebGL/V1.0";
    string fallbackHostServer = "http://127.0.0.1/CDN/WebGL/V1.0";
    var initParameters = new WebPlayModeParameters();//WebPlayModeParameters继承自InitializeParameters
    initParameters.BuildinQueryServices = new GameQueryServices();
    initParameters.RemoteServices = new RemoteServices(defaultHostServer, fallbackHostServer);
    var initOperation = package.InitializeAsync(initParameters);
    yield return initOperation;

    if (initOperation.Status == EOperationStatus.Succeed)
    {
        Debug.Log("资源包初始化成功!");
    }
    else
    {
        Debug.LogError($"资源包初始化失败:{initOperation.Error}");
    }
}

补充两个类:

cs 复制代码
    /// <summary>
    /// 资源文件偏移加载解密类
    /// </summary>
    private class FileOffsetDecryption : IDecryptionServices
    {
        /// <summary>
        /// 同步方式获取解密的资源包对象
        /// 注意:加载流对象在资源包对象释放的时候会自动释放
        /// </summary>
        AssetBundle IDecryptionServices.LoadAssetBundle(DecryptFileInfo fileInfo, out Stream managedStream)
        {
            managedStream = null;
            return AssetBundle.LoadFromFile(fileInfo.FileLoadPath, fileInfo.ConentCRC, GetFileOffset());
        }

        /// <summary>
        /// 异步方式获取解密的资源包对象
        /// 注意:加载流对象在资源包对象释放的时候会自动释放
        /// </summary>
        AssetBundleCreateRequest IDecryptionServices.LoadAssetBundleAsync(DecryptFileInfo fileInfo, out Stream managedStream)
        {
            managedStream = null;
            return AssetBundle.LoadFromFileAsync(fileInfo.FileLoadPath, fileInfo.ConentCRC, GetFileOffset());
        }

        private static ulong GetFileOffset()
        {
            return 32;
        }

    }


    /// <summary>
    /// 远端资源地址查询服务类
    /// </summary>
    private class RemoteServices : IRemoteServices
    {
        private readonly string _defaultHostServer;
        private readonly string _fallbackHostServer;

        public RemoteServices(string defaultHostServer, string fallbackHostServer)
        {
            _defaultHostServer = defaultHostServer;
            _fallbackHostServer = fallbackHostServer;
        }
        string IRemoteServices.GetRemoteMainURL(string fileName)
        {
            return $"{_defaultHostServer}/{fileName}";
        }
        string IRemoteServices.GetRemoteFallbackURL(string fileName)
        {
            return $"{_fallbackHostServer}/{fileName}";
        }
    }

(3)获取资源版本

cs 复制代码
    ...
    var package = YooAssets.GetPackage("DefaultPackage");
    var operation = package.UpdatePackageVersionAsync();
    yield return operation;

    if (operation.Status == EOperationStatus.Succeed)
    {
        //更新成功
        string packageVersion = operation.PackageVersion;
        Debug.Log($"Updated package Version : {packageVersion}");
    }
    else
    {
        //更新失败
        Debug.LogError(operation.Error);
    }

(4)更新资源清单

对于联机运行模式,在获取到资源版本号之后,就可以利用UpdatePackageManifestAsync更新资源清单了:

cs 复制代码
    ...    
    bool savePackageVersion = true;
    var package = YooAssets.GetPackage("DefaultPackage");
    var operation = package.UpdatePackageManifestAsync(packageVersion, savePackageVersion);
    yield return operation;

    if (operation.Status == EOperationStatus.Succeed)
    {
        //更新成功
    }
    else
    {
        //更新失败
        Debug.LogError(operation.Error);
    }

(5)资源包下载

cs 复制代码
    IEnumerator Download()
    {
        int downloadingMaxNum = 10;
        int failedTryAgain = 3;
        var package = YooAssets.GetPackage("DefaultPackage");

        //创建资源下载器,下载所有资源
        var downloader = package.CreateResourceDownloader(downloadingMaxNum, failedTryAgain);

        //没有需要下载的资源
        if (downloader.TotalDownloadCount == 0)
        {
            Debug.Log("TotalDownloadCount = 0");
          
            yield break;
        }

        //需要下载的文件总数和总大小
        int totalDownloadCount = downloader.TotalDownloadCount;
        long totalDownloadBytes = downloader.TotalDownloadBytes;

        //注册回调方法
        downloader.OnDownloadErrorCallback = OnDownloadErrorFunction;
        downloader.OnDownloadProgressCallback = OnDownloadProgressUpdateFunction;
        downloader.OnDownloadOverCallback = OnDownloadOverFunction;
        downloader.OnStartDownloadFileCallback = OnStartDownloadFileFunction;

        //开启下载
        downloader.BeginDownload();
        yield return downloader;

        //检测下载结果
        if (downloader.Status == EOperationStatus.Succeed)
        {
            Debug.Log("Finish");
        }
        else
        {
            Debug.Log("DownLoad Failed");
        }
    }

    private void OnStartDownloadFileFunction(string fileName, long sizeBytes)
    {
        Debug.Log("fileName:" + fileName + ",sizeBytes:" + sizeBytes);
    }

    private void OnDownloadOverFunction(bool isSucceed)
    {
        Debug.Log("isSucceed");
    }

    private void OnDownloadProgressUpdateFunction(int totalDownloadCount, int currentDownloadCount, long totalDownloadBytes, long currentDownloadBytes)
    {
        Debug.Log("totalDownloadCount:" + totalDownloadCount + ",currentDownloadCount" + currentDownloadCount + ",totalDownloadBytes:" + totalDownloadBytes + ",currentDownloadBytes" + currentDownloadBytes);
    }

    private void OnDownloadErrorFunction(string fileName, string error)
    {
        Debug.Log("DownloadError:" + fileName + ",error:" + error);
    }

(6)资源加载

cs 复制代码
//加载场景,启用可寻址功能(Enable Addressable)后,不用写全路径,直接写资源名称即可
string location = "scene_home";
var sceneMode = UnityEngine.SceneManagement.LoadSceneMode.Single;
bool suspendLoad = false;
SceneHandle handle = package.LoadSceneAsync(location, sceneMode, suspendLoad);
yield return handle;
Debug.Log($"Scene name is {handle.SceneObject.name}");

//加载预制体
AssetHandle handle0 = package.LoadAssetAsync<GameObject>("UIHome");//不用加后缀
yield return handle0;
GameObject go = handle0.InstantiateSync();
Debug.Log($"Prefab name is {go.name}");

//加载音频
AssetHandle handle1 = package.LoadAssetAsync<AudioClip>("music_background");
yield return handle1;
AudioClip audioClip = handle1.AssetObject as AudioClip;
GameObject audio = new GameObject("AudioSource");
audio.AddComponent<AudioSource>().clip= audioClip;
audio.GetComponent<AudioSource>().Play();

(7)完整TestLoad

cs 复制代码
    IEnumerator TestLoad()
    {
        //0.别忘初始化项目中这两个事件相关的类
        // 游戏管理器 注册场景事件
        GameManager.Instance.Behaviour = this;
        // 初始化事件系统
        UniEvent.Initalize();


        //1.初始化
        Debug.Log("1. 初始化");
        // 初始化资源系统
        YooAssets.Initialize();

        // 创建默认的资源包
        var package = YooAssets.CreatePackage("DefaultPackage");

        // 设置该资源包为默认的资源包,可以使用YooAssets相关加载接口加载该资源包内容。
        YooAssets.SetDefaultPackage(package);

        //2.资源系统的运行模式
        Debug.Log("2. 资源系统的运行模式");
        if (PlayMode == EPlayMode.EditorSimulateMode)
        {
            var initParameters = new EditorSimulateModeParameters();//EditorSimulateModeParameters继承自InitializeParameters
            string simulateManifestFilePath = EditorSimulateModeHelper.SimulateBuild(EDefaultBuildPipeline.BuiltinBuildPipeline.ToString(), "DefaultPackage");
            initParameters.SimulateManifestFilePath = simulateManifestFilePath;
            yield return package.InitializeAsync(initParameters);
        }
        else if (PlayMode == EPlayMode.OfflinePlayMode)
        {
            var initParameters = new OfflinePlayModeParameters();//OfflinePlayModeParameters继承自InitializeParameters
            initParameters.DecryptionServices = new FileOffsetDecryption();//需要补充这个
            yield return package.InitializeAsync(initParameters);
        }
        else if (PlayMode == EPlayMode.HostPlayMode)
        {
            // 注意:GameQueryServices.cs 太空战机的脚本类,详细见StreamingAssetsHelper.cs
            string defaultHostServer = "https://static0.xesimg.com/project-yooasset/0130Offset";
            string fallbackHostServer = "https://static0.xesimg.com/project-yooasset/0130Offset";
            var initParameters = new HostPlayModeParameters();//HostPlayModeParameters继承自InitializeParameters
            initParameters.BuildinQueryServices = new GameQueryServices();//内置资源查询服务接口
            initParameters.DecryptionServices = new FileOffsetDecryption();//如果资源包在构建的时候有加密,需要提供实现IDecryptionServices接口的实例类。
            initParameters.RemoteServices = new RemoteServices(defaultHostServer, fallbackHostServer);//远端服务器查询服务接口
            var initOperation = package.InitializeAsync(initParameters);
            yield return initOperation;

            if (initOperation.Status == EOperationStatus.Succeed)
            {
                Debug.Log("资源包初始化成功!");
            }
            else
            {
                Debug.LogError($"资源包初始化失败:{initOperation.Error}");
            }
        }
        else if (PlayMode == EPlayMode.WebPlayMode)
        {
            string defaultHostServer = "http://127.0.0.1/CDN/WebGL/V1.0";
            string fallbackHostServer = "http://127.0.0.1/CDN/WebGL/V1.0";
            var initParameters = new WebPlayModeParameters();//WebPlayModeParameters继承自InitializeParameters
            initParameters.BuildinQueryServices = new GameQueryServices();
            initParameters.RemoteServices = new RemoteServices(defaultHostServer, fallbackHostServer);
            var initOperation = package.InitializeAsync(initParameters);
            yield return initOperation;

            if (initOperation.Status == EOperationStatus.Succeed)
            {
                Debug.Log("资源包初始化成功!");
            }
            else
            {
                Debug.LogError($"资源包初始化失败:{initOperation.Error}");
            }
        }

        //3.获取资源版本:UpdatePackageVersionAsync
        Debug.Log("3. 获取资源版本");
        var operation = package.UpdatePackageVersionAsync();
        yield return operation;

        if (operation.Status == EOperationStatus.Succeed)
        {

            string packageVersion = operation.PackageVersion;
            Debug.Log($"Updated package Version : {packageVersion}");

            //4.更新资源清单:对于联机运行模式,在获取到资源版本号之后,就可以更新资源清单了:UpdatePackageManifestAsync
            //联机运行模式
            //通过传入的清单版本,优先比对当前激活清单的版本,如果相同就直接返回成功。如果有差异就从缓存里去查找匹配的清单,如果缓存里不存在,就去远端下载并保存到沙盒里。最后加载沙盒内匹配的清单文件。
            Debug.Log("4. 更新资源清单");
            bool savePackageVersion = true;
            var operation2 = package.UpdatePackageManifestAsync(packageVersion, savePackageVersion);
            yield return operation2;

            if (operation2.Status == EOperationStatus.Succeed)
            {
                //5.资源包下载
                Debug.Log("5. 资源包下载");
                yield return Download();
                //6.加载场景,启用可寻址功能(Enable Addressable)后,不用写全路径,直接写资源名称即可
                //YooAssets.LoadSceneAsync("scene_home");
                string location = "scene_home";
                var sceneMode = UnityEngine.SceneManagement.LoadSceneMode.Single;
                bool suspendLoad = false;
                SceneHandle handle = package.LoadSceneAsync(location, sceneMode, suspendLoad);
                yield return handle;
                Debug.Log($"Scene name is {handle.SceneObject.name}");
                //7.加载预制体
                AssetHandle handle0 = package.LoadAssetAsync<GameObject>("UIHome");//不用加后缀
                yield return handle0;
                GameObject go = handle0.InstantiateSync();
                Debug.Log($"Prefab name is {go.name}");
                //8.加载音频
                AssetHandle handle1 = package.LoadAssetAsync<AudioClip>("music_background");
                yield return handle1;
                AudioClip audioClip = handle1.AssetObject as AudioClip;
                GameObject audio = new GameObject("AudioSource");
                audio.AddComponent<AudioSource>().clip= audioClip;
                audio.GetComponent<AudioSource>().Play();

                //9.资源释放
                handle0.Release();
                package.UnloadUnusedAssets();
            }
            else
            {
                //更新失败
                Debug.LogError(operation.Error);
            }
        }
        else
        {
            //更新失败
            Debug.LogError(operation.Error);
        }

    }

6.YooAsset测试

无论是通过增量构建还是强制构建,都会生成一个以Build Version命名的文件夹,我们把这个文件夹统称为补丁包。补丁包里包含了游戏运行需要的所有资源,我们可以无脑的将补丁包内容覆盖到CDN目录下。

Host Play Mode下,YooAsset资源加载顺序是:先检查StreamingAsset目录,再检查同Library目录的的Yoo缓存目录(_data),最后才Host服务器下载。

Offline Play Mode下:检查StreamingAsset目录。

三、参考资料

相关推荐
向宇it12 小时前
【unity小技巧】unity 什么是反射?反射的作用?反射的使用场景?反射的缺点?常用的反射操作?反射常见示例
开发语言·游戏·unity·c#·游戏引擎
Heaphaestus,RC14 小时前
【Unity3D】获取 GameObject 的完整层级结构
unity·c#
芋芋qwq14 小时前
Unity UI射线检测 道具拖拽
ui·unity·游戏引擎
tealcwu15 小时前
【Unity服务】关于Unity LevelPlay的基本情况
unity·游戏引擎
大眼睛姑娘17 小时前
Unity3d场景童话梦幻卡通Q版城镇建筑植物山石3D模型游戏美术素材
unity·游戏美术
鹿野素材屋1 天前
Unity Dots下的动画合批工具:GPU ECS Animation Baker
unity·游戏引擎
小春熙子1 天前
Unity图形学之着色器之间传递参数
unity·游戏引擎·技术美术·着色器
虾球xz1 天前
游戏引擎学习第15天
学习·游戏引擎
Java Fans1 天前
在Unity中实现电梯升降功能的完整指南
unity·游戏引擎
GrimRaider2 天前
[Unity]TileMap开发,TileMap地图缝隙问题
unity·游戏引擎·tilemap