# 在运行时打印输出信息

原作者:Rama

此文为Logs, Printing Messages To Yourself During Runtime的原创翻译,本文内容版权归原文所有,仅供学习,如需转载望注本文地址,翻译不易,谢谢理解。

# 概述

Logs很重要,因为它通过给你反馈来让你知道:

  • 你的新函数正在被调用
  • 你的算法在运行时所用的数据
  • 给你,最终用户,调试团队报告错误
  • 在运行时的一些特殊情况下报告一个严重错误并停止执行。

这篇文章介绍了如何使用Unreal output log。

# 使用Logs

# 在游戏里

为了看到日志,你需要在运行游戏时使用-Log参数,或者在你的游戏中使用控制台命令"showlog"。

# 在编辑器里

日志信息会被输出到"Output Log"窗口中,可以通过Window -> Developer Tools -> Output Log打开输出日志窗口。

如果你以PIE模式使用编辑器时,日志功能可以默认通过Engine INI文件中的"GameCommandLine=-log"激活。如果看不到日志输出,可以像上面在游戏中一样添加"-Log"命令行参数。

# 快速使用

UE_LOG(LogTemp, Warning, TEXT("Your message"));
1

这样你不用为你的日志创建一个定制的分类,尽管这样可以让你保持所有的东西整洁清楚。

# 日志信息显示等级

日志信息显示等级可以让你更容易地控制输出信息,这允许你在代码中保持最详尽的日志语句,并在你不需要它们时禁用它们,每个日志语句声明了该日志属于哪个等级。日志信息显示等级通过每个日志基础控制,每个日志的信息显示被四件事控制:编译时信息显示,默认信息显示,初始化信息显示,运行时信息显示。

如果一个日志语句比它编译时的信息更冗余,它甚至不会被编译进游戏代码,从这里来讲日志的等级会被设置成默认信息显示,稍后可以在Engine.ini文件中被重写,或者通过命令行(运行时信息分类)被重写,一旦游戏或编辑器运行起来,那它就没办法改变日志的信息等级(不确定是不是这样)。

下面是可用的日志信息显示等级:

  • Fatal,Fatal等级的日志总是被输出到控制台和日志文件,如果日志被禁用它可能会崩溃。
  • Error,Error等级的日志被输出到控制台和日志文件,它默认是以红颜色显示的。
  • Warning,Warning等级的日志被输出到控制台和日志文件,它默认以黄颜色显示的。
  • Display,Display等级的日志会被输出到控制台和日志文件。
  • Log,Log等级的日志会被输出到日志文件,但不会被输出到游戏里的控制台,它们仍能在编辑器里通过Output Log窗口被看到。
  • Verbose,Verbose等级日志会被输出到日志文件,但不会被输出到游戏里的控制台,通常用来显示详细的细节和调试。
  • VeryVerbose,VeryVerbose等级日志会被输出到日志文件,但不会被输出到游戏里的控制台,通常用来显示非常详细的细节,这可能会让输出窗口被杂乱的信息填充。

对于DECLARE_LOG_CATEGORY_EXTERN的CompileTimeVerbosity参数,它能使用All(同使用VeryVerbose一样)和NoLogging(同使用Fatal一样)。

# 建立你自己的日志分类

我们平时使用的LogTemp意味着该信息是临时日志信息。UE4也定义了几个日志分类,比如LogActor,用来输出Actor类相关的调试信息,比如LogAnimation,用来输出Animation的调试信息,基本上,UE4给每个模块定义了一个单独的日志分类,其他的我们可以打开Output Log窗口查看。现在我们自己定义自己的日志分类。

# 日志分类宏

宏DECLARE_LOG_CATEGORY_EXTERN和DEFINE_LOG_CATEGORY应分别在你的YourGame.h和YourGame.cpp文件中。

声明日志分类的宏需要三个参数,每个声明在cpp文件中应该有对应的定义。

