虚幻引擎 logging 辅助宏

Danta1ion
Danta1ion
发布于 2024-06-20 / 83 阅读
0
0

虚幻引擎 logging 辅助宏

虚幻引擎 logging 辅助宏 1.0

虚幻引擎 logging 辅助宏 - 知乎 (zhihu.com)

虚幻引擎 logging 辅助宏 2.0

1.0版本的辅助宏实现了对 UE_LOGcheck, verify, ensure 的封装。在升级到 UE 5.4 之后遇到了几个问题。

  1. 与引擎源码 ContactModifications.h 中的 EnsureValid 冲突。虽然可以通过调整头文件顺序解决,但由于 UE 开启 unity build,头文件的引入顺序可能会被编译器调整,导致命名空间污染;
  2. 作为让人偷懒的工具,还不够懒。对于一些常见情况,可以有更好的优化。
  3. 缺少对 PrintString 的支持。
  4. UE 5.4 更新后需要一种 Not Implemented 的运行时提示方法。

基于以上几点,2.0版本引入了几个新的特性。

提示未实现方法的 MyNotImpl

现在你可以使用 IDE 一件补全函数实现,并在函数内写上:

void AMyActor::BeginPlay()
{
    MyNotImpl();
}

它相当于 PrintStringLogError 的结合,在 Editor 内可以直接看到错误。

精简 log 和 assert 家族的命名

  • MyCheck
  • MyVerify
  • MyEnsure
  • MyLog

这些涵盖了 1.0 版本的全部内容。重点在 assert 家族中,通过内部函数判断是否有 format 的情况,实现一个函数签名同时支持所有情况:

MyCheck(this);                                 // `this` is false or null.
MyCheck(this, "log");                          // log
MyCheck(this, "log: [%s]", *this->GetName());  // log: [BP_MyActor_0]

对 log 家族也进行了精简,现在可以用这样的方式输入 log:

MyLog(Error, "format [%s]", *this->GetName());

内部实现会使用字符串进行拼接。

由于新的实现当中,对一些明显更常用的 log verbosity 添加了直接的入口,如 MyErr (MyLogError),所以虽然这样需要多一步写出 verbosity,但总体速度是变快了的。

MyEnsure 添加了 PrintString 的功能

ensureMsgf 是一个非常常用的 assert 宏,由于它可以在进行判断的同时直接返回值,可以在 if 语句内直接写,同时由不会导致 编辑器 crash。这样的特性让它很受欢迎。因此我为他加上了 PrintString ,可以直接在编辑器内看到红色的报错,更好判断。

最佳实践 Best Practice

首先判断这个变量是否会导致编辑器或者引擎本身 crash,这决定了用 check 家族还是 ensure 家族来进行断言。

比如 AssetManagerInputComponent 等。或者 GetWorld() 这种基本不可能错的东西。

如果是,在通常情况下直接用 check 能解决所有问题:

UWorld* World = GetWorld();
MyCheck(World);

在 Lyra 中有用到 UE_LOG(Fatal) 它和 Check 的作用是等价的。都是生成一条 log 然后让引擎 crash。

区别是,Check 的前提是在这个变量的作用域当中,UE_LOG(Fatal) 可以无视这个条件。

UTransAssetManager& UTransAssetManager::Get()  
{  
    MyCheck(GEngine);  
  
    if (UTransAssetManager* Singleton = Cast<UTransAssetManager>(GEngine->AssetManager))  
    {  
        return *Singleton;  
    } 
    
    MyLog(Fatal, "Invalid AssetManagerClassName in DefaultEngine.ini.  It must be set to UTransAssetManager!");  
  
    // Fatal error above prevents this from being called. This statement only for compiling.  
    return *NewObject<UTransAssetManager>();  
}

小结,能用 MyCheck 就用它。

接下来是不会让引擎 crash 的游戏内运行时断言。

对于不会形成局部变量的情况,比如用 if 检查一个已经存在的 bool 或者 UObject,可以使用 MyEnsureif 行内就地判断:

