「Unity3D」(7)协程使用3种算法实现CameraShake震屏

来源:互联网 发布:资料整理软件 编辑:程序博客网 时间:2024/06/05 07:43

本文主要讨论CameraShake震屏的实现思路,但不仅限于震屏,震动算法可以震动任意属性,比如Position,Scale,Rotation,Color等等。

思路

震动,就是围绕某个固定点的波动,最后在回归固定点的过程。这个波动的模拟,有千千万万种,这里主要介绍3种,Random(随机),Periodic(周期函数Sin,Cos),PerlinNoise(柏林噪声)。

另外,在视觉预期上,震动过程是一个衰减的过程,所以在波动回归的时候需要加入渐进衰减的模拟,这样就会有更好的效果。

实现

同样,震动的实现,也有很多种,这里主要介绍使用协程的方式。我们首先实现一个协程函数。

public static IEnumerator ShakeRoutine(    float           magnitude, // 震动幅度    float           speed,     // 震动速度    float           duration,  // 震动时间    Func<float>     OnGetOriginal, // 获取固定点坐标    Action<float>   OnShake,       // 获取震动数值    ShakeType       shakeType  = ShakeType.Smooth, // 震动类型    Action          OnComplete = null // 完成回调){    // 震动耗时    var elapsed  = 0.0f;    // 随机起始点    var random   = UnityEngine.Random.Range(-1234.5f, 1234.5f);    // 获得固定点    var original = OnGetOriginal();    while (elapsed < duration)     {        elapsed    += Time.deltaTime;                  var percent = elapsed / duration;           // 当前波动        var rps     = random + percent * speed;        // 波动映射到[-1, 1]        float range;        switch (shakeType)        {            case ShakeType.Smooth:                range = Mathf.Sin(rps) + Mathf.Cos(rps);                break;            case ShakeType.PerlinNoise:                range = Mathf.PerlinNoise(rps, rps);                break;            default:                range = 0.0f;                break;        }        // 震动总时间的50%后开始衰减        if (percent < 0.5f)        {            OnShake(range * magnitude + original);        }        else         {            // 计算衰减            OnShake(range * magnitude * (2.0f * (1.0f - percent)) + original);        }        yield return null;    }    if (OnComplete != null)    {        // 完成回调        OnComplete();    }}

为了通用性协程构造比较繁琐,如果只针对Camera的震动可以写的很简洁。下面解读一下实现:

  • OnGetOriginal 为了获得震动的固定点,可以返回PositionX,ScaleY,ColorRed,等等。围绕这个固定点进行震动。

  • OnShake,每一帧协程计算出震动后数值,用于设置到target对象上。

  • 协程每一帧计算一次震动,直到duration耗尽,回调OnComplete。

  • range 会根据不同的波动算法,映射到[-1, 1]区间,最后乘以衰减和振幅,就得到了震动后的数值。

Random 波动

代码并没有体现,因为测试发现,random在振幅比较大的时候,效果不太好,但如果振幅很小很小还是不错的。下面给出Random的写法。

// 依然需要映射到[-1, 1]range = UnityEngine.Random.value * 2.0f - 1.0f;

Periodic 波动

这里我使用Mathf.Cos + Mathf.Sin的方式,其实使用Mathf.Cos或Mathf.Sin也是可以的,只不过这里叠加会有加速的效果。周期顾名思义,不会像随机那样无序,单个数值会有震荡的效果,二维数值有转圈的效果。

PerlinNoise 波动

Unity内置实现了二维的PerlinNoise算法,其原理是在一个二维纹理上取值,效果会比Random来的平滑,一般应用于地形,水波纹等自然界元素的模拟。这里体现了一种用法,可以使用不同的策略去得到PerlinNoise的坐标。其坐标是类似Repeat模式的UV坐标。

如何使用

首先利用这个协程函数,构建一个协程执行函数。

public static void Shake(    float         magnitude,     float         speed,     float         duration,     Func<float>   OnGetOriginal,    Action<float> OnShake,     ShakeType     shakeType = ShakeType.Smooth,    Action        OnComplete = null){    CoroutineExecutor.StartCoroutineTask(ShakeRoutine(magnitude, speed, duration, OnGetOriginal, OnShake, shakeType, OnComplete));}

这里我使用了协程管理器,也可以继承MonoBehaviour使用自身的协程启动。然后,在看如何使用。

public static void ShakePositionX(    this  Transform     transform,     float               magnitude,     float               speed,     float               duration,     ShakeTool.ShakeType shakeType  = ShakeTool.ShakeType.Smooth,    Action              OnComplete = null){    ShakeTool.Shake    (        magnitude,         speed,         duration,        ()  => transform.position.x,        (x) => transform.SetPositionX(x),         shakeType,         OnComplete    );}

这里进行了扩展,可以方便的直接使用transform来做ShakePositionX,比如:

transform.ShakePositionX(10.0f, 100f, 1.5f, ShakeTool.ShakeType.Smooth);

更多定制

这里Shake只是震动了一个数值,当然也可以同时震动2或3个或更多。ShakePositionX只是扩展了position x,当然也可以扩展为 xy 或 xyz,或是Scale和Rotation。比如,如果震动XY可以这么写:

public static IEnumerator ShakeRoutine(    float           magnitude,     float           speed,    float           duration,     Func<Vector2>   OnGetOriginal,    Action<Vector2> OnShake,     ShakeType       shakeType  = ShakeType.Smooth,    Action          OnComplete = null){    var elapsed  = 0.0f;     var random1  = UnityEngine.Random.Range(-RandomRange, RandomRange);    var random2  = UnityEngine.Random.Range(-RandomRange, RandomRange);    var original = OnGetOriginal();    while (elapsed < duration)     {        elapsed    += Time.deltaTime;                  var percent = elapsed / duration;           var ps      = percent   * speed;        // map to [-1, 1]        float range1;        float range2;        switch (shakeType)        {            case ShakeType.Smooth:                range1 = Mathf.Sin(random1 + ps);                range2 = Mathf.Cos(random2 + ps);                break;            case ShakeType.PerlinNoise:                range1 = Mathf.PerlinNoise(random1 + ps, 0.0f);                range2 = Mathf.PerlinNoise(0.0f, random2 + ps);                break;            default:                range1 = 0.0f;                range2 = 0.0f;                break;        }        // reduce shake start from 50% duration        if (percent < 0.5f)        {            OnShake(new Vector2(range1 * magnitude, range2 * magnitude) + original);        }        else        {            var magDecay = magnitude * (2.0f * (1.0f - percent));            OnShake(new Vector2(range1 * magDecay, range2 * magDecay) + original);        }        yield return null;    }    if (OnComplete != null)    {        OnComplete();    }}

可以看到 OnGetOriginal, OnShake参数由float变成了Vector2,更多参数同理。

public static void ShakePositionXY(    this  Transform     transform,     float               magnitude,     float               speed,     float               duration,     ShakeTool.ShakeType shakeType  = ShakeTool.ShakeType.Smooth,    Action              OnComplete = null){    ShakeTool.ShakeV2    (        magnitude,         speed,         duration,         ()   => (Vector2) transform.position,        (v2) => transform.SetPositionXY(v2),        shakeType,         OnComplete    );}

「Shake Shake」

原创粉丝点击