虚幻引擎4:变量调试组件

发表于2017-07-20
评论1 3.5k浏览

翻译:王成林(麦克斯韦的麦斯威尔 ) 审校:黄秀美(厚德载物) 


如果你想进行快速遍历,你需要有很好的调试工具。在这篇教程中我将使用设置/获取变量教程中的知识画出一个含有一列参数的窗口。基本上如果你想显示一个变量你需要将它打印在屏幕上,或者为特定的Actor创建一个自定义窗口。使用调试组件:

·       你能够调试任意actor中的任何一个变量

·       你不需要自定义任何打印函数或者窗口

该组件只能使用C 实现,但是它超级简单!(学习UE4C

这篇教程是使用虚幻引擎4.11创建的,确保使用相同版本的引擎。


理论

调试是游戏开发的重要一环。如果你在开发的过程中思考“我该如何最快修复系统中的漏洞呢?” ,你最终会得到一些工具帮助你快速地寻找漏洞并修复它。

游戏充满了漏洞,而拥有这些工具是至关重要的。游戏开发者有很多种方法:

·       创建一个记录系统,该系统允许你记录每次游戏过程(可以回放,获取变量的状态等等),所以每当别人发现一个bug时他可以将他的回放发给你(我希望写一篇关于此的教程)

·       创建一个机器人,它会在夜晚没人工作的时候测试你的游戏

·       创建一个机器人,它会无限重复你的UI和游戏循环

·       为一些特征创建自动测试:例如生成到点X的路线100000000次,检查结果

·       能够快速进入“调试”模式,该模式为你提供关于Actor状态,变量等你想要的任何内容的信息

·       ……以及更多

所有这些例子都能在虚幻引擎4中实现。我不是一名程序员,所以我不会介绍如何实现这些,但是我会给出一个谁都能懂的小例子。

我将创建一个能绑定到任何actor上的组件,该组件会显示你希望看到的变量数值。使用这个方法你不再需要在蓝图中加入一个Print节点来检查变量状态了。你将能够看到各种变量的内容。我将用到一些获取/设置变量教程中的一些知识。


概述

首先我们将创建自定义结构体以储存变量名称和变量的值,我们会在ShowDebugWidget中用到它们。

ShowDebugWidget是一个自定义的UMG窗口类,它有一个将数据传递给蓝图的事件。

这个系统的重点是ShowDebugComponent——可以被放在任何Actor中的简单的Actor组件。它会创建窗口组件并将其添加给Owner,然后将ShowDebugWidget类传递给它。


基础结构体

通常我们可以将它存储在TMap中,但是蓝图还不能拓展TMap。我将使用结构体,将变量的名称和内容作为字符串。

Visual Studio中打开你的项目,然后移动到你的项目名.h文件。对我来讲它是ShooterTutorial.h文件。在这个主类中我将创建一些新的结构体:

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
USTRUCT(BlueprintType)
struct FDebugVariable
{
    GENERATED_USTRUCT_BODY()
  
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Debug Variable")
    FName Name;
  
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Debug Variable")
    FString Value;
  
    void SetName(const FName NewValue)
    {
        Name = NewValue;
    }
  
    void SetValue(const FString NewValue)
    {
        Value = NewValue;
    }
  
    FDebugVariable()
    {
        Name = "None";
        Value = "None";
    }
  
};

编译完成后你将能够在蓝图中合成(Make)该结构体:

我们就是这样存储数据的。我们将数值作为字符串,因为要把它显示在UMG中。


自定义C UMG窗口

为了能够创建自定义UMG窗口你需要打开你的项目名.Build.cs然后将UMG添加到PublicDependency,将SlateSlateCore添加到PrivateDependency

1
2
3
4
5
6
7
8
public class ShooterTutorial : ModuleRules
{
    public ShooterTutorial(TargetInfo Target)
    {
        PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "Playfab", "Json", "JsonUtilities", "UMG"});
  
        PrivateDependencyModuleNames.AddRange(new string[] {"Playfab", "Json", "JsonUtilities", "Slate", "SlateCore" });
}

我们需要这样做,因为我们要创建自定义事件将变量数据传递给窗口。

现在打开编辑器然后添加一个拓展自UserWidget的名为ShowDebugWidget的新C 类。

我们将创建一个自定义事件将变量传递给蓝图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Fill out your copyright notice in the Description page of Project Settings.
  
#pragma once
  
#include "Blueprint/UserWidget.h"
#include "ShowDebugWidget.generated.h"
  
