(收集反馈)3.8.5 Tween 功能增强版试用(可直接拷贝到现有工程中试用)

新的缓动模块代码已经提交到:

https://github.com/dumganhar/cocos-tween-tests

试用方式

  1. 克隆或者下载此仓库
  2. assets/tween 目录拷贝到你的项目中
  3. 将项目中原有的 tween/Tween 从 cc 的导入,改为从当前 ./tween/ 目录导入

支持特性

  • 添加 Tween.reverse 接口,用于翻转某个动作或者翻转整个缓动
  • 添加 id(id_num) 接口,用于标识动作,可用于 reverse(id) 或者 reverse(tween, id) 来翻转当前缓动或者某个缓动内的动作
  • 添加 union(fromId) 接口,用于将从某个 id 开始的动作到当前动作级联成一个动作。
  • Tween 中使用 sequence/parallel/then 级联子 Tween 的时候,子 Tween 允许使用不同 target 。即:解决之前 node transform 与 color / opacity 需要分别 start 的问题
  • 添加 Tween.timeScale 接口,用于缩放缓动
  • 添加 Tween.pause/resume/pauseAllByTarget/resumeAllByTarget 接口,用于暂停、恢复缓动
  • 添加 Tween.update 接口,用于自定义缓动流程(不依赖于 target 的 start, end)
  • 添加 Tween.start(time) 接口,用于从缓动的某个时间节点开始播放
  • 添加 Tween.duration getter,用于获取当前缓动总时长

历史讨论

特别感谢 @SmallMain 参与设计与讨论。

