Stealth——01场景的基本搭建以及基础逻辑
版权申明:
- 本文原创首发于以下网站:
- 博客园『优梦创客』的空间:https://www.cnblogs.com/raymondking123
- 优梦创客的官方博客:https://91make.top
- 优梦创客的游戏讲堂:https://91make.ke.qq.com
- 『优梦创客』的微信公众号:umaketop
- 您可以自由转载,但必须加入完整的版权声明
搭建场景:
-
场景布局,设置static属性,设置Layer层,调整属性(Carmera,Lighting)
- 一般复杂的场景为了渲染效果都会做的很精细(有很多三角形构成),但是碰撞检测这种内部事件就可以简单点,会有场景的一个低模mesh来表征碰撞,降低性能消耗 //mesh collider的convex选项表示该模型是个凸多边形,(一般我们创建的都是凸多边形,像山洞啊什么的可能就为凹多边形)
- 把场景都设置为Static //光照 导航 遮挡都是静态的 //在unity中参与Lightmaps烘焙的物体必须都是静态的对象
- 增加灯光,对场景进行烘焙,把烘焙后的内容作为贴图,贴到物体上
-
设置light的效果:
添加灯光:
- Lighting烘焙: 把Light的Baking属性设置为Baked, 在需要烘焙的物体设置为static,可以在Lighting->Scene中把Precomputed realtime GI(预计算的实时光照去掉) //烘焙完成后会生成物体的光照贴图的信息(会有一个Scene同名的文件夹)贴到被设置为Static的物体上
- Light中backed Shadow Radius决定了back类型的光照照出来的阴影的散射半径,但其值为0时,表示不散射,此时hard Shadow和soft shadow没有差别,但该值越大,soft shadow的阴影散射的就越厉害
- Light中选择realtime下soft shadow的表现形式受到反射次数等参数影响,且需要在Lighting->scene中打开precomputed realtime GI,关闭Baked GI的选项
- Light中的Mixed光照模式,包含了实时计算和Baked两个功能,当物体没有动,光照没变化时,采用baked的光照信息;当物体运动时,使用precomputer teatime 实时计算光照shadows //听说用的不多?实现的是静态物体透视到动态物体上的阴影,因为静态物体不会动,所以阴影基本是固定的,bake好阴影贴图,动态的贴到动态物体上?
-
Light中的Culling Mask表示有灯光作用的物体标签类型,不选择中的标签不受光照影响
-
增加一盏场景灯和报警灯,场景灯提供场景关照,报警灯当发现玩家后进行报警的视觉和声音效果:
- 报警灯脚本:
public class AlarmLight : MonoBehaviour
{
public float fadeSpeed = 2f; //灯光报警fade的速度(默认2s变化一次)
public float hightIntensity = 4f; //最高最低亮度
public float lowIntensity = 0.5f;
public float changeMargin = 0.2f; //插值阈值
public bool alarmOn;
private float targetIntensity; //目标亮度值
private Light alarmLight;
private AudioSource audioSource;
private void Awake()
{
alarmLight = GetComponent<Light>();
audioSource = GetComponent<AudioSource>();
alarmLight.intensity = 0;
targetIntensity = hightIntensity;
}
// Update is called once per frame
void Update()
{
if (alarmOn)
{
if (!audioSource.isPlaying)
audioSource.Play();
alarmLight.intensity = Mathf.Lerp(alarmLight.intensity, targetIntensity, fadeSpeed * Time.deltaTime);
if (Mathf.Abs(targetIntensity - alarmLight.intensity) < changeMargin)
{
if (targetIntensity == hightIntensity)
targetIntensity = lowIntensity;
else
targetIntensity = hightIntensity;
}
}
else
{
if (audioSource.isPlaying)
audioSource.Stop();
alarmLight.intensity = Mathf.Lerp(alarmLight.intensity, 0, fadeSpeed * Time.deltaTime);
}
}
}
烘焙场景Lightmaps
-
//烘焙场景是使用bake类型的灯光对场景进行烘焙,生成贴图贴到物体表面,用于表现灯光效果
- Unity的官方解释:
-
烘焙的意义:单独使用 Unity 实时光源的光线时,这些光线不会自动进行反射。为了使用全局光照等技术创建更逼真的场景,我们需要启用 Unity 的预计算光照解决方案;Unity 可以计算复杂的静态光照效果(使用称为全局光照(简称 GI)的技术)并将它们存储在称为光照贴图的纹理贴图中作为参考。这一计算过程称为烘焙。对光照贴图进行烘焙时,会计算光源对场景中静态对象的影响,并将结果写入纹理中,这些纹理覆盖在场景几何体上以营造出光照效果。
- (这些光照贴图既可以包括照射到表面的直射光,也可以包括从场景内其他物体或表面反射的间接光。该光照纹理可与颜色(反照率)和浮雕(法线)之类的对象表面信息材质相关联的着色器一起使用。
- 使用烘焙光照时,这些光照贴图在游戏过程中无法改变,因此称为“静态”。实时光源可以重叠并可在光照贴图场景上叠加使用,但不能实时改变光照贴图本身。
-
通过这种方法,我们可在游戏中移动我们的光照,通过降低实时光计算量潜在提高性能,适应性能较低的硬件,如移动平台)
- 预计算实时全局光照
- 虽然静态光照贴图无法对场景中的光照条件变化作出反应,但预计算实时 GI 确实为我们提供了一种可以实时更新复杂场景光照的技术。
-
通过这种方法,可创建具有丰富全局光照和反射光的光照环境,能够实时响光照变化。这方面的一个典型例子是一天的时间系统:光源的位置和颜色随时间变化。如果使用传统的烘焙光照,这是无法实现的
- 优势和代价
- 虽然可以同时使用烘焙 GI 光照和预计算实时 GI,但要注意,同时渲染两个系统的性能开销也是各自开销的总和。我们不仅需要在视频内存中存储两组光照贴图,而且还要在着色器中进行解码的处理。
- 在什么情况下选择什么光照方法取决于项目的性质和目标硬件的性能。例如,在视频内存和处理能力局限性更大的移动端,烘焙 GI 光照方法可能具有更高性能。在具有专用图形硬件的独立计算机或最新款的游戏主机上,很可能可以使用预计算实时 GI,甚至同时使用这两个系统。
-
必须根据特定项目和所需目标平台的性质来决定采用哪种方法。请记住,在面向一系列不同硬件时,通常情况下,性能最低的硬件将决定选取哪种方法。
- 添加灯光:
-
Render Mode表示渲染的类型:Important表示逐像素渲染灯光,Not Impritant表示逐顶点渲染灯光,Auto表示自动
-
设置渲染质量:Project Setting -> Quality面板,因为我们游戏场景中的资源偏多,可能存在效率问题,所以我们我们渲染质量选择Good; 设置Rendering中 Pixel Light Cout(逐像素渲染的灯光的数量),因为我们灯光都选择的Auto的类型,所有应该Unity会挑6盏灯逐像素渲染,其他都逐顶点渲染(Unity怎么挑的???)
- 设置光照贴图的设置,之后就可以开始Bake了:
-
具体参数可以参考Unity官方文档,2018开始Unity的Lightmapper中提供了一种新的烘焙方式Progressive,此方式进行渐进式的光照贴图烘焙,一个简单的场景烘焙时间都到10个小时起
- 烘焙完成后可以在Scenes下找到和场景同名的文件夹中存放的就是烘焙后的数据,会作为贴图,贴到物体上
-
烘焙完成后的贴图自动贴到了场景物体上,此时关闭所有灯光,物体依旧具有关照的效果
添加Tag的管理类 //用来定义Tag的字符串的静态变量
- 添加转场效果 //加载场景逐渐变亮,退出场景逐渐变暗
- 使用RawImage遮挡这个画面实现,代码部分:
public class ScreenFadeInOut : MonoBehaviour
{
public float fadeSpeed = 1.5f;
private bool sceneStarting;
private RawImage rawImage;
// Start is called before the first frame update
void Start()
{
sceneStarting = true;
rawImage = this.GetComponent<RawImage>();
}
// Update is called once per frame
void Update()
{
if (sceneStarting)
{
rawImage.color = Color.Lerp(rawImage.color, Color.clear, fadeSpeed * Time.deltaTime);
if (rawImage.color.a <= 0.05f)
{
rawImage.color = Color.clear;
sceneStarting = false;
rawImage.enabled = false;
}
}
}
public void EndScene()
{
rawImage.enabled = true;
rawImage.color = Color.Lerp(rawImage.color, Color.black, fadeSpeed * Time.deltaTime);
if (rawImage.color.a > 0.95f)
SceneManager.LoadScene(0);
}
}
- 添加游戏控制器GameController //负责控制背景音乐播放,角色位置管理
- 为GameController添加Audio文件, //这里我们添加了两个Audio都设置play ON Awake和loop,区别在于normal主音量值为1 ,Panic被发现时的音量值为0 //我们要实现的效果就是当玩家被发现时,主音量逐渐降低,Panic音量逐渐提高
- 具体脚本:
public class LastPlayerSighting : MonoBehaviour
{
public Vector3 position = new Vector3(1000f, 1000f, 1000f); //表示玩家最后一次被发现的位置,如果没有被发现,就设置为默认值
public Vector3 resetPosition = new Vector3(1000f, 1000f, 1000f);
public float lightHighIntensity = 0.25f; //主灯光的亮度范围
public float lightLowIntensity = 0f;
public float lightFadeSpeed = 7f;
public float musicFadeSpeed = 1f; //音乐变化的fade速率
public bool isPlayerFound = false;
private AlarmLight alarmLightScript;
private Light mainLight; //主灯光
private AudioSource mainMusic; //主音乐和panic时播放的音乐
private AudioSource panicMusic;
private AudioSource[] sirens; //报警音乐
private const float muteVolume = 0f; //音乐的变化范围
private const float normalVolume = 0.8f;
// Start is called before the first frame update
void Start()
{
alarmLightScript = GameObject.FindGameObjectWithTag(Tags.ALARM_LIGHT).GetComponent<AlarmLight>();
mainLight = GameObject.FindGameObjectWithTag(Tags.MAIN_LIGHT).GetComponent<Light>();
mainMusic = this.GetComponent<AudioSource>();
panicMusic = this.transform.Find("Secondary_music").GetComponent<AudioSource>();
//sirens = new AudioSource[];
}
// Update is called once per frame
void Update()
{
isPlayerFound = (position != resetPosition);
//当玩家被发现时,调低主灯光,打开报警灯,淡出主音乐,淡入panic音乐, 但玩家脱离危险后恢复;
mainLight.intensity = Mathf.Lerp(mainLight.intensity, isPlayerFound ? lightLowIntensity : lightHighIntensity, lightFadeSpeed * Time.deltaTime);
alarmLightScript.alarmOn = isPlayerFound;
mainMusic.volume = Mathf.Lerp(mainMusic.volume, isPlayerFound ? muteVolume : normalVolume, musicFadeSpeed);
panicMusic.volume = Mathf.Lerp(panicMusic.volume, isPlayerFound ? normalVolume : muteVolume, musicFadeSpeed);
}
}
添加CCTV Carmera
- //CCTV闭路电视 //碰撞的触发依赖于Riggdbody组件,没有Riggdbody就不会触发trigger和collision(为了防止两个单独的collider互相触发)
- 添加Carmera的模型
- 添加mesh collider和spot光源
- 添加碰撞脚本:
public class CCTVCollision : MonoBehaviour
{
private LastPlayerSighting lastPlayerSighting;
private void Start()
{
lastPlayerSighting = GameObject.FindGameObjectWithTag(Tags.GAMECONTROLLER).GetComponent<LastPlayerSighting>();
}
private void OnTriggerStay(Collider other)
{
if (other.tag == Tags.PLAYER)
{
lastPlayerSighting.position = other.transform.position;
}
}
private void OnTriggerExit(Collider other)
{
if (other.tag == Tags.PLAYER)
{
lastPlayerSighting.position = lastPlayerSighting.resetPosition;
}
}
}
- 添加Animator使其旋转, 注意Animator中可以设置curves调整动画变化的速率
添加Laser Grid(激光栅栏)
- //一般Main Camera上会挂一个Audio Listener用于收音(一个场景只允许由一个Audio Listener)
- //在unity当前版本中Audio Source要设置Spatial Blend到3D,下面的3D Sound Settings才会生效(会有一个空间衰减的相关)
- 添加laser的模型(并使其和场景匹配),box collider,Light(红色点光源)和Audio Source
- 为Laser添加脚本,控制激光栅栏的开关和碰撞检测:
- 激光栅栏的开关:
public class LasterBlinking : MonoBehaviour
{
public float onTime; //灯灭的时间间隔
public float offTime; //灯亮的时间间隔
private float timer; //流逝的时间
private Renderer laserRenerer;
private Light laserLight;
// Start is called before the first frame update
void Start()
{
laserRenerer = GetComponent<Renderer>();
laserLight = GetComponent<Light>();
timer = 0;
}
// Update is called once per frame
void Update()
{
timer += Time.deltaTime;
if (laserRenerer.enabled && timer >= onTime)
{
laserRenerer.enabled = false;
laserLight.enabled = false;
timer = 0;
}
else if (!laserRenerer.enabled && timer >= offTime)
{
laserRenerer.enabled = true;
laserLight.enabled = true;
timer = 0;
}
}
}
- 碰撞检测:
public class LaserPlayerDetection : MonoBehaviour
{
private void OnTriggerStay(Collider other)
{
if (other.tag == Tags.PLAYER && this.GetComponent<Renderer>().enabled)
{
LastPlayerSighting.Instance.position = other.transform.position;
}
}
}