Cocos Creator 进阶:打造灵活可控的进度条动画组件(循环与分段)

引言

在 Cocos Creator 开发中,进度条(ProgressBar)的动画需求远不止"从 0 到 1"这么简单。我们常常遇到这样的场景:

  • 一次性完成多个任务,获得积分跨越多级,最后定格在对应级的对应积分;

  • 一个大型任务被拆分为多个子阶段,每个阶段内进度线性变化,但阶段之间的进度占比固定;

  • 动画过程中需要动态调整参数(如循环次数、时长),且希望视觉上连续不突兀;

  • 需要随时暂停、恢复进度动画,并准确回调当前状态。

Cocos 自带的 tween 模块虽然强大,但面对上述复杂逻辑,代码会迅速变得臃肿且难以维护。为此,我们设计并封装了两个高复用性的进度条动画组件:TweenProgressLoopComp(循环进度组件)和 TweenProgressSegmentComp(分段进度组件)。本文将深入剖析它们的核心实现原理、关键代码片段以及典型应用场景,帮助你在项目中游刃有余地驾驭各种进度动画。


一、TweenProgressLoopComp:循环 + 最终目标进度

1. 功能概述

该组件让进度条先完成若干次从 0 到 1 的循环,每次循环结束后可停留短暂间隔,最后到达指定的目标进度(可以是任意值,不一定是 1)。它完美模拟了"蓄力 - 释放"或"加载 - 完成"的视觉效果。

2. 核心属性与方法

属性/方法 说明
duration, delay, interval, easeType 基础动画参数
loopCount 循环次数(0 表示无循环,直接到目标)
targetProgress 最终停留的目标进度(0~1)
setProgress(params, isAnimate) 设置目标并播放/不播放动画
pause() / resume() 暂停/恢复动画
updateParams(params, smooth) 更新动画参数,smooth 为 true 时平滑过渡
五类回调 start, update, complete, segmentStart, segmentComplete

3. 核心代码与原理剖析

3.1 Tween 链的动态构建

组件根据 loopCount 决定构建简单动画或循环动画。下面是循环动画的构建代码:

TypeScript 复制代码
private createLoopTween(): void {
    const remainingLoops = this.getParam('loopCount', 0) - this.currentLoopCount;
    if (remainingLoops <= 0) {
        this.createFinalSegment().start();
        return;
    }

    // 第一个循环段(可能带有初始延迟)
    let fullTween = this.createLoopSegment(true);
    for (let i = 1; i < remainingLoops; i++) {
        fullTween = fullTween.then(this.createLoopSegment(false));
    }
    // 最后追加最终段
    this.currentTween = fullTween.then(this.createFinalSegment()).start();
}

private createLoopSegment(isInitial: boolean): Tween<ProgressBar> {
    return tween(this.target)
        .delay(isInitial ? this.getParam('delay', 0) : 0)
        .call(() => this.executeCallback('segmentStart', 0, this.currentLoopCount + 1))
        .to(this.getParam('duration', 1.0), { progress: 1 }, {
            easing: this.getParam('easeType', 'linear'),
            onUpdate: () => this.executeCallback('update'),
            onComplete: () => {
                this.currentLoopCount++;
                this.executeCallback('segmentComplete', 1, this.currentLoopCount);
            },
        })
        .delay(this.getParam('interval', 0.7))
        .call(() => {
            if (!this.isAnimationStopped && this.target) {
                this.target.progress = 0;  // 归零,准备下一次循环
            }
        });
}

解释

  • createLoopSegment 创建一个标准的循环段:从当前进度到1,完成后回调 segmentComplete,然后延迟 interval,最后将进度强行置0。

  • createLoopTween 根据剩余循环次数,将多个循环段用 then 串联,最后追加 createFinalSegment()

  • 这种链式构建保证了每段之间的顺序执行,且利用了 tween 的 delaycall 实现归零和间隔。

3.2 暂停与恢复的状态机

