下载 App
10/161 浏览案例分享
【案例分享】体素碰撞体不是"一个体素一个盒子":地形被挖穿之后我才搞懂的几件事
投稿方向:星火编辑器论坛「案例分享」专区 / 生态贡献度
类型:图文案例
素材来源:VoxelPhysicsWorld(Jolt)引擎文档 + 《别停下》可破坏地形与载具实测
一句话结论
体素世界的碰撞体是按"体素占用 + 平台预算"自动生成的解析形状,不是逐体素堆盒子;你手动换成 Box/Cylinder 之后,任何一次体素增删都会把它静默退回 Auto——后面那些"打不碎、穿模、挖出沟"的问题,源头都在这。
一、碰撞体到底是怎么长出来的
第一次做可破坏地形,我以为引擎会给每个实心体素挂一个碰撞盒。20×1×20 的地面按 0.25m 一格,就是 80×4×80 = 25600 个体素,真这么干物理世界当场就死了。
实际不是。默认碰撞由引擎根据体素占用和平台预算自动挑形状,逐体素堆盒子只是理论上的一种,真到那个体素数它不会选。我手上能控的只有两种干预:
-- ① 换成解析代理(轮胎、保险杠、轮舱这类规则零件)
bumper:SetCollisionBox(Vector3(0.5, 0.4, 2.4))
wheel:SetCollisionCylinderAuto(Vector3.FORWARD) -- 圆柱轴沿 volume local Z
sensor:SetCollisionSphere(0.8, Vector3(0, 0.1, 0))
-- ② 退回自动策略
volume:SetCollisionShapeAuto()
尺寸参数是完整尺寸。写 Vector3(0.5, 0.4, 2.4) 拿到的碰撞盒就是 0.5m × 0.4m × 2.4m,不是 1.0 × 0.8 × 4.8。第二个参数 localOffset 是相对体素 occupied bounds 中心的偏移,不是体积原点——体积原点在网格最小角,这个后面还会撞上一次。
解析代理只改物理形状,体素和渲染一点不动。PaintBox 涂色不会让它失效。
然后是我花了最久才接受的一条:
任何增删体素、或者拓扑分裂,都会让解析代理自动回退到 Auto。
引擎这么设计的理由是正当的——被打残的模型不该留一个和画面完全对不上的隐形圆柱。但它是静默的。我给轮胎设了圆柱代理,轮胎被打掉几个体素,代理悄悄变回占用形状,车开始以一种说不清的方式跑偏。日志里一个字都没有。
二、Assembly:把 N 个刚体合成 1 个
车身拆成底盘、保险杠、车门、车顶四块体积。如果各自是独立刚体,四个 Jolt body 之间要靠约束连,一辆车就是四个刚体加一堆约束。VoxelAssembly 走另一条路:合成一个 Jolt compound body。
local assembly = world:CreateAssembly(chassis) -- chassis 同时成为稳定玩法根
assert(assembly ~= nil)
assert(assembly:AddVolume(bumper))
assert(assembly:AddVolume(leftDoor))
assert(assembly:AddVolume(roof))
assembly:SetMemberBreakThreshold(bumper, 20000.0, 20000.0) -- N、N·m
assembly:SetMemberBreakThreshold(leftDoor, 200000.0, 120000.0)
assembly.linearDamping = 0.08
assembly.angularDamping = 0.35
合进去的是刚体和质量属性。每个 member 保留自己的 Node、VoxelGrid、材质、渲染 fallback、接触表面参数和稳定的 volumeId,体素内存不复制。所以四块还能各自涂色、各自被打碎、各自存档。
三条实测出来的规矩:
- 只能加已经 FinalizeVolumeAsDynamic 的独立动态体。静态的、没 body 的、已经在别的 Assembly 里的,会被拒绝。
- AddVolume 连着调几次,compound 只在下一个物理安全点重建一次。脚本不用自己批处理,但同一帧里、物理步发生前,别假设各 member 已经拿到同一个 body handle。
- 断裂由引擎按实际命中 member 的接触冲量和刚体惯性载荷算,脚本只配连接强度。不要监听碰撞后自己拆件——GetMemberStress() 是给你做 UI 和调试的,不是让你在 Lua 里重写一遍断裂判定。
还有个反直觉的:部件被打残之后,有效连接强度会随剩余体素占用率自动降低,碰撞形状也回退 Auto。所以"打残了反而撞不掉"不是引擎的 bug,是你缓存了旧的碰撞形状,或者旧阈值。
三、体素一多,死在哪
性能悬崖不在"体素总数"上,在单次操作搬动的体素量上。
我做过一次 5 米半径的爆炸,单次移除上千体素,同时炸出几百个碎块。那一帧直接卡住。文档里的说法是"巨帧",实测更像画面停了半秒。
-- 世界级破坏参数:建场景时一次性配好
world.minFragmentVoxels = 6 -- 小于此数的断裂群直接变碎屑,不生成刚体
world.maxFrozenFragments = 400 -- 静置碎块总预算,超了删最老的 evictable
world.spallChunkVoxels = 4 -- 崩落碎块边长,0 = 关闭崩落
world.maxSpallChunks = 24 -- 单次伤害最多崩几块
world.debrisMaxAge = 30.0 -- 碎块寿命秒数,0 = 永生
volume.evictable = false -- 重要残骸不许被预算删掉
maxFrozenFragments / maxSpallChunks / debrisMaxAge 是碎块预算三件套。移动端我从 150 / 12 / 20 起步,桌面端才敢上默认值。
剩下几条:
操作:代价
大半径单次伤害(>1000 体素 + 数百碎块):巨帧,必须拆成多帧、多次小半径调用
RebuildGeometry() 整体重网格:局部编辑改用 MarkRegionDirty(x0,y0,z0,x1,y1,z1)
大批静态体建完不做优化:建完调一次 world:OptimizeBroadPhase()
所有 member 的 collisionMask 都是 0:shared body 走 no-collide 粗筛;任一 member 恢复非零 mask 就重新进保守 broad phase
存档上有个便宜可占:从 .vox 创建的体积,没被破坏过时只存来源引用,场景文件小两个数量级,加载时自动重烘。第一次被伤害 API 破坏后自动切成全量体素快照,伤害状态精确保留。全程自动,通常不用管。
四、踩过的坑
坑 1:调色板设晚了,.vox 颜色错乱。
-- ❌ 先导入,再设调色板:.vox 已经自动认领了空槽位
local volume = world:CreateVolumeFromModel(m, 0.1, Vector3.ZERO)
world:SetPaletteEntry(1, Color(0.5, 0.5, 0.52), 6.0)
-- ✅ 手建槽位全部设完,再导入
world:SetPaletteEntry(1, Color(0.5, 0.5, 0.52), 6.0)
world:SetPaletteEntry(2, Color(0.48, 0.35, 0.22), 1.2)
local volume = world:CreateVolumeFromModel(m, 0.1, Vector3.ZERO)
手建体积固定用低槽位(1~19),20 以上留给导入。这是顺序问题,不是配置问题——配置全对,顺序错了照样花。
坑 2:材质直接取缓存资源,体积渲成纯白。
-- ❌ 共享缓存资源没绑本世界的调色板纹理
volume.material = cache:GetResource("Material", "Materials/VoxelPalette.xml")
-- ✅ 走世界自己的
volume.material = world:GetPaletteMaterial()
默认情况下根本不用设这一行——CreateVolume 手建和 .vox 烘焙都自动默认它。只有换皮肤(自定义 SurfaceShader)才需要动 volume.material。
坑 3:手改 grid 之后,动态体的碰撞还是旧的。
-- ❌ 改了体素,碰撞代理没更新
box:GetGrid():FillBox(0, 0, 0, 5, 5, 5, true)
-- ✅ 动态体要重建碰撞代理
box:GetGrid():FillBox(0, 0, 0, 5, 5, 5, true)
world:RebuildDynamicProxy(box)
伤害 API(ApplyDamage* / ApplyFillAtWorldPoint)自己会做这件事。只有手改才需要。这条和坑 1 是一类:引擎替你做的事,你自己动手时就得补上。
坑 4:.vox 体积手改后存档重载,改动全没了。
-- ❌ 绕过伤害 API 直接改 grid,来源引用还挂着
volume:GetGrid():SetSolid(3, 3, 3, false)
-- ✅ 改完脱钩,当前 grid 才会以快照形式存下来
volume:GetGrid():SetSolid(3, 3, 3, false)
volume:DetachFromVoxelModel()
存档重载时引擎按来源 .vox 原样重烘,你的修改一条不剩。这个坑发现得晚,因为编辑器里跑一次完全正常,只有退出重进才现形。
坑 5:忘了 RebuildGeometry(),体积是隐形的。
体素在,碰撞在,就是看不见。静态体积 MarkRegionDirty 或 RebuildGeometry 二选一,动态体还要额外 RebuildDynamicProxy。三件套漏一件,都是"功能在但你看不到"。
坑 6:带朝向摆放,脚印被甩走。
体积原点在网格最小角,不在中心。
local dims = volume:GetGrid():GetDimensions()
volume:GetNode().position = targetPos
+ rot * Vector3(-dims.x * 0.1 * 0.5, 0.0, -dims.z * 0.1 * 0.5)
不补偿的话,不转的时候看着正常,一转 90° 整栋房子飞出去。我第一次遇到以为是旋转轴写错了。
坑 7:订了 Update,输入晚一帧。
-- ❌ 场景(含物理步进)先于脚本执行,意图晚一帧生效
SubscribeToEvent("Update", "OnUpdate")
-- ✅ 同帧步进之前触发
SubscribeToEvent("SceneUpdate", "OnSceneUpdate")
MoveCharacter、载具 SetInput、飞行力这类意图订 SceneUpdate。物理读数(速度、位置)用哪个都行。要自控固定步长:world.updateEnabled = false 之后自己每帧 world:Update(timeStep, steps)。
坑 8:VoxelContact 死活不触发。
world.contactEventThreshold = 3.0 -- 合速 > 3 m/s 才发事件;默认 0 = 关闭
默认是关的。我在这上面反复查了两轮订阅写法,全是白查。
坑 9:悬挂挂点下面有本体体素,车直接起飞。
挂点必须在壳体之外——壳底之下,或者雕空的轮舱里。挂点垂线下方只要有本体的实体体素,悬挂射线自己命中自己,车辆火箭起飞。飞机起落架同理,支柱体素要离轮子锚点垂线两格以上。
坑 10:载具把地形挖出沟。
terrain.destructible = false -- 地形/道路防误挖必设
顺带记一个刻意的门槛语义:power < hardness 的体素完全免疫伤害。土 0.4、木 1.2、砖 6、钢 100。炸药 power 8 炸得动砖,炸不动钢。这不是 bug,是让你做材质分层的开关。同理 minDamageableVoxels 以下的残骸是惰性鹅卵石,打不碎。
五、适用边界
适合:Teardown 风格的可破坏世界。体素建筑与载具、爆炸挖掘、碎块飞溅、水面浮力、抓取推拽、铰链机关、悬挂载具、实体组装轮。地形 0.25m 一格、道具 0.1m 一格这个尺度下,20×20 米的地面是 80×4×80 网格,能挖、能塌、能存档。
别用:
- 要和 Bullet 物理(RigidBody / CollisionShape)混用的项目。 这是两套互不碰撞的世界,只共享同一个渲染 Scene。体素体积不会与 RigidBody 发生碰撞。同一个玩法只选一套,混着做会得到一堆"看得见摸不着"的物体。
- 一个 Scene 里同时建 PhysicsWorld 和 VoxelPhysicsWorld。 只能建其中之一。
- 需要 0.01m 精度建模的项目。 0.25m 一个体素,1 米就是 4 格。硬币、螺丝、纸片这类做不了,得回到小物体物理那条路。
- 想跑 HarmonyOS / Linux 的项目。 HarmonyOS 接入了构建开关,但没完成原生构建和实机验证;Linux 没经过完整验证,默认构建不启用。上线前先探测:scene:CreateComponent("VoxelPhysicsWorld") 返回 nil,就说明当前运行时没启用这个模块。
- 纯 CustomGeometry 手搓渲染。 examples/07-minecraft-voxel-world 是那个路子,不是这套系统。
六、一条可迁移的判断方法
凡是你把"引擎自动选的东西"手动换成具体形状、具体参数之后,先问一句:什么东西会让它退回去?
体素世界是"体素增删 → 解析代理回退 Auto"。渲染是"重建槽位表 → 纹理绑定失效"。光照是"新建同类型组件 → 优先级被抢"。手动覆盖都是有寿命的,找得到失效条件,才叫真的控制住了它。找不到,你就只是暂时把它按住了。
配套第二条:凡是"导入会自动认领空槽"的系统,配置必须在导入前落地。 调色板是这样,材质槽位是这样,资源注册也是这样。
(完)
星火编辑器-新手教程-关于多游戏模式的配置和触发的获取截图
星火编辑器-新手教程-关于多游戏模式的配置和触发的获取
HELLO 大家好,我是阿木木牧。最近第一版的内部小范围(几人)第一个图的测试已经结束了。 整体游戏以装备数值的崩坏而告终~ (虽然大家都很欢快。。。) 现在准备开始开发下个阶段的内容啦,就是将游戏模式拆分出来。分成单人模式,多人组队模式(合作PVE),多人匹配模式(对抗模式)。那么,怎么配置呢?说来也很简单哈~ 1,在游戏编辑器中,数据编辑器,找到地图设置。 2,编辑地图设置。比如我希望最大数量
14 赞
3 回复
04:10
自定义飘字教程截图
自定义飘字教程
之前编辑器更新的飘字,可能有不少同学还不会使用,所以我稍微写了下这篇教程,希望对大家有所帮助,更好的制作游戏
14 赞
1 回复
不支持当前设备截图
不支持当前设备
荣耀400pro
2 赞
3 回复
00:18
修复一下这个挂起过。有概率用不了的bug吧截图
修复一下这个挂起过。有概率用不了的bug吧
太绝望了[表情_豹哭][表情_豹哭][表情_豹哭][表情_豹哭]
1 赞
1 回复
00:11
人物有小概率会卡着不往前走截图
人物有小概率会卡着不往前走
3 赞
1 回复
无须申请,星火编辑器现已公测开放下载截图
无须申请,星火编辑器现已公测开放下载
—————————————————— 各位关注星火编辑器的小伙伴: 感谢大家一直以来对编辑器支持! 破晓已去,曙光降临! 星火编辑器现已开启公测! · 关于下载和资格申请 现已无需申请,请去下面的官方首页直接下载! 缘起测试是星火编辑器自诞生以来第一次对外测试! 从2022年八月开始到十一月,历时三个月。 缘起之后缘聚测试,历时又七个月! 缘聚之后,破晓降临,历时又十二个月! 曾经微弱的星火现已渐
官方
331 赞
375 回复
05:19
1截图
1
1.完成后,获得信用点的增益依旧有效,可以卡无限信用点。理论上 2.部分天赋和在一起可以实现永续,那些天赋,我下一个发,因为只能传一个视频。 3.卡,人多点就卡,知道卡死,但是手机不会卡就游戏卡
2 赞