UnityStandardAsset工程、源码分析_3_赛车游戏[玩家控制]_特效、声效

本文主要是介绍UnityStandardAsset工程、源码分析_3_赛车游戏[玩家控制]_特效、声效,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

上一章地址: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并调用它的EmyTyreSmokePlayAudio方法来释放特效。

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中的实现也很简单,调用对于AudioSourceStartStop方法,实现滑动音效的播放和停止。而较为复杂的在于引擎声音的管理,也就是我们第一章所见到的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 audioFourChannel // four Channel audio}public EngineAudioOptions engineSoundStyle = EngineAudioOptions.FourChannel;// Set the default audio options to be four channelpublic AudioClip lowAccelClip;                                              // Audio clip for low accelerationpublic AudioClip lowDecelClip;                                              // Audio clip for low decelerationpublic AudioClip highAccelClip;                                             // Audio clip for high accelerationpublic AudioClip highDecelClip;                                             // Audio clip for high decelerationpublic float pitchMultiplier = 1f;                                          // Used for altering the pitch of audio clipspublic float lowPitchMin = 1f;                                              // The lowest possible pitch for the low soundspublic float lowPitchMax = 6f;                                              // The highest possible pitch for the low soundspublic float highPitchMultiplier = 0.25f;                                   // Used for altering the pitch of high soundspublic float maxRolloffDistance = 500;                                      // The maximum distance where rollof starts to take placepublic float dopplerLevel = 1;                                              // The mount of doppler effect used in the audiopublic bool useDoppler = true;                                              // Toggle for using dopplerprivate AudioSource m_LowAccel; // Source for the low acceleration soundsprivate AudioSource m_LowDecel; // Source for the low deceleration soundsprivate AudioSource m_HighAccel; // Source for the high acceleration soundsprivate AudioSource m_HighDecel; // Source for the high deceleration soundsprivate bool m_StartedSound; // flag for knowing if we have started soundsprivate 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 sourcem_HighAccel = SetUpEngineAudioSource(highAccelClip);// 如果使用四通道则设置其他三个片段// if we have four channel audio setup the four audio sourcesif (engineSoundStyle == EngineAudioOptions.FourChannel){m_LowAccel = SetUpEngineAudioSource(lowAccelClip);m_LowDecel = SetUpEngineAudioSource(lowDecelClip);m_HighDecel = SetUpEngineAudioSource(highDecelClip);}// 开始播放的旗帜// flag that we have started the sounds playingm_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 frameprivate void Update(){// 车辆和摄像机的距离// get the distance to main camerafloat camDist = (Camera.main.transform.position - transform.position).sqrMagnitude;// 距离超过了最大距离,停止播放// stop sound if the object is beyond the maximum roll off distanceif (m_StartedSound && camDist > maxRolloffDistance*maxRolloffDistance){StopSound();}// 小于最大距离,开始播放// start the sound if not playing and it is nearer than the maximum distanceif (!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 multipliersm_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 accelerationfloat accFade = Mathf.Abs(m_CarController.AccelInput);float decFade = 1 - accFade;// get the high fade value based on the cars revsfloat highFade = Mathf.InverseLerp(0.2f, 0.8f, m_CarController.Revs);float lowFade = 1 - highFade;// adjust the values to be more realistichighFade = 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 valuesm_LowAccel.volume = lowFade*accFade;m_LowDecel.volume = lowFade*decFade;m_HighAccel.volume = highFade*accFade;m_HighDecel.volume = highFade*decFade;// adjust the doppler levelsm_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 objectprivate AudioSource SetUpEngineAudioSource(AudioClip clip){// create the new audio source component on the game object and set up its propertiesAudioSource source = gameObject.AddComponent<AudioSource>();source.clip = clip;source.volume = 0;source.loop = true;// 在音效片段的随机位置开始播放// start the clip from a random pointsource.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 rangeprivate 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;

总结

特效、声效部分分析完了。通体来说还是相对简单的,除了四通道的那个迷惑算法没太看懂之外,其他的逻辑很简单,粒子效果复用的部分值得学习。下一章分析摄像机的相关脚本,有点复杂。

这篇关于UnityStandardAsset工程、源码分析_3_赛车游戏[玩家控制]_特效、声效的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/794729

相关文章

Nginx分布式部署流程分析

《Nginx分布式部署流程分析》文章介绍Nginx在分布式部署中的反向代理和负载均衡作用,用于分发请求、减轻服务器压力及解决session共享问题,涵盖配置方法、策略及Java项目应用,并提及分布式事... 目录分布式部署NginxJava中的代理代理分为正向代理和反向代理正向代理反向代理Nginx应用场景

一文详解Python如何开发游戏

《一文详解Python如何开发游戏》Python是一种非常流行的编程语言,也可以用来开发游戏模组,:本文主要介绍Python如何开发游戏的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录一、python简介二、Python 开发 2D 游戏的优劣势优势缺点三、Python 开发 3D

Redis中的有序集合zset从使用到原理分析

《Redis中的有序集合zset从使用到原理分析》Redis有序集合(zset)是字符串与分值的有序映射,通过跳跃表和哈希表结合实现高效有序性管理,适用于排行榜、延迟队列等场景,其时间复杂度低,内存占... 目录开篇:排行榜背后的秘密一、zset的基本使用1.1 常用命令1.2 Java客户端示例二、zse

基于Python开发Windows自动更新控制工具

《基于Python开发Windows自动更新控制工具》在当今数字化时代,操作系统更新已成为计算机维护的重要组成部分,本文介绍一款基于Python和PyQt5的Windows自动更新控制工具,有需要的可... 目录设计原理与技术实现系统架构概述数学建模工具界面完整代码实现技术深度分析多层级控制理论服务层控制注

Redis中的AOF原理及分析

《Redis中的AOF原理及分析》Redis的AOF通过记录所有写操作命令实现持久化,支持always/everysec/no三种同步策略,重写机制优化文件体积,与RDB结合可平衡数据安全与恢复效率... 目录开篇:从日记本到AOF一、AOF的基本执行流程1. 命令执行与记录2. AOF重写机制二、AOF的

MyBatis Plus大数据量查询慢原因分析及解决

《MyBatisPlus大数据量查询慢原因分析及解决》大数据量查询慢常因全表扫描、分页不当、索引缺失、内存占用高及ORM开销,优化措施包括分页查询、流式读取、SQL优化、批处理、多数据源、结果集二次... 目录大数据量查询慢的常见原因优化方案高级方案配置调优监控与诊断总结大数据量查询慢的常见原因MyBAT

分析 Java Stream 的 peek使用实践与副作用处理方案

《分析JavaStream的peek使用实践与副作用处理方案》StreamAPI的peek操作是中间操作,用于观察元素但不终止流,其副作用风险包括线程安全、顺序混乱及性能问题,合理使用场景有限... 目录一、peek 操作的本质:有状态的中间操作二、副作用的定义与风险场景1. 并行流下的线程安全问题2. 顺

MyBatis/MyBatis-Plus同事务循环调用存储过程获取主键重复问题分析及解决

《MyBatis/MyBatis-Plus同事务循环调用存储过程获取主键重复问题分析及解决》MyBatis默认开启一级缓存,同一事务中循环调用查询方法时会重复使用缓存数据,导致获取的序列主键值均为1,... 目录问题原因解决办法如果是存储过程总结问题myBATis有如下代码获取序列作为主键IdMappe

Java中最全最基础的IO流概述和简介案例分析

《Java中最全最基础的IO流概述和简介案例分析》JavaIO流用于程序与外部设备的数据交互,分为字节流(InputStream/OutputStream)和字符流(Reader/Writer),处理... 目录IO流简介IO是什么应用场景IO流的分类流的超类类型字节文件流应用简介核心API文件输出流应用文

java 恺撒加密/解密实现原理(附带源码)

《java恺撒加密/解密实现原理(附带源码)》本文介绍Java实现恺撒加密与解密,通过固定位移量对字母进行循环替换,保留大小写及非字母字符,由于其实现简单、易于理解,恺撒加密常被用作学习加密算法的入... 目录Java 恺撒加密/解密实现1. 项目背景与介绍2. 相关知识2.1 恺撒加密算法原理2.2 Ja