上一章地址:UnityStandardAsset工程、源码分析_2_赛车游戏[玩家控制]_车辆核心控制
在上一章的分析中,有这么一段代码:
// 播放轮胎烟雾,粒子效果等脚本下章分析
m_WheelEffects[i].EmitTyreSmoke();
// 避免有多个轮胎同时播放声音,如果有轮胎播放了,这个轮胎就不播放了
// avoiding all four tires screeching at the same time
// if they do it can lead to some strange audio artefacts
if (!AnySkidSoundPlaying())
{
m_WheelEffects[i].PlayAudio();
}
这里就是车辆的核心控制逻辑CarController
与特效、声效的交互点了。这章我们就来分析在CarController
调用了这两个方法之后,发生了什么,如何管理特效的显示。
特效、声效
先说特效,组成特效的主要有两个方面:
- 烟尘
- 轮胎印
烟尘就是一个粒子系统,仅有一个,位置为Car/Particales/ParticleBurnoutSmoke
,而不是对应四个轮胎有四个。在轮胎发生滑动,不论是正向滑动还是反向滑动的时候,就会移动到发生滑动的轮胎的位置,并即时发出数量为1的粒子。
而轮胎印是一个TrailRenderer
,为预制件,由每个轮胎在需要时独自克隆和使用。
每个轮胎上有一个WheelEffects
类,用于管理特效发生的逻辑,上面的代码中的m_WheelEffects[i]
,就是在遍历每一个轮胎的WheelEffects
并调用它的EmyTyreSmoke
和PlayAudio
方法来释放特效。
WheelEffects
类的代码:
namespace UnityStandardAssets.Vehicles.Car
{
[RequireComponent(typeof (AudioSource))]
public class WheelEffects : MonoBehaviour
{
public Transform SkidTrailPrefab;
public static Transform skidTrailsDetachedParent;
public ParticleSystem skidParticles;
public bool skidding { get; private set; }
public bool PlayingAudio { get; private set; }
private AudioSource m_AudioSource;
private Transform m_SkidTrail;
private WheelCollider m_WheelCollider;
private void Start()
{
// 寻找烟尘粒子
skidParticles = transform.root.GetComponentInChildren<ParticleSystem>();
if (skidParticles == null)
{
Debug.LogWarning(" no particle system found on car to generate smoke particles", gameObject);
}
else
{
skidParticles.Stop();
}
m_WheelCollider = GetComponent<WheelCollider>();
m_AudioSource = GetComponent<AudioSource>();
PlayingAudio = false;
// 用于滑动结束后保留轮胎印
if (skidTrailsDetachedParent == null)
{
skidTrailsDetachedParent = new GameObject("Skid Trails - Detached").transform;
}
}
public void EmitTyreSmoke()
{
// 把粒子效果起点置于轮胎底部
skidParticles.transform.position = transform.position - transform.up*m_WheelCollider.radius;
skidParticles.Emit(1);
// 没有启动滑动协程则启动,避免重复
if (!skidding)
{
StartCoroutine(StartSkidTrail());
}
}
public void PlayAudio()
{
m_AudioSource.Play();
PlayingAudio = true;
}
public void StopAudio()
{
m_AudioSource.Stop();
PlayingAudio = false;
}
// 开始出现滑动轨迹
public IEnumerator StartSkidTrail()
{
skidding = true;
m_SkidTrail = Instantiate(SkidTrailPrefab);
// 不知道这里为什么要等待
while (m_SkidTrail == null)
{
yield return null;
}
m_SkidTrail.parent = transform;
m_SkidTrail.localPosition = -Vector3.up*m_WheelCollider.radius;
}
public void EndSkidTrail()
{
if (!skidding)
{
return;
}
skidding = false;
// 保留轮胎印,10秒后消除
m_SkidTrail.parent = skidTrailsDetachedParent;
Destroy(m_SkidTrail.gameObject, 10);
}
}
}
可以看出来,这个类并不复杂,逻辑很简单。最主要的部分在于EmitTyreSmoke
,它被CarController
调用,负责发出粒子和启用轮胎印。
// 把粒子效果起点置于轮胎底部
skidParticles.transform.position = transform.position - transform.up*m_WheelCollider.radius;
skidParticles.Emit(1);
这一段很精妙,说的是将只有一个的粒子系统转移到轮胎上并发出一个粒子,而不是使用四个粒子系统独自发出粒子,这就进行了资源的复用。
随后调用了StartSkidTrail
协程进行轮胎印的处理。不过为什么在克隆预制件后要有一个循环的等待?难道是因为预制件过大,要异步等待一段时间?预制件被克隆后放在了轮胎所在的位置并向下偏移一个轮胎半径的距离,使其紧贴地面。在没有被CarController
调用EndSkidTrail
方法之前,这个被克隆出来的TrailRenderer
会不断地形成轨迹。
而在被调用后,它的父对象被设置成了之前定义的空对象skidTrailsDetachedParent
,并在10秒后销毁,也就是车辆结束滑行后轮胎印静止不动,10秒后销毁。
至此,轮胎印和粒子特效就分析完了,接下来我们看看声效模块。
在CarController
对于声效的调用部分是:
// 避免有多个轮胎同时播放声音,如果有轮胎播放了,这个轮胎就不播放了
// avoiding all four tires screeching at the same time
// if they do it can lead to some strange audio artefacts
if (!AnySkidSoundPlaying())
{
m_WheelEffects[i].PlayAudio();
}
同特效一样,声效也是由WheelEffects
负责提供接口。这里判断了是否有音效正在播放,如果有则为了避免出现奇怪的声音而不播放,因为滑动音效每个轮胎有一个,总共四个。
WheelEffects
中的实现也很简单,调用对于AudioSource
的Start
和Stop
方法,实现滑动音效的播放和停止。而较为复杂的在于引擎声音的管理,也就是我们第一章所见到的CarAudio
脚本:
namespace UnityStandardAssets.Vehicles.Car
{
[RequireComponent(typeof (CarController))]
public class CarAudio : MonoBehaviour
{
// 这个脚本需要读取一些车辆的当前数据,来播放相应的声音
// 引擎的声音可以是一段简单的循环片段,或者它也可以是能描述引擎转速或者油门的不同的四个变化的混合片段
// This script reads some of the car's current properties and plays sounds accordingly.
// The engine sound can be a simple single clip which is looped and pitched, or it
// can be a crossfaded blend of four clips which represent the timbre of the engine
// at different RPM and Throttle state.
// 引擎片段应当平缓而不是正在升调或者降调
// the engine clips should all be a steady pitch, not rising or falling.
// 当使用四个通道的片段时
// 低加速片段:引擎转速低时,油门打开
// 高加速片段:引擎转速高时,油门打开
// 低减速片段:引擎转速低时,油门最小
// 高减速片段:引擎转速高时,油门最小
// when using four channel engine crossfading, the four clips should be:
// lowAccelClip : The engine at low revs, with throttle open (i.e. begining acceleration at very low speed)
// highAccelClip : Thenengine at high revs, with throttle open (i.e. accelerating, but almost at max speed)
// lowDecelClip : The engine at low revs, with throttle at minimum (i.e. idling or engine-braking at very low speed)
// highDecelClip : Thenengine at high revs, with throttle at minimum (i.e. engine-braking at very high speed)
// 为了得到正确的过渡音,片段音调应当符合
// For proper crossfading, the clips pitches should all match, with an octave offset between low and high.
// 总之就是使用四个声音片段插值得到平滑的声音,或者直接使用单个的声音文件
// 可以选择单一声音或者四通道
public enum EngineAudioOptions // Options for the engine audio
{
Simple, // Simple style audio
FourChannel // four Channel audio
}
public EngineAudioOptions engineSoundStyle = EngineAudioOptions.FourChannel;// Set the default audio options to be four channel
public AudioClip lowAccelClip; // Audio clip for low acceleration
public AudioClip lowDecelClip; // Audio clip for low deceleration
public AudioClip highAccelClip; // Audio clip for high acceleration
public AudioClip highDecelClip; // Audio clip for high deceleration
public float pitchMultiplier = 1f; // Used for altering the pitch of audio clips
public float lowPitchMin = 1f; // The lowest possible pitch for the low sounds
public float lowPitchMax = 6f; // The highest possible pitch for the low sounds
public float highPitchMultiplier = 0.25f; // Used for altering the pitch of high sounds
public float maxRolloffDistance = 500; // The maximum distance where rollof starts to take place
public float dopplerLevel = 1; // The mount of doppler effect used in the audio
public bool useDoppler = true; // Toggle for using doppler
private AudioSource m_LowAccel; // Source for the low acceleration sounds
private AudioSource m_LowDecel; // Source for the low deceleration sounds
private AudioSource m_HighAccel; // Source for the high acceleration sounds
private AudioSource m_HighDecel; // Source for the high deceleration sounds
private bool m_StartedSound; // flag for knowing if we have started sounds
private CarController m_CarController; // Reference to car we are controlling
// 开始播放
private void StartSound()
{
// get the carcontroller ( this will not be null as we have require component)
m_CarController = GetComponent<CarController>();
// 先设置高加速片段
// setup the simple audio source
m_HighAccel = SetUpEngineAudioSource(highAccelClip);
// 如果使用四通道则设置其他三个片段
// if we have four channel audio setup the four audio sources
if (engineSoundStyle == EngineAudioOptions.FourChannel)
{
m_LowAccel = SetUpEngineAudioSource(lowAccelClip);
m_LowDecel = SetUpEngineAudioSource(lowDecelClip);
m_HighDecel = SetUpEngineAudioSource(highDecelClip);
}
// 开始播放的旗帜
// flag that we have started the sounds playing
m_StartedSound = true;
}
// 停止播放
private void StopSound()
{
// 去除掉所有的音效片段
//Destroy all audio sources on this object:
foreach (var source in GetComponents<AudioSource>())
{
Destroy(source);
}
m_StartedSound = false;
}
// Update is called once per frame
private void Update()
{
// 车辆和摄像机的距离
// get the distance to main camera
float camDist = (Camera.main.transform.position - transform.position).sqrMagnitude;
// 距离超过了最大距离,停止播放
// stop sound if the object is beyond the maximum roll off distance
if (m_StartedSound && camDist > maxRolloffDistance*maxRolloffDistance)
{
StopSound();
}
// 小于最大距离,开始播放
// start the sound if not playing and it is nearer than the maximum distance
if (!m_StartedSound && camDist < maxRolloffDistance*maxRolloffDistance)
{
StartSound();
}
if (m_StartedSound)
{
// 根据引擎转速的插值
// The pitch is interpolated between the min and max values, according to the car's revs.
float pitch = ULerp(lowPitchMin, lowPitchMax, m_CarController.Revs);
// clamp一下,那为什么上一句不用Lerp?
// clamp to minimum pitch (note, not clamped to max for high revs while burning out)
pitch = Mathf.Min(lowPitchMax, pitch);
if (engineSoundStyle == EngineAudioOptions.Simple)
{
// 单通道,简单设置音调,多普勒等级,音量
// for 1 channel engine sound, it's oh so simple:
m_HighAccel.pitch = pitch*pitchMultiplier*highPitchMultiplier;
m_HighAccel.dopplerLevel = useDoppler ? dopplerLevel : 0;
m_HighAccel.volume = 1;
}
else
{
// for 4 channel engine sound, it's a little more complex:
// 根据pitch和音调乘数调整音调
// adjust the pitches based on the multipliers
m_LowAccel.pitch = pitch*pitchMultiplier;
m_LowDecel.pitch = pitch*pitchMultiplier;
m_HighAccel.pitch = pitch*highPitchMultiplier*pitchMultiplier;
m_HighDecel.pitch = pitch*highPitchMultiplier*pitchMultiplier;
// get values for fading the sounds based on the acceleration
float accFade = Mathf.Abs(m_CarController.AccelInput);
float decFade = 1 - accFade;
// get the high fade value based on the cars revs
float highFade = Mathf.InverseLerp(0.2f, 0.8f, m_CarController.Revs);
float lowFade = 1 - highFade;
// adjust the values to be more realistic
highFade = 1 - ((1 - highFade)*(1 - highFade));
lowFade = 1 - ((1 - lowFade)*(1 - lowFade));
accFade = 1 - ((1 - accFade)*(1 - accFade));
decFade = 1 - ((1 - decFade)*(1 - decFade));
// adjust the source volumes based on the fade values
m_LowAccel.volume = lowFade*accFade;
m_LowDecel.volume = lowFade*decFade;
m_HighAccel.volume = highFade*accFade;
m_HighDecel.volume = highFade*decFade;
// adjust the doppler levels
m_HighAccel.dopplerLevel = useDoppler ? dopplerLevel : 0;
m_LowAccel.dopplerLevel = useDoppler ? dopplerLevel : 0;
m_HighDecel.dopplerLevel = useDoppler ? dopplerLevel : 0;
m_LowDecel.dopplerLevel = useDoppler ? dopplerLevel : 0;
}
}
}
// 添加一个音效片段
// sets up and adds new audio source to the gane object
private AudioSource SetUpEngineAudioSource(AudioClip clip)
{
// create the new audio source component on the game object and set up its properties
AudioSource source = gameObject.AddComponent<AudioSource>();
source.clip = clip;
source.volume = 0;
source.loop = true;
// 在音效片段的随机位置开始播放
// start the clip from a random point
source.time = Random.Range(0f, clip.length);
source.Play();
source.minDistance = 5;
source.maxDistance = maxRolloffDistance;
source.dopplerLevel = 0;
return source;
}
// unclamped versions of Lerp and Inverse Lerp, to allow value to exceed the from-to range
private static float ULerp(float from, float to, float value)
{
return (1.0f - value)*from + value*to;
}
}
}
乍看上去有些不明所以,但仔细分析一下也不是很难。首先明确一下,这个脚本能做什么?
- 提供单通道声效,无论挡位如何,仅使用一个循环的声效片段根据引擎转速输出声效。
- 提供四通道声效,根据引擎转速和挡位发出不同的声音,非常有效地模拟了真实车辆的引擎声。
然后再分析这个脚本是如何完成以上任务的?来看Update
方法。首先进行了一波距离判断,车辆与摄像机的距离大于阈值后不播放,之后的处理都是基于需要播放声音的前提下进行的。:
// 车辆和摄像机的距离
// get the distance to main camera
float camDist = (Camera.main.transform.position - transform.position).sqrMagnitude;
// 距离超过了最大距离,停止播放
// stop sound if the object is beyond the maximum roll off distance
if (m_StartedSound && camDist > maxRolloffDistance*maxRolloffDistance)
{
StopSound();
}
// 小于最大距离,开始播放
// start the sound if not playing and it is nearer than the maximum distance
if (!m_StartedSound && camDist < maxRolloffDistance*maxRolloffDistance)
{
StartSound();
}
然后根据CarController
提供的转速值确定pitch
声调的值,在这里上限为6,下限为1,中间平滑插值:
// 根据引擎转速的插值
// The pitch is interpolated between the min and max values, according to the car's revs.
float pitch = ULerp(lowPitchMin, lowPitchMax, m_CarController.Revs);
// clamp一下,那为什么上一句不用Lerp?
// clamp to minimum pitch (note, not clamped to max for high revs while burning out)
pitch = Mathf.Min(lowPitchMax, pitch);
如果启用了单通道的模式,简单设置音效,结束:
if (engineSoundStyle == EngineAudioOptions.Simple)
{
// 单通道,简单设置音调,多普勒等级,音量
// for 1 channel engine sound, it's oh so simple:
m_HighAccel.pitch = pitch*pitchMultiplier*highPitchMultiplier;
m_HighAccel.dopplerLevel = useDoppler ? dopplerLevel : 0;
m_HighAccel.volume = 1;
}
接下来是四通道的处理办法,说实话我没太看懂,是不是缺少了什么基础知识?总之也是和单通道一样根据pitch
计算音调,四个通道同时播放:
// for 4 channel engine sound, it's a little more complex:
// 根据pitch和音调乘数调整音调
// adjust the pitches based on the multipliers
m_LowAccel.pitch = pitch*pitchMultiplier;
m_LowDecel.pitch = pitch*pitchMultiplier;
m_HighAccel.pitch = pitch*highPitchMultiplier*pitchMultiplier;
m_HighDecel.pitch = pitch*highPitchMultiplier*pitchMultiplier;
// get values for fading the sounds based on the acceleration
float accFade = Mathf.Abs(m_CarController.AccelInput);
float decFade = 1 - accFade;
// get the high fade value based on the cars revs
float highFade = Mathf.InverseLerp(0.2f, 0.8f, m_CarController.Revs);
float lowFade = 1 - highFade;
// adjust the values to be more realistic
highFade = 1 - ((1 - highFade)*(1 - highFade));
lowFade = 1 - ((1 - lowFade)*(1 - lowFade));
accFade = 1 - ((1 - accFade)*(1 - accFade));
decFade = 1 - ((1 - decFade)*(1 - decFade));
// adjust the source volumes based on the fade values
m_LowAccel.volume = lowFade*accFade;
m_LowDecel.volume = lowFade*decFade;
m_HighAccel.volume = highFade*accFade;
m_HighDecel.volume = highFade*decFade;
// adjust the doppler levels
m_HighAccel.dopplerLevel = useDoppler ? dopplerLevel : 0;
m_LowAccel.dopplerLevel = useDoppler ? dopplerLevel : 0;
m_HighDecel.dopplerLevel = useDoppler ? dopplerLevel : 0;
m_LowDecel.dopplerLevel = useDoppler ? dopplerLevel : 0;
总结
特效、声效部分分析完了。通体来说还是相对简单的,除了四通道的那个迷惑算法没太看懂之外,其他的逻辑很简单,粒子效果复用的部分值得学习。下一章分析摄像机的相关脚本,有点复杂。