DECLARE_LOG_CATEGORY_EXTERN(CategoryName, DefaultVerbosity, CompileTimeVerbosity);
1
  • CategoryName是你定义的分类的名字。
  • DefaultVerbosity是信息显示等级,在它没有被ini文件或者命令行中指定时使用。任何比这个等级更高的日志将不会被显示。
  • CompileTimeVerbosity是编译时最大的显示等级,任何大于这个显示等级的日志都不会被编译。

定义日志分类的宏只需要该分类的名字作为参数:

DEFINE_LOG_CATEGORY(CategoryName);
1

# 使用样例

你可以在游戏的不同方面使用不同的日志分类!这给了你额外的信息,因为UE_LOG在显示消息时会打印出日志分类,下面是一个例子,用来展示如何使用不同的日志等级。假如游戏中的某个系统经常出问题,在调试当时你可能想要获取详细的日志,但是当你完成调试时,你可能就不需要它们了,此时它们会输出满屏幕,你该如何做呢?使用不同的日志等级。

MyGame.H文件内容:

//General Log
DECLARE_LOG_CATEGORY_EXTERN(LogMyGame, Log, All);

//Logging during game startup
DECLARE_LOG_CATEGORY_EXTERN(LogMyGameInit, Log, All);

//Logging for your AI system
DECLARE_LOG_CATEGORY_EXTERN(LogMyGameAI, Log, All);

//Logging for a that troublesome system
DECLARE_LOG_CATEGORY_EXTERN(LogMyGameSomeSystem, Log, All);

//Logging for Critical Errors that must always be addressed
DECLARE_LOG_CATEGORY_EXTERN(LogMyGameCriticalErrors, Log, All);
1
2
3
4
5
6
7
8
9
10
11
12
13
14

MyGame.CPP文件内容:

#include "MyGame.h"

//General Log
DEFINE_LOG_CATEGORY(LogMyGame);

//Logging during game startup
DEFINE_LOG_CATEGORY(LogMyGameInit);

//Logging for your AI system
DEFINE_LOG_CATEGORY(LogMyGameAI);

//Logging for some system
DEFINE_LOG_CATEGORY(LogMyGameSomeSystem);

//Logging for Critical Errors that must always be addressed
DEFINE_LOG_CATEGORY(LogMyGameCriticalErrors);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

使用日志时的MyClass.CPP文件中:

//...
void UMyClass::FireWeapon()
{
    UE_LOG(LogMyGameSomeSystem, Verbose, TEXT("UMyClass %s entering FireWeapon()"), *GetNameSafe(this));
    //Logic
    UE_LOG(LogMyGameSomeSystem, Verbose, TEXT("UMyClass %s Attempting to fire."), *GetNameSafe(this));
    if (CheckSomething())
    {
        UE_LOG(LogMyGameSomeSystem, Log, TEXT("UMyClass %s is firing their weapon with charge of %f"), *GetNameSafe(this), GetCharge());
        //Firing logic
    }
    else
    {
        UE_LOG(LogMyGameSomeSystem, Error, TEXT("UMyClass %s CheckSomething() returned false during FireWeapon(), this is bad!"), *GetNameSafe(this));
        //Fail with grace
    }
    //More code!
    UE_LOG(LogMyGameSomeSystem, Verbose, TEXT("UMyClass %s leaving FireWeapon()"), *GetNameSafe(this));
}

void UMyClass::Tick(float DeltaTime)
{
    UE_LOG(LogMyGameSomeSystem, VeryVerbose, TEXT("UMyClass %s's charge is %f"), *GetNameSafe(this), GetCharge());
    if (something)
    {
        UE_LOG(LogMyGameSomeSystem, VeryVerbose, TEXT("Idk"));
    }
    if (somethingelse)
    {
        UE_LOG(LogMyGameSomeSystem, VeryVerbose, TEXT("Stuff"));
    }
}
//...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

