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 GameplayAbilitySystem - GameplayEffect & GameplayCue 如何設定參數