/**
 *
 */
UCLASS()
class SHOOTERTUTORIAL_API UShowDebugWidget : public UUserWidget
{
    GENERATED_BODY()
         
public:
    //* Function called when variables are updated. Returning list of updated variables */
    UFUNCTION(BlueprintNativeEvent, Category = "Variables")
    void OnVariablesAdded(const TArray<fdebugvariable>& VariablesList); //this is actual Event in Blueprint
        void OnVariablesAdded_Implementation(const TArray<fdebugvariable>& VariablesList);  // this is function that can be used in C (not visible in Blueprints)
};

你可以在这里找到更多关于将事件暴露给蓝图的内容。我们声明了OnVariablesUpdated事件,它将传递DebugVariable数组。

C 文件非常简单:

1
2
3
4
5
6
7
#include "ShooterTutorial.h"
#include "ShowDebugWidget.h"
  
void UShowDebugWidget::OnVariablesUpdated_Implementation(const TArray<fdebugvariable>& VariablesList)
{
    //C implementation is empty. Proper implementation should be in Blueprints as it is more about layout.
}

我们不会用到该函数的C 实现,因为UMG设计师更擅长使用这些变量设计布局。


UMG的父类重新设置为自定义类

在内容浏览器中创建一个名为UI_MainShowDebugVariables的新用户窗口(UserWidget)。打开它然后我们将该窗口的父类从UserWidget改为我们自定义的ShowDebugWidget

找到ShowDebugWidget类。


UMG布局

创建另一个名为Widget_DebugVariableWithName的用户窗口。这应该是一个普通的用户窗口——不需要重新设置父类。这里你可以找到它的层级(记得要删除画布面板(Canvas Panel)!):


该窗口会按照程序被添加到UI_MainShowDebugVariables中。打开事件图表然后创建一个名为UpdateVariable的函数,该函数只有一个输入:DebugVariable结构体:

以上就是该窗口的全部内容。现在我们移动到UI_MainShowDebugVariables中。它也不需要有画布面板,所以请删除它。然后添加垂直框(Vertical Box)(将它设置为根)然后将它标记为变量,这样你在图表中就能看到它了。

在事件图表中搜索OnVariableUpdated(当变量更新时):

没错,它是我们的自定义事件!由于该窗口的父级被重新设定为ShowDebugWidget,我们可以将该事件添加到图表中!

这应该一目了然。对于每个来自OnVariableUpdate事件的项目,我们将Widget_DebugVariableWithName添加到垂直框中。这就是这里做的全部内容了。


ShowDebugComponent

现在到重点了。创建一个新的拓展自ActorComponent的类,将其命名为ShowDebugComponent

这是.h文件:

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
// Fill out your copyright notice in the Description page of Project Settings.
  
#pragma once
  
