# 在运行时打印输出信息
此文为Logs, Printing Messages To Yourself During Runtime (opens new window)的原创翻译,本文内容版权归原文所有,仅供学习,如需转载望注本文地址,翻译不易,谢谢理解。
# 概述
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"));
这样你不用为你的日志创建一个定制的分类,尽管这样可以让你保持所有的东西整洁清楚。
# 日志信息显示等级
日志信息显示等级可以让你更容易地控制输出信息,这允许你在代码中保持最详尽的日志语句,并在你不需要它们时禁用它们,每个日志语句声明了该日志属于哪个等级。日志信息显示等级通过每个日志基础控制,每个日志的信息显示被四件事控制:编译时信息显示,默认信息显示,初始化信息显示,运行时信息显示。
如果一个日志语句比它编译时的信息更冗余,它甚至不会被编译进游戏代码,从这里来讲日志的等级会被设置成默认信息显示,稍后可以在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);
- CategoryName是你定义的分类的名字。
- DefaultVerbosity是信息显示等级,在它没有被ini文件或者命令行中指定时使用。任何比这个等级更高的日志将不会被显示。
- CompileTimeVerbosity是编译时最大的显示等级,任何大于这个显示等级的日志都不会被编译。
定义日志分类的宏只需要该分类的名字作为参数:
DEFINE_LOG_CATEGORY(CategoryName);
# 使用样例
你可以在游戏的不同方面使用不同的日志分类!这给了你额外的信息,因为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);
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);
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"));
}
}
//...
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);
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!"));
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() );
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")));
2
输出一个Int:
//"MyCharacter's Health is %d"
UE_LOG(YourLog,Warning,TEXT("MyCharacter's Health is %d"), MyCharacter->Health );
2
输出一个Float:
//"MyCharacter's Health is %f"
UE_LOG(YourLog,Warning,TEXT("MyCharacter's Health is %f"), MyCharacter->Health );
2
输出一个FVector:
//"MyCharacter's Location is %s"
UE_LOG(YourLog,Warning,TEXT("MyCharacter's Location is %s"),
*MyCharacter->GetActorLocation().ToString());
2
3
输出一个FName:
//"MyCharacter's FName is %s"
UE_LOG(YourLog,Warning,TEXT("MyCharacter's FName is %s"),
*MyCharacter->GetFName().ToString());
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);
2
3
# 日志颜色
Log:灰色的
//"this is Grey Text"
UE_LOG(YourLog,Log,TEXT("This is grey text!"));
2
Warning:黄色的
//"this is Yellow Text"
UE_LOG(YourLog,Warning,TEXT("This is yellow text!"));
2
Error:红色的
//"This is Red Text"
UE_LOG(YourLog,Error,TEXT("This is red text!"));
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!"));
}
2
3
4
5
6
7
8
# 快速打印提示信息
这个一个简单的调试宏,你可以在你CPP文件的开头使用它:
#define print(text) if (GEngine) GEngine->AddOnScreenDebugMessage(-1, 1.5, FColor::White,text)
然后你就能在这个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));
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!");
}
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分类
2
3
4
# 环境变量
任何命令行选项可通过环境变量UE-CmdLineArgs设置:
set UE-CmdLineArgs=\"-LogCmds=foo verbose breakon, bar off\"
# 配置文件
在Default.ini或者Engine.ini里面:
[Core.Log]
global=[default verbosity for things not listed later]
[cat]=[level]
foo=verbose break
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__ ) )
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
}
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))
# 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());
2
3
4
5