關於近段時間為何沒有更新的解釋:Find a new job.
一般來說,一個優質的商業級別的遊戲本質上就是一個複雜龐大的軟體系統。在龐大系統的開發過程中難免會出現錯誤。為了排查錯誤、校驗程式碼的正確性,遊戲引擎一般會提供一些偵錯和開發工具,一般有如下幾個:
當然,僅僅這一章節無法去完成對這些偵錯工具的闡述。本文中的紀錄檔系統主要實現了紀錄檔及程式碼堆疊資訊的輸出功能(上述的第一點),其他部分的內容後續在將其慢慢的完善。本章中的紀錄檔系統主要實現一下幾點功能:
顯示效果如下:
將紀錄檔語句分類可以讓開發者列印不同重要性的Log。比如Unity編輯器中的Console將紀錄檔語句分為了:Log、Warn、Error三個部分。在TurboEngine的設計中,我將紀錄檔分類寫為一個列舉類,並將不同的型別在二進位制不同的位中岔開,方便篩選。
//紀錄檔語句重要性等級
enum LogImportantLevel : int
{
CodeTrace = 0b00001, //最低階,用於記錄程式碼執行軌跡(白)
Info = 0b00010, //常規,顯示紀錄檔訊息(綠)
Warn = 0b00100, //較高階,用於紀錄檔警告資訊(警告)
Error = 0b01000, //高階,用於紀錄檔錯誤資訊(錯誤)
Critiacal = 0b10000, //最高階,用於關鍵紀錄檔資訊(關鍵資訊)
};
這一部分很簡單。將紀錄檔列印到Console和VS Output主要使用以下兩個函數
//to Console
printf(const char* format,...);
//to VS Output
OutputDebugStringA(const char* lpOutputString);
我一般喜歡將特定的功能封裝在自己的函數中,一方面可以作為將函數用自己的命名形式統一命名方便呼叫。另一方面,我們需要對原生函數進行功能上的拓展。OutputDebugStringA 是一個列印字串的函數,我們要將其封裝為OutputDebugStringA(const char* format,...)的形式。
//In TEString.h
//VS函數,將字串列印到Visual Studio 輸出臺(分寬字元和常規字元版本)
//--------------------------------------------------------------------------------------------------
inline void TVSOutputDebugString(PCWSTR format, ...)
{
char* pArgs = (char*)format + sizeof(format);
_vstprintf_s(TurboCore::GetCommonStrBufferW(), TurboCore::CommonStringBufferSize, format, pArgs);
::OutputDebugString(TurboCore::GetCommonStrBufferW());
}
inline void TVSOutputDebugString(PCSTR format, ...)
{
char* pArgs = (char*)format + sizeof(format);
vsnprintf(TurboCore::GetCommonStrBuffer(), TurboCore::CommonStringBufferSize, format, pArgs);
::OutputDebugStringA(TurboCore::GetCommonStrBuffer());
}
//對printf()函數的重新命名
//--------------------------------------------------------------------------------------------------
inline void TConsoleDebugString(PCSTR format, ...)
{
char* pArgs = (char*)format + sizeof(format);
printf(format, pArgs);
}
vsnprintf(char* buffer,size_t bufferSize,const char* format,...) :用於將變數格式化為字串。
在Chapter3的檔案系統中,我們利用了C語言的檔案流函數封裝了檔案的讀寫功能。在紀錄檔中,我們要利用這一個封裝類將紀錄檔寫入檔案中。
相關連結:引擎之旅 Chapter.3 檔案系統
實現的思路如下:
class TURBO_CORE_API TLogger
{
//紀錄檔模式:
enum class LogFileMode
{
DiskFile, //紀錄檔將儲存在磁碟中
TempFile //紀錄檔將以臨時檔案的形式儲存(不常用)
};
TLogger(PCSTR loggerName, LoggerBuffer::BufferSize bufferSize = LOGGER_BUFFER_DEFAULT_SIZE, int logLevelFilter = 0b11111);
TLogger(PCSTR loggerName,PCSTR logFileSavePath,LoggerBuffer::BufferSize bufferSizeLOGGER_BUFFER_DEFAULT_SIZE,int logLevelFilter = 0b11111);
//注:紀錄檔檔案不應該支援拷貝函數
TLogger(const TLogger& clone) = delete;
~TLogger();
//輸入紀錄檔到各個平臺:(Console、VSOutputTab、檔案流)
inline void InputLogToAll(PCSTR str);
inline void InputLogToAll(CHAR c);
}
//Implement
TurboEngine::Core::TLogger::TLogger(PCSTR loggerName, PCSTR logFileSavePath, LoggerBuffer::BufferSize bufferSize, int logLevelFilter)
:m_LogFileMode(LogFileMode::DiskFile),
m_LogBuffer(bufferSize),
m_LogLevelFilter(logLevelFilter),
m_IsShowCallstack(true)
{
CHAR dirPath[MAX_PATH_LEN];
//從檔案路徑中獲取檔案所在的資料夾
TAssert(TPath::GetDirectoryName(dirPath, MAX_PATH_LEN, logFileSavePath));
//判斷資料夾目錄是否存在,若不存在則建立
if (!TDirectory::Exists(dirPath))
TDirectory::CreateDir(dirPath);
//開啟檔案流
TAssert(m_LogFile.Open(logFileSavePath, TFile::FileMode::Text, TFile::FileAccess::ReadWrite_CreateAndClean));
//記錄紀錄檔的名稱和檔案路徑
TStrCpy(m_LoggerName, LOGGER_NAME_MAX_LENGTH, loggerName);
TStrCpy(m_LoggerPath, MAX_PATH_LEN, logFileSavePath);
}
//輸入字串
inline void TurboEngine::Core::TLogger::InputLogToAll(PCSTR str)
{
m_LogFile.PutStringtLine(str);
TConsoleDebugString(str);
TVSOutputDebugString(str);
}
//輸入字元
inline void TurboEngine::Core::TLogger::InputLogToAll(CHAR c)
{
m_LogFile.PutChar(c);
TConsoleDebugString(&c);
TVSOutputDebugString(&c);
}
我覺得這是一個可以單獨作為一個章節進行闡述,但是紀錄檔系統確實也涉及了這一部分的功能,因此,我把也把它寫入到本章節中。堆疊資訊在遊戲或遊戲引擎開發是一個十分重要的資訊,這個資訊可以清晰的展現了當前你列印的這一部分的具體函數呼叫路徑。
關於如何獲取到堆疊資訊,之後有時間我可以另起一章對這一部分內容進行分析。基本的類結構如下所示:
class TURBO_CORE_API TStackWalker
{
public:
TStackWalker();
TStackWalker(DWORD threadId);
TStackWalker(DWORD threadId, PCSTR symPath);
~TStackWalker();
}
public:
inline bool IsInitialized();
//獲取堆疊呼叫入口陣列
bool GetStackFrameEntryAddressAddrArray(DWORD64 outFrameEntryAddress[STACK_MAX_RECORD]);
//獲取堆疊資訊字串
void GetCallstackFramesString(PSTR output, size_t outputBufLen, int getNum, int offset);
//列印堆疊呼叫資訊
void PrintCallstackFramesLog(DWORD64 frames[STACK_MAX_RECORD]);
//列印單個棧幀資訊
void PrintSingleCallbackFrameMessage(const CallstackEntry& entry, bool bShowInCosole = false);
protected:
static BOOL _stdcall MyReadProcMem(HANDLE hProcess, DWORD64 qwBaseAddress, PVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesRead);
//初始化入口
void Init();
//獲取和初始化符號
bool InitSymbols();
//載入所以模組
bool LoadModules();
//初始化單個路徑的符號
bool InitSymbol(PCSTR symPath);
//載入單個模組
DWORD LoadModule(HANDLE hProcess, LPCSTR img, LPCSTR mod, DWORD64 baseAddr, DWORD size);
關於如何實現,具體可去網上搜尋鍵碼 StackWalker