惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
LangChain Blog
C
Check Point Blog
博客园 - Franky
V
Visual Studio Blog
云风的 BLOG
云风的 BLOG
aimingoo的专栏
aimingoo的专栏
Microsoft Security Blog
Microsoft Security Blog
V2EX - 技术
V2EX - 技术
AI
AI
Hacker News - Newest:
Hacker News - Newest: "LLM"
Jina AI
Jina AI
S
Security @ Cisco Blogs
Security Archives - TechRepublic
Security Archives - TechRepublic
H
Hacker News: Front Page
H
Hackread – Cybersecurity News, Data Breaches, AI and More
O
OpenAI News
Attack and Defense Labs
Attack and Defense Labs
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
爱范儿
爱范儿
H
Heimdal Security Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
G
Google Developers Blog
G
GRAHAM CLULEY
V
V2EX
The Register - Security
The Register - Security
人人都是产品经理
人人都是产品经理
B
Blog RSS Feed
Schneier on Security
Schneier on Security
M
MIT News - Artificial intelligence
Stack Overflow Blog
Stack Overflow Blog
Help Net Security
Help Net Security
大猫的无限游戏
大猫的无限游戏
C
CERT Recently Published Vulnerability Notes
The GitHub Blog
The GitHub Blog
V
Vulnerabilities – Threatpost
The Last Watchdog
The Last Watchdog
J
Java Code Geeks
S
Secure Thoughts
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
量子位
NISL@THU
NISL@THU
K
Kaspersky official blog
Engineering at Meta
Engineering at Meta
T
Threatpost
Recent Commits to openclaw:main
Recent Commits to openclaw:main
宝玉的分享
宝玉的分享
Security Latest
Security Latest
T
The Exploit Database - CXSecurity.com
博客园_首页
A
Arctic Wolf

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角
Unity3D 游戏开发之使用 AssetBundle 和 Xml 实现场景的动态加载 - 元视角
飞鸿踏雪 · 2015-06-15 · via 元视角

各位朋友,大家好,我是秦元培,欢迎大家关注我的博客,我的博客地址是http://qinyuanpei.com。 今天我想和大家聊聊在 Unity3D 中关于场景的动态加载的问题。众所周知在 Unity3D 游戏开发过程中,因为受到游戏容量、平台性能和热更新等诸多因素的限制,我们可能无法将所有的游戏场景打包到项目中然后相对"静态"地加载,那么这个时候就需要我们使用动态加载的方式来将游戏场景加载到场景中。博主在研究了 Unity3D 动态加载的相关资料后发现,目前 Unity3D 中实现动态加载场景的方式主要有以下两种方式:

  • 使用 BuildStreamedSceneAssetBundle()方法将场景打包为 AssetBundle:这种方法将生成一个流式的.unity3d 文件,从而实现按需下载和加载,因此这种方式特别适合 Web 环境下游戏场景的加载,因为在 Web 环境下我们可以希望的是玩家可以在玩游戏的同时加载游戏。可是因为这种打包方式仅仅是保证了场景中的 GameObject 与本地资源的引用关系而非是将本地资源打包,因此从减少游戏容量的角度来说并不是十分实用,而且当我们使用 WWW 下载完 AssetBundle 后,需要使用 Application.Load()方法来加载场景,我们知道在 Unity3D 中加载一个关卡(场景)是需要在 BuildSetting 中注册关卡的,因此在使用这种方式动态加载的时候请注意到这一点。

  • 将场景内的所有物体打包为 AssetBundle 配合相关配置文件动态生成场景:这种方法的思路是使用一个配置文件来记录下当前场景中所有物体的位置、旋转和缩放信息,然后再根据配置文件使用 Instantiate 方法逐个生成即可。这种思路是考虑到需要在一个场景中动态替换 GameObject 或者是动态生成 GameObject 的情形,使用这种方法首先要满足一个条件,即:场景内所有的物体都是预制件(Prefab)。这是由 Unity3D 的机制决定的,因为 Prefab 是一个模板,当你需要动态生成一个物体的时候就需要为其提供一个模板(Prefab)。

