用Unity實(shí)現(xiàn)《我們終將變成我們所關(guān)注的東西》

作者:Yumir
哈嘍大家好我是Yumir,前幾天偶然和朋友聊起一個(gè)很有趣的游戲,也許你在老早之前就聽(tīng)說(shuō)并且玩過(guò)《信任的進(jìn)化》。
這個(gè)游戲的作者Nicky Case是個(gè)大佬,大獎(jiǎng)拿到手軟,經(jīng)歷傳奇,因?yàn)槲覄倓偪催^(guò)他的演講視頻,所以我已經(jīng)非常了解他啦ww。但我今天要說(shuō)的是《We become what we behold》,是他的另一個(gè)游戲作品。
游戲地址:https://ncase.itch.io/wbwwb
坦白說(shuō)我看到信任的進(jìn)化的時(shí)候雖然覺(jué)得很厲害,但是并不覺(jué)得是個(gè)有趣的游戲,更像是大學(xué)老師的ppt【表打我】,但是他確實(shí)比直接去看書(shū)學(xué)博弈論更加有趣。
早先在大佬的推薦下拜讀《游戲改變世界》,讀后感慨如果我們的教育體系是一個(gè)及時(shí)反饋的游戲,那我們的學(xué)習(xí)生涯可太舒適了,但是顯然這不是一件太容易的事情,《信任的進(jìn)化》能夠用一個(gè)簡(jiǎn)短的游戲流暢讓玩家理解一個(gè)深?yuàn)W的哲學(xué)理念,是我認(rèn)為最驚艷的地方。
實(shí)際上這也是游戲一個(gè)比較不錯(cuò)的發(fā)展方向,甚至給我感覺(jué)很多大佬都是有這個(gè)意愿和期望的,只是最近好像都沒(méi)聽(tīng)到相關(guān)消息,騰訊去年支持過(guò)幾個(gè),另一個(gè)比較火的就是《onehandclapping》吧,就是那個(gè)呦呦鹿鳴的游戲。
但是,相對(duì)作者其他的游戲我還是比較能get這個(gè)網(wǎng)頁(yè)小游戲,寥寥幾筆傳達(dá)了不簡(jiǎn)單的思想,游戲流程不長(zhǎng)卻讓你看到更多的東西。
游戲的內(nèi)容是玩家扮演一名記者進(jìn)行新聞拍攝,玩家拍攝到的圖片會(huì)顯示在一個(gè)小電視上,觀眾看了之后會(huì)有不同的反應(yīng),一件一件事情引起一系列的反應(yīng)。
當(dāng)我們?cè)谟螒蚪Y(jié)束之后大罵這個(gè)無(wú)良媒體時(shí)抬頭看一眼游戲標(biāo)題“我們終將變成我們所關(guān)注的東西”,恍然大悟,我們確實(shí)在一點(diǎn)點(diǎn)的讓世界變成我們看到的樣子,這個(gè)游戲才真正的結(jié)束了。
看到有趣的游戲當(dāng)然要?jiǎng)邮謱?shí)現(xiàn)一下啦~特別這次還有熱心原作者分享的素材包,可以不用自己畫(huà)素材啦\^o^/!
----------------------------制作分割---------------------------------
一開(kāi)始的計(jì)劃是實(shí)現(xiàn)游戲中的拍照功能就好了,說(shuō)是拍照實(shí)際上就是局部截圖,試過(guò)很多方法沒(méi)有頭緒之后我決定先試著實(shí)現(xiàn)游戲的開(kāi)始界面,畢竟局部截圖的前提是全局截圖,先實(shí)現(xiàn)開(kāi)始界面也許就能懂得怎么實(shí)現(xiàn)拍照功能了。

這也是一個(gè)有趣的界面,如何實(shí)現(xiàn)小電視中有小電視中有小電視呢,可能你也想到了用小地圖的方法,利用rawimage將整個(gè)屏幕投射到小電視上不就好了么?首先動(dòng)手做一個(gè)小電視:
新建一個(gè)RawImage和一個(gè)RenderTexture。
新建一個(gè)用于拍攝的攝像機(jī)。
將RenderTexture拖拽到對(duì)應(yīng)的位置賦值。

但是攝像機(jī)并不會(huì)像我預(yù)想中的那樣將我們?cè)谟螒蛑锌吹降漠?huà)面渲染到小電視上,他渲染的是空無(wú)一物的場(chǎng)景,也就是說(shuō),UI在攝像機(jī)眼里并不是“他看到”的東西,為了讓UI處于攝像機(jī)的視野范圍我將Canvas設(shè)置為“WorldSpace”模式。

但是這樣一來(lái)只能拍到?jīng)]有RenderTexture的畫(huà)面,通過(guò)實(shí)驗(yàn)判斷是因?yàn)殇秩緮z像機(jī)畫(huà)面時(shí)畫(huà)面中的RawImage的貼圖上沒(méi)有任何東西,也就是我們需要多層渲染。

