Rayfire
总览
主要功能:
- 提供了切分算法库,可以支持unity内离线or运行时对mesh进行切分,输出切分后的mesh碎片
- 可以根据一堆mesh碎片(离线/运行时),自动构造对应的MeshCollider
- 可以根据一定规则组织这些碎片(Cluster/Connectivity),从而实现二次破碎以及结构化破碎
验证后,能够支持:
- 小型建筑物垮塌,大型建筑物比如摩天大楼啥的得考虑混合方案
- 可以支持不同材质播放不同打击特效,但是功能需要单独开发
原理
这里Rayfire插件会将进入模拟的碎片分两种:激活/非激活。而激活的碎片会受重力。
Rayfire提供了Connectivity组件,能够将一堆碎片组合为一些Cluster,此外提供Unyielding组件表示一个稳固区域。
而任意一块碎片可能会因为受力/受伤害/发生位移/…导致被激活,然后就会触发整个连通块重建,如果某个Cluster中未和Unyielding联通,则判定为不稳固,就将其激活,进而表现为垮塌。
这里Rayfire虽然提供了Debris和Dust用来播放破碎粒子特效,但是有一些限制(不能根据具体材质判断,以及对层级支持不够好等),这里需要自己做判定实现具体播啥特效。
使用上需要注意的一些地方
- IgnoreNear需要点开,否则碎片会因为刚体穿插导致游戏一开始就弹开。
- 联通块需要点开Activation/ByConnectivity,否则碎片即使不连通也不会激活(不垮塌)。
- 联通块Activation/ByVelocity和ByOffset都设一个比较小的值(0.1),这样一旦发生形变就直接激活,免得粘住墙上(不过如果有时间静止系的技能可以考虑使用)。
- Cluster如果需要进一步破碎,需要点开Demolishable
实现细节参考
一些基本概念:
- Shatter: 切分器
- Rigid : 碎片刚体
- Cluster : 碎片集合,碰撞会按一个整体考虑,支持进一步破碎
- Connectivity : 组合碎片为结构化集合,稳固判断依赖Unyielding
- Unyielding : 表示一块稳固区域
整个插件代码上分两部分:
- Components
- Classes
Components下都是可以直接挂到Gameobject上的组件,会维护一些设置
不过很多核心功能都会委托为Classes下的RFXXXXXX
这里会讨论最常用的一些组件:
1. RigidRoot
表示一堆碎片,如果挂了Connectivity则可以将碎片其组织为联通块。
支持二层嵌套,不支持递归组织。
大部分情况下不需要使用Rigid,而是会在碎片的父节点上挂RigidRoot来组织生成好的碎片mesh。
碎片mesh不需要挂其他东西,这里初始化的时候会自动给碎片挂上rigid。
2. Initialize
可以选择在Awake初始化,或者手动调用Initialize
这里初始化的时候会根据子节点中的mesh构造碎片数据,包括构造collider,检测联通等等
1 | void AwakeMethods() |
其中StartAllCoroutines这里会开启对位移和速度的检测:
1 | public void StartAllCoroutines() |
InitConnectivity用于建立联通结构,此处首先会观察是否挂了RayfireConnectivity,有的话就初始化连通性:
1 | void InitConnectivity() |
连通性计算以及联通物体破坏激活等见Connectivity
3. Conectivity
会将所有包含Rigid的child object看做Shard,此外Rigid还得满足条件:
- inactive/kinematic(dynamic会受重力,所以需要通过判断联通来被动激活)
- 有mesh
- 可以被Connectivity激活
- 至少有一个Unyielding
初始化:
此处首先初始化内部Cluster,一个Cluster可以看做一个联通碎片集合,然后开启一些检测Coroutines
1 | public void Initialize() |
这里ChildrenCor会检测childrenChanged,并设置connectivityCheckNeed
1 | IEnumerator ChildrenCor() |
关键是ConnectivityCor,这里一旦开关开启,就进行连通性判定:
1 | IEnumerator ConnectivityCor() |
ChildrenCheck会将所有碎片中tm==null(tm是Transform)的碎片移除,并从邻居信息中移除。
而CheckConnecvivity会调用CheckConnectivityRigidList,此处会重建整个碎片集合的联通块:
1 | void CheckConnectivityRigidList() |
RFCluster.ConnectivityCheck (cluster);用于判定联通,至于联通算法就是floodfill。这里会将碎片根据连通性划分为一个一个childCluster。
CheckUnyieldingRigidList会判断childCluster是否存在Unyielding节点(暴力遍历),如果有,则判定稳固,从ChildCluster中移除放到cluster中。
这样结束后cluster中的碎片都是稳固的,childCluster中的碎片都是不稳固的。
ActivateShards一方面将单片碎片激活(没有邻居),另一方面将childCluster激活(已经判定为不稳固)。
而激活会导致受重力,所以会垮塌。
还有个问题,由于这个连通性测试必须childrenChanged==true才会触发,这里触发点在哪?
在RFActivation.cs中,如果某个rigid被激活,就会调用CheckConnectivity,此时就会设置这个flag
1 | public static void ActivateRigid (RayfireRigid scr, bool connCheck = true) |
此外由于只是设置flag,并非直接调用,所以不会出现连锁调用的问题
4. Collapse
这里会维护一个完整度值(即当前剩下的碎片数量占一开始总碎片数的多少):
1 | public float AmountIntegrity { get { return (float)cluster.shards.Count / initShardAmount * 100f; } } |
如果开启了”可以根据完整度触发垮塌” RFConnInitType.ByIntegrity。
CheckConnectivity的时候如果发现这个数量小于collapseByIntegrity,就会触发StartCollapse。
1 | public void CheckConnectivity() |
StartCollapse其实也就开了一个协程,会隔一小段时间垮塌一些碎片,至于垮塌哪些碎片,可以根据大小或者随机来决定。这样就可以实现建筑物不断地崩溃的效果。
当然也可以手动让一整个建筑物逐渐垮塌。
另外垮塌还会根据Stress来触发,不过暂时用不上,暂且略过。
5.Solidity
碰撞的时候会计算一个叫finalSolidity的值:
1 | float finalSolidity = physics.solidity * limitations.solidity * RayfireMan.inst.globalSolidity; |
如果收到的impulse大于这个值,才会判定可以被破坏,此外碰撞的时候可以根据受力计算伤害。
6. 其他辅助组件
- Boom:类似触发器,激活一定范围内碎片,然后给个force
- Blade:由于插件本身支持动态切割,所以可以用Blade组件实现类似水果忍者的切割效果。但是由于引用了非开源部分,所以暂时最好规避该功能。
一些实践踩坑
1. 关于碎片互相挤开
如果不加任何设置,初始化以后,碎片和碎片之间会出现微微挤开的问题,导致出现裂纹。
这是因为物理模拟导致的,Rayfire对此的应对是,增加了一个叫IgnoreNear的开关,如果点开,会在初始化的时候进行检测,如果发现刚体穿插,就丢给Physics.IgnoreCollision,禁用二者之间的碰撞。
然而这个IgnoreNear只作用于可破坏物的碎片互相之间的检测,实际上,如果整个物件摆放在场景内,一旦和场景内的碰撞体有穿插,还是会出现挤开的问题。不过解决方案也比较简单,初始化的时候直接将其Freeze,将其固定之即可。
2. 优化(物理相关)
不过利用Freeze来避免不必要的位移实际上只是加个位移限制而已。更进一步的,只添加Freeze实际上这些碎片还是会被物理系统所处理,没有将其从物理模拟计算中剔除。
参考这里:https://docs.unity.cn/430/Documentation/Components/RigidbodySleeping.html
一个刚体当前速度低于某个值的时候就会自动sleep,这样会将其从物理模拟中移出,从而节省大量cpu,但是如果满足以下条件,就会被唤醒:
- 碰撞
- joint变化
- 属性被修改
- 受力
所以一旦可破坏物放置的时候和场景包括地面有穿插,该物体就无法sleep,造成很大浪费。
最终方案如下:游戏开始时候设置isKinematic=true。因战斗or联通块触发激活的时候手动设置isKinematic=false。实际实践中,场景摆100个房子(每个房子400+碎片),如果不设置这个FPS会直接跌到个位数。
另外Rayfire插件中,为了实现物理实时碰撞导致的碎片激活,为每个碎片开启了数个协程监控:
- 检测位移
- 检测速度
一旦碎片位移/速度超过某个阀值,就激活之。然而碎片数量多起来,这里会非常费。
实际项目中的碎片激活统统通过战斗系统,即通常碎片处于静默,只有被战斗碰撞盒判定了,才进行激活判定,本质上是个被动过程,所以可以省下这一步。
此外,为了进一步消减激活的物理碎片,一个比较简单的思路是,一旦发生破坏,可以直接剔除掉比较小的碎片,不进入模拟
更进一步的,不同可破坏物大小不一(石柱和房子可能碎片大小比例完全不同,如果按照统一的大小剔除,石柱可能就直接被剔掉了),所以需要提供额外数据来配置不同可破坏物最小碎片大小。
其他一些参考
- https://assetstore.unity.com/packages/tools/physics/destroyit-destruction-system-18811#releases DestroyIt,一个比较流行的基于碎片物件替换的可破坏物方案,可惜不支持结构化破坏
- http://www.ultimategametools.com/products/fracturing 古早插件,最早unity3就出现了,以前用的人还蛮多的。可惜年久失修,已经无人维护
- https://www.gdcvault.com/play/1023003/The-Art-of-Destruction-in 彩虹六号 GDC
- https://www.zhihu.com/question/49317501 彩虹六号血泪史
- https://zhuanlan.zhihu.com/p/393057806: Control GDC
- https://developer.nvidia.com/blast : Blast
- https://github.com/NVIDIAGameWorks/Blast : Blast github