如果你对这两种方式没有什么疑问的话,那么我觉得我们可以正式开始今天的内容了。既然今天的题目已然告诉大家是使用 AssetBundle 和 Xml 文件实现场景的动态加载,我相信大家已经明白我要使用那种方式了。好了,下面我们正式开始吧!

准备工作

在实现场景的动态加载前,我们首先要在本地准备好一个游戏场景,然后做两件事情:

  • 将场景内的所有 GameObject 打包为 AssetBundle
  • 将场景内所有的 GameObject 的信息导出为 Xml 文件 做这两件事情的时候,相当于我们是在准备食材和菜谱,有了食材和菜谱我们就可以烹制出美味佳肴了。可是在做着两件事情前,我们还有一件更为重要的事情要做,那就是我们需要将场景中使用到的 GameObject 制作成预制体(Prefab)。因为在博主的印象中,Unity3D 打包的最小粒度应该是 Prefab,所以为了保险起见,我还是建议大家将场景中使用到的 GameObject 制作成预制体(Prefab)。那么问题来了,当我们将这些 Prefab 打包成 AssetBundle 后是否还需要本地的 Prefab 文件?这里博主一直迷惑,因为理论上当我们将这些 Prefab 打包成 AssetBundle 后,我们实例化一个物体的时候实际上是在使用 AssetBundle 的 Load 方法来获取该物体的一个模板,这个模板应该是存储在 AssetBundle 中的啊!因为我的笔记本使用的是免费版的 Unity3D 无法对此进行测试,所以如果想知道这个问题结果的朋友可以等我下周到公司以后测试了再做讨论(我不会告诉你公司无耻地使用了破解版),当然如果有知道这个问题的答案的朋友欢迎给我留言啊,哈哈!这里就是想告诉大家要准备好场景中物体的预设体(Prefab),重要的事情说三遍!!!

将场景内物体打包为 AssetBundle

Unity3D 打包的相关内容这里就不展开说了,因为在官方 API 文档中都能找到详细的说明,虽然说 Unity5.0 中 AssetBundle 打包的方式发生了变化,不过考虑到大家都还在使用 4.X 的版本,所以等以后我用上了 Unity5.0 再说吧,哈哈!好了,下面直接给出代码:

[MenuItem("Export/ExportTotal----对物体整体打包")]
static void ExportAll()
{
  //获取保存路径
  string savePath=EditorUtility.SaveFilePanel("输出为AssetBundle","","New Resource","unity3d");
  if (string.IsNullOrEmpty(savePath)) return;
  //获取选择的物体
  Object[] objs=Selection.GetFiltered(typeof(Object),SelectionMode.DeepAssets);
  if (objs.Length < 0) return;
  //打包
  BuildPipeline.BuildAssetBundle(
    null,
    objs,
    savePath,
    BuildAssetBundleOptions.CollectDependencies|BuildAssetBundleOptions.CompleteAssets
  );
  AssetDatabase.Refresh();
}

将场景内物体信息导出为 Xml 文件

导出场景内物体信息需要遍历场景中的每个游戏物体,因为我们在制作场景的时候通常会用一个空的 GameObject 作为父物体来组织场景中的各种物体,因此我们在导出 Xml 文件的时候仅仅考虑导出这些父物体,因为如果考虑子物体的话,可能会涉及到递归,整个问题将变得特别复杂。为了简化问题,我们这里仅仅考虑场景中的父物体。好了,开始写代码:

[MenuItem("Export/ExportScene----将当前场景导出为Xml")]
static void ExportGameObjects()
{
  //获取当前场景完整路径
  string scenePath=EditorApplication.currentScene;
  //获取当前场景名称
  string sceneName=scenePath.Substring(scenePath.LastIndexOf("/")+1,scenePath.Length-scenePath.LastIndexOf("/")-1);
  sceneName=sceneName.Substring(0,sceneName.LastIndexOf("."));
  //获取保存路径
  string savePath=EditorUtility.SaveFilePanel("输出场景内物体","",sceneName,"xml");
  //创建Xml文件
  XmlDocument xmlDoc=new XmlDocument();
  //创建根节点
  XmlElement scene=xmlDoc.CreateElement("Scene");
  scene.SetAttribute("Name",sceneName);
  scene.SetAttribute("Asset",scenePath);
  xmlDoc.AppendChild(scene);
  //遍历场景中的所有物体
  foreach(GameObject go in Object.FindObjectsOfType(typeof(GameObject)))
  {
    //仅导出场景中的父物体
    if (go.transform.parent==null)
    {
      //创建每个物体
      XmlElement gameObject=xmlDoc.CreateElement("GameObject");
      gameObject.SetAttribute("Name",go.name);
      gameObject.SetAttribute("Asset","Prefabs/"+ go.name + ".prefab");
      //创建Transform
      XmlElement transform=xmlDoc.CreateElement("Transform");
      transform.SetAttribute("x",go.transform.position.x.ToString());
      transform.SetAttribute("y",go.transform.position.y.ToString());
      transform.SetAttribute("z",go.transform.position.z.ToString());
      gameObject.AppendChild(transform);
      //创建Rotation
      XmlElement rotation=xmlDoc.CreateElement("Rotation");
      rotation.SetAttribute("x",go.transform.eulerAngles.x.ToString());
      rotation.SetAttribute("y",go.transform.eulerAngles.y.ToString());
      rotation.SetAttribute("z",go.transform.eulerAngles.z.ToString());
      gameObject.AppendChild(rotation);
      //创建Scale
      XmlElement scale=xmlDoc.CreateElement("Scale");
      scale.SetAttribute("x",go.transform.localScale.x.ToString());
      scale.SetAttribute("y",go.transform.localScale.y.ToString());
      scale.SetAttribute("z",go.transform.localScale.z.ToString());
      gameObject.AppendChild(scale);
      //添加物体到根节点
      scene.AppendChild(gameObject);
    }
  } 

  xmlDoc.Save(savePath);
}

好了,在这段代码中我们以 Scene 作为根节点,然后以每个 GameObject 作为 Scene 的子节点,重点在 Xml 文件中记录了每个 GameObject 的名称、Prefab、坐标、旋转和缩放等信息。下面是一个导出场景的 Xml 文件的部分内容:

<Scene Name="DoneStealth" Asset="Assets/Done/DoneScenes/DoneStealth.unity">
  <GameObject Name="char_robotGuard_002" Asset="Prefabs/char_robotGuard_002.prefab">
    <Transform x="-18.99746" y="0" z="37.2443" />
    <Rotation x="0" y="0" z="0" />
    <Scale x="1" y="1" z="1" />
  </GameObject>
  <GameObject Name="fx_laserFence_lasers_003" Asset="Prefabs/fx_laserFence_lasers_003.prefab">
    <Transform x="-17.90294" y="1.213998" z="24.07678" />
    <Rotation x="0" y="90.00001" z="0" />
    <Scale x="1" y="1" z="3.735847" />
  </GameObject>
  <GameObject Name="door_generic_slide_001" Asset="Prefabs/door_generic_slide_001.prefab">
    <Transform x="-15.91264" y="-0.001293659" z="7.006886" />
    <Rotation x="0" y="90.00001" z="0" />
    <Scale x="1" y="1" z="1" />
  </GameObject>
  <GameObject Name="door_generic_slide_003" Asset="Prefabs/door_generic_slide_003.prefab">
    <Transform x="-7.910765" y="-0.001293659" z="37.01304" />
    <Rotation x="0" y="90.00001" z="0" />
    <Scale x="1" y="1" z="1" />
  </GameObject>

在这里我们假设所有的 Prefab 是放置在 Resources/Prefabs 目录中的,那么此时我们便有了两种动态加载场景的方式

  • 通过每个 GameObject 的 Asset 属性,配合 Resources.Load()方法实现动态加载
  • 通过每个 GameObject 的 Name 属性,配合 AssetBundle 的 Load()方法实现动态加载 这两种方法大同小异,区别仅仅在于是否需要从服务器下载相关资源。因此本文的主题是使用 AssetBundle 和 Xml 实现场景的动态加载,因此,接下来我们主要以第二种方式为主,第一种方式请大家自行实现吧!

动态加载物体到场景中