当你不在这个系统上工作时,所有这些日志语句肯定会充满你的屏幕,甚至之前当你在该系统上工作时,你不想每帧输出这些多重日志信息。

通过使用日志等级,你能很容易地在日志分类声明中改变日志分类等级,在ini文件或者命令行中在你需要时来隐藏/显示不同等级的日志语句。比如:

//All log statements are shown.
DECLARE_LOG_CATEGORY_EXTERN(LogMyGameSomeSystem, Log, All);

//VeryVerbose statements won't be shown.
DECLARE_LOG_CATEGORY_EXTERN(LogMyGameSomeSystem, Verbose, All);

//Neither VeryVerbose nor Verbose statements will be shown.
DECLARE_LOG_CATEGORY_EXTERN(LogMyGameSomeSystem, VeryVerbose, All);
1
2
3
4
5
6
7
8

UE4所使用的日志分类也有不同的等级,但是默认有一个更高的CompileTimeVerbosity等级,通过在DefaultEngine.ini文件中的[Core.Log]目录下添加一行LogOnline=Verbose,能让UE4的调试信息输出显示。

# 日志格式输出

输出信息:

//"This is a message to yourself during runtime!"
UE_LOG(YourLog,Warning,TEXT("This is a message to yourself during runtime!"));
1
2

输出一个FString:

%s strings are wanted as TCHAR* by Log, so use *FString()

//"MyCharacter's Name is %s"
UE_LOG(YourLog,Warning,TEXT("MyCharacter's Name is %s"), *MyCharacter->GetName() );
1
2
3
4

输出一个Bool:

//"MyCharacter's Bool is %s"
UE_LOG(YourLog,Warning,TEXT("MyCharacter's Bool is %s"), (MyCharacter->MyBool ? TEXT("True") : TEXT("False")));
1
2

输出一个Int:

//"MyCharacter's Health is %d"
UE_LOG(YourLog,Warning,TEXT("MyCharacter's Health is %d"), MyCharacter->Health );
1
2

输出一个Float:

//"MyCharacter's Health is %f"
UE_LOG(YourLog,Warning,TEXT("MyCharacter's Health is %f"), MyCharacter->Health );
1
2

输出一个FVector:

//"MyCharacter's Location is %s"
UE_LOG(YourLog,Warning,TEXT("MyCharacter's Location is %s"), 
    *MyCharacter->GetActorLocation().ToString());
1
2
3

输出一个FName:

//"MyCharacter's FName is %s"
UE_LOG(YourLog,Warning,TEXT("MyCharacter's FName is %s"), 
    *MyCharacter->GetFName().ToString());
1
2
3

输出一个FString,Int,Float

//"%s has health %d, which is %f percent of total health"
UE_LOG(YourLog,Warning,TEXT("%s has health %d, which is %f percent of total health"),
    *MyCharacter->GetName(), MyCharacter->Health, MyCharacter->HealthPercent);
1
2
3

# 日志颜色

Log:灰色的

//"this is Grey Text"
UE_LOG(YourLog,Log,TEXT("This is grey text!"));
1
2

Warning:黄色的

//"this is Yellow Text"
UE_LOG(YourLog,Warning,TEXT("This is yellow text!"));
1
2

Error:红色的

//"This is Red Text"
UE_LOG(YourLog,Error,TEXT("This is red text!"));
1
2

Fatal:高级运行时保护式中断

如果你想确保包含一些永远不会运行的代码,你自己可以扔出一个致命错误。

我已经使用了这个特性来确保对我算法不利的情况永远不会出现,它确实很有用,但是它更像是一个崩溃,所以如果你使用这个特性,不要担心,它就只时查看崩溃调用栈:

Again this is an advanced case that crashes the program, use only for extremely important circumstances.

//some complicated algorithm
if(some fringe case that you want to tell yourself if the runtime execution ever reaches this point)
{
    //"This fringe case was reached! Debug this!"
    UE_LOG(YourLog,Fatal,TEXT("This fringe case was reached! Debug this!"));
}
1
2
3
4
5
6
7
8

