2020年Unity3D應屆生面試題

一、判斷題

1 C#支持繼承多個類,達到重用代碼功能的效果。 (×)

2 修改Renderer的sharedMaterial,所有使用這個材質球的物體都會被改變,並且也改變儲存在工程裏的材質設置。 (√)

3 Unity中可以創建子線程,並在子線程中直接修改UI對象。 (×)

4 Unity不支持在協程中嵌套調用協程。 (×)

5 C#不同命名空間中可以存在相同類名。 (√)

6 Unity會自動爲MonoBehaviour子類的public變量做序列化。 (√)

7 每個枚舉成員均具有相關聯的常數值,可以設置爲負數常數。 (×)

8 只帶有 get 訪問器的屬性稱爲只讀屬性,無法對只讀屬性賦值。 (√)

9 protected成員只能被本類內部訪問,無法被子類直接訪問。 (×)

10 父物體發生Transform變化的時候,子物體跟隨一起變化,但是子物體發生變化的時候,父物體不動。 (√)

二、填空題

1 Unity中 Game 視圖可以設置分辨率,在該視圖中呈現的就是攝像機渲染的畫面。

2 gameObject.AddComponent()的時候,Test腳本的 Awake 函數會立即被調用。

3 任何遊戲對象在創建的時候都會附帶 Transform 組件,用於儲存並操控物體的位置、旋轉和縮放。

4 只在編輯器環境下運行的代碼,可以使用 UNITY_EDITOR 宏把代碼包起來。

5 Unity中可用四元數Quaternion表示 旋轉 ,不受萬向鎖影響,可以進行插值運算。

6 Unity協程中可以使用 yield return null 實現暫緩一幀,在下一幀接着往下處理。

7 transform.forward表示物體的 z 軸的方向。

8 C#中的委託類似於C/C++中的 函數指針 ,委託類型的聲明以 delegate 關鍵字開頭。

9 Unity中的 Plugins 目錄用於放置Native插件文件,Android平臺的jar文件必須放置在 Assets/Plugins/Android/libs 目錄中。

10 在移動平臺,Resources目錄中的資源通過 Resources.Load 接口來加載,如果想實現資源增量更新,則一般考慮把資源打包成 AssetBundle 資源類型。

11 定義對象間的一種一對多依賴關係,使得每當一個對象狀態發生改變時,其相關依賴對象皆得到通知並被自動更新,可以使用 觀察者 設計模式,

12 Unity中每個材質球必須綁定一個 shader (腳本),它決定了該材質的渲染方式以及可配置屬性。

13 Unity中 StreamingAssets 文件夾是隻讀的,裏面的所有文件將會被原封不動地複製制到目標平臺機器上的特定文件夾裏,不會被壓縮。在Android或iOS平臺,通過 WWW 類來讀取其中的文件。

14 當場景中有多個攝像機時,可以設置攝像機的 depth 值,調整相機的渲染順序。

15 爲了加快渲染速度和減少圖像鋸齒,貼圖被處理成由一系列被預先計算和優化過的圖片組成的文件,這樣的貼圖被稱爲 MipMap

三、問答題

1、C#中的委託是什麼

delegate int MyDelegate(int value); //聲明委託類型
C#所有的委託派生自 System.Delegate 類,委託是存有對某個方法的引用的一種引用類型變量,委託變量可以當作另一個方法的參數來進行傳遞,實現事件和回調方法。有點類似C++中的函數指針,但是又有所不同。在C++中,函數指針不是類型安全的,它指向的是內存中的某一個位置,我們無法判斷這個指針實際指向什麼,對於參數和返回類型難以知曉。而C#的委託則完全不同,它是類型安全的,我們可以清晰的知道委託定義的返回類型和參數類型。

延伸
委託和事件:
本質區別:從定義上說,委託被編譯器編譯成一個類,所以它可以像類一樣在任何地方定義,而事件被編譯成一個委託類型的私有字段和兩個公有add 和 remove 方法(有點類似於屬性的定義)不過這兩個方法都有一個參數,這個參數就是委託,所以,它只能定義在一個類裏面。

