本文由“壹伴编辑器”提供技术支
前言
在游戏开发过程中,可能会有让一个物体围绕另一个物体旋转的需求,就比如月球围绕着地球旋转,同时地球也在围绕着太阳旋转。
本文给大家分享一个实现 围绕物体旋转 的方案以及可直接使用的组件。
本文由“壹伴编辑器”提供技术支
效果展示
- 节点层级( A 为被围绕节点):
左:B 为 A 的子节点 右:两者在同一层级
- 成品组件(挂载在节点 B 上):
RotateAround 组件
- 运行效果图(动图录制帧率较低):
左:顺时针旋转且6秒转一圈 右:顺时针旋转且-y轴指向目标
左:逆时针旋转且x轴指向目标 右:逆时针旋转且10秒转一圈
本文由“壹伴编辑器”提供技术支
正文
整体思路
对于这类持续运动的实现,我们都可以在 update 中每帧进行操作。
首先假定有两个物体 A 与 B ,且二者处于同一层级或者将 A 作为 B 的父节点。我们每帧都根据 A 的位置、两者的角度以及他们之间的距离来计算 B 的位置,这样即使 A 的位置发生变化 B 也能在正确的轨道上运动。
如果两个节点处于不同的层级则可能需要经过坐标系的转换,但是原理是相同的,所以本文不讨论该情况。
并且我们可以根据二者之间的角度来让 B 的某一面始终指向 A 。
代码实现
提示:点击文章底部的 阅读原文 即可获取完整文件
首先定义一个枚举 Axis 来作为旋转时的指向选项,同时导出给外部调用:
代码语言:javascript复制export enum Axis {
PositiveX, // 正 X 轴
PositiveY, // 正 Y 轴
NegativeX, // 负 X 轴
NegativeY, // 负 Y 轴
}
定义我们需要用到的属性和参数(只有 faceToTarget 为 true 时才会显示 faceAxis 选项):
代码语言:javascript复制@property({ type: cc.Node, tooltip: '围绕旋转的目标' })
public target: cc.Node = null;
@property({ tooltip: '顺时针旋转' })
public clockwise: boolean = true;
@property({ tooltip: '旋转一圈花费的时间' })
public timePerRound: number = 10;
@property({ tooltip: '是否始终面向目标节点' })
public faceToTarget: boolean = false;
@property({
type: cc.Enum(Axis),
tooltip: '面向目标节点的轴:n- PositiveX:正 X 轴n- PositiveY:正 Y 轴n- NegativeX:负 X 轴n- NegativeY:负 Y 轴',
visible() { return this.faceToTarget }
})
public faceAxis: Axis = Axis.NegativeY;
@property({ tooltip: '自动开始旋转' })
public autoStart: boolean = false;
public angle: number = 0; // 角度
public radius: number = 0; // 半径
private isRotating: boolean = false; // 标志位,是否正在旋转
然后我们需要一个 public 的 run 函数来作为启动函数,这样就可以在其他脚本主动调用该函数了。并且在启动函数里先获取初始的角度和半径:
代码语言:javascript复制/**
* 开始围绕目标节点旋转
* @param target 目标节点
* @param clockwise 是否顺时针旋转
* @param timePerRound 旋转一圈的时间
* @param faceToTarget 是否始终面向目标节点
* @param faceAxis 面向目标节点的轴
*/
public run(target?: cc.Node, clockwise?: boolean, timePerRound?: number, faceToTarget?: boolean, faceAxis?: Axis) {
if (target) this.target = target;
if (clockwise) this.clockwise = clockwise;
if (timePerRound) this.timePerRound = timePerRound;
if (faceToTarget) this.faceToTarget = faceToTarget;
if (faceAxis) this.faceAxis = faceAxis;
if (!this.target) {
cc.log('No target!');
return;
}
// 计算初始角度和半径
this.angle = this.getAngle(this.target.getPosition(), this.node.getPosition());
this.radius = this.getDistance(this.target.getPosition(), this.node.getPosition());
// 开始
this.isRotating = true;
}
其中的 getAngle 函数用来计算两个点之间的角度:
代码语言:javascript复制/**
* 获取两点间的角度
* @param p1 点1
* @param p2 点2
*/
private getAngle(p1: cc.Vec2, p2: cc.Vec2): number {
return Math.atan(p2.y - p1.y / p2.x - p1.x);
}
而 getDistance 函数用来计算两个点之间的距离:
代码语言:javascript复制/**
* 获取两点间的距离
* @param p1 点1
* @param p2 点2
*/
private getDistance(p1: cc.Vec2, p2: cc.Vec2): number {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) Math.pow(p2.y - p1.y, 2));
}
接下来在 update 中实现主要逻辑:
代码语言:javascript复制update(dt: number) {
if (!this.isRotating || !this.target) return;
// 将角度转换为弧度
let radian = Math.PI / 180 * this.angle;
// 更新节点的位置
this.node.x = this.target.x this.radius * Math.cos(radian);
this.node.y = this.target.y this.radius * Math.sin(radian);
// 更新节点的角度
if (this.faceToTarget) {
switch (this.faceAxis) {
case Axis.PositiveX:
this.node.angle = this.angle 180;
break;
case Axis.PositiveY:
this.node.angle = this.angle 90;
break;
case Axis.NegativeX:
this.node.angle = this.angle;
break;
case Axis.NegativeY:
this.node.angle = this.angle - 90;
break;
}
}
// 计算下一帧的角度
let anglePerFrame = dt * (360 / this.timePerRound);
if (this.clockwise) this.angle -= anglePerFrame;
else this.angle = anglePerFrame;
// 重置角度,避免数值过大
if (this.angle >= 360) this.angle %= 360;
else if (this.angle <= -360) this.angle %= -360;
}
在 start 中执行自动启动的逻辑:
代码语言:javascript复制start() {
if (this.autoStart) this.run();
}
最后还需要一个 public 的 stop 函数来停止当前的旋转:
代码语言:javascript复制/**
* 停止旋转
*/
public stop() {
this.isRotating = false;
}
本文由“壹伴编辑器”提供技术支
结束语
以上皆为本菜鸡的个人观点,由于文采不太好,写得不好还请各位见谅。如果有哪些地方说的不对,还请各位指出,大家共同进步。