if (MyEnsure(bIsCurrentFrame))
{
    // ...
}

而当需要存储一个本地的对象状态时(类似 C# 的 using 或者 Python 的 with 用法),则最好使用 Log 家族来处理:

if (ACharacter* Character = GetOwner())
{

}
else
{
    MyErr("Character from current object : [%s] is invalid.", *this->GetName());
}

首先是 Ensure 在这种 if 行内不太合适,再加上,这种调用通常是有 context 含义的,使用 MyErr 显式获取父级对象的调用点,可以更方便 debug。

最后是提前返回 (early out) 的情况:

APawn* Pawn = GetPawn<APawn>();
if (!Pawn)
{
    MyErr("Pawn not exist on actor: [%s]", *this->GetName());
    return;
}

通过 logging 库,让你爱上写 log!

常规修复和改进

  1. 更改了命名,所有内部函数都加上了 _ 前缀,用以区分;
  2. 加了更完善的文档和注释;
  3. 用户最关心的配置部分放在了最前面;
  4. 可以调用的函数接口集中放在最后。

Source Code Archive

1.0

#pragma once  
#include "Logging/LogMacros.h"
///////////////////////////// Log Helpers //////////////////////////////////////

// Default category used in `UE_LOG`.
#define DEFAULT_LOG_CATEGORY LogTemp

// e.g. AActor::BeginPlay() => <Your Message> ["\Path\To\Actor.cpp:299"]
#define DEFAULT_LOG_MESSAGE(RawMessage) TEXT("%hs() => ") TEXT(RawMessage) TEXT(" [\"%hs:%d\"]")

// __FUNCTION__, Type: %hs, e.g.: AActor::BeginPlay
// __FILE__,     Type: %hs, e.g.: \Path\To\Actor.cpp
// __LINE__,     Type: %d,  e.g.: 299
// __VA_OPT__(,) (since cpp20): if `...` is empty, leave it empty as well, otherwise add a `,` to separate nearby tokens.
#define DEFAULT_LOG_ARGUMENTS(...) __FUNCTION__ __VA_OPT__(,) __VA_ARGS__, __FILE__, __LINE__

// Unreal Assertions
#define LogCheck(Condition, Format, ...)	checkf(Condition,	DEFAULT_LOG_MESSAGE(Format), DEFAULT_LOG_ARGUMENTS(__VA_ARGS__))
#define LogCheckSlow(Condition, Format, ...)	checkfSlow(Condition,	DEFAULT_LOG_MESSAGE(Format), DEFAULT_LOG_ARGUMENTS(__VA_ARGS__))
#define LogVerify(Condition, Format, ...)	verifyf(Condition,	DEFAULT_LOG_MESSAGE(Format), DEFAULT_LOG_ARGUMENTS(__VA_ARGS__))
#define LogEnsure(Condition, Format, ...)	ensureMsgf(Condition,	DEFAULT_LOG_MESSAGE(Format), DEFAULT_LOG_ARGUMENTS(__VA_ARGS__))

// Unreal Logs
#define LogVerbose(Format, ...) UE_LOG(DEFAULT_LOG_CATEGORY, Verbose,	DEFAULT_LOG_MESSAGE(Format), DEFAULT_LOG_ARGUMENTS(__VA_ARGS__))
#define LogDisplay(Format, ...) UE_LOG(DEFAULT_LOG_CATEGORY, Display,	DEFAULT_LOG_MESSAGE(Format), DEFAULT_LOG_ARGUMENTS(__VA_ARGS__))
#define LogDefault(Format, ...) UE_LOG(DEFAULT_LOG_CATEGORY, Log,	DEFAULT_LOG_MESSAGE(Format), DEFAULT_LOG_ARGUMENTS(__VA_ARGS__))
#define LogWarning(Format, ...) UE_LOG(DEFAULT_LOG_CATEGORY, Warning,	DEFAULT_LOG_MESSAGE(Format), DEFAULT_LOG_ARGUMENTS(__VA_ARGS__))
#define LogError(Format, ...)	UE_LOG(DEFAULT_LOG_CATEGORY, Error,	DEFAULT_LOG_MESSAGE(Format), DEFAULT_LOG_ARGUMENTS(__VA_ARGS__))
#define LogFatal(Format, ...)	UE_LOG(DEFAULT_LOG_CATEGORY, Fatal,	DEFAULT_LOG_MESSAGE(Format), DEFAULT_LOG_ARGUMENTS(__VA_ARGS__))

// Assertion helpers
#define CheckValid(VarMaybeNull)		LogCheck(VarMaybeNull, "`" #VarMaybeNull "` is nullptr!")
#define CheckInvalid(VarMaybeValid)		LogCheck(VarMaybeValid, "`" #VarMaybeValid "` is valid!")
#define CheckTrue(VarMaybeFalse)		LogCheck(VarMaybeFalse, "`" #VarMaybeFalse "` is false!")
#define CheckFalse(VarMaybeTrue)		LogCheck(VarMaybeTrue, "`" #VarMaybeTrue "` is true!")
#define CheckEqual(Var1, Var2)			LogCheck(Var1 == Var2, "`" #Var1 "` != `" #Var2 "`!")
#define CheckNotEqual(Var1, Var2)		LogCheck(Var1 != Var2, "`" #Var1 "` == `" #Var2 "`!")
#define EnsureValid(VarMaybeNull)		LogEnsure(VarMaybeNull, "`" #VarMaybeNull "` is nullptr!")
#define EnsureInvalid(VarMaybeValid)		LogEnsure(VarMaybeValid, "`" #VarMaybeValid "` is valid!")
#define EnsureTrue(VarMaybeFalse)		LogEnsure(VarMaybeFalse, "`" #VarMaybeFalse "` is false!")
#define EnsureFalse(VarMaybeTrue)		LogEnsure(VarMaybeTrue, "`" #VarMaybeTrue "` is true!")
#define EnsureEqual(Var1, Var2)			LogEnsure(Var1 == Var2, "`" #Var1 "` != `" #Var2 "`!")
#define EnsureNotEqual(Var1, Var2)		LogEnsure(Var1 != Var2, "`" #Var1 "` == `" #Var2 "`!")

///////////////////////////// Log Helpers //////////////////////////////////////

2.0

#pragma once  
//@formatter:off  
  
#include "Kismet/KismetSystemLibrary.h"  
#include "Logging/LogMacros.h"  
  
///////////////////////////// Declarations //////////////////////////////////////  
  
MYPROJECT_API DECLARE_LOG_CATEGORY_EXTERN(LogMy, Log, All);  

///////////////////////////// Configurations //////////////////////////////////////  
  
// Default category used in `UE_LOG`.  
#define _DEFAULT_LOG_CATEGORY LogTrans  
  
// e.g. AActor::BeginPlay() => <RawMessage> ["\Path\To\Actor.cpp:299"]  
#define _DEFAULT_LOG_MESSAGE_RAW_CHAR(RawMessage) "%hs() => " RawMessage " [\"%hs:%d\"]"  
  
// __FUNCTION__, Type: %hs, e.g.: AActor::BeginPlay  
// __FILE__,     Type: %hs, e.g.: \Path\To\Actor.cpp  
// __LINE__,     Type: %d,  e.g.: 299  
// __VA_OPT__(,) (since cpp20): if `...` is empty, leave it empty as well, otherwise add a `,` to separate nearby tokens.  
#define _DEFAULT_LOG_ARGUMENTS(...) __FUNCTION__ __VA_OPT__(,) __VA_ARGS__, __FILE__, __LINE__  
  
// Default log message used in `MyCheck`, `MyVerify` and `MyEnsure`.  
#define _DEFAULT_ASSERTION_MESSAGE(Condition) "`" #Condition "` is false or null."  
  
///////////////////////////// Log Helpers Internal, DON'T CALL DIRECTLY! //////////////////////////////////////  
  
#define _DEFAULT_LOG_MESSAGE(RawMessage) TEXT(_DEFAULT_LOG_MESSAGE_RAW_CHAR(RawMessage))  
  
#define _DEFAULT_ASSERTION_MESSAGE_EMPTY(Condition)  
  
// TODO: DO_ENSURE and CODE_ANALYSIS build configuration check, make them truly the same with UE `ensureMsgf`.  
#define _CUSTOM_ENSURE_WRAPPER(Capture, Always, InExpression, InFormat, ...) \  
    (LIKELY(!!(InExpression)) || ([Capture] () UE_DEBUG_SECTION \  
    { \  
       UKismetSystemLibrary::PrintString(this, __VA_OPT__(FString::Printf) (InFormat __VA_OPT__(,) __VA_ARGS__), true, false, FLinearColor::Red); \  
       UE_VALIDATE_FORMAT_STRING(InFormat, ##__VA_ARGS__); \  
       static std::atomic<bool> bExecuted = false; \  
       static constexpr ::UE::Assert::Private::FStaticEnsureRecord ENSURE_Static(InFormat, #InExpression, __builtin_FILE(), __builtin_LINE(), Always); \  
       if ((Always || !bExecuted.load(std::memory_order_relaxed)) && FPlaMyormMisc::IsEnsureAllowed() && ::UE::Assert::Private::EnsureFailed(bExecuted, &ENSURE_Static, ##__VA_ARGS__)) \  
       { \  
          PLAMyORM_BREAK(); \  
       } \  
       return false; \  
    } ()))  
  
// Unreal Assertions  
#define _LogCheck(Condition, Format, ...)      checkf(Condition,     _DEFAULT_LOG_MESSAGE(Format), _DEFAULT_LOG_ARGUMENTS(__VA_ARGS__))  
#define _LogCheckSlow(Condition, Format, ...)   checkfSlow(Condition,  _DEFAULT_LOG_MESSAGE(Format), _DEFAULT_LOG_ARGUMENTS(__VA_ARGS__))  
#define _LogVerify(Condition, Format, ...)     verifyf(Condition,    _DEFAULT_LOG_MESSAGE(Format), _DEFAULT_LOG_ARGUMENTS(__VA_ARGS__))  
  
// TODO: `__FUNCTION__` still prints the inner lambda name. How to fix it?  
#define _LogEnsure(Condition, Format, ...) \  
    _CUSTOM_ENSURE_WRAPPER( \  
       &, \  
       false, \  
       Condition, \  
       TEXT(__FUNCTION__ "() => " Format " [\"" __FILE__ ":" PREPROCESSOR_TO_STRING(__LINE__) "\"]") __VA_OPT__(,) \  
       __VA_ARGS__ \  
    )  
  
#define _MyCheckInner(Condition, Format, ...)      _LogCheck(Condition, Format __VA_OPT__(,) __VA_ARGS__)  
#define _MyVerifyInner(Condition, Format, ...)     _LogVerify(Condition, Format __VA_OPT__(,) __VA_ARGS__)  
#define _MyEnsureInner(Condition, Format, ...)     _LogEnsure(Condition, Format __VA_OPT__(,) __VA_ARGS__)  
  
// Unreal Logs  
#define _LogVerbose(Format, ...)    UE_LOG(_DEFAULT_LOG_CATEGORY, Verbose, _DEFAULT_LOG_MESSAGE(Format), _DEFAULT_LOG_ARGUMENTS(__VA_ARGS__))  
#define _LogLog(Format, ...)       UE_LOG(_DEFAULT_LOG_CATEGORY, Log,    _DEFAULT_LOG_MESSAGE(Format), _DEFAULT_LOG_ARGUMENTS(__VA_ARGS__))  
#define _LogDisplay(Format, ...)    UE_LOG(_DEFAULT_LOG_CATEGORY, Display, _DEFAULT_LOG_MESSAGE(Format), _DEFAULT_LOG_ARGUMENTS(__VA_ARGS__))  
#define _LogWarning(Format, ...)    UE_LOG(_DEFAULT_LOG_CATEGORY, Warning, _DEFAULT_LOG_MESSAGE(Format), _DEFAULT_LOG_ARGUMENTS(__VA_ARGS__))  
#define _LogError(Format, ...)     UE_LOG(_DEFAULT_LOG_CATEGORY, Error,   _DEFAULT_LOG_MESSAGE(Format), _DEFAULT_LOG_ARGUMENTS(__VA_ARGS__))  
#define _LogFatal(Format, ...)     UE_LOG(_DEFAULT_LOG_CATEGORY, Fatal,   _DEFAULT_LOG_MESSAGE(Format), _DEFAULT_LOG_ARGUMENTS(__VA_ARGS__))  
  
///////////////////////////// Unreal Assertions Interface //////////////////////////////////////  
  
#define MyCheck(Condition, ...)    _MyCheckInner(Condition,  _DEFAULT_ASSERTION_MESSAGE##__VA_OPT__(_EMPTY)(Condition) __VA_ARGS__)  
#define MyVerify(Condition, ...)    _MyVerifyInner(Condition, _DEFAULT_ASSERTION_MESSAGE##__VA_OPT__(_EMPTY)(Condition) __VA_ARGS__)  
#define MyEnsure(Condition, ...)    _MyEnsureInner(Condition, _DEFAULT_ASSERTION_MESSAGE##__VA_OPT__(_EMPTY)(Condition) __VA_ARGS__)  
  
///////////////////////////// Unreal Log Interface //////////////////////////////////////  
  
#define MyLog(Verbosity, Format, ...) _Log##Verbosity(Format __VA_OPT__(,) __VA_ARGS__)  
  
///////////////////////////// Unreal PrintString Interface //////////////////////////////////////  
  
#define MyPrint(Format, ...) UKismetSystemLibrary::PrintString(this, __VA_OPT__(FString::Printf) (TEXT(Format) __VA_OPT__(,) __VA_ARGS__), true, true, FLinearColor::Green)  
  
///////////////////////////// Quick logs //////////////////////////////////////  
  
#define MyErr(Format, ...) \  
    UKismetSystemLibrary::PrintString(this, __VA_OPT__(FString::Printf) (TEXT(Format) __VA_OPT__(,) __VA_ARGS__), true, false, FLinearColor::Red); \  
    MyLog(Error, Format __VA_OPT__(,) __VA_ARGS__)  
  
#define MyWarn(Format, ...) \  
    UKismetSystemLibrary::PrintString(this, __VA_OPT__(FString::Printf) (TEXT(Format) __VA_OPT__(,) __VA_ARGS__), true, false, FLinearColor::Yellow); \  
    MyLog(Warning, Format __VA_OPT__(,) __VA_ARGS__)  
  
#define MyOut(Format, ...) \  
    UKismetSystemLibrary::PrintString(this, __VA_OPT__(FString::Printf) (TEXT(Format) __VA_OPT__(,) __VA_ARGS__), true, false, FLinearColor::Green); \  
    MyLog(Display, Format __VA_OPT__(,) __VA_ARGS__)  
  
///////////////////////////// Log Not Implemented Interface //////////////////////////////////////  
  
#define MyNotImpl() \  
    MyLog(Error, "Not Implemented!") \  
    UKismetSystemLibrary::PrintString(this, TEXT(__FUNCTION__ "() => " "Not Implemented!" " [\"" __FILE__ ":" PREPROCESSOR_TO_STRING(__LINE__) "\"]"), true, false, FLinearColor::Red)  
  
#define MyNotImplLogOnly() \  
    MyLog(Error, "Not Implemented!")

3.0

Reference


评论