如视频所示,这是个控制摄像机的组件。
当摄像机过于贴近其他物体的时候,摄像机就会像弹簧一样被挤压到一起,避免摄像机穿模的尴尬情况。
不使用弹簧臂的情况对比如下。
组件使用
为了逻辑解耦和易用,功能被我封装成了两个组件,包括弹簧臂组件和弹簧臂调试组件。
弹簧臂组件
每个参数都有详细的注释以及 Tooltip,方便快速上手。
把弹簧臂组件挂载到玩家角色身上,设置跟随目标和摄像机,这个目标通常是角色的头。
使用调试组件后,在编辑器里调整弹簧臂参数时,能够实时预览。
弹簧臂调试组件
在我的示例项目中,调试组件挂载到了旁观者角色身上。
这样运行时按 Q 键即可切换到旁观者,以自由视角观察玩家位置,方便调试。
实现原理
了解了组件使用后,再简单介绍下它的实现原理。
计算位置
弹簧臂计算摄像机位置分为三段。
-
目标偏移(targetOffset)
用角色位置加上偏移得到目标偏移位置。
let worldTargetOffset = Vec3.transformMat4(v3(), this.targetOffset, rotation);
this.targetOffsetPosition = Vec3.add(v3(), trans.worldPosition, worldTargetOffset);
-
弹簧臂终点
用弹簧臂长度乘以角色背后方向,可以得到弹簧臂尺寸。
用目标偏移点加弹簧臂尺寸,得到最后的弹簧臂终点。
let v3_1 = Vec3.negate(v3(), trans.forward);
let v3_2 = Vec3.multiplyScalar(v3(), v3_1, this.armLength);
let v3_3 = Vec3.add(v3(), v3_2, this.targetOffsetPosition);
-
插槽偏移点
用弹簧臂终点加插槽偏移,得到插槽偏移点。
let worldSocketOffset = Vec3.transformMat4(v3(), this.socketOffset, rotation);
this.socketOffsetPosition = Vec3.add(v3(), v3_3, worldSocketOffset);
计算碰撞
组件通过射线扫掠的方式,检测摄像机跟场景中的其他物体是否产生碰撞。
射线扫掠是 Cocos Creator 3.8 中新增的一套射线 API 。
它能够使用一个形状往指定的方向扫掠过去,过程中形成的轨迹碰撞到物体就会被记录。
const phySys = PhysicsSystem.instance;
let sweepRay = geometry.Ray.create(
this.targetOffsetPosition.x,
this.targetOffsetPosition.y,
this.targetOffsetPosition.z,
this.raycastDirection.x,
this.raycastDirection.y,
this.raycastDirection.z
);
let isSweep = phySys.sweepSphereClosest(
sweepRay,
this.collisionProbeSize,
this.collisionLayerMask,
this.armLength,
false
);
扫掠检测到碰撞后,会再做一次从目标偏移点到插槽偏移点的射线检测,把这次碰撞到的位置设置为摄像机位置。
这么做是因为扫掠是一个几何形状,碰撞的地方可能是几何体边缘,而摄像机的位置应该在几何体正中心。
if (isSweep) {
let ray = geometry.Ray.create(
this.targetOffsetPosition.x,
this.targetOffsetPosition.y,
this.targetOffsetPosition.z,
this.raycastDirection.x,
this.raycastDirection.y,
this.raycastDirection.z
);
if (phySys.raycastClosest(
ray,
this.collisionLayerMask,
this._armLength,
false
)) {
this.socketOffsetPosition = phySys.raycastClosestResult.hitPoint;
}
}
弹簧臂调试
弹簧臂调试使用了引擎自带组件 Line。
设置完了基础参数后,使用目标偏移点和插槽偏移点绘制出一条线。
let line = this.getComponent(Line);
line.worldSpace = true;
line.width.mode = CurveRange.Mode.Constant;
line.width.constant = 0.05;
line.color.mode = GradientRange.Mode.Color;
line.color.color = math.Color.RED;
line.positions = [
this.springArm.targetOffsetPosition,
this.springArm.socketOffsetPosition
];
写在最后
后续我会继续完善这个组件,例如加上位置和旋转延迟,来支持更多情景的角色摄像机需求。
有需要的朋友,可以点击【阅读原文】获取源码。仓库接受社区提交代码,期待大家的 PR。
有任何问题或建议,欢迎在评论区留言交流!
“作者介绍:狍子,常年活跃于社区,热衷参加社区活动,拥有多年游戏客户端开发经验。