# 快速打印提示信息

这个一个简单的调试宏,你可以在你CPP文件的开头使用它:

#define print(text) if (GEngine) GEngine->AddOnScreenDebugMessage(-1, 1.5, FColor::White,text)
1

然后你就能在这个CPP文件中像往常一样使用可爱的print();

为了阻止你的屏幕被占满,你可以改变第一个参数为一个负数的键,后续任何拥有同样的键值的信息都会被移除,这方便你频繁的输出。

# 其他调试选择

# 将信息输出到屏幕

很多时候当你只想把信息输出到屏幕上的时候,你可以做:

 #include <EngineGlobals.h>
 #include <Runtime/Engine/Classes/Engine/Engine.h>
 // ...
 GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("This is an on screen message!"));
 GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, FString::Printf(TEXT("Some variable values: x: %f, y: %f"), x, y));
1
2
3
4
5

为了阻止你的屏幕被占满,你可以改变第一个参数(除了-1,-1意味着不会尝试覆盖已存在的值,只是添加新的消息),后续任何拥有同样的键值的信息都会被移除,这方便你频繁的输出某些信息。

# 将信息输出到客户端控制台

在UE4中按~可以打开客户端控制台,如果你使用PlayerController类,你可以输出信息到这个控制台,这能让控制台变成一个完全不同的日志输出空间,让我们并不需要切出游戏去看调试信息。

    PC->ClientMessage("Your Message");

    GetWorld()->GetFirstPlayerController()->ClientMessage("hello world 1!");
    UGameplayStatics::GetPlayerController(GetWorld(), 0)->ClientMessage("hello world 2!");
    GEngine->GetGamePlayer(GetWorld(), 0)->PlayerController->ClientMessage("hello world 3!");
    for (FConstPlayerControllerIterator Iterator = GetWorld()->GetPlayerControllerIterator(); Iterator; ++Iterator) {
        Iterator->Get()->ClientMessage("hello world 4!");
    }
1
2
3
4
5
6
7
8

# 日志惯例

它应用在控制台,ini文件或者环境变量里。

  • [cat] = 命令可操作的分类,对于所有的分类而言是"global"。
  • [level] = 信息日志等级,它可以是:none,error,warning,display,log,verbose,all,default

在启动时,默认被编译的会被ini文件里的设置重写,然后被命令行里的重写。

# 日志控制台命令

  • Log list - 显示所有的日志分类。
  • Log list [string] - 显示包含一个子串的所有日志分类。
  • Log reset - 重设所有的日志分类为启动时的默认分类。
  • Log [cat] - 是否显示分类[cat]
  • Log [cat] off - 禁止显示分类[cat]
  • Log [cat] on - 恢复显示分类[cat]
  • Log [cat] [level] - 设置分类[cat]的信息显示等级。
  • Log [cat] break - 在显示分类[cat]时是否会有调试中断。

# 日志命令行

-LogCmds=\"[arguments],[arguments]...\"
在启动时应用一堆控制台命令
-LogCmds=\"foo verbose, bar off\"
显示foo分类,关闭bar分类
1
2
3
4

# 环境变量

任何命令行选项可通过环境变量UE-CmdLineArgs设置:

set UE-CmdLineArgs=\"-LogCmds=foo verbose breakon, bar off\"
1

# 配置文件

在Default.ini或者Engine.ini里面:

[Core.Log]
global=[default verbosity for things not listed later]
[cat]=[level]
foo=verbose break
1
2
3
4

# 调试输出其他信息

为了自动地输出类名,函数名,行号,请看下面:

# 包含头文件

头文件内容如下:

