这是【游戏开发那些事】第64篇原创
日志系统是游戏开发中非常重要的一环,可以辅助开发团队记录重要信息,查找问题等。一个好的日志系统应该是完善且灵活的,可以根据不同场景打印开发者需要的内容。这篇文章从虚幻引擎日志系统的使用和实现开始分析,并给出一些使用上的经验和建议以及拓展,应该对大家能有所帮助。
UE日志系统概述
UE本身提供了一套强大的日志系统,支持格式化输出,按类型过滤打印,按版本过滤打印
基本使用:
//Logging an FString
UE_LOG(LogTemp, Warning, TEXT("The Actor's name is %s"), *YourActor->GetName());
//Logging a Bool
UE_LOG(LogTemp, Warning, TEXT("The boolean value is %s"), ( bYourBool ? TEXT("true") : TEXT("false") ));
//Logging an Integer
UE_LOG(LogTemp, Warning, TEXT("The integer value is: %d"), YourInteger);
//Logging a Float
UE_LOG(LogTemp, Warning, TEXT("The float value is: %f"), YourFloat);
//Logging an FVector
UE_LOG(LogTemp, Warning, TEXT("The vector value is: %s"), *YourVector.ToString());
//Logging with Multiple Specifiers
UE_LOG(LogTemp, Warning, TEXT("Current values are: vector %s, float %f, and integer %d"), *YourVector.ToString(), YourFloat, YourInteger);
//UE_LOGFMT
FString Name("SomeName");
int32 Value = 999;
UE_LOGFMT(LogTemp, Log, "Printing my Name {0} with Value {1}", Name, Value);
Log的层级:
Fatal的日志会触发断言崩溃,Error在编辑器里面显示红色(通常是比较重要的报错),Warning是警告。
我们可以通过调整log的层级来过滤是否输出,比如默认Verbose的log不会输出到文件,如果使用指令Log LogTemp Verbose。那么Verbose层级以上的所有log都会打印。
//log的层级
//"this is Grey Text"
UE_LOG(YourLog,Log,TEXT("This is grey text!"));
//"this is Yellow Text"
UE_LOG(YourLog,Warning,TEXT("This is yellow text!"));
//"This is Red Text"
UE_LOG(YourLog,Error,TEXT("This is red text!"));
namespace ELogVerbosity
{
enum Type : uint8
{
/** Not used */
NoLogging = 0,
/** Always prints a fatal error to console (and log file) and crashes (even if logging is disabled) */
Fatal,
/**
* Prints an error to console (and log file).
* Commandlets and the editor collect and report errors. Error messages result in commandlet failure.
*/
Error,
/**
* Prints a warning to console (and log file).
* Commandlets and the editor collect and report warnings. Warnings can be treated as an error.
*/
Warning,
/** Prints a message to console (and log file) */
Display,
/** Prints a message to a log file (does not print to console) */
Log,
/**
* Prints a verbose message to a log file (if Verbose logging is enabled for the given category,
* usually used for detailed logging)
*/
Verbose,
/**
* Prints a verbose message to a log file (if VeryVerbose logging is enabled,
* usually used for detailed logging that would otherwise spam output)
*/
VeryVerbose,
All = VeryVerbose,
NumVerbosity,
VerbosityMask = 0xf,
SetColor = 0x40, // not actually a verbosity, used to set the color of an output device
BreakOnLog = 0x80
};
}
Log的过滤
可以通过控制台去改变某一种标签日志的输出
//控制台输入
Log LogTemp Verbose
//也可以在启动命令里面去修改
-LogCmds="global Verbose, LogPython Verbose, LogAnimMontage off, LogDeepDriveAgent
定义自己的log类型
//General Log Header File
DECLARE_LOG_CATEGORY_EXTERN(YourLog, Log, All);
//General Log Cpp File
DEFINE_LOG_CATEGORY(YourLog);
UE Log系统的实现
基本思想:
整个日志系统利用多线程,采用定时写入机制写入文件,每隔0.2s会将缓存写入文件。
逻辑分析:
UE_LOG本身是宏定义,展开之后排除掉一些判断和检查,本质是FMsg::Logf_Internal
#define UE_LOG(CategoryName, Verbosity, Format, ...) \
{ \
static_assert(TIsArrayOrRefOfType<decltype(Format), TCHAR>::Value, "Formatting string must be a TCHAR array."); \
static_assert((ELogVerbosity::Verbosity & ELogVerbosity::VerbosityMask) < ELogVerbosity::NumVerbosity && ELogVerbosity::Verbosity > 0, "Verbosity must be constant and in range."); \
CA_CONSTANT_IF((ELogVerbosity::Verbosity & ELogVerbosity::VerbosityMask) <= ELogVerbosity::COMPILED_IN_MINIMUM_VERBOSITY && (ELogVerbosity::Warning & ELogVerbosity::VerbosityMask) <= FLogCategory##CategoryName::CompileTimeVerbosity) \
{ \
UE_LOG_EXPAND_IS_FATAL(Verbosity, PREPROCESSOR_NOTHING, if (!CategoryName.IsSuppressed(ELogVerbosity::Verbosity))) \
{ \
auto UE_LOG_noinline_lambda = [](const auto& LCategoryName, const auto& LFormat, const auto&... UE_LOG_Args) FORCENOINLINE \
{ \
TRACE_LOG_MESSAGE(LCategoryName, Verbosity, LFormat, UE_LOG_Args...) \
UE_LOG_EXPAND_IS_FATAL(Verbosity, \
{ \
FMsg::Logf_Internal(UE_LOG_SOURCE_FILE(__FILE__), __LINE__, LCategoryName.GetCategoryName(), ELogVerbosity::Verbosity, LFormat, UE_LOG_Args...); \
_DebugBreakAndPromptForRemote(); \
FDebug::ProcessFatalError(); \
}, \
{ \
FMsg::Logf_Internal(nullptr, 0, LCategoryName.GetCategoryName(), ELogVerbosity::Verbosity, LFormat, UE_LOG_Args...); \
} \
) \
}; \
UE_LOG_noinline_lambda(CategoryName, Format, ##__VA_ARGS__); \
UE_LOG_EXPAND_IS_FATAL(Verbosity, CA_ASSUME(false);, PREPROCESSOR_NOTHING) \
} \
} \
}
template <typename FmtType, typename... Types>
static void Logf_Internal(const ANSICHAR* File, int32 Line, const FLogCategoryName& Category, ELogVerbosity::Type Verbosity, const FmtType& Fmt, Types... Args){
static_assert(TAnd<TIsValidVariadicFunctionArg<Types>...>::Value, "Invalid argument(s) passed to FMsg::Logf_Internal");
Logf_InternalImpl(File, Line, Category, Verbosity, Fmt, Args...);
}
void FMsg::Logf_InternalImpl(const ANSICHAR* File, int32 Line, const FLogCategoryName& Category, ELogVerbosity::Type Verbosity, const TCHAR* Fmt, ...)
{
#if !NO_LOGGING
if (Verbosity != ELogVerbosity::Fatal)
{
FOutputDevice* LogOverride = NULL;
switch (Verbosity)
{
case ELogVerbosity::Error:
case ELogVerbosity::Warning:
case ELogVerbosity::Display:
case ELogVerbosity::SetColor:
LogOverride = GWarn;
default:
break;
}
GROWABLE_LOGF(LogOverride ? LogOverride->Log(Category, Verbosity, Buffer)
: GLog->RedirectLog(Category, Verbosity, Buffer))
}
else
{
TCHAR Message[4096];
{
FScopeLock MsgLock(&MsgLogfStaticBufferGuard);
GET_VARARGS(MsgLogfStaticBuffer, UE_ARRAY_COUNT(MsgLogfStaticBuffer), UE_ARRAY_COUNT(MsgLogfStaticBuffer) - 1, Fmt, Fmt);
FCString::Strncpy(Message, MsgLogfStaticBuffer, UE_ARRAY_COUNT(Message) - 1);
Message[UE_ARRAY_COUNT(Message) - 1] = '\0';
}
const int32 NumStackFramesToIgnore = 1;
StaticFailDebug(TEXT("Fatal error:"), File, Line, Message, false, NumStackFramesToIgnore);
}
#endif
}
GLog宏就是从FOutputDeviceRedirector里面拿到,FOutputDeviceRedirector本身也是一个FOutputDevice,里面缓存了所有的Device输出设备。
Device设备分为两种,一种是不需要缓存的LocalUnbufferedDevices,收到数据立刻写到对应的位置,一种是需要缓存的LocalBufferedDevices,需要等待Device内部的机制处理,会遍历要不要在不同的设备上都输出一遍,
#define GLog GetGlobalLogSingleton()
CORE_API FOutputDeviceRedirector* GetGlobalLogSingleton()
{
return FOutputDeviceRedirector::Get();
}
这里是主线程写到buffer里面
异步定时写到磁盘上
F:\engine\Engine\Source\Runtime\Core\Private\Misc\OutputDeviceFile.cpp 设备的构造堆栈如下
Log内容简析:
默认UE的log除了时间戳以外,还有一个【】里面写的是对1000取余的帧数。这个帧数并不是游戏实际的帧数,而是为了显示在当前时间下,这些log是不是同一帧打印的,这样有助于我们分析一些问题。
另外,在帧数后面通常会有【XXX】的标识,表示对应Log的标签,这个标签我们前面提到过,开发者是可以自定义的。
脚本系统日志打印:
如果在项目内接了lua,TS之类的脚本,那么其实脚本系统内的打印还是调用引擎内部的打印逻辑。比如unlua,其实是在luastate初始化之后,就立刻注册了一个UEPrint作为lua脚本层面的打印方法
//控制台输入
lua_register(L, "UEPrint", LogInfo);
static int LogInfo(lua_State* L)
{
const auto Msg = GetMessage(L);
UE_LOG(LogUnLua, Log, TEXT("%s"), *Msg);
return 0;
}
在lua里面就可以使用UEPrint输出日志信息
UEPrint("hello,UE")
如果想修改打印方法,直接脚本内赋值一下就好
local print = UEPrint
print("print !!!")
当然我们也可以在脚本里面直接使用静态方法
UnLua.LogInfo("hello luainfo")
关于日志的处理的一些经验
层级调整战斗服DS的log
通常来说,线上为了保证游戏性能,我们会大幅减少不必要的日志输出。比较稳定的功能都会把log打印过滤掉,在不修改代码的前提下,使用UE自带的层级过滤即可。例如
Log LogTemp Warning
Log LogDisplay Warning
这些指令,我们可以一开始在脚本加载的时候直接写到脚本的逻辑里面。当然,具体要过滤到哪些可能需要程序人肉review一下,有一些重要的信息还是要保留的,比如结算信息等
客户端外放Log调整
对于客户端来说,基本上不需要调整。因为玩家拿到的包都是shipping版本的,log基本上只有少了的error信息,或者直接干掉了
此外,为了避免日志给外挂带来一些辅助的信息(比如说通过字符串定位源码),我们可以完全不产生任何log,或者在shipping包的编译层就直接过滤掉大部分log。
类似如下的逻辑,
#if UE_GAME && UE_BUILD_SHIPPING
#define UE_LOG(CategoryName, Verbosity, Format, ...) \
IF_COMPILETIME_COND(CategoryName, Verbosity) \
{ \
static_assert(TIsArrayOrRefOfType<decltype(Format), TCHAR>::Value, "Formatting string must be a TCHAR array."); \
constexpr bool bValid = IS_VALID_LOGSHIPPING_CATEGORY(CategoryName);\
if constexpr(bValid)\
{ \
UE_LOG_EX(CategoryName, Verbosity, CRP_DECRYPTTCHAR(INNERSTR), ##__VA_ARGS__); \
} \
}
#else
#define UE_LOG(CategoryName, Verbosity, Format, ...) \
IF_COMPILETIME_COND(CategoryName, Verbosity) \
{ \
static_assert(TIsArrayOrRefOfType<decltype(Format), TCHAR>::Value, "Formatting string must be a TCHAR array."); \
UE_LOG_EX(CategoryName, Verbosity, Format, ##__VA_ARGS__); \
}
#endif
日志加密
如果确认要保留客户端的日志内容,那么肯定要对日志进行加密处理,不能让玩家拿到明文的日志信息(主要还是怕被外挂利用)。一旦做了日志加密,我们就还需要做一套工具用来解密log,使开发人员能更便捷地拿到真正的日志内容。
日志上传
在开发阶段,每个测试的程序或者QA都会输出大量的日志信息,一旦发生崩溃或者各种问题都需要尽快地拿到log去给程序去查。完善一点的团队可能都会有一个类似dump平台的网页,我们需要及时的把日志传到该系统里面,以防后面本地文件被删除而无法拿到日志内容。日志的上传功能一般都需要团队自行开发。
参考:
https://zhuanlan.zhihu.com/p/646440642
https://unrealcommunity.wiki/logging-lgpidy6i#logging-using-ue_log
往期文章推荐
游戏开发技术系列【想做游戏开发,我应该会点啥?】
虚幻引擎技术系列【使用虚幻引擎4年,我想再谈谈他的网络架构】
C++面试系列【史上最全的C++/游戏开发面试经验总结】
我是Jerish,网易游戏工程师,6年从业经验。该公众号会定期输出技术干货和游戏科普的文章,关注我回复关键字可以获取游戏开发、操作系统、面试、C++、游戏设计等相关书籍和参考资料。