# 前言

此文为Console Variables in C++ (opens new window)的原创翻译,本文内容版权归原文所有,仅供学习,如需转载望注本文地址,翻译不易,谢谢理解。

一个控制台命令是用户输入字符发送到引擎然后引擎以某种方式反应(比如console/log变量,改变内部状态)。一个控制台变量会额外地保存一些状态,这些状态稍后可通过控制台改变。通过在控制台管理器中注册控制台命令和变量,可得到自动补全和枚举,以便获取所有控制台对象(控制台命令帮助或者输出控制台变量)的列表,因此,你应该避免使用旧的Exec接口。控制台管理器会控制所有的这些事甚至更多,比如用户输入历史。

可从该网站 (opens new window)查看UE4的控制台变量和命令。

# 控制变量是啥

一个控制台变量是一些简单的数据类型,比如float,int,string,它在引擎范围内有状态,用户可以读取和设置这些状态。控制台变量有名字,用户能在输入变量名到控制台时使用自动补全。比如:

用户控制台输入 控制台输出 描述
MyConsoleVar MyConsoleVar = 0 变量的当前状态被输出到控制台
MyConsoleVar 123 MyConsoleVar = 123 LastSetBy: Constructor 变量的状态被改变,新状态被打印到控制台里
MyConsoleVar ? 可能得到多行帮助文本 打印控制台变量帮助到控制台

# 创建/注册控制台变量

所有的控制台变量在创建的时候需要被注册,下面这个例子是从引擎代码中截取的:

static TAutoConsoleVariable<int32> CVarRefractionQuality(
    TEXT("r.RefractionQuality"),
    2,
    TEXT("Defines the distortion/refraction quality, adjust for quality or performance.\n")
    TEXT("<=0: off (fastest)\n")
    TEXT("  1: low quality (not yet implemented)\n")
    TEXT("  2: normal quality (default)\n")
    TEXT("  3: high quality (e.g. color fringe, not yet implemented)"),
    ECVF_Scalability | ECVF_RenderThreadSafe);
1
2
3
4
5
6
7
8
9

我们注册了一个int32类型的控制台变量,变量名为r.RefractionQuality,它的默认值是2,有一些多行帮助文本和一些标志。这有很多标志,最重要的一个是ECVF_Cheat,该flag在IConsoleManager.h文件中被详细解释了,帮助文本会在用户在控制台变量后加"?"时来展示出来。

如果需要,你可以在一个函数里生成控制台变量:

IConsoleManager::Get().RegisterConsoleVariable(TEXT("r.RefractionQuality"),
   2,
   TEXT("Defines the distortion/refraction quality, adjust for quality or performance.\n")
    TEXT("<=0: off (fastest)\n")
    TEXT("  1: low quality (not yet implemented)\n")
    TEXT("  2: normal quality (default)\n")
    TEXT("  3: high quality (e.g. color fringe, not yet implemented)"),
   ECVF_Scalability | ECVF_RenderThreadSafe);
1
2
3
4
5
6
7
8

IConsoleManager::Get()是全局获取口,在这你可以注册一个控制台变量或者找到已经存在的变量。第一个参数是控制台变量的名字,第二个参数是默认值,此常量类型不同,也会创建不同类型的控制台变量:int,float或string(不是FString),下一个参数定义了控制台变量的帮助文本。

也可以注册一个现有变量的引用。这是方便和快捷的但是绕过了很多功能(比如线程安全,回调,sink,cheat),所以我们建议不用这个方法,实例:

FAutoConsoleVariableRef CVarVisualizeGPUSimulation(
    TEXT("FX.VisualizeGPUSimulation"),
    VisualizeGPUSimulation,
    TEXT("Visualize the current state of GPU simulation.\n")
    TEXT("0 = off\n")
    TEXT("1 = visualize particle state\n")
    TEXT("2 = visualize curve texture"),
    ECVF_Cheat
    );