#include "Components/ActorComponent.h"
#include "ShowDebugWidget.h"  //need to include this to be able to declare UShowDebugWidget
#include "WidgetComponent.h" //this as well to declare UWidgetComponent
#include "ShowDebugComponent.generated.h"
/**
* With this component you can add WidgetComponent drawing list of variables. USE THIS ONLY WHEN DEBUGGING as it 2ms per each component!
*/
UCLASS(Blueprintable, ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class SHOOTERTUTORIAL_API UShowDebugComponent : public UActorComponent
{
    GENERATED_BODY()
  
public:
    // Sets default values for this component's properties
    UShowDebugComponent();
  
    // Called when the game starts
    virtual void BeginPlay() override;
     
    // Called every frame
    virtual void TickComponent( float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction ) override;
  
    //* Choose which variables should be show in debug widget */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Variables")
    TArray<fname> VariablesNames;
  
    //** Get variables from VariablesNames and update their values */
    UFUNCTION(BlueprintCallable, Category = "Variables")
    TArray<fdebugvariable> UpdateVariables();
     
    //*Which Widget should be added to WidgetComponent */
    UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Variables")
    TSubclassOf<class ushowdebugwidget=""> ShowDebugWidgetClass;
     
    //*Do we want to use Custom Tick Interval or just Tick? */
    UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Variables")
    bool isUsingCustomTickInterval;
  
    //*If using custom tick interval store interval timer */
    UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Variables")
    float CustomTickInterval;
     
    //* Storing reference to created WidgetComponent*/
    UPROPERTY()
    UWidgetComponent* WidgetComponentRef;
    //* Storing reference to ShowDebugWidget UMG widget */
    UPROPERTY()
    UShowDebugWidget* ShowDebugWidgetRef;
};

我们详细解释一下文件中的几个重点。

1
2
3
//* Choose which variables should be show in debug widget */
  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Variables")
  TArray<fname> VariablesNames;

在这个FName数组中我们可以添加我们希望调试的变量的名字。

该数组将用于UpdateVariables()。我们来到实际的实现部分。这里你可以找到整个cpp文件:

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// With this component you can add WidgetComponent drawing list of variables. USE THIS ONLY WHEN DEBUGGING as it's 2ms per each component!
  
#include "ShooterTutorial.h"
#include "ShowDebugComponent.h"
#include "WidgetComponent.h"
  
// Sets default values for this component's properties
UShowDebugComponent::UShowDebugComponent()
{
    //sets default values
    CustomTickInterval = 0.25f;
    isUsingCustomTickInterval = true;
    bWantsBeginPlay = true;
    PrimaryComponentTick.bCanEverTick = true;
}
  
// Called when the game starts
void UShowDebugComponent::BeginPlay()
{
    Super::BeginPlay();
    //If want to use custom tick interval set the Tick interval
    if (isUsingCustomTickInterval)
    {
        PrimaryComponentTick.TickInterval = CustomTickInterval;
    }
    //create UWidgetComponent component object
    WidgetComponentRef = NewObject<uwidgetcomponent>(this);
    //Sets UWidgetComponent User widget class to display
    WidgetComponentRef->SetWidgetClass(ShowDebugWidgetClass);
    //Set UWidgetComponent draw size
    WidgetComponentRef->SetDrawSize(FVector2D(2000, 500));
    //This will work only in 4.11 - setting widget space to screen not world
    WidgetComponentRef->SetWidgetSpace(EWidgetSpace::Screen);
    //Make sure widget don't have any collision
    WidgetComponentRef->SetCollisionEnabled(ECollisionEnabled::NoCollision);
    //Add created component to the Owner.
    GetOwner()->AddComponent(FName("ShowDebugWidgetComponent"), false, GetOwner()->GetRootComponent()->GetComponentTransform(), WidgetComponentRef);
    //Attach component to the root
    WidgetComponentRef->AttachTo(GetOwner()->GetRootComponent());
    //Register component. Without this it won't be visible
    WidgetComponentRef->RegisterComponent();
    //After registering store reference to the User Widget object (User Widget)
    ShowDebugWidgetRef = Cast<ushowdebugwidget>(WidgetComponentRef->GetUserWidgetObject());
}
  
  
// Called every frame
void UShowDebugComponent::TickComponent( float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction )
{
    Super::TickComponent( DeltaTime, TickType, ThisTickFunction );
     
    //If UserWidget (UShowDebugWidget) is created call OnVariablesUpdated event so it can be managed in Blueprints to fill the data in layout.
    if (ShowDebugWidgetRef)
    {
        ShowDebugWidgetRef->OnVariablesUpdated(UpdateVariables());  // heres where we actually call our custom event
    }
}
  
TArray<fdebugvariable> UShowDebugComponent::UpdateVariables()
{
    //variables array that we will return
    TArray<fdebugvariable> Variables;
    FDebugVariable TempVar;
  
    for (int i = 0; i < VariablesNames.Num(); i )  // iterate all VariableNames that someone added to the array
    {
        UProperty *Prop = FindField<uproperty>(GetOwner()->GetClass(), VariablesNames[i]); // try to find the UProperty named as VariablesNames[i]
        if (Prop)
        {
            FString VariableToString;
            void *Value = Prop->ContainerPtrToValuePtr<void>(GetOwner()); //this function will create pointer to the value
            if (Value)
            {
                Prop->ExportTextItem(VariableToString, Value, NULL, NULL, 0);  // this function will export the value to string!
  
                TempVar.SetName(VariablesNames[i]);  //sets the name
                TempVar.SetValue(VariableToString); //sets the value
                Variables.Add(TempVar); //add it to the array that we will return
            }
        }
        else //wasn't able to find any variable
        {
            FString CantFindName = FString("not found: ") FString(VariablesNames[i].ToString());  //create simple string
            TempVar.SetName(FName(*CantFindName));
            TempVar.SetValue(FString(""));
            Variables.Add(TempVar);
        }
    }
    return Variables;
}

我们一步一步来。

构造函数:

1
2
3
4
5
6
7
8
9
// Sets default values for this component's properties
UShowDebugComponent::UShowDebugComponent()
{
    //sets default values
    CustomTickInterval = 0.25f;
    isUsingCustomTickInterval = true;
    bWantsBeginPlay = true;
    PrimaryComponentTick.bCanEverTick = true;
}

构造函数被用于设置默认值。

BeginPlay

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
void UShowDebugComponent::BeginPlay()
{
    Super::BeginPlay();
    //If want to use custom tick interval set the Tick interval
    if (isUsingCustomTickInterval)
    {
        PrimaryComponentTick.TickInterval = CustomTickInterval;
    }
    //create UWidgetComponent component object
    WidgetComponentRef = NewObject<uwidgetcomponent>(this);
    //Sets UWidgetComponent User widget class to display
    WidgetComponentRef->SetWidgetClass(ShowDebugWidgetClass);
    //Set UWidgetComponent draw size
    WidgetComponentRef->SetDrawSize(FVector2D(2000, 500));
    //This will work only in 4.11 - setting widget space to screen not world
    WidgetComponentRef->SetWidgetSpace(EWidgetSpace::Screen);
    //Make sure widget don't have any collision
    WidgetComponentRef->SetCollisionEnabled(ECollisionEnabled::NoCollision);
    //Add created component to the Owner.
    GetOwner()->AddComponent(FName("ShowDebugWidgetComponent"), false, GetOwner()->GetRootComponent()->GetComponentTransform(), WidgetComponentRef);
    //Attach component to the root
    WidgetComponentRef->AttachTo(GetOwner()->GetRootComponent());
    //Register component. Without this it won't be visible
    WidgetComponentRef->RegisterComponent();
    //After registering store reference to the User Widget object (User Widget)
    ShowDebugWidgetRef = Cast<ushowdebugwidget>(WidgetComponentRef->GetUserWidgetObject());
}

BeginPlay中我创建了WidgetComponent,它被添加到了所有者(Owner)中。我还对窗口进行了设置:

·       设置应该被组件画出的类

·       设置大小,窗口空间,碰撞

然后我存储了引用,这样我就可以在Tick中调用我们的自定义事件了。

Tick

1
2
3
4
5
6
7
8
9
10
11
// Called every frame
void UShowDebugComponent::TickComponent( float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction )
{
    Super::TickComponent( DeltaTime, TickType, ThisTickFunction );
     
    //If UserWidget (UShowDebugWidget) is created call OnVariablesUpdated event so it can be managed in Blueprints to fill the data in layout.
    if (ShowDebugWidgetRef)
    {
        ShowDebugWidgetRef->OnVariablesUpdated(UpdateVariables());  // heres where we actually call our custom event
    }
}

以上只是使用UpdateVariables()函数调用我们的OnVariablesUpdated自定义事件,UpdateVariables()函数返回一个FDebugVariable结构体的数组。

UpdateVariables

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
TArray<fdebugvariable> UShowDebugComponent::UpdateVariables()
{
    //variables array that we will return
    TArray<fdebugvariable> Variables;
    FDebugVariable TempVar;
  
    for (int i = 0; i < VariablesNames.Num(); i )  // iterate all VariableNames that someone added to the array
    {
        UProperty *Prop = FindField<uproperty>(GetOwner()->GetClass(), VariablesNames[i]); // try to find the UProperty named as VariablesNames[i]
        if (Prop)
        {
            FString VariableToString;
            void *Value = Prop->ContainerPtrToValuePtr<void>(GetOwner()); //this function will create pointer to the value
            if (Value)
            {
                Prop->ExportTextItem(VariableToString, Value, NULL, NULL, 0);  // this function will export the value to string!
  
                TempVar.SetName(VariablesNames[i]);  //sets the name
                TempVar.SetValue(VariableToString); //sets the value
                Variables.Add(TempVar); //add it to the array that we will return
            }
        }
        else //wasn't able to find any variable
        {
            FString CantFindName = FString("not found: ") FString(VariablesNames[i].ToString());  //create simple string
            TempVar.SetName(FName(*CantFindName));
            TempVar.SetValue(FString(""));
            Variables.Add(TempVar);
        }
    }
    return Variables;
}

这是这篇教程最重要的部分。这个函数通过名称搜索UProperty(变量)然后将它的值作为字符串导出!

现在如果你编译你的项目你可以创建简单的测试了。

非常简单!希望对你们有帮助!


【版权声明】

原文作者未做权利声明,视为共享知识产权进入公共领域,自动获得授权。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

标签: