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

相关文章

Springboot中分析SQL性能的两种方式详解

《Springboot中分析SQL性能的两种方式详解》文章介绍了SQL性能分析的两种方式:MyBatis-Plus性能分析插件和p6spy框架,MyBatis-Plus插件配置简单,适用于开发和测试环... 目录SQL性能分析的两种方式:功能介绍实现方式:实现步骤:SQL性能分析的两种方式:功能介绍记录

最长公共子序列问题的深度分析与Java实现方式

《最长公共子序列问题的深度分析与Java实现方式》本文详细介绍了最长公共子序列(LCS)问题,包括其概念、暴力解法、动态规划解法,并提供了Java代码实现,暴力解法虽然简单,但在大数据处理中效率较低,... 目录最长公共子序列问题概述问题理解与示例分析暴力解法思路与示例代码动态规划解法DP 表的构建与意义动

浅析如何使用Swagger生成带权限控制的API文档

《浅析如何使用Swagger生成带权限控制的API文档》当涉及到权限控制时,如何生成既安全又详细的API文档就成了一个关键问题,所以这篇文章小编就来和大家好好聊聊如何用Swagger来生成带有... 目录准备工作配置 Swagger权限控制给 API 加上权限注解查看文档注意事项在咱们的开发工作里,API

C#使用DeepSeek API实现自然语言处理,文本分类和情感分析

《C#使用DeepSeekAPI实现自然语言处理,文本分类和情感分析》在C#中使用DeepSeekAPI可以实现多种功能,例如自然语言处理、文本分类、情感分析等,本文主要为大家介绍了具体实现步骤,... 目录准备工作文本生成文本分类问答系统代码生成翻译功能文本摘要文本校对图像描述生成总结在C#中使用Deep

Spring IOC控制反转的实现解析

《SpringIOC控制反转的实现解析》:本文主要介绍SpringIOC控制反转的实现,IOC是Spring的核心思想之一,它通过将对象的创建、依赖注入和生命周期管理交给容器来实现解耦,使开发者... 目录1. IOC的基本概念1.1 什么是IOC1.2 IOC与DI的关系2. IOC的设计目标3. IOC

Go中sync.Once源码的深度讲解

《Go中sync.Once源码的深度讲解》sync.Once是Go语言标准库中的一个同步原语,用于确保某个操作只执行一次,本文将从源码出发为大家详细介绍一下sync.Once的具体使用,x希望对大家有... 目录概念简单示例源码解读总结概念sync.Once是Go语言标准库中的一个同步原语,用于确保某个操

Redis主从/哨兵机制原理分析

《Redis主从/哨兵机制原理分析》本文介绍了Redis的主从复制和哨兵机制,主从复制实现了数据的热备份和负载均衡,而哨兵机制可以监控Redis集群,实现自动故障转移,哨兵机制通过监控、下线、选举和故... 目录一、主从复制1.1 什么是主从复制1.2 主从复制的作用1.3 主从复制原理1.3.1 全量复制

Redis主从复制的原理分析

《Redis主从复制的原理分析》Redis主从复制通过将数据镜像到多个从节点,实现高可用性和扩展性,主从复制包括初次全量同步和增量同步两个阶段,为优化复制性能,可以采用AOF持久化、调整复制超时时间、... 目录Redis主从复制的原理主从复制概述配置主从复制数据同步过程复制一致性与延迟故障转移机制监控与维

Redis连接失败:客户端IP不在白名单中的问题分析与解决方案

《Redis连接失败:客户端IP不在白名单中的问题分析与解决方案》在现代分布式系统中,Redis作为一种高性能的内存数据库,被广泛应用于缓存、消息队列、会话存储等场景,然而,在实际使用过程中,我们可能... 目录一、问题背景二、错误分析1. 错误信息解读2. 根本原因三、解决方案1. 将客户端IP添加到Re

Java汇编源码如何查看环境搭建

《Java汇编源码如何查看环境搭建》:本文主要介绍如何在IntelliJIDEA开发环境中搭建字节码和汇编环境,以便更好地进行代码调优和JVM学习,首先,介绍了如何配置IntelliJIDEA以方... 目录一、简介二、在IDEA开发环境中搭建汇编环境2.1 在IDEA中搭建字节码查看环境2.1.1 搭建步