效果
金币从初始位置散开后逐个飞向指定位置,这是游戏中很常用的一个动画,效果如下:
本文使用Cocos Creator 2.4.11版本。
思路
这个效果中,分成两个阶段:
- 一定数量的金币从一个起点散开
- 这些金币逐一飞向终点
计算金币的初始散开位置
生成圆周上的等分点
金币散开的位置看似随机,但实际上是围绕起点形成一个圆。对于圆上的等分点,我们可以利用基本的三角函数来计算。例如,若要将圆分成8等分,每个点之间的夹角就是45度(360度/8)。已知圆心坐标和半径,就可以计算出每个等分点的坐标,如下图
随机偏移
为了让金币的位置看起来更自然,我们对每个点的位置进行随机偏移。这可以通过在计算出的坐标上加上一个随机向量来实现,从而让金币围绕起点呈现随机分布的效果。
位置代码如下:
typescript
/**
* 以某点为圆心,生成圆周上等分点的坐标
* @param {number} radius 半径
* @param {cc.Vec2} pos 圆心坐标
* @param {number} count 等分点数量
* @param {number} randomScope 等分点的随机波动范围
* @returns {cc.Vec2[]} 返回等分点坐标
*/
getCirclePosition(radius: number, pos: cc.Vec2, count: number, randomScope: number = 60): cc.Vec2[] {
let positions = [];
let radians = (Math.PI / 180) * Math.round(360 / count);
for (let i = 0; i < count; i++) {
let x = pos.x + radius * Math.sin(radians * i);
let y = pos.y + radius * Math.cos(radians * i);
positions.unshift(cc.v3(x + Math.random() * randomScope, y + Math.random() * randomScope, 0));
}
return positions;
}
金币是一直在旋转,还需要在Cocos Creator编辑器中为预制体节点添加旋转动画
金币飞向目标位置
计算金币到目标位置的距离
在金币飞向钱包的过程中,我们希望金币按照距离钱包的远近顺序进入。因此,需要先计算每个金币到钱包的距离。这可以通过计算每个金币位置和钱包位置之间的向量距离来实现。
typescript
let points = this.getCirclePosition(r, stPos, count);
let coinNodeList = points.map(pos => {
let coin = this.getCoinNode();
coin.setPosition(stPos);
this.node.addChild(coin);
return {
node: coin,
stPos: stPos,
mdPos: pos,
edPos: edPos,
dis: (pos as any).sub(edPos).mag()
};
});
排序和动画执行
根据计算出的距离对金币进行排序,使距离近的金币先飞入钱包。
typescript
coinNodeList = coinNodeList.sort((a, b) => {
if (a.dis - b.dis > 0) return 1;
if (a.dis - b.dis < 0) return -1;
return 0;
});
通过缓动动画系统播放金币飞向目标位置的动画
typescript
// 执行金币落袋的动画
coinNodeList.forEach((item, idx) => {
cc.tween(item.node)
.to(0.3, {position: item.mdPos})
.delay(idx * 0.01)
.to(0.5, {position: item.edPos})
.call(() => {
// 金币落袋后,将金币节点放入节点池中,并更新金币数值
this.coinNum += 20;
this.coinNumLabel.string = this.coinNum.toString();
this.coinPool.put(item.node);
})
.start();
});
这里使用节点池来重复利用金币节点,以防性能紧张
完整代码如下:
typescript
const { ccclass, property } = cc._decorator;
@ccclass
export default class CoinRewardEffect extends cc.Component {
/** 金币动画启动 */
@property(cc.Node)
startNode: cc.Node = null;
/** 金币动画终点 */
@property(cc.Node)
endNode: cc.Node = null;
/** 金币数值Label */
@property(cc.Label)
coinNumLabel: cc.Label = null;
/** 金币预制节点 */
@property(cc.Prefab)
coinPrefab: cc.Prefab = null;
/** 金币节点池 */
coinPool: cc.NodePool = null;
/** 金币数 */
coinNum: number = 1000;
onLoad() {
this.coinPool = new cc.NodePool();
this.coinNumLabel.string = this.coinNum.toString();
this.initCoinPool();
}
/** 先预先创建几个节点放入节点池中 */
initCoinPool(count: number = 20) {
for (let i = 0; i < count; i++) {
let coin = cc.instantiate(this.coinPrefab);
this.coinPool.put(coin);
}
}
/** 从节点池中取出节点 */
getCoinNode() {
let coin = null;
if (this.coinPool.size() > 0) {
coin = this.coinPool.get();
} else {
coin = cc.instantiate(this.coinPrefab);
}
return coin;
}
playAnim() {
let randomCount = 20;//Math.random() * 10 + 10;
let stPos = this.startNode.getPosition();
let edPos = this.endNode.getPosition();
this.playCoinRewardAnim(randomCount, stPos, edPos);
}
/**
* 金币飞向钱包的动画
*
* @param {number} count 金币数量
* @param {cc.Vec2} stPos 金币起始位置
* @param {cc.Vec2} edPos 金币终点位置
* @param {number} [r=130] 金币飞行的半径
*/
playCoinRewardAnim(count: number, stPos: cc.Vec2, edPos: cc.Vec2, r: number = 130) {
// 生成圆,并且对圆上的点进行排序
let points = this.getCirclePosition(r, stPos, count);
let coinNodeList = points.map(pos => {
let coin = this.getCoinNode();
coin.setPosition(stPos);
this.node.addChild(coin);
return {
node: coin,
stPos: stPos,
mdPos: pos,
edPos: edPos,
dis: (pos as any).sub(edPos).mag()
};
});
coinNodeList = coinNodeList.sort((a, b) => {
if (a.dis - b.dis > 0) return 1;
if (a.dis - b.dis < 0) return -1;
return 0;
});
// 执行金币落袋的动画
coinNodeList.forEach((item, idx) => {
cc.tween(item.node)
.to(0.3, {position: item.mdPos})
.delay(idx * 0.01)
.to(0.5, {position: item.edPos})
.call(() => {
// 金币落袋后,将金币节点放入节点池中,并更新金币数值
this.coinNum += 20;
this.coinNumLabel.string = this.coinNum.toString();
this.coinPool.put(item.node);
})
.start();
});
}
/**
* 以某点为圆心,生成圆周上等分点的坐标
* @param {number} radius 半径
* @param {cc.Vec2} pos 圆心坐标
* @param {number} count 等分点数量
* @param {number} randomScope 等分点的随机波动范围
* @returns {cc.Vec2[]} 返回等分点坐标
*/
getCirclePosition(radius: number, pos: cc.Vec2, count: number, randomScope: number = 60): cc.Vec2[] {
let positions = [];
let radians = (Math.PI / 180) * Math.round(360 / count);
for (let i = 0; i < count; i++) {
let x = pos.x + radius * Math.sin(radians * i);
let y = pos.y + radius * Math.cos(radians * i);
positions.unshift(cc.v3(x + Math.random() * randomScope, y + Math.random() * randomScope, 0));
}
return positions;
}
}
到此就实现了开头的效果。