UE4 除錯技巧分享 (二)


上一篇介紹了兩個除錯小技巧

這一篇會比較著重在中斷點的部份。

技巧分享

這篇的複雜度稍高,而且技巧互相有關連,建議按照順序看。

環境

先稍微介紹一下本篇的環境。

開發平台的部分是Windows + Visual Studio 2017;
引擎是UE4.23。

照理說引擎版本沒什麼差,不過為了保險還是提一下。

從BP中斷點找出完整的Call Stack


在使用編輯器的時候,我們常常會使用BP中斷點來追查執行順序,如同C++的中斷點一樣。
但是如果這個事件是從C++來的話,在BlueprintDebugger是無法得知的,如下圖所示。



其實我們是有辦法知道的,在BP中斷點停住的狀態下,回去Visual Studio,假設UE4 process已經是attach的狀態,按Debug->BreakAll (如圖),這時候就可以看到Call Stack了。




通常CallStack分頁會看到一大串,好像很嚇人,這時候不要慌不要著急,看到Slate那種的都略過一直往下捲。慢慢的看你應該會找到認識的函式,如下圖所示。



以這張圖為例子,就可以知道BP的Tick是從C++的Actor::Tick內呼叫ReceiveTick來的。

有時候你會看到CallStack極短,長的像這樣:


這是因為斷點剛好停在別的Thread,這時候只要去Visual Studio的Debug->Windows->Threads將Thread分頁叫出,然後跳到MainThread就可以了。

從C++中斷點找出BP呼叫的來源

前一個技巧是BP斷點想知道從哪個C++呼叫進來的。這個技巧是要說明如何從C++斷點找出是哪個BP呼叫來的。這個技巧特別重要,因為就算是Packaged Game也可以抓出BP來源。

假設我現在中斷點停在C++的程式碼,有兩種方法可以試

方法1. 

到Visual Studio的Watch視窗內輸入
{,,UE4Editor-Core}::PrintScriptCallstack()
或是
::PrintScriptCallstack()

Debug Editor的話則是輸入
{,,UE4Editor-Core-Win64-Debug}::PrintScriptCallstack() 

然後去Visual Studio的Output視窗就會看到印出來的BP Call Stack。

印象中前者是在Editor的時候用;
後者則是在Packaged Game用。


方法2.


或是在Watch視窗內分別輸入以下兩行

Stack.Node->OuterPrivate
Stack.Node->NamePrivate

然後去CallStack內找到UFunction::Invoke的函式,點兩下跳過去。
在Watch視窗就會看到OuterPrivate顯示哪一個Actor,
NamePrivate顯示哪一個函式,如下圖所示:



有時候NamePrivate會顯示"ExecuteUbergraph_xxx",這個意思是他是從BP的Event Graph來的。

屆時只好回去那個BP的EventGraph找呼叫點,反正C++的函式名稱已知,到該BP搜尋函式名稱就會有答案。


接下來要介紹比較少用但是很強大的兩種中斷點。

使用Condition Breakpoint關注特定對象事件

首先先做情境的假設,假設場景上某個Actor class的實體很多,但是問題卻只出在某一個特定的Actor,這個class有兩個函式A與B。

A函式內會有條件的呼叫B,而這個特定的Actor看起來就是條件沒過,而且條件很複雜難以直接看出沒過的原因。

這時候該如何找出問題?

如果在C++的A函式內下斷點,每個Actor呼叫到A的時候都會停下來。
如果Actor很多或是A函式呼叫頻繁,是沒有辦法篩選出壞掉對象的。

我的答案是,用編輯器內Blueprint的Debug Filter + condition breakpoint

步驟如下:

1. 先打開有問題的Actor class BP,假設是BP_CodeTraceActor。
2. 找出知道會壞掉的Actor,假設是BP_CodeTraceActorFoo。
3. 在BP編輯器內把Debug Filter選擇為BP_CodeTraceActorFoo。
4. 在BeginPlay或是Tick下BP的中斷點。如下圖

Use debug filter and place BP breakpoint.

5. 執行遊戲。
6. 搭配前面的例子,在進入中斷點的狀態,找出actor的記憶體位置並複製。

Use this to fetch particular actor address.

7. 到函式A下condition breakpoint,設定為this = [記憶體位置]。

Example of placing condition breakpoint.

8. 恢復執行,並等待A函式呼叫的時機觸發。
9. 進入中斷點的時候就是我們要的時機。


這個方法利用BP提供的Debug Filter加上Condition breakpoint,可以協助我們有效率地找到問題發生的時機。

使用Data Breakpoint找出變數修改的時機


在某些少數的情況下,我們會找不到某個變數被修改的時機。
這時候就只好用Data Breakpoint來解決這個問題了。

這邊先提我遇過哪些情況:

1. 從Memory copy連續的記憶體位置,值就可能受到變動。
2. Setter的地方太多,無法每個地方都下中斷點。
3. 從Level Sequence直接設值是無法被搜尋到的。

使用Data Breakpoint的條件

使用這個方法有一個重要的條件,變數要定義在C++。
因為BP創建的變數被封裝成連續的記憶體,所以很難找到方法找到BP變數的記憶體位置。
所以如果遇到這類的問題,建議先把變數搬進C++。
或是一開始就把變數定義在C++。

方法介紹


1. 一開始我們可以使用前一個技巧,先找出目標對象。
2. 在Watch視窗叫出變數,在變數前面加上&來獲得變數的記憶體位置。
如圖所示。

3. 到Breakpoints分頁,選擇New->Data Breakpoint...


4. 將記憶體位置填入Address欄位,選擇OK。
5. 繼續執行並想辦法觸發變數變更。
6. 斷點停下來的時候使用前面的技巧找出呼叫的來源。


在執行期間改變變數值做測試

在Visual Studio的Watch視窗是可以允許執行期間改值的。
有的時候包一個版本需要比較多的時間,這時候就可以透過Watch在執行期間改值。
先模擬如果值是正確的,後續還有沒有其他問題。
藉此減少Iteration的次數。

舉例來說,某個函式A裡面有某個integer變數值應該要是5,結果因為值是6造成問題。
這時候我們回去程式碼修改,確保值不會是6。然後重包一版測試。
結果一測發現後面有另外一個值也是錯的,這樣一來一回就要包兩個版本以上才能解決問題。

如果我們直接在Watch就把值改成6,然後繼續執行,很快就會知道後面還有一個地方要修正。
這時候一併修正就可以省掉一次包版的時間。

結論

除錯技巧分享的部份差不多就到這邊。
我想我會的項目都在這兩篇裡面了,提供給大家參考。


留言

這個網誌中的熱門文章

UE4 除錯技巧分享 (一)

UE4 GameplayAbilityTask介紹

UE4 開發常用外部工具介紹