实现金币飞入钱包的动画

效果

金币从初始位置散开后逐个飞向指定位置,这是游戏中很常用的一个动画,效果如下:

本文使用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;
  }
}

到此就实现了开头的效果。

相关推荐
LcGero18 天前
TypeScript 快速上手:泛型与工具类型
typescript·cocos creator·游戏开发
LcGero19 天前
Cocos Creator 3.x 高维护性打字机对话系统设计与实现
cocos creator·打字机
LcGero19 天前
Cocos Creator 三端接入穿山甲 SDK
sdk·cocos creator·穿山甲
LcGero20 天前
Cocos Creator平台适配层框架设计
cocos creator·平台·框架设计
LcGero21 天前
Cocos Creator 业务与原生通信详解
android·ios·cocos creator·游戏开发·jsb
LcGero22 天前
TypeScript 快速上手:前言
typescript·cocos creator·游戏开发
Setsuna_F_Seiei23 天前
CocosCreator 游戏开发 - 多维度状态机架构设计与实现
前端·cocos creator·游戏开发
CodeCaptain3 个月前
cocoscreator 2.4.x 场景运行时的JS生命周期浅析
cocos creator·开发经验
CodeCaptain4 个月前
CocosCreator 3.8.x [.gitignore]文件内容,仅供参考
经验分享·cocos creator
VaJoy5 个月前
Cocos Creator Shader 入门 (21) —— 高斯模糊的高性能实现
前端·cocos creator