美文网首页代码改变世界Unity技术分享程序员
「Unity3D」(7)协程使用3种算法实现CameraShak

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

作者: 飞机王 | 来源:发表于2017-12-06 13:15 被阅读345次

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

    相关文章

      网友评论

        本文标题:「Unity3D」(7)协程使用3种算法实现CameraShak

        本文链接:https://www.haomeiwen.com/subject/asbkixtx.html