总览

主要功能:

  • 提供了切分算法库,可以支持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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void AwakeMethods()
{
......

//收集children,构造RFShard,填充cluster.shards
SetShards();

//构造Collider
SetColliders();
//设置Rayfire物理材质
SetCollidersMaterial();

//如果开启了IgnoreNear,这里会禁用穿插碰撞盒碰撞,免得一开始就直接弹开
RFPhysic.SetIgnoreColliders (physics, cluster.shards);

//收集child和自己的unyielding,丢到unyList中
SetUnyielding();

......

//开启一些检测协程
StartAllCoroutines();

//初始化连通性
InitConnectivity();
}

其中StartAllCoroutines这里会开启对位移和速度的检测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void StartAllCoroutines()
{
// Init inactive every frame update coroutine TODO activation check per shard properties
if (inactiveShards.Count > 0)
StartCoroutine (activation.InactiveCor(this));
}

public IEnumerator InactiveCor (RayfireRigidRoot scr)
{
//此处会遍历所有的inacrtiveShards
while (scr.inactiveShards.Count > 0)
{
//检测速度和起始位置偏移量是否超过阀值
//如果超过就激活对应的碎片
}
}

InitConnectivity用于建立联通结构,此处首先会观察是否挂了RayfireConnectivity,有的话就初始化连通性:

1
2
3
4
5
6
7
8
9
10
void InitConnectivity()
{
//此处会将RayfireConnectivity赋给内部的activation
activation.connect = GetComponent<RayfireConnectivity>();
if (activation.connect != null)
{
// Init connectivity
activation.connect.Initialize();
}
}

连通性计算以及联通物体破坏激活等见Connectivity

3. Conectivity

会将所有包含Rigid的child object看做Shard,此外Rigid还得满足条件:

  1. inactive/kinematic(dynamic会受重力,所以需要通过判断联通来被动激活)
  2. 有mesh
  3. 可以被Connectivity激活
  4. 至少有一个Unyielding

初始化:
此处首先初始化内部Cluster,一个Cluster可以看做一个联通碎片集合,然后开启一些检测Coroutines

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public void Initialize()
{
SetClusterForRigidRoot();
StartAllCoroutines();
}


void SetClusterForRigidRoot ()
{
//获取碎片mesh中三角形信息,这个三角形信息会用来计算区域面积
if (type == ConnectivityType.ByMesh || type == ConnectivityType.ByBoundingBoxAndMesh)
for (int i = 0; i < cluster.shards.Count; i++)
RFTriangle.SetTriangles(cluster.shards[i], cluster.shards[i].mf);
//初始化碎片的相邻性,会根据包围盒是否相交判定是否为邻居,可能因为面积太小而丢弃
RFShard.SetShardNeibs (cluster.shards, type, minimumArea, minimumSize, percentage, seed);
}

public void StartAllCoroutines()
{
// Start cors
StartCoroutine(ChildrenCor());
StartCoroutine(ConnectivityCor());

// Init collapse
if (startCollapse == RFConnInitType.AtStart)
RFCollapse.StartCollapse(this);
}

这里ChildrenCor会检测childrenChanged,并设置connectivityCheckNeed

1
2
3
4
5
6
7
8
9
IEnumerator ChildrenCor()
{
while (checkChildren == true)
{
if (childrenChanged == true)
connectivityCheckNeed = true;
yield return null;
}
}

关键是ConnectivityCor,这里一旦开关开启,就进行连通性判定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
IEnumerator ConnectivityCor()
{
while (checkConnectivity == true)
{
// Child deleted
if (childrenChanged == true)
ChildrenCheck();

// Get not connected groups
if (connectivityCheckNeed == true)
CheckConnectivity();

yield return null;
}
}

ChildrenCheck会将所有碎片中tm==null(tm是Transform)的碎片移除,并从邻居信息中移除。
而CheckConnecvivity会调用CheckConnectivityRigidList,此处会重建整个碎片集合的联通块:

1
2
3
4
5
6
void CheckConnectivityRigidList()
{
RFCluster.ConnectivityCheck (cluster);
CheckUnyieldingRigidList (cluster);
ActivateShards (soloShards);
}

RFCluster.ConnectivityCheck (cluster);用于判定联通,至于联通算法就是floodfill。这里会将碎片根据连通性划分为一个一个childCluster。
CheckUnyieldingRigidList会判断childCluster是否存在Unyielding节点(暴力遍历),如果有,则判定稳固,从ChildCluster中移除放到cluster中。
这样结束后cluster中的碎片都是稳固的,childCluster中的碎片都是不稳固的。

ActivateShards一方面将单片碎片激活(没有邻居),另一方面将childCluster激活(已经判定为不稳固)。
而激活会导致受重力,所以会垮塌。

还有个问题,由于这个连通性测试必须childrenChanged==true才会触发,这里触发点在哪?
在RFActivation.cs中,如果某个rigid被激活,就会调用CheckConnectivity,此时就会设置这个flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void ActivateRigid (RayfireRigid scr, bool connCheck = true)
{
......
if (connCheck == true)
scr.activation.CheckConnectivity();
}
public void CheckConnectivity()
{
if (byConnectivity == true && connect != null)
{
connect.connectivityCheckNeed = true;
connect = null;
}
}

此外由于只是设置flag,并非直接调用,所以不会出现连锁调用的问题

4. Collapse

这里会维护一个完整度值(即当前剩下的碎片数量占一开始总碎片数的多少):

1
public float AmountIntegrity { get { return  (float)cluster.shards.Count / initShardAmount * 100f; } }

如果开启了”可以根据完整度触发垮塌” RFConnInitType.ByIntegrity。
CheckConnectivity的时候如果发现这个数量小于collapseByIntegrity,就会触发StartCollapse。

1
2
3
4
5
6
7
public void CheckConnectivity()
{
// Start collapse by integrity
if (startCollapse == RFConnInitType.ByIntegrity)
if (AmountIntegrity < collapseByIntegrity)
RFCollapse.StartCollapse(this);
}

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插件中,为了实现物理实时碰撞导致的碎片激活,为每个碎片开启了数个协程监控:

  • 检测位移
  • 检测速度
    一旦碎片位移/速度超过某个阀值,就激活之。然而碎片数量多起来,这里会非常费。

实际项目中的碎片激活统统通过战斗系统,即通常碎片处于静默,只有被战斗碰撞盒判定了,才进行激活判定,本质上是个被动过程,所以可以省下这一步。

此外,为了进一步消减激活的物理碎片,一个比较简单的思路是,一旦发生破坏,可以直接剔除掉比较小的碎片,不进入模拟
更进一步的,不同可破坏物大小不一(石柱和房子可能碎片大小比例完全不同,如果按照统一的大小剔除,石柱可能就直接被剔掉了),所以需要提供额外数据来配置不同可破坏物最小碎片大小。

其他一些参考