為了讓小電視里面的小電視顯示新的小電視,最簡(jiǎn)單直接的方法就是假嵌套——也就是復(fù)制多份RawImage排列出鏡中鏡的假象,畢竟當(dāng)圖足夠小的時(shí)候,圖里的小電視有沒(méi)有畫(huà)面就看不出來(lái)了,但如果我鼠標(biāo)運(yùn)動(dòng)到小電視上呢?
另一個(gè)辦法則是我從假嵌套的方法中提煉思考得到的,通過(guò)查閱資料得知unity可以利用Texture2D提取指定的圖像像素,那么要如何使圖像不斷地渲染疊加呢?我是這樣實(shí)現(xiàn)的:
?? IEnumerator Show(Rect rect)
??? {
??????? RenderTexture rt = new RenderTexture((int)rect.width, (int)rect.height, 0);
??????? theCamera.targetTexture = rt;
??????? Texture2D screenShot = new Texture2D((int)rect.width, (int)rect.height, TextureFormat.RGB24, false);
??????? while (true)
??????? {
??????????? yield return new WaitForEndOfFrame();
??????????? //theCamera.Render();
??????????? RenderTexture.active = rt;
??????????? screenShot.ReadPixels(new Rect(0,0, Screen.width, Screen.height), 0, 0);
? ??????????screenShot.Apply();
?
??????????? rawImage1.texture = screenShot;
???????????
??????????? //theCamera.targetTexture = null;
??????????? RenderTexture.active = null;
??????? }
??? }
?
在Start方法中啟用這個(gè)協(xié)程,該協(xié)程會(huì)在每次攝像機(jī)渲染完成之后提取相機(jī)的RenderTexture中的像素,并復(fù)制給小電視的Texture,這樣一來(lái)在下一次渲染的時(shí)候小電視上面就是有畫(huà)面的了,疊加之后就形成我們需要的效果。

由于UI的模式是WorldSpace,所以只能用射線檢測(cè)的方法來(lái)確定鼠標(biāo)圖標(biāo)的位置,這樣一來(lái)對(duì)另一個(gè)場(chǎng)景中的功能實(shí)現(xiàn)思路就清晰了很多。
我是這樣拆解我的實(shí)現(xiàn)過(guò)程(思路)的:
首先我需要實(shí)現(xiàn)可以拍到局部圖像的相機(jī),并且這個(gè)相機(jī)不會(huì)拍到用來(lái)表示相機(jī)的黑框。
為了得到照片中拍到的游戲物體信息,我需要將相關(guān)物體都加上碰撞器,并且使相機(jī)黑框在場(chǎng)景中有所屬的碰撞體。
以上所有的功能實(shí)現(xiàn)后再實(shí)現(xiàn)動(dòng)畫(huà)效果以及內(nèi)容判斷邏輯的測(cè)試。
在開(kāi)始制作之前需要搭建場(chǎng)景,除了需要用到的RawImage之外其他的物體都用Sprite2D圖片精靈,并且設(shè)置相應(yīng)的Tag,由于相機(jī)只會(huì)根據(jù)“Canvas”的“Layer”來(lái)過(guò)濾渲染對(duì)象,所以需要兩個(gè)Canvas,分別設(shè)置Layer。
場(chǎng)景中依舊需要兩個(gè)相機(jī),將用于拍攝的相機(jī)的Size調(diào)整為合適大小,CulingMask設(shè)置為屏蔽相機(jī)黑框,使攝像機(jī)既可以拍攝到和我們?cè)O(shè)置的相機(jī)黑框一樣大小的圖像,又不會(huì)將相機(jī)黑框渲染出來(lái)。

相機(jī)的移動(dòng)則是通過(guò)射線檢測(cè)實(shí)現(xiàn)的。
raycast = Physics2D.Raycast(Camera.main.ScreenToWorldPoint(Input.mousePosition), Vector2.zero);
if (raycast.transform != null)
{
??? transform.position = raycast.point;
??? camCollider.transform.position = raycast.point;
??? myCamera.transform.position = new Vector3(raycast.point.x, raycast.point.y, myCamera.transform.position.z);
}
?
新建一個(gè)RenderTexture拖拽到用于攝像的相機(jī)上,并且在腳本中持有他,當(dāng)鼠標(biāo)左鍵按下時(shí)將攝像機(jī)的RenderTexture讀取到對(duì)應(yīng)的Raw Image上,于是我們就可以想拍多少層小電視就拍多少層小電視啦。

之所以要另外新建一個(gè)RenderTexture是因?yàn)樵诖a中控制生成的RenderTexture比較不方便,通過(guò)面板可以更直觀的控制,并且方便像素讀取。

接下來(lái)制作一個(gè)和黑框等大的碰撞體,將游戲物體的組件設(shè)置如下:

