斯坦福UE4 + C++课程学习记录 12:Debug入门

目录

1. 更新炸药桶

2. 输出调试信息

3. 使用断点

4. 使用断言

5. 完整代码


        课程中首先提到了一个重要的点,就是尽量不要使用UE的热重载,即修改保存C++代码后不关闭UE直接使用它的编译功能。因为这个功能不太稳定,比较容易出Bug,就算UE提示编译完成也有几率出错(我自己也遇到过,还因为这个bug反复排查自己的代码...)。所以,在修改C++代码后尽量选择关闭UE,再在VS中使用编译打开UE,这样可以尽可能确保代码生效。

        这节会在之前第6节实现的炸药桶基础上,展示一些在UE中Debug的技巧。

1. 更新炸药桶

        在之前的第6节,我们已经实现了炸药桶受击后发出冲击力的效果。但在上一节的内容中,我们更改了魔法粒子的蓝图,修改了自定义碰撞预设Projectile的配置,导致现在的炸药桶不能再被魔法粒子触发了。因此,在开始Debug相关的内容前,我们需要对炸药桶进行一些改变。

        之所以炸药桶不爆炸,是因为没有执行SurExplosiveBarrel.cpp中的OnActorHit函数。在第6节的内容中我提到“暂时没有理解代码中OnActorHit()的调用方式,待以后补充”,所以刚好在这个地方学习一下。

        在官方文档中对这个函数的说明略少,因此从课程提供的项目源码入手。在仔细阅读了课程提供的源码后,我得知OnActorHit函数是以“组件命中时(OnComponentHit)”事件的形式被UE自动调用的(UE蓝图中已经有OnComponentHit这个事件节点了,代码中传入的一大堆输入其实也是这个节点的引脚)。所以,我们在C++中编写的函数需要绑定到这个事件上才能生效。在之前第6节进行实现时,我复制了完整的课程源码并运行成功,但出于精简知识点的目的,我一边热重载一边删除其中的代码,在当时项目也的确正常运行了,不知道这是不是UE热重载的bug...

        在补充了绑定事件的代码后,炸药桶可以成功被魔法粒子触发了,完整代码见末尾。这里要注意,打开炸药桶的“模拟物理”,否则炸药桶会在原地一动不动;关闭炸药桶的“模拟生成命中事件”,否则炸药桶一落地就自动爆炸,如图12-1:

图12-1 开启“模拟生成命中事件”

2. 输出调试信息

        这部分将介绍几个在UE开发中输出调试信息的方法,方便日后我们开发时的debug工作。

        首先,使用日志是各种计算机开发都必不可少的环节,在UE中可以使用UE_LOG来输出日志。课程给出了英文的wiki,其中对日志做了比较详细的介绍。示例用法如下:

UE_LOG(LogTemp, Log, TEXT("OtherActor is %s, at game time %f"), *GetNameSafe(OtherActor), GetWorld()->TimeSeconds);

        三个输入分别为:
        1. 日志显示的类别名,我们可以为不同模块的日志添加不同的类别名,以便我们使用过滤器快速筛选需要的日志消息 ;
        2. 日志显示的级别,级别越高显示的内容越重要,显示的颜色越鲜艳,相应的输出消息数量也越少;
        3. 输出的内容,TEXT()用于将字符串转为UE所需格式,且使用UNICODE编码支持更多符号。GetNameSafe在对象为空时返回空,这样就不需要额外判空,此外它将返回FString类型,要注意字符串类型前要加一个星号。

        对于具有图形化界面的UE来说,它不止可以在日志输出打印调试信息,还可以直接在运行关卡时实时打印各种调试信息,如我们之前在打开宝箱时使用过的射线检测、以及魔法攻击时显示的圆形等等。除了直接显示图形,UE也支持显示字符串。这段代码实现了,在魔法粒子击中的地方显示位置信息的字符串:

