记录--实现金币飞入钱包的动画

发布时间 2023-11-20 18:41:28作者: 林恒

这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

 

效果

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

思路

这个效果中,分成两个阶段:

  • 一定数量的金币从一个起点散开
  • 这些金币逐一飞向终点

计算金币的初始散开位置

生成圆周上的等分点

金币散开的位置看似随机,但实际上是围绕起点形成一个圆。对于圆上的等分点,我们可以利用基本的三角函数来计算。例如,若要将圆分成8等分,每个点之间的夹角就是45度(360度/8)。已知圆心坐标和半径,就可以计算出每个等分点的坐标,如下图

随机偏移

为了让金币的位置看起来更自然,我们对每个点的位置进行随机偏移。这可以通过在计算出的坐标上加上一个随机向量来实现,从而让金币围绕起点呈现随机分布的效果。

 位置代码如下:

/**
   * 以某点为圆心,生成圆周上等分点的坐标
   * @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编辑器中为预制体节点添加旋转动画

金币飞向目标位置

计算金币到目标位置的距离

在金币飞向钱包的过程中,我们希望金币按照距离钱包的远近顺序进入。因此,需要先计算每个金币到钱包的距离。这可以通过计算每个金币位置和钱包位置之间的向量距离来实现。

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();
});

这里使用节点池来重复利用金币节点,以防性能紧张

完整代码如下:

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;
  }
}

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

本文转载于:

https://juejin.cn/post/7302618003886194714

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。