参考
Cocos Creator 3D 物理模块介绍
Cocos Creator 3.0 3D 物理讲解
选择适合你项目的物理系统
一、3D 物理简介
Cocos Creator 3.0 目前支持轻量的碰撞检测系统 builtin 和具有物理模拟的物理引擎 cannon.js ,以及功能完善强大的 bullet 的 asm.js/wasm 版本(ammo.js),并为开发者提供了高效统一的组件化工作流程和便捷的使用方法。
1.碰撞检测: builtin
builtin 仅有碰撞检测的功能,相对于其它的物理引擎,它没有复杂的物理模拟计算。如果您的项目不需要这一部分的物理模拟,那么可以考虑使用 builtin,这将使得游戏的包体更小。
若使用 builtin 进行开发,请注意以下几点:
- builtin 只有 trigger 类型的事件。
- Collider 中的 isTrigger 无论值真假,都为运动学类型的触发器。
2.物理引擎: cannon.js
cannon.js 是一个开源的物理引擎,它使用 js 语言开发并实现了比较全面的物理功能,如果您的项目需要更多复杂的物理功能,那么您可以考虑使用它。 cannon.js 模块大小约为 141KB。
3.物理引擎: ammo.js
ammo.js 是 bullet 物理引擎的 asm.js/wasm 版本,由 emscripten 工具编译而来。 Bullet 具有完善的物理功能,以及更佳的性能,未来我们也将在此投入更多工作。
需要注意的是,目前 ammo.js 模块具有 1.5MB 左右的大小。
4. 不使用物理
若不需要用到任何物理相关的组件和接口,可以取消黄色框的勾选,这样在发布时将有更小的包体。
注:若处于取消勾选的状态,项目将不可以使用物理相关的组件和接口,否则运行时将会报错。
5.对比
- builtin 极其轻量的碰撞检测系统,仅支持盒、球、胶囊体形状和触发事件
- cannon.js 纯 js 开发的物理引擎,支持大部分特性,易于扩展,但性能不够好,包体约为 141 KB
- ammo.js 由 bullet 物理引擎编译而来,支持所有特性,性能最优,但包体较大且不易扩展,其中 js 版本的包体约为 1.32 MB,wasm 版本的包体约为 690 KB
二、2D 物理简介
Cocos Creator 3.0 支持内置的轻量 Builtin 物理系统和强大的 Box2D 物理系统。Builtin 物理系统只提供了碰撞检测的功能,对于物理计算较为简单的情况,我们推荐使用 Builtin 物理模块,这样可以避免加载庞大的 Box2D 物理模块并构建物理世界的运行时开销。而 Box2D 物理模块提供了更完善的交互接口和刚体、关节等已经预设好的组件。
你可以根据需要来选择适合自己的物理模块,通过编辑器主菜单中的 项目 -> 项目设置 -> 功能裁剪 切换物理模块的使用。
三、物理组件
刚体rigidbody :刚体是指在运动中和受力作用后,形状和大小不变,而且内部各点的相对位置不变的物体。
碰撞体collider:碰撞体是给物体加一个判定框,当碰撞框重叠的时候,两物体发生碰撞。碰撞体是检测物理碰撞的框架,他永远跟随物体的刚体移动,不会产生偏差。
- 刚体组件(RigidBody) 它负责操控物理对象与运动相关的属性,以及配置分组,一个节点最多一个
- 碰撞器组件(Collider) 它负责操控物理对象上形状相关的属性,一个节点可以有多个
- 触发器(Trigger) 它是勾选上了IsTrigger的碰撞器组件,它会像幽灵一样穿透其它物体,并提供穿透开始、保持,以及结束的事件
是否一定要加刚体组件是提问最多的,这里介绍一个小技巧:
- 需要配置分组,加
- 需要设置为运动学或者动力学类型(不加意味着是静态类型),加
1.刚体
刚体是组成物理世界的基本对象,可以让一个节点受到物理影响并产生反应。刚体组件用于控制模拟相关的部分属性:
- Linear Damping 线性阻尼,用于减小刚体的线性速率,值越大物体移动越慢
- Angular Damping 角阻尼,用于减小刚体的旋转速率,值越大刚体旋转越慢
阻尼参数的范围在 0 到无穷之间,0 意味着无阻尼,无穷意味着满阻尼。一般情况下阻尼的值是在 0 ~ 0.1 之间。
- Linear Factor 线性因子,可影响刚体在每个轴向的线性速度变化,值越大刚体移动越快
- Angular Factor 旋转因子,可影响刚体在每个轴向的旋转速度变化,值越大刚体旋转越快
因子是 Vec3 的类型,相应分量的数值用于缩放相应轴向的速度变化,默认值都为 1,代表缩放为 1 倍,即无缩放。
// TypeScript
const rigidBody = this.getComponent(RigidBody);
目前刚体类型包括 STATIC、DYNAMIC 和 KINEMATIC 三种。参考Unity 物理系统之刚体、碰撞体、触发器
动态 Dynamic
- 默认类型
- 受重力和附加力的影响,进而改变速度。
- 性能消耗最高的类型。
- 可通过 MovePosition、MoveRotation 来移动。
- 可通过 velocity 来改变速度。
- 用来模拟移动的物体。
运动学 Kinematic
- 不受力的影响。
- 性能消耗比 Dynamic 少,比 Static 多。
- 可通过 MovePosition MoveRotation 来移动。
- 可通过 velocity 来改变速度。
- 用来模拟大多数时候不动的物体,极少数时候需要移动。比如门。
静态 Static
- 不受力的影响,物理系统不会对该物体进行计算。
- 性能消耗最低的类型。
- 不可通过 MovePosition MoveRotation 来移动。
- 不可通过 velocity 来改变速度。
- 用来模拟从不移动的物体。
cocos文档是这样写的:
STATIC,表示静态刚体,可用于描述静止的建筑物,若物体需要持续运动,应设置为 KINEMATIC 类型;
DYNAMIC,表示动力学刚体,能够受到力的作用,请通过物理规律来运动物体,并且请保证质量大于 0;
KINEMATIC,表示运动学刚体,通常用于表达电梯这类平台运动的物体,请通过 Transfrom 控制物体运动;
经过测试,KINEMATIC类型确实无法通过改变速度让其动起来。
图中白色为静态,蓝色为运动学,黄色为动力学。其中白色和蓝色都是操控的变换信息,很明显的看出几个表现
- 白色和蓝色之间会出现穿透现象
- 白色的静态物体也可以运动
- 两个黄色方块表现不同,白色上方的静止不动,蓝色上方的会跟随着运动
以上现象的原因是:
- 静态和运动学都不会受到力的作用,所以产生了穿透,这是正常现象
- 静态物体的确是可以运动的,静态是指在时空中,每一个时刻都是静态,不会考虑其它时刻的状态
- 与静态物体不同,运动学物体会根据附近时刻估算出运动状态(比如速度),又由于摩擦力的作用,因此带动了黄色方块
2.刚体质心
目前质心固定在刚体组件绑定的节点上,质心和碰撞体是相对关系。通过调整形状的偏移 center,可以使质心在形状上进行偏移。
参考
放空大佬在不在 有个问题好想沟通一下!
@开发团队 关于物理系统重心的疑问,可能是BUG?
目前我们的质心是固定的,改不了,所小车的坐标点就是质心的位置。你可以看一下小车的根节点是不是在小车底部,这个时候新建一个空节点,然后把小车放入这个空节点下,调整位置,让空节点看起来处于小车的正中心就行,这个时候再把碰撞组件由原来的小车根节点移到空节点上,这样质心的修改就成功了
在 Cocos Creator 3D 里目前质心和重心是重合的,节点位置就是重心的位置,所以要改重心只能调整节点。center 的改动目前对重心是没有任何效果的,对外力才有影响
3.休眠
休眠刚体时,会将刚体所有的力和速度清空,使刚体停下来。
处于休眠状态中的物体,不会再对其进行碰撞检测和模拟。这会节约大量的CPU开销。
// 休眠
if (rigidBody.isAwake) {
rigidBody.sleep();
}
// 唤醒
if (rigidBody.isSleeping) {
rigidBody.wakeUp();
}
4.让刚体动起来
针对不同的类型,让刚体运动的方式不同:
- 对于静态刚体,应当尽可能保持物体静止,但仍然可以通过 Transform 来改变物体的位置。
- 对于运动学刚体,应当通过改变 Transform 使其运动。
- 对于动力学刚体,需要改变其速度,有以下几种方式:
(1)通过重力
刚体组件提供了 useGravity 属性,将 useGravity 属性设置为 true。
(2)通过施加力
刚体组件提供了 applyForce 接口,签名为:
applyForce (force: Vec3, relativePoint?: Vec3)
根据牛顿第二定律,可对刚体某点上施加力来改变物体的原有状态。
rigidBody.applyForce(new Vec3(200, 0, 0));
(3)通过扭矩
力与冲量也可以只对旋转轴产生影响,使刚体发生转动,这样的力叫做扭矩。
刚体组件提供了 applyTorque 接口,签名为:
applyTorque (torque: Vec3)
通过此接口可以施加扭矩到刚体上,因为只影响旋转轴,所以不需要指定作用点。
(4)通过施加冲量
刚体组件提供了 applyImpulse 接口,签名为:
applyImpulse (impulse: Vec3, relativePoint?: Vec3)
根据动量守恒,对刚体某点施加冲量,由于物体质量恒定,从而使刚体改变原有状态。
rigidBody.applyImpulse(new Vec3(5, 0, 0));
(5)通过改变速度
刚体组件提供了 setLinearVelocity 接口,可用于改变线性速度,签名为:
setLinearVelocity (value: Vec3)
示例:
rigidBody.setLinearVelocity(new Vec3(5, 0, 0));
刚体组件提供了 setAngularVelocity 接口,可用于改变旋转速度,签名为:
setAngularVelocity (value: Vec3)
示例:
rigidBody.setAngularVelocity(new Vec3(5, 0, 0));
参考拉小登博客 让刚体听我的——ApplyForce、ApplyImpulse、SetLinearVelocity
1.力,循序渐进——ApplyForce
顾名思义,ApplyForce方法会在刚体上施加一个力。学过物理力学的同学都知道,F=ma,有了力F就有了加速度a,有了加速度,物体就会有速度,就会慢慢动起来。(但是不会立马动起来,因为力不会直接影响速度)。
举个简单的例子,小明推一个静止的箱子,箱子不会立马飞出去,而是慢慢的、越来越快的动起来(减速也一样)。
2.速度,叠加——ApplyImpulse
与ApplyForce不同,ApplyImpulse不会产生力,而是直接影响刚体的速度。通过ApplyImpulse方法添加的速度会与刚体原有的速度叠加,产生新的速度。
3.一触即发——SetLinearVelocity
setLinearVelocity与ApplyImpulse一样,直接影响刚体的速度。不一样的是,setLinearVelocity添加的速度会覆盖刚体原有的速度。不过,在SetLinearVelocity方法不会自动唤醒sleeping的刚体,所以在调用该方法之前,记得将刚体body.wakeUp()一下。
5.限制刚体的运动
(1)通过休眠
休眠刚体时,会将刚体所有的力和速度清空,使刚体停下来。
(2)通过阻尼
刚体组件提供了 linearDamping 和 angularDamping 属性:
- linearDamping 属性用于设置线性阻尼。
- angularDamping 属性用于设置旋转阻尼。
阻尼参数的范围建议在 0 到 1 之间,0 意味着没有阻尼,1 意味着满阻尼。
注:执行部分接口,例如施加力或冲量、改变速度、分组和掩码会尝试唤醒刚体。
(3)通过因子
刚体组件提供了 linearFactor 和 angularFactor 属性:
- linearFactor 属性用于设置线性因子。
- angularFactor 属性用于设置旋转因子。
因子是 Vec3 的类型,相应分量的数值用于缩放相应轴向的速度变化,默认值都为 1,表示缩放为 1 倍,即无缩放。
注意:
- 将因子某分量值设置为0,可以固定某个轴向的移动或旋转。
- 在 cannon 和 ammo 后端中,因子作用的物理量不同,cannon 中作用于速度,ammo 中作用于力。
使用矩形碰撞盒,碰撞后发现模型的移动轨迹从直线变歪。将angularFactor 的三个轴属性全部改为0即可解决。
6.地面判断
角色是否在地面上,可以通过法线的指向来得知,对应的条件为normal.y > 0。有时候还需要知道地面的倾斜情况,这里通过normal.y的大小就能知道,越接近于1,哪么地面就越平(法线垂直向上);越接近于0,哪么地面就越陡峭。
7.跳跃
一旦能够确定是否在地面后,就可以加上跳跃行为了。首先需要明确的一点是,这里使用的是动力学类型的刚体(原因是我希望通过物理引擎接管刚体后得到更加真实的物理反馈),所以需要通过改变物理数值来达到目的,例如直接将角色线性速度的y分量设置为5,如下图(图中碰撞体用的是胶囊体,这与实际场景相关,可自行更改)。
直接改y轴速度,可以良好的工作在完全水平的地面上。但有时候跳跃还需要根据地面斜度来做出不同的行为,这里提供一个思路:提供一个配置因子,范围为0到1,根据因子将当前法线和垂直法线插值,得出目标法线,再乘以跳跃的速率,进而得出期望的跳跃速度,将其加入到角色的线性速度之中。
8.行走和站立
站立行为,如果希望角色不会摔倒,哪么直接将AngularFactor全设置为0即可。行走行为,可根据角色的运动状态来实现,假设需要将角色往x轴运动,哪么就修改角色x轴的线性速度。因此控制行走,只需要两个量,一个是运动方向,另一个是运动速率。
做出好的运动控制的唯一窍门就是控制好速度,但这也是最容易错误使用的点:
- 随意设置速度,这是运动不真实最根本的原因
- 设置过大的速度,这将很容易出现穿透现象,可以尝试上文介绍的多步模拟技术
- 没有掌握控制速度的方式,有些开发者直接修改为某个固定值大小,这当然不是错误的做法,但修改速度之前应当考虑清楚这是不是想要的结果。
- 有个小技巧是,如果是跳跃行为,可以考虑直接设置为固定值;
- 如果是行走行为,更好的方式是根据上个时刻的状态进行修改
- 使用了Kinematic类型的刚体,期望物理引擎会帮助你处理好碰撞行为,通过上文可以知道这是不可能的。
- 但实际上有很多好的角色控制都基于Kinematic进行实现,这里面的技术细节远比本文介绍的复杂。
四、分组 掩码
1.碰撞矩阵
默认情况下只有一个 DEFAULT 分组,新建分组默认不与其它组碰撞。点击 + 按钮可以新增分组。
注:新增分组的 index 和 name 均不能为空,且不能与现有项重复。
注:分组不可以删除,但可以修改分组的名称。
这张表列出了所有的分组,你可以通过勾选来决定哪两组会进行碰撞检测。如上图所示,DEFAULT和water是否会进行碰撞检测将取决于是否选中了对应的复选框。
根据上面的规则,在这张表里产生的碰撞对有:
- DEFAULT - water
- DEFAULT - DEFAULT
而不进行碰撞检测的分组对有:
- water - water
通过刚体组件上的 Group 属性来配置对应的物理元素的分组:
在Cocos Creator 3.0 正式版震撼来袭!中,有提到碰撞矩阵相关变化:
[IMPROVE] 重构 3d 物理碰撞矩阵,碰撞矩阵仅用于初始化刚体的分组和掩码,不再维护更新,废除 Use Collision Matrix 配置项
2.设置掩码
参考Cocos Creator 3D 物理碰撞group和mask设置方法
a.假设场景中有:场景障碍物,玩家A,玩家B,穿墙的导弹
- 障碍物阻挡玩家A和玩家B
- 玩家A和玩家B不碰撞,但会被导弹击中
- 导弹可以穿越障碍物,但会击中玩家A和玩家B
b.设置分组group
- 设置障碍物的collider的group为0,(注意!!!代码中实际设置为1<<0)
- 设置玩家A的collider的group为1,(注意!!!代码中实际设置为1<<1)
- 设置玩家B的collider的group为2,(注意!!!代码中实际设置为1<<2)
- 设置导弹的collider的group为3,(注意!!!代码中实际设置为1<<3)
c.以障碍物为例,分析过程如下:
只要以下条件为真就会进行检测:(GroupA & MaskB) && (GroupB & MaskA)
障碍物和A会阻挡
- A的分组是0010,所以障碍物的mask为1<<1
- 障碍物的分组是0001,所以A的mask为1<<0(这一条作为分析障碍物时得到的额外信息,以下不再标注)
同理,障碍物和B会阻挡
- B的分组是0100,所以障碍物的mask为1<<2
- 障碍物的分组是0001,所以B的mask为1<<0
障碍物和导弹不碰撞,即导弹能穿墙,此时把上面两部分合并,得到:
障碍物的collider的mask为(1<<1)|(1<<2)(障碍物碰撞玩家A和玩家B)
如果障碍物和导弹碰撞,和上面一样:
- 导弹的分组是1000,所以障碍物的mask为1<<3
- 障碍物的分组是0001,所以导弹的mask为1<<0
然后障碍物的mask再次合并,就变为(1<<1)|(1<<2)|(1<<3)
d.设置掩码mask(将需要互相碰撞的group掩码或进来(加进来也可以))
- 设置障碍物的collider的mask为(1<<1)|(1<<2)(障碍物碰撞玩家A和玩家B)
- 设置玩家A的collider的mask为(1<<0)|(1<<3)(玩家A碰撞障碍物和导弹)
- 设置玩家B的collider的mask为(1<<0)|(1<<3)(玩家B碰撞障碍物和导弹)
- 设置导弹的collider的mask为(1<<1)|(1<<2)(导弹碰撞玩家A和玩家B)
如果导弹不能穿墙,需要更改mask为
- 设置障碍物的collider的mask为(1<<1)|(1<<2)|(1<<3)(加入导弹)
- 设置导弹的collider的mask为(1<<1)|(1<<2)|(1<<0)(加入障碍物)
如果玩家之间互相碰撞,需要更改mask为
- 设置玩家A的collider的mask为(1<<0)|(1<<3)|(1<<2)(加入玩家B)
- 设置玩家B的collider的mask为(1<<0)|(1<<3)|(1<<1)(加入玩家A)
示例代码如下
var c0 = blocks.getComponent(ColliderComponent);
c0.setGroup(1 << 0);
c0.setMask((1 << 1) | (1 << 2));
var c1 = playerA.getComponent(ColliderComponent);
c1.setGroup(1 << 1);
c1.setMask((1 << 0) | (1 << 3));
var c2 = playerA.getComponent(ColliderComponent);
c2.setGroup(1 << 2);
c2.setMask((1 << 0) | (1 << 3));
var c3 = missle.getComponent(ColliderComponent);
c3.setGroup(1 << 3);
c3.setMask((1 << 1) | (1 << 2));
五、物理材质
物理材质有摩擦系数,回弹系数。
目前物理材质以碰撞体为单位进行设置,每个 Collider 都具有一个 material 的属性(不设置时, Collider 将会引用物理系统中的默认物理材质)。 应用到 Collider 同样也分编辑器操作和代码操作两种方式。
编辑器内操作,只需要将资源拖入到cc.PhysicMaterial属性框中即可,如下图所示:
代码中操作:
const collider = this.node.getComponent(Collider);
collider.material = newPmtl;
六、物理事件
Cocos Creator 3.0 的物理事件包括 触发事件 和 碰撞事件,分别由 触发器 和 碰撞器 产生。
1.触发器和碰撞器
- Is Trigger 属性为 true ,是触发器。当发生碰撞时,触发器不会产生碰撞效果,所以触发器只用于碰撞检测。
- Is Trigger 属性为 false ,是碰撞器。当发生碰撞时,碰撞器会产生碰撞效果,所以碰撞器既可以进行碰撞检测,又可以产生物理效果。
两者的区别如下:
- 触发器不会与其它触发器或者碰撞器做更精细的检测。
- 碰撞器与碰撞器会做更精细的检测,并会产生碰撞数据,如碰撞点、法线等。
2.触发事件和碰撞事件区别
- 触发事件由触发器生成,碰撞事件根据碰撞数据生成。
- 触发事件可以由触发器和另一个触发器/碰撞器产生。
- 碰撞事件需要由两个碰撞器产生,并且至少有一个是动力学刚体。
参考
《学Unity的猫》——第十章:Unity的物理碰撞,流浪喵星计划
碰撞器、触发器
触发器在生活有很多例子,假设进入到一些门口时,当我们踏入到一定范围,门会自动打开,这其实就是触发器在现实生活中的例子,游戏中依然也是,例如,人物走回家的时候会自动的恢复血量,这其实就是在使用触发器
3.触发事件
- onTriggerEnter 触发开始
- onTriggerStay 触发保持
- onTriggerExit 触发结束
需要通过注册事件来添加相应的回调:
- 通过this.getComponent(Collider)获取到 Collider
- 通过 Collider 的 on 或者 once 方法注册相应事件的回调
- 注:Collider 是所有碰撞组件的父类。
代码示例:
public start () {
let Collider = this.getComponent(Collider);
Collider.on('onTriggerStay', this.onTrigger, this);
}
private onTrigger (event: ITriggerEvent) {
console.log(event.type, event);
}
经过实测,触发事件,仍然需要其中一个组件是刚体
4.碰撞事件
- onCollisionEnter 碰撞开始
- onCollisionStay 碰撞保持
- onCollisionExit 碰撞结束
代码示例:
public start () {
let Collider = this.getComponent(Collider);
Collider.on('onCollisionStay', this.onCollision, this);
}
private onCollision (event: ICollisionEvent) {
console.log(event.type, event);
}
七、射线检测
官方文档 射线检测
浅析射线检测 raycast 的使用 !Cocos Creator 3D !
首先,我们看到的视角是这样子的。假设我们点击其中屏幕中的一个位置(图中的红点点)。
因为这个视角是摄像机提供的,我们就把这个点点和摄像机组合一条射线。
接着,检查这条射线穿过了那些物体,这些物体中可能就有我们点击的对象。
也可以这么理解,你用眼睛看着一块区域,伸出手指。你可以看到手指头挡住了一点视线,从你的视线做经过手指这个点画一条射线,这个射线穿过的物体,就刚好是你想要点击的物体。
creator 3d 提供了三种检测方案,可以参考https://github.com/cocos-creator/test-cases-3d中的raycast示例:
1.基于物理碰撞器的射线检测:
onEnable () {
systemEvent.on(SystemEventType.TOUCH_START, this.onTouchStart, this);
}
onDisable () {
systemEvent.off(SystemEventType.TOUCH_START, this.onTouchStart, this);
}
onTouchStart (touch: Touch, event: EventTouch) {
this.cameraCom.screenPointToRay(touch.getLocationX(), touch.getLocationY(), this._ray);
if (PhysicsSystem.instance.raycast(this._ray)) {
const r = PhysicsSystem.instance.raycastResults;
for (let i = 0; i < r.length; i++) {
const item = r[i];
if (item.collider.node.uuid == this.modelCom.node.uuid) {
this.modelCom.material = this.rayMaterial;
}
}
} else {
this.modelCom.material = this.defaultMaterial;
}
}
2.基于模型的射线检测:
onTouchStart(touch: Touch, event: EventTouch) {
const point = touch.getLocation();
this.cameraCom.screenPointToRay(point.x, point.y, this._ray);
if (geometry.intersect.rayModel(this._ray, this.modelCom.model!)) {
this.modelCom.material = this.rayMaterial;
} else {
this.modelCom.material = this.defaultMaterial;
}
}
3.基于 UITransform 组件的射线检测:
onTouchStart(touch: Touch, event: EventTouch) {
this.label.string = '点击文字测试射线检测';
const point = touch.getLocation();
this.canvas.cameraComponent!.screenPointToRay(point.x, point.y, this._ray);
const uiTrans = this.label.getComponent(UITransform)!;
uiTrans.getComputeAABB(this._aabb);
if (geometry.intersect.rayAABB(this._ray, this._aabb)) {
this.label.string = '检测成功';
}
}
4.API
- raycast: 射线可以碰撞所有的可以检测的物体(多个).
- raycastClosest:射线碰到一个物体以后,停止检测,直接返回一个物体(一个).
/**
* 检测所有的碰撞盒,并记录所有被检测到的结果,通过 PhysicsSystem.instance.raycastResults 访问结果。
* @param worldRay 世界空间下的一条射线
* @param mask 掩码,默认为 0xffffffff
* @param maxDistance 最大检测距离,默认为 10000000,目前请勿传入 Infinity 或 Number.MAX_VALUE
* @param queryTrigger 是否检测触发器
* @return boolean 表示是否有检测到碰撞盒
*/
raycast(worldRay: geometry.Ray, mask?: number, maxDistance?: number, queryTrigger?: boolean): boolean;
/**
* 检测所有的碰撞盒,并记录与射线距离最短的检测结果,通过 PhysicsSystem.instance.raycastClosestResult 访问结果。
* @param worldRay 世界空间下的一条射线
* @param mask 掩码,默认为 0xffffffff
* @param maxDistance 最大检测距离,默认为 10000000,目前请勿传入 Infinity 或 Number.MAX_VALUE
* @param queryTrigger 是否检测触发器
* @return boolean 表示是否有检测到碰撞盒
*/
raycastClosest(worldRay: geometry.Ray, mask?: number, maxDistance?: number, queryTrigger?: boolean): boolean;
八、射箭案例
1.射箭与回收箭——运动学、动力学、事件
射箭的第一步是拉弓,箭需要完全跟随弹性绳骨骼一起运动,不希望箭受到物理规则的影响,此时应将箭的刚体设置为 Kinematic 类型;第二步是松开弹性绳发射箭,这时希望给箭设置初速度后,可以按照物理规则进行运动,因此将箭的刚体设置为 Dynamic 类型
回收箭的大致过程是在箭射出去后,一旦触碰到触发区域就将其还原到弓上。这可以通过制作监听区域来实现,首先利用碰撞体组件拼凑出区域,同时将碰撞体组件的 IsTrigger 勾选上。(下图中的蓝色地板为监听区域)
易误点:
- 利用修改变换信息来操作动力学(Dynamic)类型的刚体,应当通过速度、力或冲量等物理层的数值
- 利用静态(Static)类型的刚体来监听事件,静态刚体只会和带有类型为运动学或动力学的刚体产生事件,可以更改为运动学(Kinematic)类型,或者事件通过另一方进行注册
- 监听事件时仅监听了触发开始(OnTriggerEnter),但是误以为包括了触发保持(OnTriggerStay)和结束(OnTriggerExit)
2.瞄准——碰撞矩阵(过滤检测)、射线检测、静态平面
瞄准是射箭前的步骤,准心处于箭头指向所在的射线上,在十字架前面加一个静态平面碰撞体,然后利用射线检测就可以得到准心的位置;
静态平面只是用来做射线检测,给它专门建立一个分组,并且不与箭、苹果等等物体进行检测,这是最通用的性能优化方法
调用射线检测方法时,设置传入掩码为仅和静态平面检测,即 0b10(二进制表示法)。
注:这里传入了mask参数,代码注释中描述得并不清楚:@param mask 掩码,默认为 0xffffffff
。实际上,mask参数是指要检测的物理类型,比如此例中的RaycastPlane的index是1,即第2位,就要传0b10
易误点:
- 分不清刚体组件上的分组和节点上的层。这两者的概念类似,但是使用者不同,分组的使用者是物理模块,层的使用者是渲染模块
- 对掩码的理解不到位,不知传何值。这里提供一个小技巧,以一个能够筛选出Others的掩码举例,首先Others的index值为2,哪么只要让二进制掩码从右往左的顺序第2位为1,就能让Others通过筛选,也就是ob100(这里强烈建议不要随便更改分组的索引)
- 误认为射线检测接口的返回值是击中的数据。获取结果有专门的接口,此处设计是为了强调这是个复用对象。为了减少垃圾内存,每次调用接口只会更新它们的数据,而不是重新生成新的(若需要持久记录,哪么可以克隆一份)
3.网格碰撞器组件(MeshCollider)
- mesh 网格碰撞器引用的网格资源,用于初始化网格碰撞体
- convex 是否使用网格的凸包近似,网格顶点数应尽量小于255(通过它可以支持任意凸类碰撞体和动力学刚体)
注:cannon.js对网格碰撞器组件支持程度很差,只允许与球碰撞器产生检测。 注:convex功能目前仅ammo.js后端支持。
4.射击苹果——静态网格、凸包、多步模拟(步长调整)
一般的苹果都带有凹面,处理好凹类或带连续平滑不规则曲面的模型都非常棘手,这是因为目前成熟的理论和技术都建立在离散、凸包的世界之上(微积分中用差分近似表示微分就是最典型的范例)。
在实时物理引擎中,对于这类物体只能支持到静态或运动学类型的刚体层级,对于动力学就束手无策了。然而不幸的是,真实的苹果运动表现强烈依赖动力学,这种情况只能给苹果填加凸包形式的网格碰撞体(需将 convex 勾选上),再加上一个动力学刚体,用近似物体去参与模拟
运动表现与模拟参数有非常大的关系,穿透是最具有代表性的现象,这可以通过缩减步长和增加步数来实现,调整步长有个小技巧:输入分式,即 1/Frame,其中 Frame 表示帧率
易误点:
在带有未勾选 convex 的网格碰撞器上添加了的动力学刚体,与其它物体产生了穿透现象,或者说完全没有反应,这是典型的错误使用,只有勾上 convex 的才能支持动力学刚体
对一个顶点数极多的模型开启了 convex,过多的顶点数会使凸包的面数增多,这对性能有很大的影响,而且实际上并不需要面数特别多的凸包,一般建议模型的顶点数应小于 255
-
开启凸包后,模型的凹面处的接触不贴近,这是正常现象,现在的实时技术是将模型用多个凸包组合来解决,如下图所示
只调整了步长,但未调整步数,这两者需要相互配合才有效果。小技巧是,步数可以随意设置较大的值,步长根据最大的速度值进行调整,值越大,步长应当越小
九、小车案例
上图结构也是通过真实车的结构简化而来的,设计好结构后,还需要调整每个部分的属性:
重心: 车的重心应该要低一些,否则可能会很容易翻车;
车身和车轮:车辆运动应该是很平稳的,可以把摩擦力系数都设置为 0,另外车轮要比车身低一些,这样在碰到障碍物后车辆会有晃动的效果,用来模拟避震;
挡板:因为车身摩檫力设置为 0 了,为了防止车滑起来,加一个摩檫力不为 0 的挡板;
刚体:默认质量为 10,这里可以改成 200;因为摩檫力都为 0,避免车一直滑动,将阻力(damping)设置为 0.9;角速度因子 x 轴向设置成 0.5,减低车在 x 轴向的旋转抖动。