FString CombStr = FString::Printf(TEXT("Hit at %s"), *Hit.ImpactPoint.ToString());
// 获取世界,位置,打印的内容,需要attach的actor,颜色,持续时间,是否有影子
DrawDebugString(GetWorld(), Hit.ImpactPoint, CombStr, nullptr, FColor::Green, 2.0f, true);

        这样保存编译后进入关卡,就可以看见我们的调试信息,其窗口可在 窗口 -> 开发者工具 -> 输出日志 中选择打开。左键控制Gideon攻击后,可以看见我们设置的LogTemp被成功打印出来。

图12-2 UE输出日志

        如果是在VS中利用调试打开的UE,同样的日志消息还会输出到VS的输出窗口中:

图12-3 VS输出日志

         在游戏中,利用DrawDebugString打印的字符串也正常显示了:

图12-4 DrawDebugString效果

3. 使用断点

        有程序开发经验的朋友应该对打断点都不陌生,断点调试是Debug的一大利器。通过使用断点让程序停在某一个地方,然后在开发者的监视下逐步执行,这对找到程序bug和梳理程序运行流程都有很大帮助。UE开发的断点有两种,一种是普通的利用VS打断点,另一种则是直接在蓝图系统中打断点。

        在VS打断点的方法很简单,在任意一行需要中断的代码前点击一下,显示一个红色的圆圈即可,这里我选择在控制炸药桶爆炸的代码处设置断点。

图12-5 VS设置断点

        随后在VS中点击调试(或直接按F5),运行关卡,向炸药桶发射魔法粒子。此时会直接跳回VS界面,断点处会显示黄色的小箭头来表示代码执行的位置,此时就可以进行各种调试操作了。如果此时回到UE界面,会发现整个UE都卡住,直到我们在VS点击继续才会继续运行。另外,为了调试更加方便地步入(step into)UE的代码,我们可以在Epic中对应版本引擎的“选项”中选择下载“输入调试用符号”。

        关于VS调试还有一个值得注意的点,如果遇到打了断点却没有触发的情况,有可能是因为C++自动优化了代码,这时可以尝试将VS的“解决方案配置”(在调试按钮的旁边)从“Development Editor”切换为“DebugGame Editor”,这将防止代码自动优化,输出更加详细的测试信息,但也会增加编译时间。有关VS调试的技巧方法等不属于UE开发的内容,大家可以自行了解。

图12-6 VS运行到断点

        在蓝图中设置断点的方式同样简单,打开蓝图后在任意节点右键选择“添加断点”即可。运行后游戏同样会停留在断点处,此时蓝图编辑器的上方会出现一系列调试用的控制按钮。与在VS打断点的不同,我们可以在蓝图中实时看见程序的流向,这在之前第10节优化宝箱动画的文章中已经介绍过了。


4. 使用断言

        断言也是在Debug阶段广泛使用的手段,它类似于if判断会验证一个表达式的真假,但区别在于断言不需要像if那样编写大量重复的判断和打印日志代码,一旦断言检查到不符合预期,程序会直接中断甚至抛出异常,利于开发者定位问题;且断言只在debug阶段有效,对于打包好的程序是不会生效的。

        以角色的PrimaryAttack作为例子,首先将PrimaryAttack_TimeElapsed函数添加一个判空的逻辑,这完全是出于提高程序健壮性的正确考虑:

void ASurCharacter::PrimaryAttack_TimeElapsed() {
	if (ProjectileClass) {
		// 获取模型右手位置
		FVector RightHandLoc = GetMesh()->GetSocketLocation("Muzzle_01");

		// 朝向角色方向,在角色的右手位置生成
		FTransform SpawnTM = FTransform(GetActorRotation(), RightHandLoc);

		// 此处设置碰撞检测规则为:即使碰撞也总是生成
		FActorSpawnParameters SpawnParams;
		SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
		SpawnParams.Instigator = this;

		GetWorld()->SpawnActor<AActor>(ProjectileClass, SpawnTM, SpawnParams);
	}
}

        但这样的写法却可能产生类似这样的问题:在这段代码开发很久之后,你已经遗忘了具体细节,或者这根本就由你的另一个同事开发。偶然情况下,调用这个函数时ProjectileClass为空,此时这段代码不会运行,但程序也没有任何提示,这无疑加大了debug的工作量。

        要解决这个问题,一种方法是在else写好错误的输出信息,另一种就是使用UE的断言。只需将if处改为 if (ensure(ProjectileClass)),ensure就是UE中的断言函数。此外还有check,但两者不同之处在于ensure提示错误但不会中断程序,check会直接结束你的关卡测试,所以通常使用ensure。

        为了测试ensure的效果,我们在UE中手动指定Player蓝图类的ProjectileClass为空,如图12-7所示:

