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

本文主要讨论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」

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值