[{"content":"《万物机》是一款从水、火、土和种子出发，通过组合投入、发现配方，让机器持续运转并逐步扩展物质宇宙的小游戏。\n未知实验会使用千问生成虚构物品的名称、描述与 emoji，游戏内持续显示“AI 合成内容”标识。国内版本不提供现金充值或道具内购；激励视频只在玩家主动选择时展示，并按界面明示的规则发放能量或机器加速奖励。\n游戏仍在开发和平台审核阶段，后续进展会在本站记录。\n","permalink":"https://ragdoll-ing.com/projects/matter-machine/","summary":"从基础物质出发，通过持续生产与未知实验发现配方的小游戏。","title":"万物机"},{"content":"版本与生效日期：2026-08-20\n运营者以小游戏平台和备案系统公示的主体为准。个人信息权利、客服与投诉请求可发送至 mattermachine.service@outlook.com。\n一、处理的信息 登录时，微信或 TapTap 向我们提供当前小游戏内的用户唯一标识。我们不读取头像、昵称、手机号、通讯录、位置、相册或麦克风。\n为保存玩法进度，我们处理随机玩家编号、平台类型、库存、配方发现、持续动作、能量、广告奖励次数、实验记录及创建、登录时间。登录令牌保存在平台缓存中，服务端只保存摘要，默认三十日失效；幂等命令记录默认保存七日。\n为防止异常请求，服务端瞬时读取连接来源地址，只保存不可逆哈希形成的短期频控计数，超过一天后清理。运行日志包含请求号、接口、状态、错误码、耗时和异常类型，不记录登录凭证、平台用户编号、实验投入正文、模型提示词或模型正文，默认保存七日。\n实验只向千问发送游戏内物质名称、描述和投入数量，不发送玩家编号或平台身份。AI 调用记录保存模型名称、Token 数、耗时和 HTTP 状态。\n玩家主动观看激励视频时，微信广告或 TapTap Dirichlet/TapADN 可能依其隐私规则处理设备、广告标识和诊断信息，实际范围以对应平台的隐私保护指引和广告后台公示为准。\n二、使用目的 上述信息仅用于登录识别、保存和同步游戏进度、结算广告奖励、生成实验内容、保障服务安全、排查故障和响应投诉。\n三、保存与保护 服务端位于中国境内。玩法记录在账号存续期间保存；注销后平台用户标识会被替换为无法回连原身份的随机墓碑标识，旧登录态全部撤销，历史玩法记录仅以匿名记录保留。法律法规另有保存要求的，从其规定。\n四、您的权利 您可以在游戏“关于与帮助”中重新查看本政策、联系客服并注销账号。注销不可撤销，再次进入会创建全新进度。查阅、更正、复制、限制处理或删除请求可发送至 mattermachine.service@outlook.com。\n五、未成年人 未成年人应在监护人指导下使用本游戏。我们不主动收集年龄、实名或其他敏感身份信息；平台依法另有要求时，以平台规则和监管要求为准。\n","permalink":"https://ragdoll-ing.com/privacy.html","summary":"万物机隐私政策。","title":"隐私政策"},{"content":"版本与生效日期：2026-08-20\n一、服务内容 本游戏提供物质生产、配方发现、离线结算和实验生成内容。国内版本不提供现金充值或道具内购。激励视频由玩家主动选择，完整观看后按界面展示的规则发放奖励；中途关闭、加载失败或当日次数用尽时不发放。\n二、AI 生成内容 未知实验会使用千问生成物品名称、描述和 emoji，玩家界面以“AI 合成内容”持续标识。AI 内容只用于虚构游戏世界，不构成科学、医疗、投资或其他专业建议。\n三、使用规则 不得利用修改客户端、自动化脚本、重放请求、伪造广告完播、攻击接口或其他方式获取不当奖励、干扰服务或影响其他玩家。我们可以对异常请求进行频控，对违法违规账号停止服务，并保留必要证据。\n四、账号与数据 账号按微信或 TapTap 当前小游戏内的用户标识自动建立，不跨平台合并。您可以在游戏内注销账号；注销会撤销全部登录态并切断平台身份关联，无法恢复原进度，再次进入将创建新账号。\n五、知识产权 游戏代码、界面、文案和自有资源由权利人保留权利。平台、广告和第三方组件继续遵守各自协议与许可证。未经授权不得复制、反编译、传播或用于其他商业服务。\n六、服务变化 为修复故障、满足平台或监管要求、调整赛季内容和保障安全，我们可能更新或暂停部分功能。协议或隐私规则发生实质变化时，将更新版本并重新取得确认。\n七、联系 客服、个人信息权利和侵权投诉可发送至 mattermachine.service@outlook.com。\n","permalink":"https://ragdoll-ing.com/agreement.html","summary":"万物机用户协议。","title":"用户协议"},{"content":"我的文章目录 UE5 ECS编程Gamplay目录总览 前言 虽然 Vibe Coding 已经不是什么新鲜词了，但目前网上的案例大多集中在 Web 和 App 领域，游戏圈里的深度分享相对少一些。\n我早几年就尝试过把 AI 引入工作流，但当时的工具链实在太难用，工具调用什么的经常失灵，综合效率甚至比不上去对话框里手动复制粘贴。但最近半年重新尝试后，我发现现在的模型能力在工程环境下已经可以出活了，能帮我解决掉大量繁琐的杂活。这篇分享只讲我一些经验，以及它到底怎么帮我省下的时间。\n警告：我目前是一个人在做独立游戏，因此会存在大量欺师灭祖的操作，很多东西是完全自己怎么爽怎么来，很多操作公司里大概是不让搞的。另外如有谬误欢迎指正。\n用到的工具和一些环境配置 我用的IDE是Rider，然后AI用的是Kilo Code插件+重活gemini+轻活GLM。当然，IDE无所谓，VSC也一样，用Rider主要是因为引擎用的UE。\nKilo：主流的Claude Code和CodeX什么的我当然也都用过，主要还是Kilo开源免费（当然API不是免费的）而且定制起来更加简单一些。而且我用的是IDE插件版本，不是CLI，主要是因为插件的集成度会比CLI要好一些，比如可以很简单的在IDE里跳转。\nGemini：一般Gemini用来做策划和一些需要较大上下文任务，最开始的时候我用GLM做策划，后来发现不太行，指令遵循有点太强了，缺乏策划应该有的脑洞，换到了Gemini之后发现emmmm好像都不太行，但Gemini胜在上下文大，可以直接让他用浏览器借（huan）鉴（pi）其他游戏的资料，GLM很容易就爆了。\nGLM：GLM就负责把文档翻译成脚本，或者脚本翻译成文档，因为我随时都有可能修改两者，而它就是用来处理这种简单的脏活累活，而且这种活由于上下文充裕且明确可以说成功率非常高。这样我就可以在一个时间只专注于一件事情，同时还可以避免诸如“文档，注释，代码命名，代码功能各说各的”这种事。用它主要是因为便宜的多，在我小半年的提示词迭代下+精细控制上下文的情况下他已经可以胜任了，当然更详尽的提示词配合opus4.5等更强的模型，用起来也确实会比GLM更加省心（自主能力更强，嘴遁次数少）一点（当然也贵的多，一个小时干掉几十块常有）。\nMCP我只配了playwright和一个计算器，拿来操作浏览器查资料和处理一些简单的数学计算（毕竟众所周知大模型不会算数也不会数数），别的大多也都用不太上，还会白占上下文。\n自定义的Agent我只做了一个主策划，大多数时候已经够用了，主策用来设计系统，创建表头，填表等等等等，最开始我还做了个数值策划用来调数据，后来发现直接做成主策的SKill就可以了，多出来个Agent完全没必要。\n表格和文档一律使用MarkDown，当然我知道在国内游戏开发有很多奇怪的路径依赖，比如Excel导表，然后写成吨成吨的导入导出插件来尽可能的让策划不写代码，成吨成吨的枚举等等，甚至在Excel里写lua等各种匪夷所思的操作。但对于当下的AI来说，这一大堆流程其实都可以砍掉了，具体流程后面会讲，而且Excel和Word这些办公软件都太重了，装个Skill支持Excel可能并不如直接修改MD来的简单快捷。\n至于画流程图这种，绝大多数MarkDown渲染器都支持Mermaid和PlantUML以及LaTeX，AI可以把这些写好，但大多数时候AI并不会主动写这些，需要在提示词中提及，如果不提及的话AI大概就不会用他们，而且我们完全不用学习如何写这些，只需要看写出来的结果对不对就可以了，并且AI也可以理解对应的结构化文本。\n这样下来，嘴遁的时候完全不需要离开IDE了。\n主要工作流程 策划先行，单一系统 如果最开始跟AI说那种一句话需求，你比方说：“给我写个RPG游戏策划案，要二次元的”，结果一定是叽里呱啦喷出来一大堆似是而非的东西，咋一看挺好，但细看会发现什么都没说，所有的内容都会处于一种流于形式的状态。虽然这个案子直接给程序Agent一样可以产出一些东西，但一定会把屎山开局就堆得很高，以至于后续完全无法维护。\n所以我的经验是，最开始先从单一核心系统开始，而不是直接设计整个架构。\n在这里引用猴叔打过的一个比方，一个游戏好比是一家医院，我们一次关注构建的系统只仅仅是一个科室，然后不同的科室通过一些“小纸条”链接起来，比如耳鼻喉科需要拍片子就给患者一个小纸条，凭此小纸条就可以去影像科拍片子了，这些不同的科室功能组合起来成为了一个医院。\n而游戏呢，移动系统处理移动的时候发生了碰撞，经过检查发现是子弹命中了玩家，这种时候我们就可以提交一个小纸条给伤害系统，各种不同的系统组合起来成为了一个游戏。\n当我们制作的时候先从单一系统开始，比如我们在设计移动系统（碰撞，挤开，沿边滑动等）是完全不考虑伤害系统的，也就是俗称的解耦，软件工程学同样适用于AI。\n设计的时候遵循ECS架构，这种结构对游戏逻辑非常友好，至于具体细节可以看我另一篇文章开发杂谈:在UE5使用ECS驱动游戏逻辑，这里不再赘述架构细节。\n如何抽象? 我在与一些朋友交流的时候会发现，有些时候我们会知道这个系统的功能，比如移动系统，显然移动系统是处理移动的，但是代码怎么写，抽象不出来怎么办？（虽然我从来没有过这种困惑，我一般读完需求脑子里自动出现流程图）根据我与朋友们的交流在这里可以分享一些经验，如何思考和抽象：\n针对游戏场景，我们最常见的一种情况就是Tick，说人话就是这一帧应该做什么，就比如还是最简单的移动系统，这种时候先考虑我一般会先考虑数据结构，也就是处理移动的时候通常需要哪些数据，或者说哪些数据很明显是跟移动相关的，此时我们可以拍脑袋即答：位置和速度，而且功能也可以具体化为这一帧的位置通常可以根据上一帧的速度和位置计算出来。这里我们简单的判定速度和位置是两个向量，那直接创建一个结构体就好了，它就是Component，然后System就一行location+=velocity*delta，这样一个单一核心系统就做好了。\n没错，刚开始就是可以这么简单的，完全不需要考虑其他，因为软件肯定是要迭代维护的，后面一定会再加东西，比如加速度，角速度，质量等，然后就是初中物理知识，我相信大家都会，绝大多数Gameplay代码都只需要计算这些初中内容就可以了。\n函数作为类的数据(委托,Lambda etc.),而不是类的功能(成员函数) 但软件开发还有一种情况比较特殊，就是函数。比如有一种情况，有一种角色buff每走10米恢复1HP（你怎么知道我最近在玩海克斯大乱斗）。很明显，这时移动跟一大堆东西耦合起来了，这种情况我直接讲最佳实践吧，就是在MovementComponent中添加一个多播委托OnMove，函数参数为角色实体和一个float delta（也就是当前这一帧移动了多少）在移动完成的时候广播这个委托，然后buff被添加的时候直接给这个多播委托绑定一个函数，函数的功能就是根据移动直接加血，buff被移除的时候删掉这个绑定。这样就等效于，buff系统侵入式的修改了移动系统的处理流程，但不影响移动系统的逻辑干净程度，也就是开洞，因为在移动系统的视角，我只是在移动完成的时候广播通知我移动了，至于我移动了会造成什么影响，移动系统并不关心。\n表头怎么设计 通常做完一个单一功能的运行时系统之后，就可以上手设计对应的表头了，一般表头可以直接从上述数据结构中提取出来，如何分辨也非常简单，不是运行时特有的数据基本就可以判定为填表数据，比如当前的血量肯定不是填表数据，这个一定是根据运行时环境计算出来的，而升一级加多少血就是填表数据。\n表结构一般来讲是分两种，一种我通常称之为二维表，显然就是Excel那种形式，另一种我大概会称之为json表或者XML表，区别就是填写各种各样的字段或者说键值对，而不是二维的形式。一般来讲第二种形式的表达能力会比单纯的Excel强很多，可以设计层次结构还可以塞数组（但好像大多数策划都不喜欢配json）\n但这些现在来看都有点拘于形式了，完全没有必要再像之前一样拉表，我们可以用更直接的方式，直接使用编程语言创建一个结构体实例然后直接指定对应的变量值，最后作为常量存放到一个单例里，OK，这就是填表数据了，本质的作用跟之前一样的同时表达能力最强，可以直接绑委托可以直接写算式甚至还能吃到编译期优化，AI和人用起来都更顺手。\n而且这跟编程语言是无关的，至于Lua，C#，还是TS什么都可以（我用的C++），AI都能写，而且直接写代码的话上下文也会更加精准，AI可以直接搜索函数引用等等等等。\n但是编程语言，又有一些太自由了，AI随随便便就给你胡乱继承组合那么几次就会导致屎山再次喷发，或者创建那么百八十个枚举（这些习惯都从哪学的），所以在配表这一环节是禁止AI创建新的结构体新的类的，只能在一些地方（比如游戏启动的时候初始化的时候）创建类的对象或者结构体的实例，然后对这些对象和实例进行修改后存为填表数据。\n题外话：但其实本来资深策划就都是会写代码的（尽管我认为只要是策划就应该会写），之前的方案大多更像是一种为了让那些不会写代码的策划也能工作，但现在策划的想法与实际落地的编程之间的鸿沟已经被AI抹掉了很多，这些流程都可以砍掉了，只需要看AI写的对不对就可以，甚至在严格规定写法+提供样例的情况下AI几乎不会配错（我目前在这套流程下AI几乎没怎么看见配错的情况了），连着一步都可以省掉。\n填表又怎么填？先做多，再精简 这一步很多时候我喜欢叫“拍脑袋”环节，AI也可以在这一方面进行加速。\n一般我的流程是先直接让AI写文档，AI很容易就喷出来一堆乱七八糟的东西，而且会有大量的模糊地带，比如他会写XX技能的效果是让目标受到电击，让目标感电，等等等等，但AI不会写感电是什么效果，被电了是不能攻击不能移动还是什么，都有什么区别完全没有。\n这种情况我会直接不管，先让AI继续喷射，比如先让AI做100个敌人，要求多样化。等AI喷射结束大概扫一眼删掉那些明显有问题的，然后让AI逐个词条进行详细设计，并且分门别类，去除重复的，注意一点在分门别类和去除重复的时候一定要给样例，不可重复逼供，不然AI一定会原地打转，他会认为自己设计的完美无缺，然后列出一项项成果给自己打出五星评价。\n就拿LOL举例子，击飞和眩晕这两个buff的区别就是击飞不能解除，也就意味着比如有一个bool变量bCanCleanse的值这两个buff一定是不一样的，这样AI才会往对应的方向思考，并真正有效的迭代文档。\n等文档精确度达到一定水平，就可以将其翻译为代码了，再接下来就是反复迭代的工作了。\n这样下来综合效率确实比之前要高不少，因为我很喜欢拍脑袋，然后做出来玩发现不满意又删掉，而且还喜欢拖延，曾经一个月就憋出来三四个敌人（不包含美术资源），导致我之前的开发效率很低，后来尝试工作流接入AI，一个星期就做了20多个敌人（中途抛弃的恐怕得有几百个），然后心安理得的玩了半个多月游戏，即便如此综合效率依然可以说翻倍。\nAI提示词迭代 好用的工具肯定是迭代出来的，任务完结的时候，我经常会根据我不爽的地方修改提示，目前发现最管用的方案就是提供样例，曾经我尝试过让AI自己根据任务历史修改提示词的方法让AI自动迭代，但是很快就会发现AI并不能从中吸取错误，而且还会让提示词重复一些废话浪费上下文，我推测是AI其实对任务的实际目标缺乏方向感（毕竟只是一个上下文匹配机），所以这一步还是得人来做。\n美术资产制作？ todo：\n原文发表于知乎。\n","permalink":"https://ragdoll-ing.com/posts/ai-game-development/","summary":"\u003ch2 id=\"我的文章目录\"\u003e我的文章目录\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"/posts/ue5-ecs-gameplay-index/\"\u003eUE5 ECS编程Gamplay目录总览\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e虽然 Vibe Coding 已经不是什么新鲜词了，但目前网上的案例大多集中在 Web 和 App 领域，游戏圈里的深度分享相对少一些。\u003c/p\u003e\n\u003cp\u003e我早几年就尝试过把 AI 引入工作流，但当时的工具链实在太难用，工具调用什么的经常失灵，综合效率甚至比不上去对话框里手动复制粘贴。但最近半年重新尝试后，我发现现在的模型能力在工程环境下已经可以出活了，能帮我解决掉大量繁琐的杂活。这篇分享只讲我一些经验，以及它到底怎么帮我省下的时间。\u003c/p\u003e","title":"开发杂谈：如何嘴遁（AI)做游戏"},{"content":"这里主要放一些 UE5 的 ECS 与 Gameplay 相关技术文章。\nQQ交流群：826681048\nUE5的ECS，MASS细节指北 开发杂谈：在 UE5 使用 ECS 驱动游戏逻辑 开发杂谈：如何嘴遁（AI）做游戏 UE5 MassActor 详解 MassLOD（待写） 原文发表于知乎。\n","permalink":"https://ragdoll-ing.com/posts/ue5-ecs-gameplay-index/","summary":"\u003cp\u003e这里主要放一些 UE5 的 ECS 与 Gameplay 相关技术文章。\u003c/p\u003e\n\u003cp\u003eQQ交流群：826681048\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"/posts/ue5-mass-notes/\"\u003eUE5的ECS，MASS细节指北\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"/posts/ue5-ecs-gameplay/\"\u003e开发杂谈：在 UE5 使用 ECS 驱动游戏逻辑\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"/posts/ai-game-development/\"\u003e开发杂谈：如何嘴遁（AI）做游戏\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"/posts/ue5-mass-actor/\"\u003eUE5 MassActor 详解\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eMassLOD（待写）\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003cp\u003e原文发表于\u003ca href=\"https://zhuanlan.zhihu.com/p/25696977704\"\u003e知乎\u003c/a\u003e。\u003c/p\u003e","title":"UE5 ECS编程Gamplay目录总览"},{"content":"UE5 ECS编程与Mass插件目录总览\n前言 本文将详细解释在Unreal5中MassGameplay插件是如何处理ECS与原有的Actor-Component系统结合的，以及一些典型需求的示例，由于网上资料基本没有官方文档也是潦草带过，如有疏漏或者谬误还请评论区指教。\n注：目前版本5.5.3\n重中之重MassAgentComponent 首先就是MassAgentComponent，打开MassAgentComponent.h，我们最先看到的是这样一个枚举\n看似很多，但其实核心就是两种状态，分别是木偶（Puppet）和Agent（代理），此处木偶的含义就是该Actor是MassEntity所创建，而代理则是Actor创建MassEntity，其要解决的核心问题就是生命周期和所有权的问题，因为逻辑上Actor和Entity是存在于两个世界里，两个世界需要交流与同步，有时Actor放在关卡里并给予了这个组件，则这时候就是代理状态即运行时先生成Actor，然后Actor再去创建Mass里创建对应的实体。也有时我们通过EntityManager等创建了一批Entity，这些Entity需要再渲染世界世界中有所表现，这时候就是Entity去World里创建Actor，这时候状态就是Puppet也就是Actor是Entity的木偶。之所以上面看着这么多只仅仅是因为部分操作是延迟执行的以及解决网络复制等问题，在使用上我们只需要区分这两种就可以了。\n正常状态比较简单，MassAgentComponent在注册的时候会向UMassAgentSubsystem提交创建申请，流程还是比较简单的在此不表，至于UMassAgentSubsystem是什么以及还有很多长得跟他很像的Subsystem都是干什么的下文会提。\nUMassVisualizationTrait：复杂的开始 各种样例里可能会提到UMassVisualizationTrait，这也是官方提供的工程中实际使用的让Entity能够被渲染出来的方案，但是，这个玩意真的超级复杂，一点开有一大堆东西等着我，让我们慢慢道来。\n首先是UMassVisualizationTrait，这个已经被弃用了，官方推荐使用它的两个子类也就是Movable和Stationary，看名字也知道一个能动一个不能动。\n其次是UMassDistanceVisualizationTrait，这个也被弃用了，意思大概跟上面的那个差不多，带Distance的和不带的区别就是他们两个的LOD参数结构不同\n这个是FMassDistanceLODParameters\n这个是FMassVisualizationLODParameters，就是不带Distance的VisualizationTrait用的\n他们两个的唯一区别就是一个只关心距离，一个还要关心平截头体，也就是相机的参数，并且DistanceLOD不关心数量上限，只要距离关系符合那就是对应的LOD\n在这里我还要提一嘴在UMassLODSubsystem里有这么一个参数bUsePlayerPawnLocationInsteadOfCamera，注释是这样写的\n简单解释一下就是说这个值为真的话，计算lod的时候就只会考虑玩家控制的Pawn的位置，而不是玩家相机的位置，这在俯视角游戏中与DistanceTrait的相性最好，而第三人称视角的还是用另一个吧\n而具体LOD如何计算怎么控制就是另一篇文章了，还是先回到UMassVisualizationTrait上来。\nUMassVisualizationTrait的配置内容\n看起来这些参数都很好理解，这里挑几个迷惑性比较强的解释一下。\n首先就是表示子系统，在后面我也会多次提到UMassRepresentationSubsystem，此处的表示的意思就是控制Entity以何种方式表示的意思，表示的方式有很多种，除了Actor也可以使用InstanceStaticMesh，如果需要额外的定制也可以继承UMassRepresentationSubsystem，官方就自定义了一个人群表示的子系统。\n但是他又很复杂的一点就是，他只负责干这个，连生成Actor也不是他干的，而是丢给了另一个UMassActorSpawnerSubsystem去做，包括移动，他也是不管的。\n而除了UMassRepresentationSubsystem，还有一个很奇怪的东西叫表示Actor管理类，也就是UMassRepresentationActorManagement，其中比较常用的就是\nActor生成前和生成后的委托\n这俩可以控制在当前实体的表示为Actor时，生成Actor对其进行一些修改，通过继承ActorManagement和Trait并覆写OnPostActorSpawn和BuildTemplate让Actor生成出来的时候把一些数据配进去，这样可以把需要配数据的地方集中在一起，还是蛮不错的。\n为什么我的Actor不动 接下来我就要讲一些更加深层次的东西，涉及到了一些真正的Mass与Actor交互的问题并给出解决方案\n首先刚才我们讲了，在Mass中，表现有两种方式分别是Actor和Instanced，然后根据设想，我们在EntityConfig中加上了UMassMovableVisualizationTrait以及Movement什么的，OK，应该可以了，然后游戏一运行，诶，为什么不动\u0026hellip;\u0026hellip;\n然后去扒官方的CitySample，也就是黑客帝国那个，发现在UMassMovableVisualizationTrait中配置的Actor里，也有MassAgentComponent，里面配了有几个叫什么Sync的东西，然后配了发现如下逻辑：\n有一个叫UMassAgentSyncTrait的东西，他是负责处理Mass和World之间同步的基类，所有负责同步的都是来自他的派生比如UMassAgentFeetLocationSyncTrait，而在他们的BuildTemplate中，会有这样一句BuildContext.GetMutableObjectFragmentInitializers()xxxxxxxxx。在这里面也配置了对应的Actor生成出来的时候要对Actor做什么的函数，但是该配置只在该SyncTrait添加进MassAgentComponent中时才会生效，也就是说，如果你在EntityConfig中配了xxxxSyncTrait+UMassMovableVisualizationTrait里配Actor，那个MutableObjectFragmentInitializers就直接不运行，其原因就在刚刚的UMassAgentSubsystem中，UMassAgentSubsystem每帧都会运行一个叫HandlePendingInitialization的函数，它的作用就是处理上一帧所有的MassAgentComponent的请求，该函数分为两个部分也就是下面这样\n这也对应了我最开始讲的代理和木偶两种状态，其中代理状态很正常，就是根据配置的Entity模板生成一个Entity，但木偶状态就不一样了，因为木偶状态是现有的Entity再有的Actor，本来Entity生成时会有一个模板，在生成的Actor中MassAgentComponent中还有一个模板，但两个模板只会调用MassAgentComponent的ObjectFragmentInitializers，就导致了一些问题的发生。\n而除了Actor，还有一种表现方式是InstancedStaticMesh，而控制他的方法在UMassUpdateISMProcessor与UMassStationaryISMSwitcherProcessor，分别是会动的与不会动的，但是这俩只能作为样例而存在（UMassStationaryISMSwitcherProcessor甚至还有一些逻辑上的小bug），因为工程中我们极大可能会用到ISM的CustomData来手动控制一些表现，官方的CitySample样例中人物的顶点动画也用到了这个，所以官方也额外写了一个Processor并禁用了这两个简单的，所以有需要就看看这俩和官方的样例。\nInstancedActor又是什么 如果经常翻代码或者看roadmap会发现官方还有一个插件叫InstancedActor，这个也跟Mass有点关系，它的核心目标是将关卡中的Actor近乎无痛的转成MassEntity，就比如场景中的树可能需要能砍，这样一个简单的StaticMesh就不能满足需求了，但是这种Actor又很多，全都在场景里也很卡，而InstancedActor就是为了解决这个而存在的。\n但是目前这个功能用起来很麻烦，要配置的很多，功能也不是很完整，但是很有潜力，啥时候更新的差不多了我再在这里更\n原文发表于知乎。\n","permalink":"https://ragdoll-ing.com/posts/ue5-mass-actor/","summary":"\u003cp\u003e\u003ca href=\"/posts/ue5-ecs-gameplay-index/\"\u003eUE5 ECS编程与Mass插件目录总览\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e本文将详细解释在Unreal5中MassGameplay插件是如何处理ECS与原有的Actor-Component系统结合的，以及一些典型需求的示例，由于网上资料基本没有官方文档也是潦草带过，如有疏漏或者谬误还请评论区指教。\u003c/p\u003e","title":"UE5 MassActor详解"},{"content":"快过年了，不要再讨论什么ue,unity,blender,maya,houdini之类的了。你带你的嗡嗡响的笔记本和大油头回到家并不能给你带来任何实质性作用，朋友们兜里掏出一大把钱吃喝玩乐，你默默的在家里摆弄你的破笔记本。亲戚朋友吃饭问你收获了什么，你说我做了一个在UE使用ECS架构的STG，亲戚们懵逼了，你还在心里默默嘲笑他们，笑他们不懂你日日夜夜撸模型拼UI有多开心，填表越填越丰富的成就感，也笑他们分不清楚逻辑帧和渲染帧，不知道什么是帧同步什么是状态同步，甚至连一个单子不过就是自函子范畴上的一个幺半群都不懂。你父母的同事都在说自己的子女一年的收获，儿子买了个房，女儿买了个车，姑娘升职加薪了，你的父母默默无言，说我的儿子忽悠了几个倒霉蛋天天一起窝出租屋里澡都不洗捣鼓那几个破电脑，精神状态也越来越不正常了。\n原文发表于知乎。\n","permalink":"https://ragdoll-ing.com/posts/indie-game-joke/","summary":"\u003cp\u003e快过年了，不要再讨论什么ue,unity,blender,maya,houdini之类的了。你带你的嗡嗡响的笔记本和大油头回到家并不能给你带来任何实质性作用，朋友们兜里掏出一大把钱吃喝玩乐，你默默的在家里摆弄你的破笔记本。亲戚朋友吃饭问你收获了什么，你说我做了一个在UE使用ECS架构的STG，亲戚们懵逼了，你还在心里默默嘲笑他们，笑他们不懂你日日夜夜撸模型拼UI有多开心，填表越填越丰富的成就感，也笑他们分不清楚逻辑帧和渲染帧，不知道什么是帧同步什么是状态同步，甚至连一个单子不过就是自函子范畴上的一个幺半群都不懂。你父母的同事都在说自己的子女一年的收获，儿子买了个房，女儿买了个车，姑娘升职加薪了，你的父母默默无言，说我的儿子忽悠了几个倒霉蛋天天一起窝出租屋里澡都不洗捣鼓那几个破电脑，精神状态也越来越不正常了。\u003c/p\u003e","title":"独游笑话"},{"content":"UE5 ECS编程与Mass插件目录总览\n前言 这篇文章描述了笔者在制作一款STG独立游戏时是如何使用UE5新的ECS插件来组织游戏逻辑的,主要缘由是因为ECS的核心思想与传统的OOP相差极大,为了给同团队的小伙伴解释这个\u0026quot;不是那么主流的架构\u0026quot;,而且我的具体实现方案也是相当的缝合怪,所以终成此文\n另外由于本人技术水平所限,很可能有大量冗余以及非最优解,大量违背祖宗的操作,仅供思路参考,如果觉得不正确的欢迎留言讨论,本文会根据开发进展保持长期更新修正\n而且由于我想要做到面面俱到,又以计算机专业大二学生也能看懂为基准,同时又使用了UE这种超级巨无霸作为引擎,导致文章内容较为繁琐,可能充斥着很多细枝末节的内容,看得懂的可以选择性跳过\n什么是ECS,ECS的优势在哪? 这里首先推荐一下猴叔的回答,在这里感谢他对我深入理解这个编程范式以及后续的修正提供了不小的帮助,后续也有很多编程手法来自猴叔,比如Buff机制与伤害系统\n延伸阅读：如何评价《守望先锋》架构设计？\nECS又称实体组件系统,是一种软件架构模式,这里解释一下纯ECS是怎么一回事\n-E是Entity,就是实体,更加形象的说法是一捆数据 其中只有一个唯一标识,除此之外什么都没有,其目的用于描述一捆数据,注意是一捆,后面我会解释为什么这样描述,用代码表示就是\nstruct Entity { int Index; } -C是Component,也就是组件 其表示一条数据,比如角色的HP,用简单易懂的代码表示就是\nstruct Component { } struct HitPointComponent:public Component { int HitPoint; } -S是System,也就是系统 表示一个操作,用于控制所有的核心处理流程,运作机制是筛选出所有符合条件的实体,并操作这些实体\n举个例子就是ForceActionSystem,检索所有含有ForceComponent和VelocityComponent的Entity,并做Velocity+=Delta*Force之类的运算,再比如SonicBoom(音爆),检索所有含有VelocityComponent和没有SonicBoomVFXComponent的Entity,给他们加上音爆特效,而不关心他们都是些什么\n纯ECS的一些简单的理念 ECS的组件可以动态增删,从而可以把一个东西完全变成另一个东西,由此带来的可扩展性和更好的维护性,因为system只关心,entity有什么,没有就不管\nSystem里只放逻辑,Component里只放数据,从而做到数据与逻辑解耦,Entity用来把一些Component捆到一起来组成一个对象\n某一个Component类型的所有对象直接塞进一个顺序存储的数据结构里比如稀疏数组或者什么数据结构里,如果是稀疏数组的话Entity就是数组下标,直接访问所有按下标访问所有Component数组即可得到一个对象(稀疏数组允许中间有空隙,如果下标无效即意味着Entity没有这个Component),当然也有基于archetype的,UE和unity使用的就是这种方法\nSystem处理的时候可以把Component们打包成一个Chunk,做成CPUcache友好的方式,可以极大的增加IO速度\nComponent之间不能通信,System也不行\nComponent没有函数,System没有状态\n同类型的Component一个Entity只能拥有一个\nSystem之间可以有一些顺序耦合,等等等等\n因此传说中的纯ECS可以做到以下的特性: -解耦,降低复杂度,可维护性好\n-代码可重用性,这个项目用完了下个项目还能接着用,即插即用\n-方便协作,因为每个程序员只需要关心自己的一亩三分地,尤其是在Component粒度切得足够小的情况下\n-cache友好,运行效率高\n-天然适合多线程运行,因为读写关系都确定了\n看起来爽的批爆,但是这些都是有代价的,落地的时候会有大量的妥协\n为什么要用UE来实践而不是Unity的Dots? 因为我个人更习惯c++一些,UE轮子多,省事,而且UE开源,不用打哑谜对暗号瞎猜,而且这俩哥们的ECS框架,可以说是一模一样的,UE的甚至更先进一点\n下面开始正片前置\n纵观UE的ECS插件Mass框架 官方文档连接:\nMass Entity 官方概览\n更加深入的讲解视频,有点硬\nMass 深入讲解视频\n首先ECS中的Component在UE中叫Fragment,因为名字已经被别的用了\n在这里讲一些需要注意的点:\n首先UE的ECS的世界和原本的Actor-Component,UObject什么的是完全两个世界,他们之间通过有限的同步沟通,并不是杂糅在一起的,所以我们尽可能的保证,干净整洁,尽量不做两个世界互相染指的事,方法后面会讲\nArchetype:它的意义是存储所有相同构成的Entity的数据,相同构成即为拥有相同的组件类型组合(这句话说起来好绕),这样Archetype直接存储对应Fragment的数组,所有数组的Num相同,想访问直接走下标,没有空隙,速度极快(赞赏),当然实现细节还是挺多的这里不多赘述可以直接看源码\n不过这种方法也是有代价的,当改变Entity的构成的时候,比如增加了新的Fragment,会导致整个Entity迁移到另一个Archetype里,会有一些额外的开销,最致命的是无法通过指针来保存一个Fragment或者Entity,因为他们随时会因为这个操作而无效,具体移动的函数签名为void FMassArchetypeData::MoveEntityToAnotherArchetype(const FMassEntityHandle Entity, FMassArchetypeData\u0026amp; NewArchetype)也就是下面截图的这里\nSharedFragment:也就是共享的片段,在内存中只保留一份,并且引擎提供了Const的版本,框架保证了部分Entity可以共享相同的片段,比如一些初始值,比如人类或者怪物的移动参数可以是ConstSharedFragment,如果你定义了部分实体是人部分是怪物挂有不同的ConstSharedFragment,那么这些Entity就会正确的寻找到对应的Fragment\nUMassEntityTraitBase:用来配置一些Fragment来组合成一个更高级的具备现实意义的逻辑特征,这个注释写的也挺明白的\nUMassProcessor:用来配置一个更高级的具备现实意义的逻辑行为,这个压根没写注释,有些解释说Processor是ECS的System在UE中的对应,但我通过观察源码发现似乎并不妥当,ECS中的S一般只会包含一次查询处理的操作,但在官方的比如LODProcessor进行了多次查询并计算,所以Processor之于System,更类似于Trait之于Component,而在UE中ECS的S对应的更像是EntityQuery,这个类怎么用看源码就可以了,大多都很简单\nUMassCompositeProcessor:UMassProcessor的子类,看名字也知道是一坨Processor的组合,其更大的意义在于提供了并行功能,他可以根据内部的Processor提供的Order信息\n也就是After和Before\n来将旗下的所有Procesoor画出一个DAG,交给UE的TaskGraph去处理,还是比较方便的\n但!代价也是有的,还记得上述的Archetype吗,这个假定了在Processor处理时Entity的构成是不会改变的,也就是说构成的改变会延迟处理,而UE在一帧中,只会根据Tick的每个阶段组各合成一个CompositeProcessor运行\n这会带来很多的麻烦,也就是说无法在同一帧内通过增删Fragment来传递信息,必须要等到下一帧,而有些操作可能一帧要做完,或者需要中途增删很多次,从而延迟好几帧,这明显是不对的,不过后续我还是找到了解决方案\n上面说的是ECS的内容,接下来讲一些如何把ECS跟UE原本庞大的系统接起来的\nSubsystem:子系统,当成守望先锋中说的单例看就可以了,因为在EntityQuery的依赖中也可以配置这个,它在这里存在的意义更像是守望先锋的ECS分享中的单例组件,而且他配置了多线程读写配置,方便画DAG\nUMassTranslator:专门用于ECS世界与UE原本世界进行同步的类,继承自UMassProcessor,源码注释挺清楚的\n然后我们又会发现一个问题,内存不连续了!这个没有办法的根本不可能避免,总有事物需要同步,而且uobject多线程操作会有一些影响GC什么的,所以在写入Uobject的时候需要标注为GameThread\n在这里分享一个比官方原版直接设置更快的MassToActorTransform同步方法,我精简了一些细节,来自Github,这也是我推荐阅读的文章,包含了一些细节\nMegafunk/MassSample\n官方原版:\nvoid UMassSceneComponentLocationToActorTranslator::Execute(FMassEntityManager\u0026amp; EntityManager, FMassExecutionContext\u0026amp; Context) { EntityQuery.ForEachEntityChunk(EntityManager, Context, [this](FMassExecutionContext\u0026amp; Context) { const TConstArrayView\u0026lt;FMassSceneComponentWrapperFragment\u0026gt; ComponentList = Context.GetFragmentView\u0026lt;FMassSceneComponentWrapperFragment\u0026gt;(); const TArrayView\u0026lt;FTransformFragment\u0026gt; LocationList = Context.GetMutableFragmentView\u0026lt;FTransformFragment\u0026gt;(); const int32 NumEntities = Context.GetNumEntities(); for (int32 i = 0; i \u0026lt; NumEntities; ++i) { if (USceneComponent* AsComponent = ComponentList[i].Component.Get()) { AsComponent-\u0026gt;SetWorldLocation(LocationList[i].GetTransform().GetLocation() + FVector(0.f, 0.f, AsComponent-\u0026gt;Bounds.BoxExtent.Z)); } } }); } 速度更快的版本,减少了直接SetWorldLocation一些额外的无用检测\nvoid UTransformMassToActorTranslator::Execute(FMassEntityManager\u0026amp; EntityManager, FMassExecutionContext\u0026amp; Context) { EntityQuery.ForEachEntityChunk(EntityManager, Context, [this](FMassExecutionContext\u0026amp; Context) { const TConstArrayView\u0026lt;FMassSceneComponentWrapperFragment\u0026gt; ComponentList = Context.GetFragmentView\u0026lt;FMassSceneComponentWrapperFragment\u0026gt;(); const TConstArrayView\u0026lt;FTransformFragment\u0026gt; TransformList = Context.GetFragmentView\u0026lt;FTransformFragment\u0026gt;(); const int32 NumEntities = Context.GetNumEntities(); for (int32 EntityIndex = 0; EntityIndex \u0026lt; NumEntities; ++EntityIndex) { if (USceneComponent* Component = ComponentList[EntityIndex].Component.Get()) { const FTransform\u0026amp; Transform = TransformList[EntityIndex].GetTransform(); FastUpdateComponentWorldTransform(Component, Transform); } } }); } void UTransformMassToActorTranslator::FastUpdateComponentWorldTransform(USceneComponent* InComponent, const FTransform\u0026amp; InTransform) { //Data InComponent-\u0026gt;SetComponentToWorld(InTransform); InComponent-\u0026gt;UpdateBounds(); //Physics if (UPrimitiveComponent* PrimitiveComponent = Cast\u0026lt;UPrimitiveComponent\u0026gt;(InComponent)) { FBodyInstance BodyInstance = PrimitiveComponent-\u0026gt;BodyInstance; FPhysicsCommand::ExecuteWrite(BodyInstance.ActorHandle, [\u0026amp;](const FPhysicsActorHandle\u0026amp; Actor) { FPhysicsInterface::SetGlobalPose_AssumesLocked(BodyInstance.ActorHandle, InTransform); }); } //Render InComponent-\u0026gt;MarkRenderTransformDirty(); //Children for (auto ChildrenComponent : InComponent-\u0026gt;GetAttachChildren()) { FTransform ChildrenWorldTransform = ChildrenComponent-\u0026gt;GetRelativeTransform() * InTransform; FastUpdateComponentWorldTransform(ChildrenComponent, ChildrenWorldTransform); } } 如何生成Entity:一般我们会调用UMassSpawnerSubsystem这个子系统中的函数\n而不是EntityManager中的CreateEntity或者什么的别的指令(虽然最后还是调他),因为在UMassSpawnerSubsystem中的DoSpawning做了一些额外的操作,其中第二个函数提供了类似构造函数的功能,SpawnData为参数,InitializerClass为初始化类,在源码中唯一一个用的到的地方为UMassSpawnLocationProcessor这个,来根据SpawnData中提供的Transform把Entity生成在对应的地方,而这个参数在Processor中由AuxData提供,仿照这个写法,也可以让Processor接受一些参数进行处理,写起来方便一些\nUMassSpawnLocationProcessor执行的一些细节,可以在Log里看到一些信息\n接下来我的ProjectileSpawnProcessor也用到了这种手法\n现在我们已经基本了解了UE的ECS基本调性,接下来我将施展大量违背祖宗的操作,真正的正片开始了\n在UE中使用ECS的一些改造 首先借鉴一些思想(比如函数式和数据驱动),对上述的纯ECS进行了改造,诞生出一个既不OOP也不FP更不DOP的ECS plus pro max,纯度堪比神罗,解决了部分ECS在工程中落地的痛点\n当然了软件开发没有银弹,一招鲜吃遍天是不可能的,解决一些问题的同时会引入其他代价\n在一个现成的引擎中使用ECS有哪些问题? 所有逻辑都在system,数据都在Component,切分的粒度不好把控,切的小了写起来又臭又长,切的大了又不方便复用 Entity之间的交互不好处理,两个Entity交互(比如碰撞处理)那肯定会造成cachemiss 与原本的OOP代码沟通十分不便 抽象要求较高,对大脑不友好 所以接下来就是我在UE中使用ECS的几条总结 首先就是ECS用于只处理游戏逻辑,与渲染逻辑隔离,可以使用类似unity的fixedupdate,如果考虑网络的话甚至还能顺带着方便做帧同步(不过我没试过) 基本放弃使用Actor-Component那些现成的处理逻辑,我曾经想在ecs里绑定胶囊体的碰撞回调,但是在两个世界中辗转腾挪太麻烦了\u0026hellip;. 使用ECS的主要目的是逻辑职责划分清晰复用性强,方便制定策划与程序都能理解的逻辑架构,至于性能,那点L2缓存命中率的提升真的不是特别重要 放弃纠结随机读写带来的cachemiss,这几乎是不可避免的 Fragment里可以放函数,因为函数也是数据,更学术点的叫法是Closure(闭包)或者lambda,在UE中对应的可以TDelegate,也可以是TFunction,当然c++的原生函数指针也可以 Fragment里放的函数只写简单的脚本,尽量不写复杂逻辑 Fragment尽量把关联性强的数据都放在一起,不要切的粒度太细,比如速度加速度加加速度都应该写进移动Fragment里,不然写起来真的很繁琐 Processor用于处理一整个不可分割的数据处理管线,一次流程可能进行多次查询,每次查询的Entity也可能不同,有点类似显卡的渲染管线 Processor可以不指定前后执行顺序,目前项目中没有指定过并且运行良好 Processor只要涉及了UObject,直接设置为gamethread,甚至多线程都不是特别重要,因为我使用ECS的目的不是处理海量实体 游戏的逻辑世界运行起来像是一张巨大的2D表,Processor们疯狂检索对应的数据并处理,与模拟的区别就在于游戏世界有玩家的输入,不过也可以看做新增的一列叫PlayerInput 策划填表的内容我一般放进ConstSharedFragment中,因为填表内容与ConstSharedFragment的特性简直完美符合(不可变,内存唯一),而运行时的信息则是复制一份放进Fragment中,非常契合 Entity之间的交互(如碰撞)延迟处理,例如检测到碰撞,将碰撞信息(谁撞了谁,碰撞角度什么的)存进SharedFragment或者Subsystem里,其他Processor或者直接在SubsystemTick的时候统一处理 有的时候我们会发现一个emmm\u0026hellip;.东西,比如背包里的一块石头,他究竟应该是一个Entity,还是单纯的就是一个struct,这种时候我会觉得还是看有没有扩展的必要来决定,作为一个Entity肯定是写起来更加麻烦的,但是有的时候作为Entity可能会更加灵活,比如我把石头丢掉地上,可能就只需要加个tag或者删掉引用就能搞定,而不需要在根据一个struct再去生成一个entity,或者类似饥荒那种的食物有新鲜度的设定,一个Processor就能处理所有的在地上的食物和在包里的食物,还能根据引用比如如果Entity的所有者是冰箱则损耗减半等,struct可能就不会那么方便 实际的例子 现在我举个实际的例子,由于各个游戏的数据处理流程可能千差万别,所以我以最常见的移动处理器和伤害系统为例,这另两个系统代表了两种典型处理,其区别在于移动处理器每帧处理所有会移动的Entity,或者模拟游戏中每帧处理所有会耗电的Entity,我称之为迭代处理,而伤害系统则是每帧清空缓存的所有伤害信息,或者碰撞系统也是每帧清空缓存的所有碰撞信息,我称之为交互处理\n目前项目是2D清版射击,所以游戏逻辑都是2D的,目前一次基于速度的移动逻辑大概可以抽象为以下几步\n根据力或者速度甚至是直接一个函数得出来一个本帧期望的delta delta结合场景的信息和阻挡响应函数(比如沿边滑动或者反弹)返回一个落点 移动,并根据修正过的delta更新速度 当然还有基于位置的\n还是得出来一个delta,但直接移动 根据约束(比如阻挡约束或者弹力约束)修正位置 根据修正过的位置更新速度 这里,我暂时使用基于位置的移动所以,我总结了四次查询,依赖和具体做什么如下\nF2DTransformFragment:只有一个2D的位置和一个旋转值\nF2DMovementFragment:里面包含了速度,加速度,力,和质量\nF2DShapeFragment:一个简单的2D图形信息,目前我只用到了圆形和矩形\nFShapeCellLocationFragment和UMassEnhancedShapeSubsystem:这俩放在一起说是因为在移动逻辑中,需要高频查询xx附近的xx,这种时候直接遍历时间复杂度达到了On的平方,所以肯定需要一个用于加速查询的全局数据结构来降低时间复杂度,所以Entity需要储存一个该Entity与加速结构的查询关系,至于这个数据结构是什么就根据情况来选择了,什么八叉树kd树或者直接简单点切格子都可以,在这里我选择了UE自带的THierarchicalHashGrid2D,这个数据结构要是细讲又能水一篇文章所以在此不表,只放张注释的截图\nFColliderHitDelegateFragment和F2DTransformConstraintsFragment:就是俩函数,存了发生如果碰撞的话发生的事情和一些位置约束(比如Block)\n而这些查询都做了什么呢,具体如下\nDirectMove:就是根据力什么的算速度然后直接移动,只关心移动和位置信息\nShapeSync:这个查询只负责将移动后的信息同步给上面的讲的加速结构,由于目前我的游戏项目中只有有形状的会考虑是否发生碰撞,所以这个查询到的Entity与上面的DirectMove查询到的Entity是不同的,会更少一些\nHit:顾名思义,看看谁撞了,把信息存下来延迟处理\nConstraintSolving:处理不同Entity之间的约束\n这样,一个2DMovementProcessor就完成了它的任务,所有的移动处理都走此Processor\n而伤害系统则是处理所有的伤害信息,伤害信息可能来自上面讲的碰撞,碰撞的时候会尝试根据两个Entity生成一个伤害信息并添加进伤害系统的待处理队列,每帧伤害系统都会将待处理的队列清空,而处理的时候会根据Entity携带的BuffList对伤害信息进行修改等等,这个猴叔讲的很清楚这里就不赘述了:\n猴叔：Gameplay 相关文章\n蓝图(lua)与c++之争 曾经我经常会因为这个逻辑究竟是应该写在c++里,还是蓝图(lua)里而纠结,而ECS解决了我这个痛点,我的方案是写在Processor用C++写,因为这个写成了之后几乎不太变动,而在Processor中触发的委托或者函数,使用蓝图或者脚本语言搞定,这里是需要经常迭代修改的,而且由于只需要关心\u0026quot;在这个时候发生了什么\u0026quot;,所以需要的心智负担也会轻很多,不太需要太多的上下文信息,策划也可以简单写写或者连连看,而具体怎么绑进去,反正最后都是函数指针那就都是体力活了在此不表\n原文发表于知乎。\n","permalink":"https://ragdoll-ing.com/posts/ue5-ecs-gameplay/","summary":"\u003cp\u003e\u003ca href=\"/posts/ue5-ecs-gameplay-index/\"\u003eUE5 ECS编程与Mass插件目录总览\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e这篇文章描述了笔者在制作一款STG独立游戏时是如何使用UE5新的ECS插件来组织游戏逻辑的,主要缘由是因为ECS的核心思想与传统的OOP相差极大,为了给同团队的小伙伴解释这个\u0026quot;不是那么主流的架构\u0026quot;,而且我的具体实现方案也是相当的缝合怪,所以终成此文\u003c/p\u003e","title":"开发杂谈:在UE5使用ECS驱动游戏逻辑"},{"content":"UE5 ECS编程与Mass插件目录总览\n目前版本5.3.2，本文章仅讨论MASS在与UE自身系统结合时遇到的问题，并不探讨ECS框架的哲学问题，如有疏忽错误，欢迎评论区指出\n不要在编辑器下复制EntityConfigAsset(UE5.3.2存在,UE5.4修了,但没完全修） 因为内部是使用一个guid来判断唯一性的，但是复制的时候会顺带把guid也复制了（WTF？）就会导致同时使用这俩的时候只会指向其中一个。这个问题会在不经意间触发，尤其是当我们想配置两个差不多的Entity的时候我们会本能的复制一份，从而触发。\n不要复制带有AgentComponent的Actor,问题跟上面那个guid一模一样\u0026hellip;.(5.4仍然存在) Processor自动注册到游戏阶段具体是怎么个流程？ 首先大多数流程在MassProcessingPhaseManager.h文件夹下，主要控制由FMassProcessingPhaseManager完成，而FMassProcessingPhaseManager则是放在了USimulationSubsystem下，OnWorldBeginPlay的时候自动启动相关逻辑。\n简单来讲，每一帧（主线程Tick，下同）分为很多个Phase，就是下面这些\n每一个Phase都会有一个对应的FMassProcessingPhase，每一个FMassProcessingPhase都只会对应一个UMassCompositeProcessor（重要），UMassCompositeProcessor看名字也知道是一个由很多个Processor组合起来的Processor，而这个FMassProcessingPhase继承自FTickFunction，所以它也就拥有了Tick的能力，他们是由上述的FMassProcessingPhaseManager注册到主循环中的。\n然后每一个Phase要做的事情就是，开始的时候检查一些修改，比如有没有新的Archetypes什么的，然后根据这些来判断是否需要重新构建对应的UMassCompositeProcessor，构建时收集所有的UMassProcessor子类，然后根据Processor其下OwnedQueries中依赖的Fragment（重要），配置好的执行依赖（FMassProcessorExecutionOrder配置）计算出一个依赖图，如下图这个\n，一切前置任务都搞定之后，再把Processor包成Task丢给TaskGraph去执行，等待他们执行完了，这一Phase才会结束。\n看似很美好，但是其中包含了无数的小细节需要注意，接下里我一个个讲\n更新CompositeProcessor依赖的时候不检查ConstSharedFragment（UE5.3.2） 当你想在FMassEntityQuery中只配置一个ConstSharedFragment，你会发现即使目前有对应的Entity，但是仍然无法触发对应的Processor，原因就在此。\n修改Entity对应的Archetype的时候需要延迟修改，那么延迟到什么时候？ 无论是否开启并行模式，都会在每一个Phase的结尾进行这个操作，包括AddRemoveFragment，销毁Entity等，这个操作是在游戏线程下的，在其注释中有提到，在Processor运行时，不应该对Archetype进行修改，所以在EntityManager中有这样一个函数\nProcessingScopeCount就是一个原子变量，在每一次有Processor进行操作时都会对这个变量+1，从而检查当前是否有正在进行的操作，来避免多线程bug。\n但是这会引入另一个问题，那就是无法在同一帧内通过添加或删除一些片段来协同其他Processor做部分逻辑，如果需要在同一帧内完成的操作只能延迟处理或者加一个Fragment，并在其中存入对应的上下文信息然后下一帧解决，或者通过Subsystem去做，增加不少代码量。\n不经意的耦合会触发多线程bug吗？ 很多时候会有Entity耦合其他Entity的情况，比如在官方的MassAvoidanceProcessors中，需要检查周围Entity的Velocity，在这种时候，官方给了一个FMassEntityView来解决对应的需求，如下\n但是，它不是线程安全的，并且提供的API允许写入，也就意味着，是有可能会出现多线程bug的，如果有时候发生了莫名的bug，可以在这个方向稍微看一下。\nFragment的初始化流程是怎样的,以及有什么暗坑需要注意? 官方的资料中,推荐了UMassObserverProcessor来初始化Fragment,但是在阅读源码的时候我们会发现与UE自身的UObject耦合时还有另一个东西可以用来初始化,比如这个\n上图中的Initializers会在UMassObserverProcessor之后调用，记住这个结论就行了，源码在UMassAgentSubsystem::HandlePendingInitialization(),其UMassObserverProcessor的调用时机是SpawnerSystem-\u0026gt;SpawnEntities(EntityTemplate, NewEntityCount, Entities);这一行结束的时候，由FEntityCreationContext的析构函数触发\n关于SpawnEntities也有一些可以说道的东西,比如下图这个函数可以以一种更加高级的方式控制Entity生成\n这里还有一个隐形坑,如果你实现了更高级的Processor来处理Entity,注意不要在这个你自定义的Processor中增删Fragment,因为这样会导致Archetype的变更,也就是Entity不在原来的地方了,而FEntityCreationContext又是存储的Archetype中的位置,不是EntityHandle,就会导致UMassObserverProcessor失效\n如果在ConstShareFragment中放置软引用,在构建模板时要记得加载再构建 在构建entity模板的时候会获取结构体然后计算一个hash值来存入模板中,用于判断模板是否相同,但是由于软指针在未加载的时候为空,但实际上我们的意图是有实际指向的对象,与此同时又忘记了加载,就会导致存入错误的模板\nECS,很奇妙吧\n原文发表于知乎。\n","permalink":"https://ragdoll-ing.com/posts/ue5-mass-notes/","summary":"\u003cp\u003e\u003ca href=\"/posts/ue5-ecs-gameplay-index/\"\u003eUE5 ECS编程与Mass插件目录总览\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e目前版本5.3.2，本文章仅讨论MASS在与UE自身系统结合时遇到的问题，并不探讨ECS框架的哲学问题，如有疏忽错误，欢迎评论区指出\u003c/p\u003e\n\u003ch2 id=\"不要在编辑器下复制entityconfigassetue532存在ue54修了但没完全修\"\u003e不要在编辑器下复制EntityConfigAsset(UE5.3.2存在,UE5.4修了,但没完全修）\u003c/h2\u003e\n\u003cp\u003e因为内部是使用一个guid来判断唯一性的，但是复制的时候会顺带把guid也复制了（WTF？）就会导致同时使用这俩的时候只会指向其中一个。这个问题会在不经意间触发，尤其是当我们想配置两个差不多的Entity的时候我们会本能的复制一份，从而触发。\u003c/p\u003e","title":"UE5的ECS，MASS细节指北"},{"content":"最近在研究Chaos系统时偶然间在UE中发现了一种巧妙的SOA数据结构,目前来看似乎没有文章讲过,所以特地分享一下,目前版本UE5.2preview1\nSOA和AOS 全称是Array of structures (AOS)和Structure of arrays (SOA),简单来说就是两种数据的组织方式,一种把整个结构体数据组织成数组,一种把结构体数据的每一项单独抽成数组组织成结构体,鉴于知乎上已经有大佬简单清晰的说明了他俩的区别与优劣,所以在这里贴个链接,更加详细的解释看大佬说的就可以了\n胡渊鸣：优化数据排布，让你的程序加速 4 倍！\n在这里我们只需要简单粗暴的下个结论,SOA在顺序访存上相较于AOS有着不小的优势,还可以通过SIMD等操作提高计算效率,所以在很多应用领域中都可以通过这种方式来对运行效率进行优化,但是SOA写起来会有一点点,怪怪的\n// Structure of arrays struct Vector{ float x[1000]; float y[1000]; float z[1000]; }; 显然在工程中直接这么写既不优雅也不方便维护,所以在UE中,FManagedArrayCollection出现了,概括一点说FManagedArrayCollection就是为了更方便维护SOA做的一个轻量化抽象,而在这里对应的数组,就是TManagedArray,相关源码在Engine/Source/Runtime/Experimental/Chaos/Public/GeometryCollection目录下,目前主要在Chaos下的GeometryCollection中有用到,十分有趣\n关于FManagedArrayCollection简单使用在声明的上方的注释中Epic已经非常热心的写明了\n简单来说,FManagedArrayCollection有数个Group,而在Group中,又包含有数个Attribute,每个Attribute都是一个数组,而FManagedArrayCollection为你提供的功能,就是保证单个Group中所有Attribute数组的元素数量,都是相同的.甚至还可以runtime添加Group和Attribute,看起来就像这样\nManagedArrayCollection{ Vertex{ Vector Position[90]; Vector VertexColor[90]; } Face{ IntVector Index[30]; } } //执行AddElement(10,\u0026#34;Vertex\u0026#34;)后 ManagedArrayCollection{ Vertex{ Vector Position[100]; Vector VertexColor[100]; } Face{ IntVector Index[30]; } } //再执行AddAttribute\u0026lt;Vector\u0026gt;(\u0026#34;Normal\u0026#34;, \u0026#34;Vertex\u0026#34;)后 ManagedArrayCollection{ Vertex{ Vector Position[100]; Vector VertexColor[100]; Vector Normal[100]; } Face{ IntVector Index[30]; } } 看起来是不是大大减少了SOA的维护成本和理解成本,接下来我们再浅析一下具体实现,如有谬误,还请大佬指正\n实现 首先目光转到FManagedArrayCollection包含的成员变量,暂不考虑序列化的情况下,有且只有三个成员变量分别是下图这仨:\n可以看到在这里是用FName来标识group的,FManagedArrayCollection本身存储了两个Map,一个用于存储GroupInfo,GroupInfo中目前就一个size信息,而另一个Map则存储了真正的SOA抽象数据,FKeyType直接采用的TTuple\u0026lt;FName, FName\u0026gt;,第一个FName为Attribute,第二个为Group(其实我想吐槽一下为什么不是反过来的,理论上来讲不应该Group是Attribute的上级么),至于FValueType则放一些相关数据,如下图\n其中关键数据就在于用了一个枚举来存储类型和一个指向真正存储数据的数组的指针,枚举存储类型信息的操作也很简单,点进去就会发现是一堆宏,然后声明的时候把类型列表inline进去\n声明的地方\n内联的一堆数据结构,如果后面需要扩展支持的类型的话直接在这里加就可以\n至于剩下的crud,dirty和序列化什么的,就都是老生常谈的内容了,随便打开一个看看就可以举一反三了,本质上都是根据上面那两个map来检索数据,在此不表\n用法 直接继承FManagedArrayCollection类便可,内部存储的数组可以直接写成成员变量,然后在构造函数里把他们全都塞进上述的两个map里,就像FGeometryCollection里写的那样\n塞进map里\n各种各样的声明\n至于TManagedArray,点进去我们可以看到其基类FManagedArrayBase注释中说明了这个数组的特点,即不允许随意缩放数组大小,仅相关联的Managers可以这么做,而Managers,仅有一个上面啰里啰嗦了这么多的FManagedArrayCollection,从而在防止了误操作带来的debug地狱\n一点点总结 其实这个东西自己写一个也不难,不过好在UE自己提供了这么个玩意,以后想要一个类似需求就可以参考这个实现,而且很大的一个优势就是轻量简单易懂,整套东西加起来也不过寥寥两三千行,大部分还都是喜闻乐见的内容,在如此简单轻量的前提的同时还是ue高效的破碎系统的基石,就很强\n谨以此文,分享这两天挖源码的收获,如有谬误,还请大佬在评论区指出\n原文发表于知乎。\n","permalink":"https://ragdoll-ing.com/posts/ue5-managed-array-collection/","summary":"\u003cp\u003e最近在研究Chaos系统时偶然间在UE中发现了一种巧妙的SOA数据结构,目前来看似乎没有文章讲过,所以特地分享一下,目前版本UE5.2preview1\u003c/p\u003e\n\u003ch3 id=\"soa和aos\"\u003eSOA和AOS\u003c/h3\u003e\n\u003cp\u003e全称是Array of structures (AOS)和Structure of arrays (SOA),简单来说就是两种数据的组织方式,一种把整个结构体数据组织成数组,一种把结构体数据的每一项单独抽成数组组织成结构体,鉴于知乎上已经有大佬简单清晰的说明了他俩的区别与优劣,所以在这里贴个链接,更加详细的解释看大佬说的就可以了\u003c/p\u003e","title":"(UE5)Chaos代码解析:FManagedArrayCollection,一种优雅的SOA结构实现"},{"content":"前言 很多时候我们可能需要在运行时编辑网格,或者干脆直接程序生成,大多数时候可能用UProceduralMeshComponent就搞定了,但是目前版本(5.1)UProceduralMeshComponent还处于实验阶段,虽说足够易用但有时候还是会有其他问题,这时候就需要深入底层,去研究一下从我们建立的indexbuffer,vertexbuffer,是如何扔给管线去渲染的,如何更新他们更加优雅,以及如果想实现自己的Vertex shader来配合管线等等,好了开始吧\n现成的轮子们 首先来总结下UE里现成的轮子都有哪些,以及他们都是怎么做的\nFDynamicMeshBuilder 首先是FDynamicMeshBuilder,其实如果研究mesh最简单的不是UProceduralMeshComponent而是FDynamicMeshBuilder,寥寥两三百行就把如何最简单的配置图元方法展示了出来,提供的方法也简明扼要,在类声明上面的注释里也有说明\n通过全局搜索也会发现该类在引擎中大部分都是用来绘制debug信息用的\n接下来我们直接跳到Draw函数,很明显它就是那个把信息丢给管线的函数,至于各个参数的含义在此不表,注释里写的蛮清楚了\n点进去,会发现Draw函数首先将一些渲染资源(vertexbuffer,indexbuffer,VertexFactory和PrimitiveUniformBuffer)调用RegisterDynamicResource扔给了PDI,这里的PDI其实就是一个我们需要绘制的视口接口类\n前两种buffer很好理解,index直接继承自FIndexBuffer,vertex由顶点位置颜色切向UV组成,而buffer类名里有Pooled,很明显跟普通的buffer相比添加了池化的功能,池化实现也很简单,从一个FDynamicMeshBufferAllocator类的池中分配资源\n而VertexFactory在这里的类型继承自FLocalVertexFactory和FDynamicPrimitiveResource,FDynamicPrimitiveResource只是一个接口,而FLocalVertexFactory是用的最多的基类,里面配置了一些VS和顶点布局之类的东西,比如上面组成顶点信息的几个值都可以在这里找到,除此之外还有那个PrimitiveUniformBuffer也在FLocalVertexFactory里有所说明,所以除非你需要自定义VS否则大多时候直接用FLocalVertexFactory就可以,更细节的内容这个有更好的文章讲过了在这里贴个链接就不做重复工作了\nCreating a Custom Mesh Component in UE4（Part 2）\n至于RegisterDynamicResource函数,点进去会发现他注册并初始化了对应的RenderResource,具体的函数就是FRenderResource::InitResource(),这个函数在下面要说的其他mesh类中也会被调用,无论形式如何,所有渲染资源的初始化都会最终走到这里\n初始化完渲染资源再往下就是配置图元了,把上面注册的渲染资源设置上,渲染选项配置正确,最后PDI-\u0026gt;DrawMesh,清空类下面的指针,因为已经交由PDI托管了.\nUProceduralMeshComponent 接下来是大名鼎鼎的UProceduralMeshComponent(PMC),比上述的FDynamicMeshBuilder稍复杂一点点,也是我们用的最多的程序生成mesh类,同时也是一个Component,这样还可以方便与现有的gameplaye框架结合组装actor\n首先我先简明扼要的描述一下UE的多线程渲染是怎么回事,结合PMC自顶向下的描述如何组织数据结构与数据同步,头次接触的朋友可能会一头雾水(比如我),结尾我会给出写的贼棒的大佬们的参考文章\nUE中将渲染线程从游戏线程中分离了出来(其实还有个RHI线程,不过这里不谈),两条线程并行,且拥有隔离开来的数据数据防止竞争,对应一下就是\nGameThread RenderThread UProceduralMeshComponent FProceduralMeshSceneProxy FProcMeshSection FProcMeshProxySection 类似的命名规则在源码中比比皆是,基本可以认为AnythingProxy就是游戏线程Anything在渲染线程的替身(砸瓦鲁多!),分别存储了在各自线程中各自需要的数据,比如上表中FProcMeshSection就比FProcMeshProxySection多放了一个包围盒,包围盒显然不是渲染线程该关心的数据,而FProcMeshProxySection则相较于FProcMeshSection多了Material和VF等数据,而且就算是相同的buff数据,存储方式也是不同的\n接下来再说生命周期,游戏线程里的数据自然是我们创建组件的的时候创建,而渲染线程的资源则是引擎帮我们自动创建好的,通过覆写CreateSceneProxy()函数来创建不同的代理(第一行的宏是测性能用的,这里我们并不关心)\n通过调用MarkRenderStateDirty()来重新生成代理类,剩下的就不需要我们操心了\n另外其实很多时候不需要调用MarkRenderStateDirty(),通过查找在UActorComponent中的注释,我们会发现还有其他的标记方法:\n在其他源码中,如UCableComponent,我们会发现标记为MarkRenderDynamicDataDirty(),这样可以不用整个更新,改为这个标记后调用的就是CreateRenderState_Concurrent了,这样在这个函数中只需要把新的数据copy过去,可以省下部分操作,其余的MarkRenderTransformDirty和MarkRenderInstancesDirty同理\n让我们回到FProceduralMeshSceneProxy的构造函数中,第一句注释就会看到先把所有的section从游戏线程copy过来,顶点数据copy的时候甚至还用的是上面FDynamicMeshBuilder中相同的顶点类型的同时甚至还有一个专门用于转换的函数\u0026hellip;\u0026hellip;..后续会发现之所以用它是因为FStaticMeshVertexBuffers提供了基于这个类型初始化的接口InitFromDynamicVertex,而这个函数其实也不复杂,不愿绕圈圈直接用FProcMeshVertex其实也行\u0026hellip;..\n再接下来就是渲染资源初始化了,其实调用的还是FRenderResource::InitResource(),注意这个函数可以在游戏线程执行,而且这个代理类的构造函数是在游戏线程执行的,不过一样还是抛给渲染线程做的\n再往下就是设置材质可见性什么的了\n那么如何把数据抛给管线呢?\n我们可以看到在GetDynamicMeshElements()这个函数中有曾相识的操作,配置buffer和vf,然后渲染类型,最后AddMesh(),跟上面的FDynamicMeshBuilder如出一辙.\n那我想要更新这些数据来做到运行时编辑呢,或者新增一个Section?\n我们可以从PMC中看到UpdateMeshSection和CreateMeshSection两类函数,CreateMeshSection函数还是在操作PMC中的Section,并在最后一行看到了我们前面曾经看到过的MarkRenderStateDirty(),也就是重新创建一个代理类来满足新增的Section,从而实现新增.\n而更新Section则不同,首先还是更新PMC中的数据,然后打一个更新包,更新包中包含了需要的数据,把更新包压入渲染线程\n而渲染线程更新的逻辑则是先把代理类中的数据更新好,然后会出现一大长串的copy,用于向RHI提交我们的tertex四君子,最后RHI再把数据塞进显存\n显然这个过程其实是有很多问题的,比如这里数组长度其实是不可增的,如果需要增加的话肯定会溢出,所以只能整个删掉重来,或者当只需要在一个大大的mesh里更新一点点数据数据也是整个copy\n而且万一buff很大且多,以DX11为例,在RHILockBuffer的时候会flush,所以我在测试的时候大批量更新会顿卡,DX12同理(感谢群里大佬答疑)\n所以如果需求更加特别导致PMC不能满足需求,UE还给我们提供了一个更加离谱的东西,那就是接下来要说的了\nUDynamicMeshComponent 其实这个组件的使用在已经有非常专业的文章来讲解了,翻译的版本也在知乎上,UE5里只是改了个名字\nRuntime Mesh Component 相关文章\n那么在这里前人说过的内容就不多说了,主要还是来看一下上述问题在这里是如何解决的\n在对应的FDynamicMeshSceneProxy中,所有的buffer,vf什么的都放进了FMeshRenderBufferSet这个对象里\n然后点进去会发现 它给予了用户更多可操控的手段,比如仅更新被更改过的buffer\n也可以当buffer不够大的时候使用UpdateRHI()释放调RHI资源重新申请,经测试居然可以规避掉上述顿卡的问题,不过之前曾经怀疑频繁申请释放会不会造成显存碎片等问题,后经资料查询+源码对比会发现,在DX环境下UE会使用BuddySystem来分配upload buffer和default buffer,可以在Engine\\Source\\Runtime\\D3D12RHI\\Private\\D3D12Allocation.h下看到,所以应该不会有很大的问题\n至于更新大范围内的小部分数据还有UOctreeDynamicMeshComponent可供选择,不过需要自行维护一个八叉树,该类没有提供更好的方法,如何使用就只能自己扣源码了,Google上这个类的信息甚至连一页都搜不出来,不过源码在对整体有把握之后其实难度已经不高了,剩下的就是发挥工匠精神的时候\n自定义UPrimitiveComponent 经过上面的探索,会发现其实自定义一个PrimitiveComponent也不是很难,综合一下抄一抄很容易就能跑起来,不过具体值不值得还是要再商酌一下的,这里贴一个自制的很简单的类MinecraftVoxelComponent,很多东西没处理凑活着看吧,祝生活愉快\n参考 图形编程介绍\n剖析虚幻渲染体系-开篇说明 - 0向往0 - 博客园\n原文发表于知乎。\n","permalink":"https://ragdoll-ing.com/posts/ue5-runtime-mesh/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e很多时候我们可能需要在运行时编辑网格,或者干脆直接程序生成,大多数时候可能用UProceduralMeshComponent就搞定了,但是目前版本(5.1)UProceduralMeshComponent还处于实验阶段,虽说足够易用但有时候还是会有其他问题,这时候就需要深入底层,去研究一下从我们建立的indexbuffer,vertexbuffer,是如何扔给管线去渲染的,如何更新他们更加优雅,以及如果想实现自己的Vertex shader来配合管线等等,好了开始吧\u003c/p\u003e","title":"(UE5)运行时mesh生成与编辑"},{"content":"引言 国际惯例先上代码\nhttps://github.com/other3000/AbundantVoxel\ngithub.com/other3000/AbundantVoxel\n如标题所示,本系列文章将简单阐述一种大规模(2^3^16)runtime可编辑体素的尝试,体素值为MaterialID(uint32),限于篇幅原因本文重点先放在数据结构方向且目前只在CPU上,渲染方案目前还在搞,具体思想参考论文\nhttps://graphics.tudelft.nl/Publications-new/2020/CBE20/ModifyingCompressedVoxels-main.pdf\ngraphics.tudelft.nl/Publications-new/2020/CBE20/ModifyingCompressedVoxels-main.pdf\n该插件是基于该论文并稍微修改部分实现后在UE中的简单尝试,仅作为思考笔记记录下来,后续该文章也有可能会改不少,如有纰漏还请各位大佬指正,有相关想法的也欢迎评论交流\n数据结构 其算法思路相较传统的八叉树划分,该数据结构将Location转换为了index,然后相同的节点统统合并,这样就变成了整张DAG图存储的其实是所有变化的区域怎么变化的,从而实现了更大程度的压缩,但是弊端在于每次查找必须细分到底\nNode DAG中的节点有两种类型,一种为InternalNode,一种为DataNode,两种节点大小与组织方式相同,数据编码方式如上图所示,两者的区别在于InternalNode只存储下一个节点索引,而DataNode存储的是对应的那个体素实际的数据,所以目前每个体素存储的数据都是一个uint32,这个uint32可以是很多东西,比如当前是几号材质,或者单纯的一个指针\n所有节点大小均为9*4Byte,前8个uint32就是上述的数据,最后一个uint32用于存储引用计数,表示这个节点被引用了多少次,注意这个引用计数存储的不是在上一层有多少个指针指向了该节点,而是算上了上面所有层的一路乘下来的引用计数,这么说可能有点抽象所以我举个例子:\n假设我让一个8x8x8的区域内体素值都为114,那么我们就可以得到一个层级关系如下:\nNodePool[0] = InternalNode={Data[8]={1,1,1,1,1,1,1,1},Refcount=1};\nNodePool[1] = InternalNode={Data[8]={2,2,2,2,2,2,2,2},Refcount=8};\nNodePool[2] = DataNode={Data[8]={114,114,114,114,114,114,114,114},Refcount=64};\n所以如果我们想要获得有多少个存储数据为114的体素,那么就只需要找到所有包含114的DataNode,然后把所有找到的DataNode里114体素的数量*Refcount全都加起来,就可以获得一共有多少了114号体素了,比如上面的示例,包含114的DataNode就一种,这一种DataNode里包含了8个114号体素了,所以一共有8x64=512个114号体素.\nclass DAG TArray\u0026lt;Node\u0026gt; NodePool：没什么可说的,就是一个超大的数组,初始化DAG的时候直接给它分配0xFFFFF个元素,避免内存碎片 TQueue\u0026lt;uint32\u0026gt; FreeIndex：用来放引用计数归零的索引,提高内存复用 TMap\u0026lt;uint32[8], NodeIndex\u0026gt; InternalMap：同下 TMap\u0026lt;uint32[8], NodeIndex\u0026gt; DataMap：用来查询节点的索引,也方便后续快速插入,hashFunction参考https://github.com/aappleby/smhasher/wiki/MurmurHash3 uint8 MaxDepth:最大深度,决定了从根节点往下查询多少次达到数据节点,也决定了体素索引的最大值,计算方法为 $2^{MaxDepth+1}-1$ ,比如最大深度为15,则体素索引的最大值为65535 体素的增删改查 思路流程图,图片来自论文\n具体步骤有一点绕,我还是比较推荐看源码或者看上面的图,在这里只描述部分操作细节以及为什么这么做\n1.向下查找时如何根据输入向量获得对应子树 #define GET_BIT(x,y) ((x)\u0026gt;\u0026gt;(y)\u0026amp;1) FORCEINLINE uint8 GetChildIndex(const FIntVector\u0026amp; vector, uint32 level) { uint8 tempx = GET_BIT(vector.X, level); uint8 tempy = GET_BIT(vector.Y, level) \u0026lt;\u0026lt; 1; uint8 tempz = GET_BIT(vector.Z, level) \u0026lt;\u0026lt; 2; return tempx | tempy | tempz; } 规则的数据结构优美之处,可以通过位运算快速拿到,具体操作也比较简单,直接获得对应位然后XYZ做个偏移就解决了\n2.插入的时候快速判断当前这个节点有没有 所以我们用了一个Map存储,Key为节点,Value为这个节点的索引,如果插入的时候检查hash有的话则直接返回这个节点的索引,并且该节点引用计数+1,节约了大量遍历的时间\n3.什么时候该把这个节点从池子里扔掉呢 类似上图中的最后一步,最上面左边的根节点经过修改后重新插入了一个新的节点,旧节点的引用计数-1,当为0的时候自然就可以放进回收队列里并在map中清除它,从而实现了内存复用\n一些应用测试 目前是模拟了地质分层,如上图,假设16种不同的岩石与材质一一对应,然后从下到上一层层糊上去,糊的时候加点Noise,在MaxDepth=15也就是整个65536^3的情况下占用内存大约在20m左右,估计多加几种材质还会翻翻,但是在这个数量级下我觉得已经可以了\n后续可以改进的方向 uint32存储引用计数有点浪费,以后在最后这个uint32里可能还会加上一些其他数据,比如当前节点的深度或者顺带记录一下该节点是中间节点还是底层节点或者多线程锁什么的 可以加多生产者单消费者的多线程优化,异步操作 像OpenVDB那样的访存cache,这样就可以一步到底,省下多次随机访问的性能消耗 亦或者可以把InternalNode加宽,类似B树,进一步降低向下访问的消耗 一些废话:\n类似Minecraft?\n是的,我想构建一种可以实现更大规模的方法\nMinecraft体素也挺多的啊,它是怎么做的?\n一个chunk就是一个单纯的int[16][16][256]的数组,然后不同chunk走hash\n最后,感谢阅读\n参考: https://graphics.tudelft.nl/Publications-new/2020/CBE20/ModifyingCompressedVoxels-main.pdf Milo Yip：以体素建构三维游戏世界 探寻可能：体素引擎，来自元宇宙的基本问题2·实现路径 https://www.cse.chalmers.se/~uffe/dolonius2017i3d.pdf https://github.com/aappleby/smhasher/wiki/MurmurHash3 https://github.com/Phyronnaz/HashDAG https://github.com/DavidWilliams81/cubiquity 原文发表于知乎。\n","permalink":"https://ragdoll-ing.com/posts/ue5-runtime-voxel-dag/","summary":"\u003ch2 id=\"引言\"\u003e引言\u003c/h2\u003e\n\u003cp\u003e国际惯例先上代码\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/other3000/AbundantVoxel\"\u003ehttps://github.com/other3000/AbundantVoxel\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003egithub.com/other3000/AbundantVoxel\u003c/p\u003e\n\u003cp\u003e如标题所示,本系列文章将简单阐述一种大规模(2^3^16)runtime可编辑体素的尝试,体素值为MaterialID(uint32),限于篇幅原因本文重点先放在数据结构方向且目前只在CPU上,渲染方案目前还在搞,具体思想参考论文\u003c/p\u003e","title":"(UE5)关于runtime edit voxel的尝试1.用DAG压缩体素"},{"content":"我是 Ragdolling，一名游戏开发者。\n这里主要记录游戏开发、UE5、ECS、AI 和独立开发实践。文章首先要对未来的自己有用：把真实问题、实现思路和踩坑过程说明白，也希望偶然来到这里的人能少绕一点路。\n你也可以在知乎找到我。\n联系 网站与游戏相关问题可以发送至 mattermachine.service@outlook.com。\n","permalink":"https://ragdoll-ing.com/about/","summary":"关于物质札记与作者。","title":"关于"},{"content":"请发送邮件至 mattermachine.service@outlook.com 联系我们。\n提交时请说明 客服问题：平台、随机玩家编号和问题发生时间。 个人信息请求：需要查阅、更正、复制、限制处理或删除的范围。 侵权投诉：权利证明、涉嫌侵权内容和可联系的方式。 普通投诉请只提供处理问题所需的信息。账号注销请直接使用游戏内“关于与帮助”中的“注销账号”；注销不可撤销，再次进入会创建全新进度。\n","permalink":"https://ragdoll-ing.com/support.html","summary":"万物机客服、投诉与个人信息权利联系渠道。","title":"客服、投诉与个人信息权利"}]