后续改进(可能会在 3.8.5 中)

  1. @SmallMain 关于任意类型和 string 类型的缓动提案 ( issue ) ,思路很好,也不破坏兼容性 :+1:已实现PR
  2. 添加 .running getter 、 Tween.getRunningCount() 静态方法,running 用于判断当前缓动是否正在运行中,getRunningCount 静态方法用于获取当前目标对象关联的所有的缓动实例的个数 (已实现PR
  3. 支持 Tween.from/fromTo 接口 ( issue ) , from 用于指定缓动的起始值,其创建的动作会从起始值缓动到当前值。fromTo 则直接同时指定起始值和结束值,不考虑当前值。
  4. Tween 内部 action 可能需要考虑使用对象池优化,避免频繁重复创建销毁对象,Tween 本身也需要有复位接口 ( issue )
  5. Tween.to 支持可变属性,比如缓动 position 属性,如果 position 持续在变化,那么 Tween.to 要最终缓动到 position 更新后的位置。
  6. 添加 Tween.complete/completeAll/completeByTarget 接口,用于让缓动立马完成。( issue
  7. 支持 punch、shake

Updated

2024.6.13: 支持 Tween 的 pause / resume 状态自动与 Node 的 active / destroy 状态关联:PR 17141

即:

  • 当 node 非激活状态时,它关联的所有 tween 实例将被暂停;
  • 当 node 激活状态时,它关联的所有 tween 实例将被恢复;
  • 当 node 销毁时,它关联的所有 tween 实例将被停止。

2024.6.14: 实现上述改进1, 即支持缓动字符串类型和定制任意对象:PR 17154 ,用法如下:

  1. 缓动字符串
class StringTarget {
    string = '';
}

用例 1(整型的字符串)

const t = new StringTarget();
t.string = '0';

// 这里 string 的值的类型可以是 number 或能转换为 number 的字符串
tween(t).to(1, { string: 100 }).start(); 
tween(t).to(1, { string: '100' }).start();

用例 2(浮点字符串)

const t = new StringTarget();
t.string = '10';

// 从 10 缓动到 110, 缓动过程中 t.string 的值始终保持小数点后两位有效数字
// 比如:'10.00' -> '43.33' -> '76.67' -> '110.00'
tween(t).to(1, { string: { value: 110, toFixed: 2 } }).start(); // 注意,这里使用 value 表示目标值,toFixed 表示要保留的小数位

用例3(自定义处理)

const o = { 
    _position: v3(0, 0, 0),
    set position(v) {
        this._position.set(v)
    },
    get position() {
        return this._position;
    },
    gold: "¥0.00",
    exp: '1000/1000',
    lv: 'Lv.100',
    attack: '100 points',
    health: '10.00',
};

const tweenFormat = {
    currency(value: number): TTweenCustomProperty<string> {
        return {
            value: `¥${value}`,
            progress(start: number, end: number, current: string, ratio: number): string {
                return `¥${lerp(start, end, ratio).toFixed(2)}`;
            },
            convert(v: string): number {
                return Number(v.slice(1));
            },
        };
    },

    health(value: number): TTweenCustomProperty<string> {
        return {
            value: `${value}`,
            toFixed: 2,
        };
    },

    exp(value: number): TTweenCustomProperty<string> {
        return {
            value: () => `${value}/1000`,
            progress(start: number, end: number, current: string, ratio: number): string {
                return `${lerp(start, end, ratio).toFixed(0)}/1000`;
            },
            convert(v: string): number {
                return Number(v.slice(0, v.indexOf('/')));
            },
        };
    },

    lv(value: number): TTweenCustomProperty<string> {
        return {
            value: `Lv.${value}`,
            progress(start: number, end: number, current: string, ratio: number): string {
                return `Lv.${lerp(start, end, ratio).toFixed(0)}`;
            },
            convert(v: string): number {
                return Number(v.slice(v.indexOf('.') + 1));
            },
        };
    },
};

tween(o).to(1, { 
    position: v3(90, 0, 0),
    gold: tweenFormat.currency(100),
    health: tweenFormat.health(1),
    exp: tweenFormat.exp(0),
    lv: tweenFormat.lv(0),
}).start();
  1. 定制任意对象的缓动流程
class MyObject {
    angle = 0;
    str = '';
    private _myProp = new MyProp();

    set myProp(v) {
        this._myProp.x = v.x;
        this._myProp.y = v.y;
    }

    get myProp() {
        return this._myProp;
    }
}

const o = new MyObject();
o.myProp.x = 1;
o.myProp.y = 1;

tween(o)
    .by(1, { myProp: {
        value: new MyProp(100, 100), // 目标值
        progress: MyProp.lerp, // 提供自定义对象缓动过程
        clone: v => v.clone(), // 提供克隆函数
        add: MyProp.add, // 如果用 by 动作,则需要提供 add 方法
        sub: MyProp.sub, // 如果用 by 动作,并且有 reverse 操作,则除了 add 方法,还需要提供 sub 方法
        legacyProgress: false, // 设置为 false 表示启用新的基于对象参数类型的 progress 回调,旧的方式在缓动对象类型是遍历对象中值为 number 的属性然后统一回调 number 参数给 progress
    } }).id(123)
    .reverse(123) // 翻转
    .start();

2024.6.29 添加 Tween.updateUntil API,用于不确定时间间隔的缓动,可用于追踪动态物体。

大家有任何想法也欢迎加入讨论 或者新建 issue 进行讨论。谢谢。

13赞

迄今为止 Cocos 最完善的 Tween System :open_mouth:,感谢你的耐心跟进

2赞

nice~~~~~ :+1:

很满足666

:+1::+1::+1::+1::+1::+1:

强烈建议在 3.8.5 中内置,尽快推出,尽快试用,代码就是不断改才会更完美,别怕有 bug,催催催。

1赞

已经实现的功能,代码已经内置进 3.8.5 了。

抽出这个模块,是为了方便大家提前体验,还有一部分同学可能不想升级引擎,那么也可以直接使用新的 Tween 的功能。

2赞

哦,我说哪里不对 :laughing:,3.8.4 怎么跳过去了,有没有具体点的 3.8.4 和 3.8.5 的发布时间。 :cow:

384只是特别版本专门支持鸿蒙的,要不合并进384吧 :laughing: image

我看彳亍,给引擎组加鸡腿,提个速。

鸿蒙特供版,不加其它功能的。

3.8.4 只带鸿蒙的改动,不夹带私货。
3.8.5 社区版,估计一个月后会有。

期待3.8.5,辛苦你们了

:+1: :+1: :+1:

这个版本怎么同时处理vec3不同分量不同变化的缓动呢?
如同时进行X轴匀速,Y轴加速度,Z轴反复横跳这种
能不使用回调与call实现吗

当前这个版本还不支持,目前只能分别缓动 x, y, z 来实现,并在 onUpdate 回调中调用 setPosition 来实现。此功能我们后续考虑一下。

主题内容已经更新,参考主题内容的 Updated 章节。
由于试用收到的反馈较少,试用仓库中的代码还未更新缓动字符串自定义任意对象缓动流程的功能。

很不错,绝大部分动画需求都能被现在的 Tween System 满足了!

唯一的遗憾是这个 update api,能否在 3.8.5 实现:


关于这一点,其实通过 springs 这种来实现这类目标位置会变化的动画最好,因为如果目标 position 突然变近了,那么会造成当前位置突然退后,即使是变远了,也会造成突然的加速,比较不自然。

不过这个功能好像也有它的应用场景,在实现某些必须要求物体在指定时间内完成的动画时就有用了,但是依然可以通过实时改变 springs 的速度来模拟这种需求,所以我觉得无限时间 update api 还是比较必要的。

牛皮plus,这tween终于真正是个tween了

添加内置 bezier 和 catmullRom 曲线支持

1赞