上一章地址: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;
總結
特效、聲效部分分析完了。通體來說還是相對簡單的,除了四通道的那個迷惑算法沒太看懂之外,其他的邏輯很簡單,粒子效果複用的部分值得學習。下一章分析攝像機的相關腳本,有點複雜。