虚幻引擎4:变量调试组件
翻译:王成林(麦克斯韦的麦斯威尔 ) 审校:黄秀美(厚德载物)
如果你想进行快速遍历,你需要有很好的调试工具。在这篇教程中我将使用设置/获取变量教程中的知识画出一个含有一列参数的窗口。基本上如果你想显示一个变量你需要将它打印在屏幕上,或者为特定的Actor创建一个自定义窗口。使用调试组件:
· 你能够调试任意actor中的任何一个变量
· 你不需要自定义任何打印函数或者窗口
该组件只能使用C 实现,但是它超级简单!(学习UE4的C )
这篇教程是使用虚幻引擎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,将Slate和SlateCore添加到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(变量)然后将它的值作为字符串导出!
现在如果你编译你的项目你可以创建简单的测试了。
非常简单!希望对你们有帮助!
【版权声明】
原文作者未做权利声明,视为共享知识产权进入公共领域,自动获得授权。