游戏开发中日志系统的方方面面(UE引擎向)

这是【游戏开发那些事】第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都会打印。

20b67429f300d922d1a3b9b0f3a3a7ee.png

//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的过滤

可以通过控制台去改变某一种标签日志的输出

a1a5f4a8f99c1059bbe0c0a381e8e740.png

//控制台输入
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会将缓存写入文件。

d6784ea61fad5d2568834ab192fdacba.png

逻辑分析:

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里面

1e602131c922416d213a9be87f0a7439.png

异步定时写到磁盘上

b0421684114274ba2a9222eaf859698a.pngF:\engine\Engine\Source\Runtime\Core\Private\Misc\OutputDeviceFile.cpp 设备的构造堆栈如下

8d1b65dbaf8868f21e46e07a177831f4.png

Log内容简析:

默认UE的log除了时间戳以外,还有一个【】里面写的是对1000取余的帧数。这个帧数并不是游戏实际的帧数,而是为了显示在当前时间下,这些log是不是同一帧打印的,这样有助于我们分析一些问题。

另外,在帧数后面通常会有【XXX】的标识,表示对应Log的标签,这个标签我们前面提到过,开发者是可以自定义的。

2dc814185381210fb8cbbe543e900bb1.pngd7f3d59e2ba429d471194cb85e7820a6.png

脚本系统日志打印:

如果在项目内接了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

 往期文章推荐 

15f0323cd3b2d81b9866697c042ad540.png

游戏开发技术系列【想做游戏开发,我应该会点啥?】

939a08bb5f1796335a3a41692680870a.png

虚幻引擎技术系列【使用虚幻引擎4年,我想再谈谈他的网络架构】

f317df5e0408bfc5e502e86b59a6f385.jpeg

C++面试系列【史上最全的C++/游戏开发面试经验总结】

我是Jerish,网易游戏工程师,6年从业经验。该公众号会定期输出技术干货和游戏科普的文章,关注我回复关键字可以获取游戏开发、操作系统、面试、C++、游戏设计等相关书籍和参考资料。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值