1
2
3
4
5
6
7
8
9

这里面不再有变量类型。

# 获取控制台变量状态

通过使用注册的变量我们能有效地获取RegisterConsoleVariable所创建控制台变量的状态,比如:

// only needed if you are not in the same cpp file
extern TAutoConsoleVariable<int32> CVarRefractionQuality;
// get the value on the game thread
int32 MyVar = CVarRefractionQuality.GetValueOnGameThread();
1
2
3
4

使用获取函数,比如GetInt(),GetFloat(),GetString(),来确定控制台变量的状态在实现上可能会有点慢,因为虚函数调用,潜在缓存丢失等,为了最好的性能,你应该使用同类型的注册变量。为了获取这个变量的引用指针,你可以存储注册函数的返回结果或在你需要这个变量前调用FindConsoleVariable,比如:

static const auto CVar = IConsoleManager::Get().FindConsoleVariable(TEXT("TonemapperType"));
int32 Value = CVar->GetInt();
1
2

此处的static保证控制台变量命名搜索(内部以map机制实现)只在该代码第一次调用时执行,如果变量直到引擎关闭被销毁掉的这个存在期间不会变动,那这样做是没问题的。

# 如何追踪控制台变量的改变

如果在一个控制台变量改变时你想执行一些特定的代码,你有3种方法可选择:

  1. 最简单也是最好的:你可以将变量的旧状态存在子系统中,然后每一帧检查它们是否不一样,这样你可以自由地控制在什么时候发生,比如是渲染线程还是游戏线程,还是流线程,是在tick之前/之后还是在渲染的时候。当你检测到不一样时,你复制控制台变量状态然后执行你的特定代码:

    void MyFunc()
    {
        int GBufferFormat = CVarGBufferFormat.GetValueOnRenderThread();
    
        if(CurrentGBufferFormat != GBufferFormat)
        {
            CurrentGBufferFormat = GBufferFormat;
    
            // custom code
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  2. 你可以注册一个控台变量sink,比如:

    static void MySinkFunction()
    {
        bool bNewAtmosphere = CVarAtmosphereRender.GetValueOnGameThread() != 0;
    
        // by default we assume the state is true
        static bool GAtmosphere = true;
    
        if (GAtmosphere != bNewAtmosphere)
        {
            GAtmosphere = bNewAtmosphere;
    
            // custom code
        }
    }
    
    FAutoConsoleVariableSink CMyVarSink(FConsoleCommandDelegate::CreateStatic(&MySinkFunction));
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    这个sink会在渲染前的主线程调用,这个函数并不获取控制台变量名字/指针因为这可能会经常导致错误的行为。如果多个控制台变量(比如r.SceneColorFormat,r.GBufferFormat)应该都触发这个改变,最好在它们都改变后再调用这段代码,而不是一个接着一个调用。

  3. 最后一个方法,使用回调函数,你应该尽量避免使用这个方法因为如果不小心使用它可能会导致几个问题:

    • 循环调用导致死锁,我们应该避免死锁但是回调机制并不能明确避免。
    • 回调可能在Set被调用时的任何时候发生,你的代码需要应对所有情况,比如初始化,序列化,通常你可以认为它常在主线程上运行。

    我们建议不要使用这个方法除非你解决了上面提到的几个问题:

    void OnChangeResQuality(IConsoleVariable* Var)
    {
        SetResQualityLevel(Var->GetInt());
    }
    
    CVarResQuality.AsVariable()
        ->SetOnChangedCallback(FConsoleVariableDelegate::CreateStatic(&OnChangeResQuality));
    
    1
    2
    3
    4
    5
    6
    7

# 控制台变量的行为和模式

  • 控制台变量应该反映出用户的输入,不必是系统的状态(比如MotionBlur 0/1,一些平台可能不会支持它),变量的状态不应该被代码改变,否则用户可能会怀疑他是否输错了因为变量并不是他指定的状态或者怀疑因为其他变量的状态他并没有能力改变控制台变量。
  • 要总是提供友好的解释说明变量是用来干什么的,能被指定哪些值让它有意义。
  • 大部分控制台变量只为了开发,所以指定ECVF_Cheat标志变量是好主意,当然如果能使用某些定义来指定这些变量什么时候不被编译可能会更好,比如(#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST))。
  • 变量名在具有描述性的同时要尽量的小,避免使用否定含义,比如EnableMotionBlur,MotionBlurDisable,MBlur,HideMotionBlur,使用大小写来让它更方便和容易读。
  • 对于缩进,你要尽量使用固定宽度字体输出(而不是按比例的)。
  • 在引擎初始化的时候注册变量是很重要的,因为这样自动补全和DumpConsoleCommands和Help命令可以工作。

如果你想了解更多细节,请阅读ConsoleManager.h。

# 加载控制台变量

在引擎启动的时候,控制台变量的状态可从Engine/Config/ConsoleVariables.ini文件中读取并被加载,这个地方是为本地开发者预留的,它不应该作为项目的设置,你可以在这个文件中找到更多的细节:

; This file allows you to set console variables on engine startup (In undefined order).
; Currently there is no other file overriding this one.
; This file should be in the source control database (for the comments and to know where to find it)
; but kept empty from variables.
; A developer can change it locally to save time not having to type repetitive
; console variable settings. The variables need to be in the section called [Startup].
; Later on we might have multiple named sections referenced by the section name.
; This would allow platform specific or level specific overrides.
; The name comparison is not case sensitive and if the variable does not exist, it is ignored.
;
; Example file content:
;
; [Startup]
; FogDensity = 0.9
; ImageGrain = 0.5
; FreezeAtPosition = 2819.5520 416.2633 75.1500 65378 -25879 0

[Startup]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

你也可以将这些设置放在Engine/Config/BasEngine.ini文件中:

[SystemSettings]
r.MyCvar = 2

[SystemSettingsEditor]
r.MyCvar = 3
1
2
3
4
5

这些设置也可以来自Script/Engine.RendererSettings,这些项目设置像下面这样:

UPROPERTY(config, EditAnywhere, Category=Optimizations, meta=(
    ConsoleVariable="r.EarlyZPassMovable",DisplayName="Movables in early Z-pass",
    ToolTip="Whether to render movable objects in the early Z pass. Need to reload the level!"))
    uint32 bEarlyZPassMovable:1;
1
2
3
4

这些设置可以在编辑器UI中改变,项目设置不应该混杂可扩展性设置,为了避免优先级问题。

其他设置可以来自可扩展性特性,请查看Config/BaseScalability.ini或可扩展性相关文档来查看更多信息。

# 命令行

命令行允许你设置控制台变量,调用控制台命令或执行命令,下面是一个例子:

UE4Editor.exe GAMENAME -ExecCmds="r.BloomQuality 12;vis 21;Quit"
1

在这我们执行了3条命令,注意以这种方法设置控制台变量需要你省略"=",但是在一个ini文件中你不能省略"="。

# 优先级

控制台变量可以从各种源码中被重写,比如user/editor/project设置,命令行,consolevariables.ini等,为了在保持某些重写(命令行的重写)的同时能再修改一些设置,比如项目设置可从编辑器UI中改变,我们引入了优先级,现在所有的设置都可被任何顺序应用。

请看IConsoleManager.h:

// lowest priority (default after console variable creation)
ECVF_SetByConstructor =         0x00000000,
// from Scalability.ini
ECVF_SetByScalability =         0x01000000,
// (in game UI or from file)
ECVF_SetByGameSetting =         0x02000000,
// project settings
ECVF_SetByProjectSetting =      0x03000000,
// per device setting
ECVF_SetByDeviceProfile =       0x04000000,
// per project setting
ECVF_SetBySystemSettingsIni =   0x05000000,
// consolevariables.ini (for multiple projects)
ECVF_SetByConsoleVariablesIni = 0x06000000,
// a minus command e.g. -VSync 
ECVF_SetByCommandline =         0x07000000,
// least useful, likely a hack, maybe better to find the correct SetBy...
ECVF_SetByCode =                0x08000000,
// editor UI or console in game or editor
ECVF_SetByConsole =             0x09000000,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

在有些情况中,你可能看到日志输出:

Console variable 'r.MyVar' wasn't set (Priority SetByDeviceProfile < SetByCommandline)
1

这是被有意设计的,否则会引起一些代码问题,比如命令行强制用户设置。在优先级里我们能看到谁是最后一次设置变量的也是很有用的,你可以在获取变量状态时得到这个信息:

> r.GBuffer

r.GBuffer = "1"      LastSetBy: Constructor
1
2
3

# 未来可能添加的特性

  • 现在控制台变量只能被C++创建,在后面可能会改变这点。
  • 我们考虑添加枚举和布尔类型,但是要做到这点还有很多问题要解决。现在我们建议使用int,或者如果有需要,使用strings。
  • 帮助文本对用户是方便的,但是将其保存到可执行程序中让作弊者也更方便,我们考虑添加一个define来阻止帮助文本会被编译进可执行程序里。

# 取消注册控制台变量

UnregisterConsoleVariable方法允许你移除控制台变量,至少从用户的角度是这样的。但是为了不发生崩溃在指针获取数据时变量仍旧存在(有未注册flag)。如果一个新的变量以同样的名字注册,旧的变量会被恢复,标志flag也从新的变量获得,这样DLL加载或不加载都能工作,甚至变量状态也不会丢失,注意这个不适用于控制台变量引用。

# 实践

下面是笔者自己的实践样例:

TestDebug.h文件内容:


#include "CoreMinimal.h"
#include "printCurrent.h"
#include "GameFramework/Actor.h"
#include "Kismet/GameplayStatics.h"
#include "Engine/World.h"
#include "TestDebug.generated.h"

UCLASS()
class TESTTHIRDPERSON_API ATestDebug : public AActor
{
    GENERATED_BODY()
    
public:	
    // Sets default values for this actor's properties
    ATestDebug();
    int currentValue = 0;
    
    //void OnValueChange(IConsoleVariable* Var);


protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;

public:	
    // Called every frame
    virtual void Tick(float DeltaTime) override;

};

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

TestDebug.cpp文件内容:

// Fill out your copyright notice in the Description page of Project Settings.


#include "TestDebug.h"

static TAutoConsoleVariable<int32> MyCmd(
    TEXT("MyCmd"),
    2,
    TEXT("this is my first variable,")
    TEXT("change it to other values"),
    ECVF_RenderThreadSafe
);

// Sets default values
ATestDebug::ATestDebug()
{
     // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = true;
}


void OnValueChange(IConsoleVariable * Var)
{
    GEngine->AddOnScreenDebugMessage(-1, 1.f, FColor::Red, FString::Printf(TEXT("MyCmd Value is changed by delegate:%d!"), Var->GetInt()));
}

// Called when the game starts or when spawned
void ATestDebug::BeginPlay()
{
    Super::BeginPlay();
    MyCmd.AsVariable()->SetOnChangedCallback(FConsoleVariableDelegate::CreateStatic(&OnValueChange));

}

// Called every frame
void ATestDebug::Tick(float DeltaTime)
{

    Super::Tick(DeltaTime);

    int tempInt = MyCmd.GetValueOnGameThread();
    if (currentValue != tempInt) {
        currentValue = tempInt;
        GEngine->AddOnScreenDebugMessage(-1, 1.f, FColor::Red, FString::Printf(TEXT("MyCmd Value:%d!"), currentValue));
    }
}


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

上面是使用两种方法来追踪变量的值,第一种是在Tick函数时刻检查,第二种是使用委托,当控制台变量更改时触发委托函数。