暂停时记录关键状态,恢复时根据状态重建动画。下面是 resume 中处理循环动画的核心代码:

TypeScript 复制代码
// 暂停时保存
this.pauseProgress = this.target.progress;
this.animationType = this.activeParams.loopCount === 0 ? 'simple' : 'loop';
this.stopCurrentTween();

// 恢复时(循环动画)
const elapsedInSegment = this.pauseProgress; // 当前段已执行比例 (0~1)
const remainingInSegment = 1 - elapsedInSegment;
const remainingDuration = totalDuration * remainingInSegment;

// 先补完当前段
this.currentTween = tween(this.target)
    .to(remainingDuration, { progress: 1 }, {
        easing: this.activeParams.easeType,
        onComplete: () => {
            this.currentLoopCount++;
            // 继续剩余循环
            const remainingLoops = this.activeParams.loopCount! - this.currentLoopCount;
            if (remainingLoops > 0) {
                this.currentTween = this.buildRemainingLoopTween(remainingLoops).start();
            } else {
                this.currentTween = this.createFinalSegment().start();
            }
        }
    })
    .start();

关键点

  • 暂停时记录了当前进度 pauseProgress,该值就是当前段内的完成比例(因为每段都是从0到1)。

  • 恢复时先计算当前段剩余时长,用 to 动画补完当前段。

  • 当前段完成后,再根据剩余循环次数构建后续段或最终段。

3.3 平滑参数更新(smooth 模式)

updateParamssmooth 模式复用暂停/恢复机制,实现无缝参数变更:

TypeScript 复制代码
public updateParams(params: AnimationParams, smooth: boolean = false): void {
    Object.assign(this.activeParams, params);
    if (this.currentTween && !this.isAnimationStopped && !this.isPaused) {
        if (smooth) {
            this.pause();   // 记录当前状态
            this.resume();  // 基于新参数重建动画
        } else {
            this.restartAnimation();
        }
    }
}

二、TweenProgressSegmentComp:多段进度,精准映射

1. 功能概述

该组件将进度条划分为多个段,每段占整体进度的比例固定,但每段对应的数值范围可以自定义。支持两种模式:

  • 均匀值模式:每段数值范围均匀。

  • 不均匀值模式:每段数值范围可自定义(累加或绝对值)。

2. 核心原理:值与进度的双向映射

这是分段组件的数学基础。内部维护 segmentData 数组,包含每段的起始值、结束值以及占整体进度的比例(固定为 1/段数)。两个核心转换函数如下:

TypeScript 复制代码
private valueToProgress(value: number): number {
    if (value >= this.maxValue) return 1;
    if (value <= 0) return 0;

    if (this._segmentMode === SegmentMode.EVEN_PROGRESS_EVEN_VALUE) {
        return value / this.maxValue;
    }
    const segmentIndex = this.findSegmentForValue(value);
    const segment = this.segmentData[segmentIndex];
    const segmentRatio = (value - segment.startValue) / (segment.endValue - segment.startValue);
    return (segmentIndex + segmentRatio) / this.segmentData.length;
}

private progressToValue(progress: number): number {
    if (progress >= 1) return this.maxValue;
    if (progress <= 0) return 0;

    if (this._segmentMode === SegmentMode.EVEN_PROGRESS_EVEN_VALUE) {
        return progress * this.maxValue;
    }
    const segmentCount = this.segmentData.length;
    const segmentIndex = Math.min(Math.floor(progress * segmentCount), segmentCount - 1);
    const segment = this.segmentData[segmentIndex];
    const segmentProgress = (progress - segmentIndex / segmentCount) * segmentCount; // 0~1
    return segment.startValue + segmentProgress * (segment.endValue - segment.startValue);
}

解释

  • valueToProgress:找到值所在的段,计算段内比例,再转换为整体进度。

  • progressToValue:根据整体进度定位段,计算段内进度,再映射为数值。

  • 这两个函数保证了无论动画如何驱动 progress,我们都能实时得到准确的 currentValue

