在 Cocos Creator 3.x 版本中, Tween系统代替了原来的Action系统。很多朋友不明白Tween到底是什么,Tween原理是什么?怎么使用Tween?
今天就来详细了解一下,希望能帮助到大家加深对Tween的了解,并快速掌握Tween的使用方法。
一、Tween到底是什么?
Tween 又称为缓动系统,可以用于变换位置、旋转、缩放和颜色等常规动画信息,也支持延迟,队列,并行等动作行为。
首先我们来看一段案例代码:
tween(this.node)
//to,在第1秒的时候放大为2倍,位置为(50,50),角度变化到90
.to(1,{scale:2,position:cc.v2(50,50),rotation:90})
//by,在第2秒时,缩放变化了2倍(现在是原始大小的4倍),位置变化了(50,50),现在位置为(100,100)
.by(1,{scale:2, position:cc.v2(50,50)})
//在第3秒时缩放为原始大小,位置设置为(0,0),然后加了一个缓动效果 backOut
.to(1,{scale:1,position:cc.v2(0,0)},{easing:"backOut"})
.start();
通过上面演示的代码,我们可以知道,Tween可以同时设置多个属性, 在调用 start 时会将之前生成的 action 队列重新组合生成一个 cc.sequence 队列,依次执行。
Tween的特点
支持以链式结构的方式创建一个动画序列
不局限于节点上的属性
Tween有2种设置方式
to 改变到某个值(绝对值)
by 变化值(相对值)
二、Tween 缓动系统原理
为了加深理解,来看一张UML图:
此UML图中,Node(目标节点)、Action(动作) 、ActionManager(动作管理) 类, 相信大家都比较熟悉了,这里就不作介绍。
这里还涉及到一些比较新的内容:Tween 、TweenAction、props 、ITweenOption下面分别介绍。
1、Tween类是干嘛的?
通过上面UML 图的接口,我们可以清晰地看到,Tween其实有两个主要功能:
在使用的时候,可以添加一个或者多个Action到内部缓冲,也可添加多个Action组合的串行Action或者并行Action。
Tween类重要成员属性:
Tween类重要接口:
target: 设置目标对象
delay: 添加一个DelayAction
to: 添加一个TweenAction来指定每个属性变换到多少-目标值
by: 添加一个TweenAction来指定每个属性变换多少-变化值
union: 将缓冲中的多个Action变成一个串行Action
sequence: 将指定的多个Action或者Tween变成一个串行Action后添加到缓冲中
parallel: 将指定的多个Action或者Tween变成一个并行Action后添加到缓冲中
start: 开始运行,此时会创建最终的Action,并且启动此Action
stop: 停止Action的运行
2、TweenAction是做什么的?
TweenAction类从ActionInterval派生,UML图中简化成从Action派生,能够针对目标对象的多个任意属性,以指定缓动方式进行变换。
只要目标对象的属性能够直接赋值和取值,这个TweenAction就能够胜任。
前面的Tween类by接口和to接口就是用TweenAction实现。
它的灵活和强大之处在于,不用像以前那样,每个属性都要实现重复代码的Action类。
3、props是什么?
props是TweenAction需要进行变换的属性。
props 本质就是一个键值对数据结构,键为对象的属性名称,值为进行变换的值。
例如:
{ angle: 90, position: v3(200, 200, 0)}
4、ITweenOption接口
ITweenOption接口,其实就是进行变换的缓动方式以及变换过程中的回调,从UML 图中我们也可以看到,
可以指定如下属性:
easing: 缓动方式
progress: 插值函数,用来自定义缓动方式
onStart: 缓动动作启动时的回调函数
onUpdate: 缓动动作更新时的回调函数
onComplete: 缓动动作完成时的回调函数
三、Tween 的使用
明白了Tween 的原理和重要的接口后,使用就比较简单了。
使用步骤可以简单分为三步:
Step1:创建一个针对目标的Tween对象
let tween = new Tween(this.node);
Step2:添加执行过程
tween.to(1.0, {angle:90,position:v3(100,100,0) });
Step3:开始执行tween对象
tween.start();
四、Tween的源码实现
如果想深入了解Tween的朋友,这里我贴了官方tween.ts和tween-action.ts 源码,建议花点时间完整的看一下,这样用起来会更加得心应手。
官方tween.ts源码:
/*
Copyright (c) 2020-2023 Xiamen Yaji Software Co., Ltd.
https://www.cocos.com/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
import { warn } from '@base/debug';
import { cclegacy } from '@base/global';
import { TweenSystem } from './tween-system';
import { ActionInterval, sequence, repeat, repeatForever, reverseTime, delayTime, spawn } from './actions/action-interval';
import { removeSelf, show, hide, callFunc } from './actions/action-instant';
import { Action, FiniteTimeAction } from './actions/action';
import { ITweenOption } from './export-api';
import { TweenAction } from './tween-action';
import { SetAction } from './set-action';
// https://medium.com/dailyjs/typescript-create-a-condition-based-subset-types-9d902cea5b8c
type FlagExcludedType = { [Key in keyof Base]: Base[Key] extends Type ? never : Key };
type AllowedNames = FlagExcludedType [keyof Base];
type KeyPartial = { [P in K]?: T[P] };
type OmitType = KeyPartial >;
// eslint-disable-next-line @typescript-eslint/ban-types
type ConstructorType = OmitType;
/**
* @en
* Tween provide a simple and flexible way to action, It's transplanted from cocos creator。
* @zh
* Tween 提供了一个简单灵活的方法来缓动目标,从 creator 移植而来。
* @class Tween
* @param [target]
* @example
* tween(this.node)
* .to(1, {scale: new Vec3(2, 2, 2), position: new Vec3(5, 5, 5)})
* .call(() => { log('This is a callback'); })
* .by(1, {scale: new Vec3(-1, -1, -1), position: new Vec3(-5, -5, -5)}, {easing: 'sineOutIn'})
* .start()
*/
export class Tween {
private _actions: Action[] = [];
private _finalAction: Action | null = null;
private _target: T | null = null;
private _tag = Action.TAG_INVALID;
constructor (target?: T | null) {
this._target = target === undefined ? null : target;
}
/**
* @en Sets tween tag
* @zh 设置缓动的标签
* @method tag
* @param tag @en The tag set for this tween @zh 为当前缓动设置的标签
*/
tag (tag: number): Tween {
this._tag = tag;
return this;
}
/**
* @en
* Insert an action or tween to this sequence.
* @zh
* 插入一个 tween 到队列中。
* @method then
* @param other @en The rear tween of this tween @zh 当前缓动的后置缓动
*/
then (other: Tween): Tween {
if (other instanceof Action) {
this._actions.push(other.clone());
} else {
this._actions.push(other._union());
}
return this;
}
/**
* @en
* Sets tween target.
* @zh
* 设置 tween 的 target。
* @method target
* @param target @en The target of this tween @zh 当前缓动的目标对象
*/
target (target: T): Tween {
this._target = target;
return this;
}
/**
* @en
* Start this tween.
* @zh
* 运行当前 tween。
*/
start (): Tween {
if (!this._target) {
warn('Please set target to tween first');
return this;
}
if (this._finalAction) {
TweenSystem.instance.ActionManager.removeAction(this._finalAction);
}
this._finalAction = this._union();
this._finalAction.setTag(this._tag);
TweenSystem.instance.ActionManager.addAction(this._finalAction, this._target as any, false);
return this;
}
/**
* @en
* Stop this tween.
* @zh
* 停止当前 tween。
*/
stop (): Tween {
if (this._finalAction) {
TweenSystem.instance.ActionManager.removeAction(this._finalAction);
}
return this;
}
/**
* @en
* Clone a tween.
* @zh
* 克隆当前 tween。
* @method clone
* @param target @en The target of clone tween @zh 克隆缓动的目标对象
*/
clone (target: T): Tween {
const action = this._union();
return tween(target).then(action.clone() as any);
}
/**
* @en
* Integrate all previous actions to an action.
* @zh
* 将之前所有的 action 整合为一个 action。
*/
union (): Tween {
const action = this._union();
this._actions.length = 0;
this._actions.push(action);
return this;
}
/**
* @en
* Add an action which calculates with absolute value.
* @zh
* 添加一个对属性进行绝对值计算的 action。
* @method to
* @param duration @en Tween time, in seconds @zh 缓动时间,单位为秒
* @param props @en List of properties of tween @zh 缓动的属性列表
* @param opts @en Optional functions of tween @zh 可选的缓动功能
* @param opts.progress @en Interpolation function @zh 缓动的速度插值函数
* @param opts.easing @en Tween function or a lambda @zh 缓动的曲线函数或lambda表达式
*/
to (duration: number, props: ConstructorType, opts?: ITweenOption): Tween {
opts = opts || Object.create(null);
(opts as any).relative = false;
const action = new TweenAction(duration, props, opts);
this._actions.push(action);
return this;
}
/**
* @en
* Add an action which calculates with relative value.
* @zh
* 添加一个对属性进行相对值计算的 action。
* @method by
* @param duration @en Tween time, in seconds @zh 缓动时间,单位为秒
* @param props @en List of properties of tween @zh 缓动的属性列表
* @param opts @en Optional functions of tween @zh 可选的缓动功能
* @param [opts.progress]
* @param [opts.easing]
* @return {Tween}
*/
by (duration: number, props: ConstructorType, opts?: ITweenOption): Tween {
opts = opts || Object.create(null);
(opts as any).relative = true;
const action = new TweenAction(duration, props, opts);
this._actions.push(action);
return this;
}
/**
* @en
* Directly set target properties.
* @zh
* 直接设置 target 的属性。
* @method set
* @param props @en List of properties of tween @zh 缓动的属性列表
* @return {Tween}
*/
set (props: ConstructorType): Tween {
const action = new SetAction(props);
this._actions.push(action);
return this;
}
/**
* @en
* Add a delay action.
* @zh
* 添加一个延时 action。
* @method delay
* @param duration @en Delay time of this tween @zh 当前缓动的延迟时间
* @return {Tween}
*/
delay (duration: number): Tween {
const action = delayTime(duration);
this._actions.push(action);
return this;
}
/**
* @en
* Add a callback action.
* @zh
* 添加一个回调 action。
* @method call
* @param callback @en Callback function at the end of this tween @zh 当前缓动结束时的回调函数
* @return {Tween}
*/
// eslint-disable-next-line @typescript-eslint/ban-types
call (callback: Function): Tween {
const action = callFunc(callback);
this._actions.push(action);
return this;
}
/**
* @en
* Add a sequence action.
* @zh
* 添加一个队列 action。
* @method sequence
* @param args @en All tween that make up the sequence @zh 组成队列的所有缓动
*/
sequence (...args: Tween[]): Tween {
const action = Tween._wrappedSequence(...args);
this._actions.push(action);
return this;
}
/**
* @en
* Add a parallel action.
* @zh
* 添加一个并行 action。
* @method parallel
* @param args @en The tween parallel to this tween @zh 与当前缓动并行的缓动
*/
parallel (...args: Tween[]): Tween {
const action = Tween._wrappedParallel(...args);
this._actions.push(action);
return this;
}
/**
* @en
* Add a repeat action.
* This action will integrate before actions to a sequence action as their parameters.
* @zh
* 添加一个重复 action,这个 action 会将前一个动作作为他的参数。
* @param repeatTimes @en The repeat times of this tween @zh 重复次数
* @param embedTween @en Optional, embedded tween of this tween @zh 可选,嵌入缓动
*/
repeat (repeatTimes: number, embedTween?: Tween): Tween {
/** adapter */
if (repeatTimes === Infinity) {
return this.repeatForever(embedTween);
}
const actions = this._actions;
let action: any;
if (embedTween instanceof Tween) {
action = embedTween._union();
} else {
action = actions.pop();
}
actions.push(repeat(action, repeatTimes));
return this;
}
/**
* @en
* Add a repeat forever action.
* This action will integrate before actions to a sequence action as their parameters.
* @zh
* 添加一个永久重复 action,这个 action 会将前一个动作作为他的参数。
* @method repeatForever
* @param embedTween @en Optional, embedded tween of this tween @zh 可选,嵌入缓动
*/
repeatForever (embedTween?: Tween): Tween {
const actions = this._actions;
let action: any;
if (embedTween instanceof Tween) {
action = embedTween._union();
} else {
action = actions.pop();
}
actions.push(repeatForever(action as ActionInterval));
return this;
}
/**
* @en
* Add a reverse time action.
* This action will integrate before actions to a sequence action as their parameters.
* @zh
* 添加一个倒置时间 action,这个 action 会将前一个动作作为他的参数。
* @method reverseTime
* @param embedTween @en Optional, embedded tween of this tween @zh 可选,嵌入缓动
*/
reverseTime (embedTween?: Tween): Tween {
const actions = this._actions;
let action: any;
if (embedTween instanceof Tween) {
action = embedTween._union();
} else {
action = actions.pop();
}
actions.push(reverseTime(action as ActionInterval));
return this;
}
/**
* @en
* Add a hide action, only for node target.
* @zh
* 添加一个隐藏 action,只适用于 target 是节点类型的。
*/
hide (): Tween {
const action = hide();
this._actions.push(action);
return this;
}
/**
* @en
* Add a show action, only for node target.
* @zh
* 添加一个显示 action,只适用于 target 是节点类型的。
*/
show (): Tween {
const action = show();
this._actions.push(action);
return this;
}
/**
* @en
* Add a removeSelf action, only for node target.
* @zh
* 添加一个移除自己 action,只适用于 target 是节点类型的。
*/
removeSelf (): Tween {
const action = removeSelf(false);
this._actions.push(action);
return this;
}
/**
* @en
* Add a destroySelf action, only for node target.
* @zh
* 添加一个移除并销毁自己 action,只适用于 target 是节点类型的。
*/
destroySelf (): Tween {
const action = removeSelf(true);
this._actions.push(action);
return this;
}
/**
* @en
* Stop all tweens
* @zh
* 停止所有缓动
*/
static stopAll (): void {
TweenSystem.instance.ActionManager.removeAllActions();
}
/**
* @en
* Stop all tweens by tag
* @zh
* 停止所有指定标签的缓动
*/
// eslint-disable-next-line @typescript-eslint/ban-types
static stopAllByTag (tag: number, target?: object): void {
TweenSystem.instance.ActionManager.removeAllActionsByTag(tag, target as any);
}
/**
* @en
* Stop all tweens by target
* @zh
* 停止所有指定对象的缓动
*/
// eslint-disable-next-line @typescript-eslint/ban-types
static stopAllByTarget (target?: object): void {
TweenSystem.instance.ActionManager.removeAllActionsFromTarget(target as any);
}
private _union (): Action {
const actions = this._actions;
let action: Action;
if (actions.length === 1) {
action = actions[0];
} else {
action = sequence(actions);
}
return action;
}
private _destroy (): void {
this.stop();
}
private static readonly _tmp_args: Tween[] | Action[] = [];
private static _wrappedSequence (...args: Action[] | Tween[]): ActionInterval {
const tmp_args = Tween._tmp_args;
tmp_args.length = 0;
for (let l = args.length, i = 0; i < l; i++) {
const arg = tmp_args[i] = args[i];
if (arg instanceof Tween) {
tmp_args[i] = arg._union();
}
}
return sequence.apply(sequence, tmp_args as any);
}
private static _wrappedParallel (...args: Action[] | Tween[]): FiniteTimeAction {
const tmp_args = Tween._tmp_args;
tmp_args.length = 0;
for (let l = args.length, i = 0; i < l; i++) {
const arg = tmp_args[i] = args[i];
if (arg instanceof Tween) {
tmp_args[i] = arg._union();
}
}
return spawn.apply(spawn, tmp_args as any);
}
}
cclegacy.Tween = Tween;
/**
* @en
* tween is a utility function that helps instantiate Tween instances.
* @zh
* tween 是一个工具函数,帮助实例化 Tween 实例。
* @param target @en The target of the result tween @zh 缓动的目标
* @returns Tween 实例
* @example
* tween(this.node)
* .to(1, {scale: new Vec3(2, 2, 2), position: new Vec3(5, 5, 5)})
* .call(() => { log('This is a callback'); })
* .by(1, {scale: new Vec3(-1, -1, -1)}, {easing: 'sineOutIn'})
* .start()
*/
export function tween (target?: T): Tween {
return new Tween(target);
}
cclegacy.tween = tween;
/**
* @en
* tweenUtil is a utility function that helps instantiate Tween instances.
* @zh
* tweenUtil 是一个工具函数,帮助实例化 Tween 实例。
* @deprecated please use `tween` instead.
*/
export function tweenUtil (target?: T): Tween {
warn('tweenUtil\' is deprecated, please use \'tween\' instead ');
return new Tween(target);
}
cclegacy.tweenUtil = tweenUtil;
官方tween-action.ts源码:
/*
Copyright (c) 2020-2023 Xiamen Yaji Software Co., Ltd.
https://www.cocos.com/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
import { warnID, warn } from '@base/debug';
import { VERSION } from '@base/global';
import { easing } from '../core';
import { ActionInterval } from './actions/action-interval';
import { ITweenOption } from './export-api';
/** adapter */
function TweenEasingAdapter (easingName: string): string {
const initialChar = easingName.charAt(0);
if (/[A-Z]/.test(initialChar)) {
easingName = easingName.replace(initialChar, initialChar.toLowerCase());
const arr = easingName.split('-');
if (arr.length === 2) {
const str0 = arr[0];
if (str0 === 'linear') {
easingName = 'linear';
} else {
const str1 = arr[1];
switch (str0) {
case 'quadratic':
easingName = `quad${str1}`;
break;
case 'quartic':
easingName = `quart${str1}`;
break;
case 'quintic':
easingName = `quint${str1}`;
break;
case 'sinusoidal':
easingName = `sine${str1}`;
break;
case 'exponential':
easingName = `expo${str1}`;
break;
case 'circular':
easingName = `circ${str1}`;
break;
default:
easingName = str0 + str1;
break;
}
}
}
}
return easingName;
}
/** checker */
function TweenOptionChecker (opts: ITweenOption): void {
const header = ' [Tween:] ';
const message = ` option is not support in v + ${VERSION}`;
const _opts = opts as unknown as any;
if (_opts.delay) {
warn(`${header}delay${message}`);
}
if (_opts.repeat) {
warn(`${header}repeat${message}`);
}
if (_opts.repeatDelay) {
warn(`${header}repeatDelay${message}`);
}
if (_opts.interpolation) {
warn(`${header}interpolation${message}`);
}
if (_opts.onStop) {
warn(`${header}onStop${message}`);
}
}
export class TweenAction extends ActionInterval {
private _opts: any;
private _props: any;
private _originProps: any;
constructor (duration: number, props: any, opts?: ITweenOption) {
super();
if (opts == null) {
opts = Object.create(null);
} else {
/** checker */
TweenOptionChecker(opts);
/** adapter */
if (opts.easing && typeof opts.easing === 'string') {
opts.easing = TweenEasingAdapter(opts.easing) as any;
}
// global easing or progress used for this action
if (!opts.progress) {
opts.progress = this.progress;
}
if (opts.easing && typeof opts.easing === 'string') {
const easingName = opts.easing as string;
opts.easing = easing[easingName];
if (!opts.easing) { warnID(1031, easingName); }
}
}
this._opts = opts;
this._props = Object.create(null);
for (const name in props) {
// filtering if
// - it was not own property
// - types was function / string
// - it was undefined / null
// eslint-disable-next-line no-prototype-builtins
if (!props.hasOwnProperty(name)) continue;
let value = props[name];
if (typeof value === 'function') {
value = value();
}
if (value == null || typeof value === 'string') continue;
// property may have custom easing or progress function
let customEasing: any; let progress: any;
if (value.value !== undefined && (value.easing || value.progress)) {
if (typeof value.easing === 'string') {
customEasing = easing[value.easing];
if (!customEasing) warnID(1031, value.easing);
} else {
customEasing = value.easing;
}
progress = value.progress;
value = value.value;
}
const prop = Object.create(null);
prop.value = value;
prop.easing = customEasing;
prop.progress = progress;
this._props[name] = prop;
}
this._originProps = props;
this.initWithDuration(duration);
}
clone (): TweenAction {
const action = new TweenAction(this._duration, this._originProps, this._opts);
this._cloneDecoration(action);
return action;
}
startWithTarget (target: Record): void {
ActionInterval.prototype.startWithTarget.call(this, target);
const relative = !!this._opts.relative;
const props = this._props;
for (const property in props) {
const _t: any = target[property];
if (_t === undefined) { continue; }
const prop: any = props[property];
const value = prop.value;
if (typeof _t === 'number') {
prop.start = _t;
prop.current = _t;
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
prop.end = relative ? _t + value : value;
} else if (typeof _t === 'object') {
if (prop.start == null) {
prop.start = {}; prop.current = {}; prop.end = {};
}
for (const k in value) {
// filtering if it not a number
// eslint-disable-next-line no-restricted-globals
if (isNaN(_t[k])) continue;
prop.start[k] = _t[k];
prop.current[k] = _t[k];
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
prop.end[k] = relative ? _t[k] + value[k] : value[k];
}
}
}
if (this._opts.onStart) { this._opts.onStart(this.target); }
}
update (t: number): void {
const target = this.target;
if (!target) return;
const props = this._props;
const opts = this._opts;
let easingTime = t;
if (opts.easing) easingTime = opts.easing(t);
const progress = opts.progress;
for (const name in props) {
const prop = props[name];
const time = prop.easing ? prop.easing(t) : easingTime;
const interpolation = prop.progress ? prop.progress : progress;
const start = prop.start;
const end = prop.end;
if (typeof start === 'number') {
prop.current = interpolation(start, end, prop.current, time);
} else if (typeof start === 'object') {
// const value = prop.value;
for (const k in start) {
// if (value[k].easing) {
// time = value[k].easing(t);
// }
// if (value[k].progress) {
// interpolation = value[k].easing(t);
// }
prop.current[k] = interpolation(start[k], end[k], prop.current[k], time);
}
}
target[name] = prop.current;
}
if (opts.onUpdate) { opts.onUpdate(this.target, t); }
if (t === 1 && opts.onComplete) { opts.onComplete(this.target); }
}
progress (start: number, end: number, current: number, t: number): number {
return current = start + (end - start) * t;
}
}