event MyDelegate myevent; //定義事件
委託相當於一系列函數的抽象類,這一系列函數要求擁有相同的參數和返回值;而事件(event)相當於委託的一個實例,事件是委託類型的成員,委託可以定義在類外面,而事件只能定義在類裏面。
事件使用 發佈-訂閱(publisher-subscriber) 模型。
發佈器(publisher) 是一個包含事件和委託定義的對象。事件和委託之間的聯繫也定義在這個對象中。發佈器(publisher)類的對象調用這個事件,並通知其他的對象。
訂閱器(subscriber) 是一個接受事件並提供事件處理程序的對象。在發佈器(publisher)類中的委託調用訂閱器(subscriber)類中的方法(事件處理程序)。

爲什麼需要事件?
事件最常用的應用場景是圖形用戶界面(GUI),如一個按鈕點擊事件,菜單選擇事件,文件傳輸完成事件等。簡單的說,某件事發生了,你必須要作出響應。你不能預測事件發生的順序。只能等事件發生,再作出相應的動作來處理。觸發事件的類本身對怎樣處理事件不感興趣。按鈕說:“我被點過了”,響應類作出合適的響應。

2、值類型與引用類型的區別

1.值類型存儲在棧(stack)中,引用類型數據存儲在堆(heap)中,內存單元中存放的是堆中存放的地址。
2.值類型存取快,引用類型存取慢。
3.值類型表示實際數據,引用類型表示指向存儲在內存堆中的數據的指針和引用。
4.棧的內存是自動釋放的,堆內存是.NET中會由GC來自動釋放。
5.值類型繼承自System.ValueType,引用類型繼承自System.Object。

延伸
數據結構的堆和棧:
堆和棧都是一種數據項按序排列的數據結構。
棧就像裝數據的桶,具有後進先出性質;堆像一棵倒過來的樹,堆是一種經過排序的樹形數據結構,每個結點都有一個值。堆的存取是隨意,這就如同我們在圖書館的書架上取書,雖然書的擺放是有順序的,但是我們想取任意一本時不必像棧一樣,先取出前面所有的書。

內存結構:
在這裏插入圖片描述
棧中分配局部變量空間;
堆區是向上增長的用於分配程序員申請的內存空間;
靜態區是分配靜態變量、全局變量空間的;
只讀區是分配常量和程序代碼空間的;

3、接口Interface與抽象類abstract class的區別

接口和抽象類是支持抽象定義的兩種機制。
接口是完全抽象的,只能聲明方法,而且只能聲明public的方法,不能聲明private及protected的方法,不能定義方法體,也不能聲明實例變量。抽象類是可以有私有的方法或者私有的變量,如果一個類中有抽象方法,那麼就是抽象類。
一個類可以實現多個接口,但一個類只能繼承一個抽象類。
接口強調特定功能的實現,具有哪些功能,而抽象類強調所屬關係。
儘管接口實現類及抽象類的子類都必須要實現相應的抽象方法,但實現的形式不同。接口中的每一個方法都是抽象方法,都只是聲明的, 沒有方法體,實現類必須都要實現;而抽象類的子類可以有選擇地實現,只實現其中的抽象方法,覆蓋其中已實現了的方法。

延伸
如何弱化代碼依賴關係:
在代碼的控制流中,調用關係和依賴關係幾乎是完全吻合的,如果缺乏良好的封裝與接口提取,那麼調用者必須掌握被調用者的代碼實現。而抽象良好的接口,能夠使控制流對代碼的依賴實現反轉,比如面向同一個接口協議,被調用者需要在協議的約束下對提供的服務進行實現,它的代碼依賴協議的制定,而調用者只用依據協議按需獲取服務即可,在控制流上依賴接口,而不再需要在代碼上依賴被調用者,此即是從接口到被調用者的控制流-代碼依賴關係反轉。
代碼依賴關係弱化,意味着業務可以模塊化、組件化,拆分的功能組團可以以“插件”的方式並行獨立開發維護,這種隔離大大提升開發運維效率,同時獨立部署的能力也更加符合軟硬件發展的趨勢。

