一、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);
};
-
virtual void PostInitializeComponents() override
函数:
(1)PostInitializeComponents
是一个虚函数,它在Actor
的所有组件初始化完成后会被调用。这是在游戏中处理 Actor 的初始化和设置属性的好地方。
(2)在PostInitializeComponents
中,可以确保所有组件已经初始化并且可以安全地访问和操作它们。这对于需要依赖于组件之间的相互关系的初始化逻辑很有用。
(3)我们的爆炸桶Explosive Barrel
,需要在它初始化时设置某些物理属性或绑定事件。这些是可以在PostInitializeComponents
中完成的事情。 -
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);
}
- 事件绑定:是一种机制,它允许将特定的事件(比如碰撞、输入等)与处理该事件的函数关联起来。这种机制使得当某个事件发生时,关联的函数会被自动调用,从而执行相应的逻辑。
- 绑定事件可以使代码更加模块化和灵活。可以在一个地方定义事件处理逻辑,而在需要的地方绑定这些逻辑,而不必在每个地方都重复代码。
OnComponentHit
和OnActorHit
的关系:
OnComponentHit
是UPrimitiveComponent
的一个事件,表示该组件发生了碰撞。OnActorHit
是我们定义的一个函数,用于处理组件发生碰撞时的逻辑。- 通过
MeshComp->OnComponentHit.AddDynamic(this, &ASurExplosiveBarrel::OnActorHit);
这一行代码,将MeshComp
组件的OnComponentHit
事件与ASurExplosiveBarrel
类的OnActorHit
函数绑定在了一起。这意味着每当MeshComp
发生碰撞时,OnActorHit
函数就会被自动调用。
AddDynamic
函数的作用:
AddDynamic
是一个宏,用于将事件与函数动态绑定。当OnComponentHit
事件触发时,OnActorHit
函数会被调用。- 它可以用于以下场景:
①处理碰撞:当一个爆炸桶被子弹击中时,桶发生爆炸并对周围物体产生影响。
②处理输入:当玩家按下某个键时,角色执行特定动作。
③处理触发器:当玩家进入某个区域时,触发某个事件(如打开门、播放音效等)。
二、输出调试信息
- 附上UE日志的参考链接:
https://unrealcommunity.wiki/logging-lgpidy6i/
- 游戏开发中,常用的
Debug
手段有三种:
① 让函数返回错误码,由调用函数者处理错误
② 运用编程语言提供的异常处理函数,将异常抛出或就地修复
③ 断言Assert
,对程序中那些不确定是否正确的东西进行检测,以便开发者在开发过程中尽早发现问题。 UE_LOG
是虚幻引擎提供的 C++宏,用于打印日志到控制台或者引擎的LOG 窗口中。
- 如果是在VS中利用调试打开的UE,同样的日志消息还会输出到VS的输出窗口中 作者:surkea https://www.bilibili.com/read/cv19004443/ 出处:bilibili
UE_LOG
提供三个参数:
① Log 类型Categories
② Log 级别 - UE_LOG 日志级别
③ 要打印的信息,包含一个 UE 的字符串,外加需要格式化的变量参数。
%s - 字符串
%d - 整数
%f - 浮点数
- 使用
TEXT()
宏包裹的即为传入的字符串格式,可以在格式中使用上述参数来匹配后续传入的变量。类似printf
函数的用法,我们可以打印变量中的数据。
- 示例用法如下:
UE_LOG(LogTemp, Log, TEXT("OtherActor is %s, at game time %f"), *GetNameSafe(OtherActor), GetWorld()->TimeSeconds);
- 三个输入分别为:
① 日志显示的类别名,以便使用过滤器快速筛选需要的日志消息 ;
② 日志显示的级别,级别越高显示的内容越重要,显示的颜色越鲜艳,相应的输出消息数量也越少;
③ 输出的内容,可使用UNICODE
编码支持更多符号。GetNameSafe
在对象为空时返回空,这样就不需要额外判空,此外它将返回FString
类型,要注意字符串类型前要加一个星号。
- 使用 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
)
- 使用蓝图打印日志:用蓝图打印日志是临时调试最常用的方式之一,也最为易用。使用
Print String
节点即可。Print String
节点本身提供了颜色,持续时间等可调整参数。 - 除了直接显示调试图形,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);
- 还可以使用可视化日志进行调试,附上参考链接:
https://dev.epicgames.com/documentation/zh-cn/unreal-engine/visual-logger-in-unreal-engine?application_version=5.2
三、使用断点
- UE开发的断点有两种:一种是普通的利用VS打断点,另一种则是直接在蓝图系统中打断点。
- 用VS打断点时需要注意的点:
(1)为了调试更加方便地步入(step into
)UE的代码,我们可以在Epic中对应版本引擎的“选项”中选择下载“输入调试用符号”。
(2)如果遇到打了断点却没有触发的情况,有可能是因为C++自动优化了代码,这时可以尝试将VS的“解决方案配置”(在调试按钮的旁边)从“Development Editor
”切换为“DebugGame Editor
”,这将防止代码自动优化,输出更加详细的测试信息也会增加编译时间。 - 在蓝图中设置断点的方式:打开蓝图后在任意节点右键选择“添加断点”即可。运行后游戏同样会停留在断点处,此时蓝图编辑器的上方会出现一系列调试用的控制按钮,也可在此时查看对应参数值。与在VS打断点的不同,我们可以在蓝图中实时看见程序的流向。
四、使用断言
- 断言也是在
Debug
阶段广泛使用的手段,它类似于if
判断会验证一个表达式的真假,但区别在于断言不需要像if
那样编写大量重复的判断和打印日志代码,一旦断言检查到不符合预期,程序会直接中断甚至抛出异常,利于开发者定位问题;且断言只在debug
阶段有效,对于打包好的程序是不会生效的。 - 在角色的
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
。