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