/*
    Joy String 
        Current Class, File, and Line Number!
            by Rama
            
    PreProcessor commands to get 
        a. Class name
        b. Function Name
        c. Line number 
        d. Function Signature (including parameters)
        
    Gives you a UE4 FString anywhere in your code that these macros are used!
    
    Ex: 
        You can use JOYSTR_CUR_CLASS anywhere to get a UE4 FString back telling you 
        what the current class is where you called this macro!
    
    Ex:
        This macro prints the class and line along with the message of your choosing!
        VSCREENMSG("Have fun today!");
    <3  Rama
*/
#pragma once

//Current Class Name + Function Name where this is called!
#define JOYSTR_CUR_CLASS_FUNC (FString(__FUNCTION__))

//Current Class where this is called!
#define JOYSTR_CUR_CLASS (FString(__FUNCTION__).Left(FString(__FUNCTION__).Find(TEXT(":"))) )

//Current Function Name where this is called!
#define JOYSTR_CUR_FUNC (FString(__FUNCTION__).Right(FString(__FUNCTION__).Len() - FString(__FUNCTION__).Find(TEXT("::")) - 2 ))
  
//Current Line Number in the code where this is called!
#define JOYSTR_CUR_LINE  (FString::FromInt(__LINE__))

//Current Class and Line Number where this is called!
#define JOYSTR_CUR_CLASS_LINE (JOYSTR_CUR_CLASS + "(" + JOYSTR_CUR_LINE + ")")
  
//Current Function Signature where this is called!
#define JOYSTR_CUR_FUNCSIG (FString(__FUNCSIG__))


//Victory Screen Message
// 	Gives you the Class name and exact line number where you print a message to yourself!
#define VSCREENMSG(Param1) (GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, *(JOYSTR_CUR_CLASS_LINE + ": " + Param1)) )

#define VSCREENMSG2(Param1,Param2) (GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, *(JOYSTR_CUR_CLASS_LINE + ": " + Param1 + " " + Param2)) )

#define VSCREENMSGF(Param1,Param2) (GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, *(JOYSTR_CUR_CLASS_LINE + ": " + Param1 + " " + FString::SanitizeFloat(Param2))) )

//UE LOG!
#define V_LOG(LogCat, Param1) 		UE_LOG(LogCat,Warning,TEXT("%s: %s"), *JOYSTR_CUR_CLASS_LINE, *FString(Param1))

#define V_LOG2(LogCat, Param1,Param2) 	UE_LOG(LogCat,Warning,TEXT("%s: %s %s"), *JOYSTR_CUR_CLASS_LINE, *FString(Param1),*FString(Param2))

#define V_LOGF(LogCat, Param1,Param2) 	UE_LOG(LogCat,Warning,TEXT("%s: %s %f"), *JOYSTR_CUR_CLASS_LINE, *FString(Param1),Param2)

#define V_LOGM(LogCat, FormatString , ...) UE_LOG(LogCat,Warning,TEXT("%s: %s"), 	*JOYSTR_CUR_CLASS_LINE, *FString::Printf(TEXT(FormatString), ##__VA_ARGS__ ) )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

# 打印类名和行号

在包含了头文件后,可以像下面这样:

//~~~ Tick ~~~
void AEVCoreDefense::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
    //~~~~~~~~~~~~~

    VSCREENMSG("Got Here!");  //Class and line number get printed for you! ♥ Rama
}
1
2
3
4
5
6
7
8

# V_LOG

每个V_LOG宏都会将你想要的使用的日志分类名字作为第一个参数:

#define V_LOG(LogCat, Param1)   UE_LOG(LogCat,Warning,TEXT("%s: %s"), *JOYSTR_CUR_CLASS_LINE, *FString(Param1))
1

# V_LOGM

V_LOGM特殊在你要输出任意数量,任何类型的变量,就像标准的UE_LOG函数!

样例:

int32 Health = 100;
float ArmorPct = 52.33;
FVector Location(33,12,1);

V_LOGM(Joy, "Health: %d, ArmorPct: %f, Loc: %s",  Health, ArmorPct, *Location.ToString());
1
2
3
4
5