3. 分段动画的构建

当调用 setProgress(targetValue) 时,组件会遍历所有经过的段,为每个段创建子 tween。下面是正向遍历的代码片段:

TypeScript 复制代码
private createProgressTween(): void {
    const direction = this.targetValue > this.currentValue ? 1 : -1;
    const startSegment = this.currentSegment;
    const endSegment = this.findSegmentForValue(this.targetValue);

    let tweenChain: Tween<ProgressBar> | null = null;
    let isFirstSegment = true;

    if (direction > 0) {
        for (let seg = startSegment; seg <= endSegment; seg++) {
            const segEndValue = (seg === endSegment) ? this.targetValue : this.segmentData[seg].endValue;
            const segTween = this.createSegmentTween(seg, segEndValue, isFirstSegment, direction);
            tweenChain = tweenChain ? tweenChain.then(segTween) : segTween;
            if (seg < endSegment) {
                tweenChain = tweenChain.delay(this.getParam('interval', 0.5));
            }
            isFirstSegment = false;
        }
    } // 反向类似...
    // 最后添加 complete 回调
    this.currentTween = tweenChain.call(() => this.executeCallback('complete')).start();
}

每个段的具体动画由 createSegmentTween 生成:

TypeScript 复制代码
private createSegmentTween(segmentIndex: number, targetValue: number, isFirstSegment: boolean, direction: 1 | -1): Tween<ProgressBar> {
    const segment = this.segmentData[segmentIndex];
    const startProgress = this.valueToProgress(this.currentValue);
    const targetProgress = this.valueToProgress(targetValue);
    const progressDelta = Math.abs(targetProgress - startProgress);
    const segmentFullProgress = segment.progressSize; // 1/段数
    const progressRatio = progressDelta / segmentFullProgress;
    const duration = this.getParam('duration', 1.0) * progressRatio; // 按比例缩放时长

    return tween(this.target)
        .call(() => {
            if (isFirstSegment) this.executeCallback('start');
        })
        .delay(isFirstSegment ? this.getParam('delay', 0) : 0)
        .to(duration, { progress: targetProgress }, {
            easing: this.getParam('easeType', 'linear'),
            onStart: () => {
                // 如果刚好在段边界,触发 segmentStart
                if (this.approxEqual(this.currentValue, segment.startValue) ||
                    this.approxEqual(this.currentValue, segment.endValue)) {
                    this.executeCallback('segmentStart', segmentIndex);
                }
            },
            onUpdate: (target) => {
                this.currentValue = this.progressToValue(target.progress);
                this.executeCallback('update', segmentIndex);
            },
            onComplete: () => {
                this.currentValue = targetValue;
                // 更新当前段索引
                if (direction > 0 && targetValue >= segment.endValue - 1e-6) {
                    this.currentSegment = segmentIndex + 1;
                } else if (direction < 0 && targetValue <= segment.startValue + 1e-6) {
                    this.currentSegment = segmentIndex - 1;
                }
                this.executeCallback('segmentComplete', segmentIndex);
            },
        });
}

关键点

  • 时长 duration 根据该段实际需要移动的进度比例进行缩放(例如只走半段,时长减半)。

  • 通过 valueToProgressprogressToValue 保证数值与进度同步。

  • 段完成后更新 currentSegment,便于下一段构建。

4. 暂停恢复的复杂状态管理

由于分段动画涉及多段,暂停时需要保存足够的信息:

TypeScript 复制代码
private pauseState: {
    progress: number;
    currentValue: number;
    currentSegment: number;
    targetValue: number;
    remainingDuration: number;  // 当前段剩余时长(秒)
    direction: 1 | -1;
} | null = null;