腳本上聲明了一個(gè)list數(shù)組,用于存儲(chǔ)在碰撞體中的游戲物體的引用,這樣一來(lái)通過(guò)獲取該腳本上的這個(gè)list數(shù)組就可以進(jìn)行新聞?lì)愋偷呐袛?,通過(guò)不同的Tag返回的數(shù)值來(lái)控制不同的反饋。這里我只寫(xiě)了兩個(gè)最簡(jiǎn)單的情況,并不打算實(shí)現(xiàn)復(fù)雜的邏輯,你可以試著去分析一下。
?? private int NewType(List<GameObject> something)
??? {
??????? int flag = 0;
??????? if (something.Count == 0)
??????? {
??????????? return 0;
??????? }
??????? foreach (GameObject item in something)
??????? {
??????????? if (item.tag == "TV")
??????????? {
??????????????? flag = 1;
??????????? }
??????? }
??????? if (flag != 0)
??????? {
??????????? return flag;
??????? }
??????? //篩選特殊人物返回特定值,剩下就是群眾演員
??????? List<GameObject> gg = new List<GameObject>();
??????? foreach (GameObject item in something)
??????? {
?
??????? }
?
??????? return -1;
??? }
?
最后就是拍攝的動(dòng)畫(huà)效果實(shí)現(xiàn)了,因?yàn)閿z像機(jī)移動(dòng)的目標(biāo)點(diǎn)不固定所以無(wú)法直接用自帶的動(dòng)畫(huà)系統(tǒng)解決,這里添加了一個(gè)Dotween插件,在官方商店可以免費(fèi)下載。

在腳本開(kāi)頭引用該命名空間(DG.Tweening)就可以開(kāi)始手寫(xiě)動(dòng)畫(huà)了,別怕,其實(shí)一點(diǎn)也不復(fù)雜。
由于在動(dòng)畫(huà)期間玩家不能再控制相機(jī)移動(dòng)所以我們需要一個(gè)布爾值來(lái)限制操作,當(dāng)鼠標(biāo)按下時(shí)讀取當(dāng)前攝制的圖像,并且開(kāi)始拍攝協(xié)程,在協(xié)程的開(kāi)始關(guān)閉鼠標(biāo)控制,結(jié)束時(shí)再解開(kāi)。
首先將黑框上攜帶的RawImage顯示出來(lái),并且開(kāi)始一段主攝像機(jī)移向攝像攝像機(jī)并縮小視口的動(dòng)畫(huà)。
rawImage.gameObject.SetActive(true);
rawImage.texture = texture;
mainCamera.DOOrthoSize(myCamera.orthographicSize, 2.0f);
mainCamera.transform.DOMove(myCamera.transform.position, 2.0f);
?
上一段動(dòng)畫(huà)播放結(jié)束后,將主攝像機(jī)的位置切換到小電視的位置,這我手動(dòng)測(cè)量了一下在這個(gè)位置的攝像機(jī)Size,直接賦值。
TVRawImage.texture = texture;
rawImage.gameObject.SetActive(false);
mainCamera.transform.position = new Vector3(TVRawImage.transform.position.x, TVRawImage.transform.position.y, mainCamera.transform.position.z);
mainCamera.orthographicSize = 0.7f;
?
同時(shí)需要根據(jù)圖像內(nèi)容顯示新聞標(biāo)題,標(biāo)題部分的動(dòng)畫(huà)我是通過(guò)自帶的動(dòng)畫(huà)錄制的,錄制方法在我的第一篇文章有提到過(guò):用Unity做一個(gè)萌萌噠游戲(附資源)
停頓一段時(shí)間之后開(kāi)始縮小視圖,這里需要在腳本中記錄攝像機(jī)的初始位置信息,再分兩個(gè)階段動(dòng)畫(huà)恢復(fù)即可。
//縮小視圖
mainCamera.DOOrthoSize(mainCamOrthographicSize / 2, 1.0f);
?
yield return new WaitForSeconds(1.5f);
?
mainCamera.DOOrthoSize(mainCamOrthographicSize, 1.0f);
mainCamera.transform.DOMove(mainCamPoint, 1.0f);
?
這樣一來(lái)就大功告成啦,不知道會(huì)不會(huì)像seed一樣有同學(xué)發(fā)散出更多的內(nèi)容,如果有的話可以和大家分享一下哦。

知乎上可以分析的內(nèi)容始終有限,詳細(xì)內(nèi)容可以下載我的Github項(xiàng)目進(jìn)行了解。
https://github.com/peiyl/WeBecomeWhatWeBeholdByUnity

寫(xiě)下這篇文章的時(shí)候一直在感慨,內(nèi)容看著簡(jiǎn)單,背后其實(shí)經(jīng)歷了無(wú)數(shù)次失敗,在多次嘗試簡(jiǎn)化后才是現(xiàn)在簡(jiǎn)單的幾行代碼,在制作的過(guò)程中幾乎是一步一個(gè)坑,占著地利瘋狂騷擾皮皮關(guān)的各位老師答疑解惑,最終終于將所有功能實(shí)現(xiàn)出來(lái)。
在此感謝皮皮關(guān)的諸位老師,和無(wú)私分享源碼素材的原作者。
Nicky Case的其他游戲:https://ncase.me/

歡迎加入游戲開(kāi)發(fā)群歡樂(lè)攪基:610475807
對(duì)游戲開(kāi)發(fā)感興趣的童鞋可戳這里進(jìn)一步了解:http://www.levelpp.com/