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 开发中的实用技巧。转载需注明出处。

相关推荐
实心儿儿1 小时前
Linux —— 库的制作和原理(3)
linux·运维·服务器
yyuuuzz1 小时前
独立站部署的几个常见技术问题
运维·服务器·网络·云计算·aws
hzxpaipai2 小时前
网站建设哪家好?从性能、后台和运维看派迪科技的建站思路
运维·科技
a珍爱上了a强2 小时前
配置uboot启动参数,linux启动过程打印每个模块初始化的耗时时间
linux·运维·服务器
heimeiyingwang2 小时前
【架构实战】Nginx七层负载均衡:从配置到原理,从入门到精通
nginx·架构·负载均衡
OCR_133716212752 小时前
技术解析:护照OCR查验核心逻辑,跨境身份核验的技术实现路径
大数据·运维·人工智能
CQU_JIAKE2 小时前
5.7[Q]
linux·运维·服务器
wanhengidc2 小时前
算力服务器的应用场景
运维·服务器·人工智能·安全·web安全·智能手机
Harvy_没救了2 小时前
【容器技术-Docker】Docker镜像
运维·docker·容器