4、Unity實現跨平臺的原理

Unity的跨平臺技術是通過一個Mono虛擬機實現的。就是通過Mono將C#腳本代碼編譯成CIL,然後Mono運行時利用JIT或者AOT將CLI編譯成目標平臺的原生代碼實現的。
不過這個虛擬機更新太慢,不能很好地適應衆多的平臺,所以後來推出了IL2CPP,把本來應該再mono的虛擬機上跑的中間代碼轉換成cpp代碼,這樣再把生成的cpp代碼,利用c++的跨平臺特性,在各個平臺上通過對各平臺都有良好優化的native c++編譯器編譯,以獲得更高的效率和更好的兼容性。

延伸
講講IL:
IL是.NET框架中間語言(Intermediate Language)的縮寫。使用.NET框架提供的編譯器可以直接將源程序編譯爲.exe或.dll文件,但此時編譯出來的程序代碼並不是CPU能直接執行的機器代碼,而是一種中間語言IL(Intermediate Language)。
使用中間語言的優點有兩點,一是可以實現平臺無關性,既與特定CPU無關;二是隻要把.NET框架某種語言編譯成IL代碼,就實現.NET框架中語言之間的交互操作(這就是爲什麼unity3D裏面可以c#和js混編)。
在Mac OS上,因爲iOS的現有限制,面向iOS的C#代碼會通過AOT編譯技術直接編譯爲ARM彙編代碼。而在Android上,應用程序會轉換爲IL,啓動時再進行JIT編譯。

講講JIT:
JIT:即時編譯(Just In-Time compile),這是.NET運行可執行程序的基本方式,編譯一個.NET程序時,編譯器將源代碼翻譯成中間語言,它是一組可以有效地轉換爲本機代碼且獨立於CPU的指令。當執行這些指令時,實時(JIT)編譯器將它們轉化爲CPU特定的代碼。部分加密軟件通過掛鉤JIT來進行IL加密,同時又保證程序正常運行。JIT也會將編譯過的代碼進行緩存,而不是每一次都進行編譯。所以說它是靜態編譯和解釋器的結合體。

AOT:靜態編譯,它在程序運行之前就編譯好了。

5、四元數的作用

四元數用於表示旋轉。
其相對於歐拉角的優點:
1.避免萬向鎖。
2.只需要一個4維的四元數就可以執行繞任意過原點的向量的旋轉,方便快捷,在某些實現下比旋轉矩陣效率更高。
3.可以提供平滑插值。

延伸
什麼是歐拉角?
用一句話說,歐拉角就是物體繞座標系三個座標軸(x,y,z軸)的旋轉角度。
1,靜態:即繞世界座標系三個軸的旋轉,由於物體旋轉過程中座標軸保持靜止,所以稱爲靜態。
2,動態:即繞物體座標系三個軸的旋轉,由於物體旋轉過程中座標軸隨着物體做相同的轉動,所以稱爲動態。
物體的任何一種旋轉都可分解爲分別繞三個軸的旋轉,但分解方式不唯一。
unity 3D歐拉角的旋轉順序(父子關係)是y-x-z。
unity中最簡單的萬向鎖就是先讓X軸旋轉90度,z軸旋轉和y軸旋轉效果是一樣。

講講萬向鎖:
萬向鎖(英語:Gimbal lock)是在使用動態歐拉角表示三維物體的旋轉時出現的問題。
萬向節死鎖的根本問題是歐拉角(EulerAngles)保存的信息不足以描述空間中的唯一轉向。

6、Unity腳本生命週期與執行順序

名稱 觸發時機 用途
Awake 腳本實例被創建時調用 用於遊戲對象的初始化,注意Awake的執行早於所有腳本的Start函數
OnEnable 當對象變爲可用或激活狀態時被調用
Start Update函數第一次運行之前調用 用於遊戲對象的初始化
Update 每幀調用一次 用於更新遊戲場景和狀態
FixedUpdate 每個固定物理時間間隔調用一次 用於物理狀態的更新
LateUpdate 每幀調用一次(在update之後調用) 用於更新遊戲場景和狀態,和相機有關的更新一般放在這裏
OnGUI 渲染和處理OnGUI事件
OnDisable 當前對象不可用或非激活狀態時被調用
OnDestroy 當前對象被銷燬時調用

延伸
Awake與Start:
Awake和Start只會調用一次,當遊戲過程中調整腳本的可見狀態時,會分別調用OnEnable, OnDisable函數,而Awake和Start將不會再調用。
Start可能不會被立刻調用,比如我們之前沒有讓其enable,當腳本被enable時,Start纔會被調用。
不同對象之間的Awake順序是不得而知的。
如下,MyBhv腳本在Awake中初始化speed=1f;執行完下面的代碼,speed的值是多少?

var bhv = go.AddComponent<MyBhv>()
bhv.speed = 3f;

答: 3f,因爲Awake會先執行。

Update與FixedUpdate:
同:當MonoBehaviour啓用時,其在每一幀被調用。都是用來更新的。
異:Update()每一幀的時間不固定,受場景渲染的複雜程度,還有輸入的一系列事件等等各種原因影響,遊戲畫面的幀率是在不斷變化的。
FixedUpdate()每幀與每幀之間相差的時間是相對固定的(值爲Time.fixedDeltaTime),默認是0.02s,可以通過Edit->ProjectSettings->Time來設置。物理相關的處理(比如Rigidbody)一般在FixedUpdate()中。

Update與LateUpdate:
LateUpdate是在所有Update函數調用後被調用。可用於調整腳本執行順序。例如當物體在Update裏移動時,跟隨物體的相機可以在LateUpdate裏實現。
有2個不同的腳本同時在Update中控制一個物體,那麼當其中一個腳本改變物體方位、旋轉或者其他參數時,另一個腳本也在改變這些東西,那麼這個物體的方位、旋轉就會出現一定的反覆。如果還有個物體在Update中跟隨這個物體移動、旋轉的話,那跟隨的物體就會出現抖動。 如果是在LateUpdate中跟隨的話就會只跟隨所有Update執行完後的最後位置、旋轉,這樣就防止了抖動。

7、講講你對Unity的協程的理解

協程不是線程。協程的實現原理是迭代器,而迭代器的實現原理是狀態機。
unity中協程執行過程中,通過 yield return XXX,將程序掛起,去執行接下來的內容。在遇到 yield return XXX語句之前,協程方法和一般的方法是相同的,也就是程序在執行到 yield return XXX語句之後,接着纔會執行的是 StartCoroutine()方法之後的程序,走的還是單線程模式,僅僅是將 yield return XXX語句之後的內容暫時掛起,等到特定的時間才執行。
那麼掛起的程序什麼時候才執行?協同程序主要是Update()方法之後,LateUpdate()方法之前調用的。

通過設置MonoBehaviour腳本的enabled對協程是沒有影響的,但如果gameObject.SetActive(false)則已經啓動的協程則完全停止了,即使在InspectorgameObject激活還是沒有繼續執行。也就說協程雖然是在MonoBehvaviour啓動的(StartCoroutine),但是協程函數的地位完全是跟MonoBehaviour是一個層次的,不受MonoBehaviour的狀態影響,但跟MonoBehaviour腳本一樣受gameObject控制,也應該是和MonoBehaviour腳本一樣每幀輪詢yield 的條件是否滿足。

協程不是隻能做一些簡單的延遲,如果只是單純的暫停幾秒然後在執行就完全沒有必要開啓一個協程。
協程的真正作用是分步做一些比較耗時的事情,比如加載遊戲裏的資源。

延伸
講講進程、線程、協程:
進程擁有自己獨立的堆和棧,既不共享堆,亦不共享棧,進程由操作系統調度。
線程擁有自己獨立的棧和共享的堆,共享堆,不共享棧,線程亦由操作系統調度(標準線程是的)。
協程和線程一樣共享堆,不共享棧,協程由程序員在協程的代碼裏顯示調度。

協程和線程的區別是:協程避免了無意義的調度,由此可以提高性能,但也因此,程序員必須自己承擔調度的責任,同時,協程也失去了標準線程使用多CPU的能力。
打個比方吧,假設有一個操作系統,是單核的,系統上沒有其他的程序需要運行,有兩個線程 A 和 B ,A 和 B 在單獨運行時都需要 10 秒來完成自己的任務,而且任務都是運算操作,A B 之間也沒有競爭和共享數據的問題。現在 A B 兩個線程並行,操作系統會不停的在 A B 兩個線程之間切換,達到一種僞並行的效果,假設切換的頻率是每秒一次,切換的成本是 0.1 秒(主要是棧切換),總共需要 20 + 19 * 0.1 = 21.9 秒。如果使用協程的方式,可以先運行協程 A ,A 結束的時候讓位給協程 B ,只發生一次切換,總時間是 20 + 1 * 0.1 = 20.1 秒。如果系統是雙核的,而且線程是標準線程,那麼 A B 兩個線程就可以真並行,總時間只需要 10 秒,而協程的方案仍然需要 20.1 秒。

講講狀態機:
狀態機是有限狀態自動機的簡稱,是現實事物運行規則抽象而成的一個數學模型。一個狀態機至少要包含兩個狀態。例如一個自動門,就有 open 和 closed 兩種狀態,我們通常所說的狀態機是有限狀態機,也就是被描述的事物的狀態的數量是有限個,例如自動門的狀態就是兩個 open 和 closed 。
在這裏插入圖片描述
closed 狀態下,如果讀取開門信號,那麼狀態就會切換爲 open 。open 狀態下如果讀取關門信號,狀態就會切換爲 closed 。
狀態機的四大概念:
1 State ,狀態。一個狀態機至少要包含兩個狀態。例如上面自動門的例子,有 open 和 closed 兩個狀態。
2 Event ,事件。事件就是執行某個操作的觸發條件或者口令。對於自動門,“按下開門按鈕”就是一個事件。
3 Action ,動作。事件發生以後要執行動作。例如事件是“按開門按鈕”,動作是“開門”。編程的時候,一個 Action 一般就對應一個函數。
4 Transition ,變換。也就是從一個狀態變化爲另一個狀態。例如“開門過程”就是一個變換。
總結一下,狀態機不是實際機器設備,而是一個數學模型,通常體現爲一個狀態轉換圖。涉及到的相關概念是 State 狀態,Event 事件,Action 動作,Transition 轉換。狀態機是計算機科學的重要基礎概念之一,也可以說是一種總結歸納問題的思想,應用範圍非常廣泛。

四、場景題

1、現在打出的Android包啓動閃退,應該怎麼定位問題?

使用ADB真機調試,通過日誌定位問題。

延伸
講講ADB:
ADB(Android Debug Bridge)是Android SDK中的一個工具, 使用ADB可以直接操作管理Android模擬器或者真實的Andriod設備。
ADB主要功能有:
1、在Android設備上運行Shell(命令行);
2、管理模擬器或設備的端口映射;
3、在計算機和設備之間上傳/下載文件;
4、將電腦上的本地APK軟件安裝至Android模擬器或設備上;

2、現在要開發一個點擊屏幕開炮發射子彈的功能,說下你的做法?

首先把子彈進行抽象,把屬性和行爲方法提煉出來,比如具有速度、威力、碰撞大小等屬性,具有飛行、碰撞和傷害等行爲。
封裝子彈的抽象類,可以不繼承MonoBehaviour。
監聽屏幕點擊事件,觸發開炮邏輯。子彈通過對象池管理,複用子彈,防止因爲頻繁創建銷燬帶來的性能問題。另外,子彈的座標更新,可以統一由一個彈道控制器的Update遍歷每個子彈對象來計算,而不是每個子彈都掛一個MonoBehaviour去更新,因爲MonoBehaviour的Update是通過反射被調用的,如果有1000顆子彈,就會調用1000次反射,這樣性能上比較差。

延伸
如果現在要做好幾種彈道的子彈,可以繼承子彈基類,拓展出多種子彈子類,子類中各自實現自己的UpdatePosition接口,彈道管理器通過Update遍歷每個子彈調用基類的UpdatePosition接口。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章