首先我们来定义一个根据配置文件动态加载 AssetBundle 中场景的方法 LoadDynamicScene

/// <summary>
/// 根据配置文件动态加载AssetBundle中的场景
/// </summary>
/// <param name="bundle">从服务器上下载的AssetBundle文件</param>
/// <param name="xmlFile">AssetBundle文件对应的场景配置文件</param>
public static void LoadDynamicScene(AssetBundle bundle,string xmlFile)
{
  //加载本地配置文件
  XmlDocument xmlDoc=new XmlDocument();
  xmlDoc.LoadXml(((TextAsset)Resources.Load(xmlFile)).text);
  //读取根节点
  XmlElement root=xmlDoc.DocumentElement;
  if (root.Name == "Scene")
  {
    XmlNodeList nodes = root.SelectNodes("/Scene/GameObject");
    //定义物体位置、旋转和缩放
    Vector3 position = Vector3.zero;
    Vector3 rotation = Vector3.zero;
    Vector3 scale=Vector3.zero;
    //遍历每一个物体
    foreach (XmlElement xe1 in nodes)
    {
      //遍历每一个物体的属性节点
      foreach(XmlElement xe2 in xe1.ChildNodes)
      {
        //根据节点名称为相应的变量赋值
        if (xe2.Name == "Transform")
        {
          var x = float.Parse(xe2.GetAttribute("x"));
          var y = float.Parse(xe2.GetAttribute("y"));
          var z = float.Parse(xe2.GetAttribute("z"));
          position = new Vector3(x, y, z);
        } else if (xe2.Name == "Rotation")
        {
          var x = float.Parse(xe2.GetAttribute("x"));
          var y = float.Parse(xe2.GetAttribute("y"));
          var z = float.Parse(xe2.GetAttribute("z"));
          rotation = new Vector3(x, y, z);
        } else {
          var x = float.Parse(xe2.GetAttribute("x"));
          var y = float.Parse(xe2.GetAttribute("y"));
          var z = float.Parse(xe2.GetAttribute("z"));
          scale = new Vector3(x, y, z);
        }
      }
      //生成物体
      var obj = bundle.Load(xe1.GetAttribute("Name"));
      GameObject go=(GameObject)GameObject.Instantiate(obj, position, Quaternion.Euler(rotation));
      go.transform.localScale=scale;
    }
  }
}

因为该方法中的 AssetBundle 是需要从服务器下载下来的,因此我们需要使用协程来下载 AssetBundle:

IEnumerator Download()
{
  WWW _www = new WWW ("http://localhost/DoneStealth.unity3d");
  yield return _www;
  //检查是否发生错误
  if (string.IsNullOrEmpty (_www.error)) 
  {
    //检查AssetBundle是否为空
    if (_www.assetBundle!=null)
    {
      LoadDynamicScene(_www.assetBundle,"DoneStealth.xml");
    }
  }
}

好了,现在运行程序,可以发现场景将被动态地加载到当前场景中:),哈哈

效果展示 效果展示

小结

  • 使用这种方式来加载场景主要是为了提高游戏的性能,如果存在大量重复性的场景的时候,可以使用这种方式来减小游戏的体积,可是这种方式本质上是一种用时间换效率的方式,因为在使用这种方法前,我们首先要做好游戏场景,然后再导出相关的配置文件和 AssetBundle,从根本上来讲,工作量其实没有减少。
  • 当场景导出的 Xml 文件中的内容较多时,建议使用内存池来管理物体的生成和销毁,因为频繁的生成和销毁是会带来较大的内存消耗的。说到这里的时候,我不得不吐槽下公司最近的项目,在将近 300 个场景中只有 30 个场景是最终发布游戏时需要打包的场景,然后剩余场景将被用来动态地加载到场景中,因为领导希望可以实现动态改变场景的目的,更为郁闷的是整个场景要高度 DIY,模型要能够随用户拖拽移动、旋转,模型和材质要能够让用户自由替换。从整体上来讲,频繁地销毁和生成物体会耗费大量资源,因此如果遇到这种情况建议还是使用内存池进行管理吧!

好了,今天的内容就是这样子了,如果大家对此有什么疑问,欢迎给我留言,谢谢大家!