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

一、C++重写OnActorHit()事件

直接贴上代码便于理解:(先不考虑日志内容)

  • SExplosiveBarrel.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);

}; 
  1. virtual void PostInitializeComponents() override函数:
    (1)PostInitializeComponents 是一个虚函数,它在 Actor 的所有组件初始化完成后会被调用。这是在游戏中处理 Actor 的初始化设置属性的好地方。
    (2)在 PostInitializeComponents 中,可以确保所有组件已经初始化并且可以安全地访问和操作它们。这对于需要依赖于组件之间的相互关系的初始化逻辑很有用。
    (3)我们的爆炸桶 Explosive Barrel,需要在它初始化时设置某些物理属性绑定事件。这些是可以在 PostInitializeComponents 中完成的事情。

  2. UFUNCTION() void OnActorHit(...)函数
    (1) OnActorHit 是一个在 Actor 碰撞时会被调用的函数。为了能绑定到事件,它必须使用 UFUNCTION宏。
    (2)参数解释:
    UPrimitiveComponent* HitComp: 发生碰撞的组件。
    AActor* OtherActor: 与之碰撞的另一个 Actor。
    UPrimitiveComponent* OtherComp: 与之碰撞的另一个组件。
    FVector NormalImpulse: 碰撞的冲击力。
    const FHitResult& Hit: 碰撞的详细信息。
    (3)参数提供了详细的碰撞信息,可以根据这些信息来执行特定逻辑,比如触发爆炸效果、改变物理属性等。

  • SExplosiveBarrel.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);
} 
  1. 事件绑定:是一种机制,它允许将特定的事件(比如碰撞、输入等)与处理该事件的函数关联起来。这种机制使得当某个事件发生时,关联的函数会被自动调用,从而执行相应的逻辑。
  • 绑定事件可以使代码更加模块化和灵活。可以在一个地方定义事件处理逻辑,而在需要的地方绑定这些逻辑,而不必在每个地方都重复代码。
  1. OnComponentHitOnActorHit关系
  • OnComponentHitUPrimitiveComponent的一个事件,表示该组件发生了碰撞。
  • OnActorHit是我们定义的一个函数,用于处理组件发生碰撞时的逻辑。
  • 通过MeshComp->OnComponentHit.AddDynamic(this, &ASurExplosiveBarrel::OnActorHit);这一行代码,将 MeshComp组件的OnComponentHit事件与 ASurExplosiveBarrel类的OnActorHit函数绑定在了一起。这意味着每当MeshComp发生碰撞时,OnActorHit函数就会被自动调用。
  1. AddDynamic函数的作用
  • AddDynamic是一个,用于将事件与函数动态绑定。当 OnComponentHit事件触发时,OnActorHit函数会被调用。
  • 它可以用于以下场景
    处理碰撞:当一个爆炸桶被子弹击中时,桶发生爆炸并对周围物体产生影响。
    处理输入:当玩家按下某个键时,角色执行特定动作。
    处理触发器:当玩家进入某个区域时,触发某个事件(如打开门、播放音效等)。

二、输出调试信息

  1. 附上UE日志的参考链接

https://unrealcommunity.wiki/logging-lgpidy6i/

  1. 游戏开发中,常用的 Debug 手段有三种:
    ① 让函数返回错误码,由调用函数者处理错误
    ② 运用编程语言提供的异常处理函数,将异常抛出或就地修复
    ③ 断言Assert,对程序中那些不确定是否正确的东西进行检测,以便开发者在开发过程中尽早发现问题。
  2. UE_LOG是虚幻引擎提供的 C++,用于打印日志到控制台或者引擎的LOG 窗口中。
  • 如果是在VS中利用调试打开的UE,同样的日志消息还会输出到VS的输出窗口中 作者:surkea https://www.bilibili.com/read/cv19004443/ 出处:bilibili
  1. UE_LOG提供三个参数
    ① Log 类型Categories
    ② Log 级别 - UE_LOG 日志级别
    ③ 要打印的信息,包含一个 UE 的字符串,外加需要格式化的变量参数。

%s - 字符串
%d - 整数
%f - 浮点数

  • 使用TEXT()包裹的即为传入的字符串格式,可以在格式中使用上述参数来匹配后续传入的变量。类似printf函数的用法,我们可以打印变量中的数据。
  1. 示例用法如下:
