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

相关文章

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

SWAP作物生长模型安装教程、数据制备、敏感性分析、气候变化影响、R模型敏感性分析与贝叶斯优化、Fortran源代码分析、气候数据降尺度与变化影响分析

查看原文>>>全流程SWAP农业模型数据制备、敏感性分析及气候变化影响实践技术应用 SWAP模型是由荷兰瓦赫宁根大学开发的先进农作物模型,它综合考虑了土壤-水分-大气以及植被间的相互作用;是一种描述作物生长过程的一种机理性作物生长模型。它不但运用Richard方程,使其能够精确的模拟土壤中水分的运动,而且耦合了WOFOST作物模型使作物的生长描述更为科学。 本文让更多的科研人员和农业工作者

MOLE 2.5 分析分子通道和孔隙

软件介绍 生物大分子通道和孔隙在生物学中发挥着重要作用,例如在分子识别和酶底物特异性方面。 我们介绍了一种名为 MOLE 2.5 的高级软件工具,该工具旨在分析分子通道和孔隙。 与其他可用软件工具的基准测试表明,MOLE 2.5 相比更快、更强大、功能更丰富。作为一项新功能,MOLE 2.5 可以估算已识别通道的物理化学性质。 软件下载 https://pan.quark.cn/s/57

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

国产游戏崛起:技术革新与文化自信的双重推动

近年来,国产游戏行业发展迅猛,技术水平和作品质量均得到了显著提升。特别是以《黑神话:悟空》为代表的一系列优秀作品,成功打破了过去中国游戏市场以手游和网游为主的局限,向全球玩家展示了中国在单机游戏领域的实力与潜力。随着中国开发者在画面渲染、物理引擎、AI 技术和服务器架构等方面取得了显著进展,国产游戏正逐步赢得国际市场的认可。然而,面对全球游戏行业的激烈竞争,国产游戏技术依然面临诸多挑战,未来的