Skip to content
Tween - 补间动画

Tween - 补间动画

阅读本文大概需要 15 分钟。

在游戏制作过程中,我们经常需要实现一些常见的动画效果,例如将一个物体平滑地移到另一个位置、让一个物体以匀速放大、或者让一个数字逐渐过渡等。这些需求可以通过使用 Tween 动画来实现,它提供了简单且强大的工具来创建各种平滑的过渡效果。

1.Tween是什么?

补间(动画)(来自in-between维基百科)是一个概念,允许你以平滑的方式更改对象的属性。你只需告诉它哪些属性要更改,当补间结束运行时它们应该具有哪些最终值,以及这需要多长时间,补间引擎将负责计算从起始点到结束点的值。

说通俗一点,Tween就是一个封装好的函数库,能够实现一些插值运算,方便定义和操作一些补间组。能够使代码的结构看起来更加清晰!

2.使用步骤

2.1.将全局补间组添加到update

Tween本身不会运行,需要通过update对Tween进行驱动,推荐在主循环中添加对应代码,如下:

ts
@Component
export default class GameStart extends Script {
    protected async onStart(): Promise<void> {
        // 注意开启该脚本的update,不然tween的update就也不会执行
        this.useUpdate = true
    }

    protected onUpdate(dt: number): void {
        // 这个大写的TWEEN,是指全局补间组。所有实例化的Tween,都会添加到这个组里,然后统一进行update。所以如果不将全局补间组添加到update,所有补间动画就都不会执行
        TweenUtil.TWEEN.update();
    }
}
@Component
export default class GameStart extends Script {
    protected async onStart(): Promise<void> {
        // 注意开启该脚本的update,不然tween的update就也不会执行
        this.useUpdate = true
    }

    protected onUpdate(dt: number): void {
        // 这个大写的TWEEN,是指全局补间组。所有实例化的Tween,都会添加到这个组里,然后统一进行update。所以如果不将全局补间组添加到update,所有补间动画就都不会执行
        TweenUtil.TWEEN.update();
    }
}

2.2.创建一个Tween并开始播放

这里只演示一个最基础的用法

ts
// 基本格式:
let tween = new Tween({ 对象 }).to({ 对象 }, 补间时间).onUpdate((value) => {
    // 可以在这里使用补间对象来进行补间操作
    console.log(value)
})
// 开始播放Tween动画
tween.start();
// 基本格式:
let tween = new Tween({ 对象 }).to({ 对象 }, 补间时间).onUpdate((value) => {
    // 可以在这里使用补间对象来进行补间操作
    console.log(value)
})
// 开始播放Tween动画
tween.start();

3.Tween的相关接口

3.1.控制Tween动画

  • start和stop

用来控制一个Tween的开始和停止

ts
tweenA.start(); // 开始播放
tweenA.stop(); // 停止播放
tweenA.start(); // 开始播放
tweenA.stop(); // 停止播放
  • chain

制作多个彼此衔接的动画,例如一个动画在另一个动画结束后开始,可以通过 chain 来实现

typescript
tweenA.chain(tweenB);  //tweenB 在 tweenA 之后开始动画,故可以制作一个无限循环的动画
tweenB.chain(tweenA);
tweenA.chain(tweenB);  //tweenB 在 tweenA 之后开始动画,故可以制作一个无限循环的动画
tweenB.chain(tweenA);
  • repeat

制作循环动画,接收一个用于描述循环次数的参数

typescript
tweenA.repeat(10);
tweenA.repeat(10);
  • delay

用于控制动画之间的延迟

typescript
tweenA.delay(1000);
tweenA.start()
tweenA.delay(1000);
tweenA.start()

3.2.回调函数

  • onStart==>tween动画开始前的回调函数
  • onStop==>tween动画结束后的回调函数
  • onUpdate==>在tween动画每次更新后执行
  • onComplete==>在tween动画全部结束后执行
typescript
let tweenA = new TweenUtil.Tween({}).to({}, 1000).
    onStart(() => {
		// 添加逻辑
    }).
    onUpdate(() => {
		// 添加逻辑
    }).
    onStop(() => {
		// 添加逻辑
    }).
    onComplete(() => {
		// 添加逻辑
    })
let tweenA = new TweenUtil.Tween({}).to({}, 1000).
    onStart(() => {
		// 添加逻辑
    }).
    onUpdate(() => {
		// 添加逻辑
    }).
    onStop(() => {
		// 添加逻辑
    }).
    onComplete(() => {
		// 添加逻辑
    })

3.3.缓动函数和缓动方式

  • 缓动函数

Linear ==> 线性匀速运动效果

Quadratic ==> 二次方的缓动(t^2)

Cubic ==> 三次方的缓动()

Quartic ==> 四次方的缓动()

Quintic ==> 五次方的缓动

Sinusoidal ==> 正弦曲线的缓动()

Exponential ==> 指数曲线的缓动()

Circular ==> 圆形曲线的缓动()

Elastic ==> 指数衰减的正弦曲线缓动()

Back ==> 超过范围的三次方的缓动

Bounce ==> 指数衰减的反弹缓动

  • 缓动方式

easeIn(In) ==> 加速,先慢后快

easeOut(Out) ==> 减速,先快后慢

easeInOut(InOut) ==> 前半段加速,后半段减速

使用例子:

