引擎之旅 Chapter.4 日志系统
关于近段时间为何没有更新的解释:Find a new job.
引言
一般来说,一个优质的商业级别的游戏本质上就是一个复杂庞大的软件系统。在庞大系统的开发过程中难免会出现错误。为了排查错误、校验代码的正确性,游戏引擎一般会提供一些调试和开发工具,一般有如下几个:
- 日志及代码追踪:日志系统一般提供向控制台等页面打印字符串的功能;在打印中也能够清晰的显示调用的堆栈信息,以便于定位代码错误的位置。
- 调试绘图功能:引擎会提供在游戏场景中绘制辅助线的功能,这些辅助线能清晰的表示范围、方向等信息以供游戏开发者进行调试。
- 内置菜单:游戏编辑器的一些全局设置,通过不同的设置,方便游戏开发者对特定渲染、逻辑等进行调试。
- 内置控制台:对于游戏引擎来说,项目为非控制台程序,因此我们无法用简单的使用printf方法将日志输出至控制台。内置控制台就是游戏编辑器中收集和显示日志的窗体。
- 性能剖析与统计:方便游戏开发者定位性能瓶颈(一个重要的模块)
当然,仅仅这一章节无法去完成对这些调试工具的阐述。本文中的日志系统主要实现了日志及代码堆栈信息的输出功能(上述的第一点),其他部分的内容后续在将其慢慢的完善。本章中的日志系统主要实现一下几点功能:
- 日志语句可分类,且不同的分类有相关颜色的提示。
- 日志可打印到控制台窗体、Vistual Studio输出框。
- 日志可存储至特定的日志文件中。
- 日志语句可展示相关的堆栈信息。
显示效果如下:
- 不显示相关堆栈信息
- 显示相关堆栈信息
日志语句的分类
将日志语句分类可以让开发者打印不同重要性的Log。比如Unity编辑器中的Console将日志语句分为了:Log、Warn、Error三个部分。在TurboEngine的设计中,我将日志分类写为一个枚举类,并将不同的类型在二进制不同的位中岔开,方便筛选。
//日志语句重要性等级
enum LogImportantLevel : int
{
CodeTrace = 0b00001, //最低级,用于记录代码执行轨迹(白)
Info = 0b00010, //常规,显示日志消息(绿)
Warn = 0b00100, //较高级,用于日志警告信息(警告)
Error = 0b01000, //高级,用于日志错误信息(错误)
Critiacal = 0b10000, //最高级,用于关键日志信息(关键信息)
};
控制台窗体 和 VSOutput Tab的日志打印
这一部分很简单。将日志打印到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
引擎之旅 Chapter.4 日志系统的更多相关文章
- 引擎之旅 Chapter.1 高分辨率时钟
目录 游戏中的时间线 真实时间线 游戏时间线 全局时钟的实现方式 我们如何理解时间.在现实生活中,时间就是一个有方向的直线.从一个无穷远到另一个无穷远.用数学去抽象地思考,它就是一个从无穷小到无穷大的 ...
- 引擎之旅 Chapter.2 线程库
预备知识可参考我整理的博客 Windows编程之线程:https://www.cnblogs.com/ZhuSenlin/p/16662075.html Windows编程之线程同步:https:// ...
- Android日志系统Logcat源代码简要分析
文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6606957 在前面两篇文章Android日志系 ...
- Android应用程序框架层和系统运行库层日志系统源代码分析
文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6598703 在开发Android应用程序时,少 ...
- Android日志系统驱动程序Logger源代码分析
文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6595744 我们知道,在Android系统中, ...
- Nginx之旅系列 - Nginx日志功能 PK Linux内核printk
题记:Nginx之旅系列是用来记录Nginx从使用到源码学习的点点滴滴,分享学习Nginx的快乐 Nginx 首页: http://nginx.org/ Nginx日志功能 PK Linux内核pri ...
- MySQL日志系统
body { font-family: Helvetica, arial, sans-serif; font-size: 14px; line-height: 1.6; padding-top: 10 ...
- MySQL 笔记整理(2) --日志系统,一条SQL查询语句如何执行
笔记记录自林晓斌(丁奇)老师的<MySQL实战45讲> 2) --日志系统,一条SQL查询语句如何执行 MySQL可以恢复到半个月内任意一秒的状态,它的实现和日志系统有关.上一篇中记录了一 ...
- ELK统一日志系统的应用
收集和分析日志是应用开发中至关重要的一环,互联网大规模.分布式的特性决定了日志的源头越来越分散, 产生的速度越来越快,传统的手段和工具显得日益力不从心.在规模化场景下,grep.awk 无法快速发挥作 ...
随机推荐
- Taurus.MVC 如何升级并运行在NET6、NET7
前言: 之前计划帮某公司架构一个从WPF转向Web的低代码的开发平台,并构思为Taurus.MVC 新增微服务的基础功能模块,提供便捷的微服务开发方式,因中途合作中止,代码开发部分后续再上. 最近看到 ...
- SP8496 NOSQ - No Squares Numbers 题解
To SP8496 这道题可以用到前缀和思想,先预处理出所有的结果,然后 \(O(1)\) 查询即可. 注意: 是不能被 \(x^2(x≠1)\) 的数整除的数叫做无平方数. \(d\) 可以为 \( ...
- input函数的高级使用
经典的a+b问题终于重出江湖了 a=input('a = ') b=input('b = ') print(a+b)//error,因为此时ab是字符串类型,其加号起到的是连接的作用 所以这就是类型转 ...
- CF455ABoredom
题目大意: 给你一个由 \(n\) 个整数构成的序列 \(a\),玩家可以进行几个步骤,每一步他可以选择序列中的一个元素(我们把它的值定义为 \(a_k\))并删除它,此时值等于 \(a_{k + 1 ...
- super与this关键字图解和java继承的三个特点
java继承的三个特点 java语言是单继承的 一个类的直接父类只能有一个 class A{} class B extends A{}//正确 class C{} class D extends A, ...
- Bert不完全手册7. 为Bert注入知识的力量 Baidu-ERNIE & THU-ERNIE & KBert
借着ACL2022一篇知识增强Tutorial的东风,我们来聊聊如何在预训练模型中融入知识.Tutorial分别针对NLU和NLG方向对一些经典方案进行了分类汇总,感兴趣的可以去细看下.这一章我们只针 ...
- Unhandled Exception: MissingPluginException(No implementation found for method launch on channel)
在添加依赖包时,可能会出现Unhandled Exception: MissingPluginException(No implementation found for method launch o ...
- HDU 5362 Just A String 指数型母函数
题面 Description 用m种字母构造一个长度为n的字符串,如果一个字符串的字母重组后可以形成一个回文串则该串合法,问随机构成的长为n的字符串的合法子串数目期望值. Input 第一行一整数T表 ...
- 【Java】idea同时运行多个一样的类
点击"Edit Configurations..." 在左侧选中需要重复运行的类 单击"Modify options" 选择"Allow multip ...
- 通过宏封装实现std::format编译期检查参数数量是否一致
背景 std::format在传参数量少于格式串所需参数数量时,会抛出异常.而在大部分的应用场景下,参数数量不一致提供编译报错更加合适,可以促进我们更早发现问题并进行改正. 最终效果 // 测试输出接 ...