图12-7 Player设置

        运行关卡后,一旦左键攻击,UE就会卡住,并输出红色的Error日志信息。如果是在VS中调试打开的UE,还会直接跳转回VS中显示错误。另外,ensure只会在编译完成后触发提示一次,若要每次出错都提示,可以选择ensureAlways。


5. 完整代码

SurExplosiveBarrel.h

#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SurExplosiveBarrel.generated.h"

class UStaticMeshComponent;
class URadialForceComponent;

UCLASS()
class SURKEAUE_API ASurExplosiveBarrel : public AActor
{
	GENERATED_BODY()
	
public:	
	
	ASurExplosiveBarrel();

protected:

	UPROPERTY(VisibleAnywhere)
	UStaticMeshComponent* MeshComp;

	UPROPERTY(VisibleAnywhere)
	URadialForceComponent* ForceComp;

	virtual void PostInitializeComponents() override;

	// 必须使用UFUNCTION宏才能绑定事件
	UFUNCTION()
	void OnActorHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);

};

SurExplosiveBarrel.cpp

#include "SurExplosiveBarrel.h"
#include "PhysicsEngine/RadialForceComponent.h"
#include "Components/StaticMeshComponent.h"
#include "DrawDebugHelpers.h"

// Sets default values
ASurExplosiveBarrel::ASurExplosiveBarrel()
{
 
	MeshComp = CreateDefaultSubobject<UStaticMeshComponent>("MeshComp");
	// UE中的“模拟物理”选项
	MeshComp->SetSimulatePhysics(true);
	// 等同于在UE中将“碰撞预设”设置为“PhysicsActor”
	MeshComp->SetCollisionProfileName(UCollisionProfile::PhysicsActor_ProfileName);
	RootComponent = MeshComp;

	ForceComp = CreateDefaultSubobject<URadialForceComponent>("ForceComp");
	ForceComp->SetupAttachment(MeshComp);

	ForceComp->Radius = 750.0f;			 // 爆炸范围
	ForceComp->ImpulseStrength = 700.0f; // 冲击力
	ForceComp->bImpulseVelChange = true; // 忽略质量大小;见UE中ForceComp的“冲量速度变更”
}

// PostInitializeComponents在Actor初始化完毕后再调用
void ASurExplosiveBarrel::PostInitializeComponents()
{
	// 执行该函数原本的功能
	Super::PostInitializeComponents();
	// 绑定到OnComponentHit事件上
	MeshComp->OnComponentHit.AddDynamic(this, &ASurExplosiveBarrel::OnActorHit);
}

void ASurExplosiveBarrel::OnActorHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
	ForceComp->FireImpulse();

	// log信息的category,log/warning/error等表示日志的详细程度,打印的文字内容

	UE_LOG(LogTemp, Log, TEXT("OtherActor is %s, at game time %f"), *GetNameSafe(OtherActor), GetWorld()->TimeSeconds);
	UE_LOG(LogTemp, Warning, TEXT("HHHHHHHHHHHHH"));

	FString CombStr = FString::Printf(TEXT("Hit at %s"), *Hit.ImpactPoint.ToString());
	// 获取世界,位置,打印的内容,需要attach的actor,颜色,持续时间,是否有影子
	DrawDebugString(GetWorld(), Hit.ImpactPoint, CombStr, nullptr, FColor::Green, 2.0f, true);
}

猜你喜欢

转载自blog.csdn.net/surkea/article/details/127178815