typescript
new Tween({}).to({}, 1000).onUpdate().easing(TweenUtil.Easing.Cubic.Out)
new Tween({}).to({}, 1000).onUpdate().easing(TweenUtil.Easing.Cubic.Out)

3.4.插值函数

使用插值函数可以控制Tween的运动曲线,实际应用案例可以参考这篇帖子:感受下无限导弹的魅力吧! - 资源/心得分享 创作者论坛 (ark.online)

typescript
testTween() {
    const startLoc = this.gameObject.worldTransform.position;
    // 设置tween的起始位置
    let loc = { x: startLoc.x, y: startLoc.y, z: startLoc.z };
    // 创建新的Tween对象
    const newTween = new Tween(loc);
    console.log("tween created.")

    // 使用插值,to方法里传入的是数组,这里请注意传入的数组是分别针对x,y,z的
    newTween.to({ x: [100, 200, 100, loc.x], y: [100, 200, 100, loc.y], z: [100, 200, 100, loc.z] })
        // 使用interpolation方法传入贝塞尔插值
        .interpolation(TweenUtil.Interpolation.Bezier)
        .onUpdate((val, time) => {
            // 在update中更新代码附加对象的位置
            this.gameObject.worldTransform.position = new Vector(val.x, val.y, val.z);
        })
        .start()
        // 无限重复
        .repeat(Infinity);
}
testTween() {
    const startLoc = this.gameObject.worldTransform.position;
    // 设置tween的起始位置
    let loc = { x: startLoc.x, y: startLoc.y, z: startLoc.z };
    // 创建新的Tween对象
    const newTween = new Tween(loc);
    console.log("tween created.")

    // 使用插值,to方法里传入的是数组,这里请注意传入的数组是分别针对x,y,z的
    newTween.to({ x: [100, 200, 100, loc.x], y: [100, 200, 100, loc.y], z: [100, 200, 100, loc.z] })
        // 使用interpolation方法传入贝塞尔插值
        .interpolation(TweenUtil.Interpolation.Bezier)
        .onUpdate((val, time) => {
            // 在update中更新代码附加对象的位置
            this.gameObject.worldTransform.position = new Vector(val.x, val.y, val.z);
        })
        .start()
        // 无限重复
        .repeat(Infinity);
}

4.效果演示

4.1.数字补间动画

ts
/**
 * 开启一个数字的补间动画
 * @param oldNum 旧值
 * @param targetNum 目标值
 * @param time 时间(毫秒)
 */
private NumTweenStart(oldNum: number, targetNum: number, time: number) {
    let tween = new Tween({ value: oldNum }).to({ value: targetNum }, time).onUpdate((obj) => {
        // 每一次更新,让控件mNum_txt显示的内容为补间值
        this.mNum_txt.text = obj.value.toFixed(2);
    })
    tween.start();
}
/**
 * 开启一个数字的补间动画
 * @param oldNum 旧值
 * @param targetNum 目标值
 * @param time 时间(毫秒)
 */
private NumTweenStart(oldNum: number, targetNum: number, time: number) {
    let tween = new Tween({ value: oldNum }).to({ value: targetNum }, time).onUpdate((obj) => {
        // 每一次更新,让控件mNum_txt显示的内容为补间值
        this.mNum_txt.text = obj.value.toFixed(2);
    })
    tween.start();
}

4.2.UI补间动画

ts
/**
 * 开启一个移动UI的补间动画
 * @param targetX 目标位置x
 * @param targetY 目标位置y
 */
private UITweenStart(targetX: number, targetY: number) {
    let targetPosition = new Vector2(targetX, targetY)
    let tween = new Tween(this.mUITween_img.position).to(targetPosition, 1000).onUpdate((obj) => {
        // 每一次更新,改变图片mUITween_img的位置
        this.mUITween_img.position = obj;
    })
    tween.start()
}
/**
 * 开启一个移动UI的补间动画
 * @param targetX 目标位置x
 * @param targetY 目标位置y
 */
private UITweenStart(targetX: number, targetY: number) {
    let targetPosition = new Vector2(targetX, targetY)
    let tween = new Tween(this.mUITween_img.position).to(targetPosition, 1000).onUpdate((obj) => {
        // 每一次更新,改变图片mUITween_img的位置
        this.mUITween_img.position = obj;
    })
    tween.start()
}

4.3.物体补间动画

ts
private async ObjTweenStart(targetX: number, targetY: number, targetZ: number) {
    // 查找物体
    let targetObj = await GameObject.asyncFindGameObjectById("FE9CD8A8")
    // 根据传入值创建目标点坐标
    let targetPosition = new Vector(targetX, targetY, targetZ)
    let tween = new Tween(targetObj.worldTransform.position).to(targetPosition, 1000).onUpdate((obj) => {
        // 每一次更新,改变物体targetObj的位置
        targetObj.worldTransform.position = obj;
    })
    tween.start()
}
private async ObjTweenStart(targetX: number, targetY: number, targetZ: number) {
    // 查找物体
    let targetObj = await GameObject.asyncFindGameObjectById("FE9CD8A8")
    // 根据传入值创建目标点坐标
    let targetPosition = new Vector(targetX, targetY, targetZ)
    let tween = new Tween(targetObj.worldTransform.position).to(targetPosition, 1000).onUpdate((obj) => {
        // 每一次更新,改变物体targetObj的位置
        targetObj.worldTransform.position = obj;
    })
    tween.start()
}