public pause(): void {
    if (this.currentTween && !this.isPaused && this.target) {
        const progress = this.target.progress;
        const direction = this.targetValue > this.currentValue ? 1 : -1;
        const segmentProgress = this.calculateSegmentProgress(this.currentValue, this.currentSegment);
        const remainingRatio = direction > 0 ? (1 - segmentProgress) : segmentProgress;
        const remainingDuration = this.getParam('duration', 1.0) * remainingRatio;

        this.pauseState = { progress, currentValue: this.currentValue, currentSegment: this.currentSegment, targetValue: this.targetValue, remainingDuration, direction };
        this.stopCurrentTween();
        this.isPaused = true;
    }
}

恢复时,对第一段使用固定的 remainingDuration,后续段仍用标准时长:

TypeScript 复制代码
private createResumeTween(remainingFirstSegmentDuration: number, direction: 1 | -1): void {
    // 遍历剩余段,第一段使用 fixedDuration = remainingFirstSegmentDuration
    for (let seg = startSegment; seg <= endSegment; seg++) {
        const segTween = this.createResumeSegmentTween(seg, segEndValue, isFirstSegment, direction,
            isFirstSegment ? remainingFirstSegmentDuration : undefined);
        // 链式组合...
    }
}

private createResumeSegmentTween(segmentIndex: number, targetValue: number, isFirstSegment: boolean, direction: 1 | -1, fixedDuration?: number): Tween<ProgressBar> {
    const duration = fixedDuration !== undefined ? fixedDuration : this.getParam('duration', 1.0);
    // 其余与 createSegmentTween 类似...
}

这种设计保证了从暂停点无缝续播,不会出现跳帧或时长错误。


三、循环组件 vs 分段组件:如何选择?

维度 循环组件 (Loop) 分段组件 (Segment)
核心诉求 重复填充 + 最终定格 将进度按阶段划分,每段值可独立定义
进度-值关系 线性,0→1 循环 分段线性,支持均匀或自定义值映射
典型回调需求 循环次数、当前循环进度 段索引、段内进度、整体进度
暂停恢复复杂度 中等(需记录循环次数、段内进度) 较高(需记录当前段、剩余时长、方向)
适用场景 加载动画、充能、倒计时 任务流程、等级经验、多步骤操作

两者并非互斥,有时甚至可以组合使用------例如分段组件的每一段内部再使用循环效果,不过这就需要更复杂的嵌套封装了。


四、扩展思考

  1. 编辑器可视化配置:可以将两个组件的参数暴露在 Cocos Creator 的属性检查器中,方便策划调整。

  2. 性能优化:当段数极多时,可使用单 tween + 手动计算进度替代链式 tween,避免大量对象创建。

  3. 更多映射模式 :支持进度非均匀分布(自定义每段占整体比例),只需修改 progressSize 计算逻辑即可。

结语

通过封装 TweenProgressLoopCompTweenProgressSegmentComp,我们得以在 Cocos Creator 项目中快速实现高复用性的复杂进度动画。它们不仅解决了基础 tween 难以处理的多段、循环需求,更提供了完善的暂停恢复、参数热更新和回调机制,让开发者能更专注于业务逻辑,而非动画细节。

如果你也在为进度条动画的复杂性而烦恼,不妨尝试基于这两个思路封装自己的组件,或直接参考本文的实现。


本文由作者原创,旨在分享 Cocos 开发中的实用技巧。转载需注明出处。

相关推荐
ping某1 天前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质3 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
Inhand陈工3 天前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智3 天前
ARP代理--工作原理
运维·网络·arp·arp代理
shushangyun_3 天前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化
施努卡机器视觉3 天前
SNK施努卡侧滑门锁上滑轮总成自动化装配线,从零件到组件,全流程精密制造方案
运维·自动化·制造
AC赳赳老秦3 天前
用 OpenClaw 搭建服务器故障应急响应系统,自动处理 80% 常见运维故障
android·运维·服务器·python·rxjava·deepseek·openclaw
java_cj3 天前
深入kube-apiserver认证机制:从Bearer Token到mTLS的完整认证链解析
linux·运维·服务器·云原生·容器·kubernetes