版權申明:
- 本文原創首發於以下網站:
- 博客園『優夢創客』的空間: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;
}
}
}