前言
Unity 中的补间动画(Tween Animation)是一种在两个关键帧之间自动生成平滑过渡的动画技术。一般在程序动画中使用,如位置、旋转、缩放、颜色等的变化。
这里介绍的框架在这基础上,加入了时间节点(延时执行、反复执行)和行为树的概念,把动画抽象在“节点”概念之下。

节点类型
链式节点
链式节点相当于容器,本身没有具体的执行逻辑,管理了节点列表,在合适的时机标记链式节点为完成状态,根据完成的条件分为以下节点,类似行为树。
- 顺序节点:按顺序执行节点,上一个节点执行完才执行下一个,全部完成以后标记完成
- 并行节点:并行执行节点,全部完成以后标记完成
执行节点
执行完回调以后就标记完成
条件节点
Update中判断条件,返回True以后标记完成
时间节点
延后当Timer完成后标记完成,Timer可以简单延迟,也可以反复执行一定次数以后标记完成。
值节点
值节点才是传统意义上的补间动画,就是通过输入时间,计算出值的结果,每帧执行,下面具体介绍。
值节点
值节点主要由两个部分组成,一个是缓动函数,一个是插值函数。
在Update先通过缓动函数,默认为线性函数,根据流逝的时间计算出当前的进度。
再调用插值函数,根据当前进度算出结果值,默认为线性函数。
根据值节点派生出的Color、Vector、Float节点,其实就是插值函数不一样。
缓动函数(Easing Function)
缓动函数是一种用于控制动画或过渡效果速度变化的数学函数。
它的主要作用是使动画或过渡效果看起来更加自然和真实。在没有缓动函数的情况下,动画可能会以恒定的速度进行,显得生硬和不流畅。
缓动函数可以改变动画在不同阶段的速度,常见的效果包括:
- 加速:动画开始时速度较慢,然后逐渐加快。
- 减速:动画开始时速度较快,然后逐渐减慢。
- 先加速后减速:动画开始时加速,然后在接近结束时减速。
- 先减速后加速:动画开始时减速,然后在中间或接近结束时加速。
//--------------------------------------------------
// Author : David Ochmann
// Website:https://easings.net/
//--------------------------------------------------
using System;
namespace UniFramework.Tween
{
/// <summary>
/// 公共补间方法
/// </summary>
public static class TweenEase
{
public static class Linear
{
public static float Default(float t, float b, float c, float d)
{
return c * t / d + b;
}
public static float EaseIn(float t, float b, float c, float d)
{
return c * t / d + b;
}
public static float EaseOut(float t, float b, float c, float d)
{
return c * t / d + b;
}
public static float EaseInOut(float t, float b, float c, float d)
{
return c * t / d + b;
}
}
public static class Sine
{
public static float EaseIn(float t, float b, float c, float d)
{
return -c * (float)Math.Cos(t / d * ((float)Math.PI / 2)) + c + b;
}
public static float EaseOut(float t, float b, float c, float d)
{
return c * (float)Math.Sin(t / d * ((float)Math.PI / 2)) + b;
}
public static float EaseInOut(float t, float b, float c, float d)
{
return -c / 2f * ((float)Math.Cos((float)Math.PI * t / d) - 1) + b;
}
}
public static class Quad
{
public static float EaseIn(float t, float b, float c, float d)
{
return c * (t /= d) * t + b;
}
public static float EaseOut(float t, float b, float c, float d)
{
return -c * (t /= d) * (t - 2) + b;
}
public static float EaseInOut(float t, float b, float c, float d)
{
if ((t /= d / 2) < 1) return c / 2 * t * t + b;
return -c / 2 * ((--t) * (t - 2) - 1) + b;
}
}
public static class Cubic
{
public static float EaseIn(float t, float b, float c, float d)
{
return c * (t /= d) * t * t + b;
}
public static float EaseOut(float t, float b, float c, float d)
{
return c * ((t = t / d - 1) * t * t + 1) + b;
}
public static float EaseInOut(float t, float b, float c, float d)
{
if ((t /= d / 2) < 1) return c / 2 * t * t * t + b;
return c / 2 * ((t -= 2) * t * t + 2) + b;
}
}
public static class Quart
{
public static float EaseIn(float t, float b, float c, float d)
{
return c * (t /= d) * t * t * t + b;
}
public static float EaseOut(float t, float b, float c, float d)
{
return -c * ((t = t / d - 1) * t * t * t - 1) + b;
}
public static float EaseInOut(float t, float b, float c, float d)
{
if ((t /= d / 2) < 1) return c / 2 * t * t * t * t + b;
return -c / 2 * ((t -= 2) * t * t * t - 2) + b;
}
}
public static class Quint
{
public static float EaseIn(float t, float b, float c, float d)
{
return c * (t /= d) * t * t * t * t + b;
}
public static float EaseOut(float t, float b, float c, float d)
{
return c * ((t = t / d - 1) * t * t * t * t + 1) + b;
}
public static float EaseInOut(float t, float b, float c, float d)
{
if ((t /= d / 2) < 1) return c / 2 * t * t * t * t * t + b;
return c / 2 * ((t -= 2) * t * t * t * t + 2) + b;
}
}
public static class Expo
{
public static float EaseIn(float t, float b, float c, float d)
{
return (t == 0) ? b : c * (float)Math.Pow(2, 10 * (t / d - 1)) + b;
}
public static float EaseOut(float t, float b, float c, float d)
{
return (t == d) ? b + c : c * (-(float)Math.Pow(2, -10 * t / d) + 1) + b;
}
public static float EaseInOut(float t, float b, float c, float d)
{
if (t == 0) return b;
if (t == d) return b + c;
if ((t /= d / 2) < 1) return c / 2 * (float)Math.Pow(2, 10 * (t - 1)) + b;
return c / 2 * (-(float)Math.Pow(2, -10 * --t) + 2) + b;
}
}
public static class Circ
{
public static float EaseIn(float t, float b, float c, float d)
{
return -c * ((float)Math.Sqrt(1 - (t /= d) * t) - 1) + b;
}
public static float EaseOut(float t, float b, float c, float d)
{
return c * (float)Math.Sqrt(1 - (t = t / d - 1) * t) + b;
}
public static float EaseInOut(float t, float b, float c, float d)
{
if ((t /= d / 2) < 1) return -c / 2 * ((float)Math.Sqrt(1 - t * t) - 1) + b;
return c / 2 * ((float)Math.Sqrt(1 - (t -= 2) * t) + 1) + b;
}
}
public static class Back
{
public static float EaseIn(float t, float b, float c, float d)
{
float s = 1.70158f;
return c * (t /= d) * t * ((s + 1) * t - s) + b;
}
public static float EaseOut(float t, float b, float c, float d)
{
float s = 1.70158f;
return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b;
}
public static float EaseInOut(float t, float b, float c, float d)
{
float s = 1.70158f;
if ((t /= d / 2) < 1) return c / 2 * (t * t * (((s *= (1.525f)) + 1) * t - s)) + b;
return c / 2 * ((t -= 2) * t * (((s *= (1.525f)) + 1) * t + s) + 2) + b;
}
}
public static class Elastic
{
public static float EaseIn(float t, float b, float c, float d)
{
if (t == 0) return b; if ((t /= d) == 1) return b + c; float p = d * .3f;
float a = c;
float s = p / 4;
return -(a * (float)Math.Pow(2, 10 * (t -= 1)) * (float)Math.Sin((t * d - s) * (2 * (float)Math.PI) / p)) + b;
}
public static float EaseOut(float t, float b, float c, float d)
{
if (t == 0) return b; if ((t /= d) == 1) return b + c; float p = d * .3f;
float a = c;
float s = p / 4;
return (a * (float)Math.Pow(2, -10 * t) * (float)Math.Sin((t * d - s) * (2 * (float)Math.PI) / p) + c + b);
}
public static float EaseInOut(float t, float b, float c, float d)
{
if (t == 0) return b; if ((t /= d / 2) == 2) return b + c; float p = d * (.3f * 1.5f);
float a = c;
float s = p / 4;
if (t < 1) return -.5f * (a * (float)Math.Pow(2, 10 * (t -= 1)) * (float)Math.Sin((t * d - s) * (2 * (float)Math.PI) / p)) + b;
return a * (float)Math.Pow(2, -10 * (t -= 1)) * (float)Math.Sin((t * d - s) * (2 * (float)Math.PI) / p) * .5f + c + b;
}
}
public static class Bounce
{
public static float EaseIn(float t, float b, float c, float d)
{
return c - Bounce.EaseOut(d - t, 0, c, d) + b;
}
public static float EaseOut(float t, float b, float c, float d)
{
if ((t /= d) < (1 / 2.75f))
{
return c * (7.5625f * t * t) + b;
}
else if (t < (2 / 2.75f))
{
return c * (7.5625f * (t -= (1.5f / 2.75f)) * t + .75f) + b;
}
else if (t < (2.5f / 2.75f))
{
return c * (7.5625f * (t -= (2.25f / 2.75f)) * t + .9375f) + b;
}
else
{
return c * (7.5625f * (t -= (2.625f / 2.75f)) * t + .984375f) + b;
}
}
public static float EaseInOut(float t, float b, float c, float d)
{
if (t < d / 2) return Bounce.EaseIn(t * 2, 0, c, d) * .5f + b;
else return Bounce.EaseOut(t * 2 - d, 0, c, d) * .5f + c * .5f + b;
}
}
}
}
插值函数(Lerp Function)
值函数根据当前进度算出结果值,默认为线性函数,一般情况下不需要改动,只要更换缓动函数就可以实现不同的动态效果。
调用
void Start()
{
// 原地停留1秒,然后向上移动,停留1秒,然后同时缩小并回归原位。
ITweenChain tween = UniTween.AllocateSequence();
tween.Delay(1f).
Append(this.transform.TweenMove(0.5f, new Vector3(0, 256, 0))).
Delay(1f).
SwitchToParallel().
Append(this.transform.TweenScaleTo(0.5f, new Vector3(0.2f, 0.2f, 1f))).
Append(this.transform.TweenMove(0.5f, new Vector3(0, 0, 0)));
this.gameObject.PlayTween(tween);
}
总结
这一套补间系统融合了行为树,定时器,和补间动画,方便拓展,功能强大,调用直观。
参考
UniFramework/UniFramework/UniTween at main · gmhevinci/UniFramework