UE_LOG(LogTemp, Log, TEXT("OtherActor is %s, at game time %f"), *GetNameSafe(OtherActor), GetWorld()->TimeSeconds); 
  • 三个输入分别为:
    ① 日志显示的类别名,以便使用过滤器快速筛选需要的日志消息 ;
    ② 日志显示的级别,级别越高显示的内容越重要,显示的颜色越鲜艳,相应的输出消息数量也越少;
    ③ 输出的内容,可使用UNICODE编码支持更多符号。GetNameSafe在对象为空时返回空,这样就不需要额外判空,此外它将返回FString类型,要注意字符串类型前要加一个星号。
  1. 使用 C++ 在屏幕上打印日志
  • 有时我们希望日志直接反馈在屏幕上,这就需要用到 AddOnScreenDebugMessage 函数了。
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::White, TEXT("This message will appear on the screen!"));
  • 参数如下:
    ① 参数 1:信息 ID(uint64)。若指定 ID,下一次打印会替换前一次显示的信息。注意:-1代表每次都会打印。
    ② 参数 2:消息显示的时间。(float
    ③ 参数 3:消息显示的颜色。(FColor
    ④ 参数 4:消息的内容。(TEXT
    ⑤ 参数 5(可选):显示在屏幕上侧(true)还是下侧(false)。(Bool
    ⑥ 参数 6(可选):字体的缩放大小(2D vector
  1. 使用蓝图打印日志:用蓝图打印日志是临时调试最常用的方式之一,也最为易用。使用Print String节点即可。Print String节点本身提供了颜色,持续时间等可调整参数。
  2. 除了直接显示调试图形,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);
  1. 还可以使用可视化日志进行调试,附上参考链接:

https://dev.epicgames.com/documentation/zh-cn/unreal-engine/visual-logger-in-unreal-engine?application_version=5.2

三、使用断点

  1. UE开发的断点有两种:一种是普通的利用VS打断点,另一种则是直接在蓝图系统中打断点。
  2. 用VS打断点时需要注意的点:
    (1)为了调试更加方便地步入(step into)UE的代码,我们可以在Epic中对应版本引擎的“选项”中选择下载“输入调试用符号”。
    (2)如果遇到打了断点却没有触发的情况,有可能是因为C++自动优化了代码,这时可以尝试将VS的“解决方案配置”(在调试按钮的旁边)从“Development Editor”切换为“DebugGame Editor”,这将防止代码自动优化,输出更加详细的测试信息也会增加编译时间
  3. 在蓝图中设置断点的方式:打开蓝图后在任意节点右键选择“添加断点”即可。运行后游戏同样会停留在断点处,此时蓝图编辑器的上方会出现一系列调试用的控制按钮,也可在此时查看对应参数值。与在VS打断点的不同,我们可以在蓝图中实时看见程序的流向

四、使用断言

  1. 断言也是在Debug阶段广泛使用的手段,它类似于if判断会验证一个表达式的真假,但区别在于断言不需要像if那样编写大量重复的判断和打印日志代码,一旦断言检查到不符合预期,程序会直接中断甚至抛出异常,利于开发者定位问题;且断言只在debug阶段有效,对于打包好的程序是不会生效的
  2. 在角色的PrimaryAttack_TimeElapsed函数中已有一个判空的逻辑,这完全是出于提高程序健壮性的正确考虑:
void ASurCharacter::PrimaryAttack_TimeElapsed() {
	if (ProjectileClass) {
		...
	}
}

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

  • 要解决这个问题:一种方法是在else写好错误的输出信息,另一种就是使用UE的断言。
  • 只需将if处改为if (ensure(ProjectileClass))ensure就是UE中的断言函数。此外还有check,但两者不同之处在于ensure提示错误但不会中断程序,check会直接结束你的关卡测试,所以通常使用ensure
  • 运行关卡后,一旦断言为false,UE就会卡住,并输出红色的Error日志信息。如果是在VS中调试打开的UE,还会直接跳转回VS中显示错误。
  • 另外,ensure只会在编译完成后触发提示一次,若要每次出错都提示,可以选择ensureAlways
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值