3 - 对象碰撞入门

本文将初步介绍UE5中对象物理碰撞的相关概念,对对象碰撞、碰撞组件、碰撞事件、碰撞通道等概念有初步理解,并通过创建一个躲避球Actor类和投掷躲避球的敌人Character类来熟悉如何使用定时器、创建使用物理材质、如何生成一个Actor。

概念

碰撞(Collision)是大多数游戏的核心。在许多游戏中,碰撞是玩家对环境作出反应的主要方式,无论是奔跑、跳跃还是射击,环境也会做出相应的反应,让玩家落地、被击中等。

碰撞组件

在UE5中,主要有两种类型的组件可以影响碰撞并受到碰撞的影响:

  • 网格体 Mesh:就是我们见到的模型,小到一个立方体,大到一个精细的人物模型。对于要物理模拟的网格体,应该越简单越好(例如就几个三角形)。可以发生碰撞的网格体类型如下:
    • 静态网格体 Static Mesh:不会发生改变的网格体;
    • 骨骼网格体 Skeletal Mesh:拥有骨骼和一些姿势的网格体,允许设置动画。例如,角色网格体就是骨骼网格体。
    • 程序化网格体 Procedural Mesh:根据参数程序化生成的网格体。
  • 形状对象 Shape Objects:线框模式下表示的简单网格体,通过引发和接收碰撞事件来充当碰撞对象。它的本质是不可见的网格体,常用的类型如下:
    • 长方体碰撞组件:C++的Box Component;
    • 球体碰撞组件:C++的Sphere Component;
    • 胶囊体碰撞组件:C++的Capsule Component;

在UE5中,所有提供几何信息和碰撞的组件均继承自 Primitive 组件。这个组件是包含任何几何信息的所有组件的基础。

碰撞事件

两个物体相互碰撞时,可能会发生以下两种情况:

  1. 两个物体相互重叠,就好像另一个对象不存在一样,此时会调用 Overlap 事件
  2. 两个物体相互碰撞并阻止对方继续前进,此时会调用 Block 事件

在上一篇射线检测入门中,我们学习了如何更改对象对特定检测通道的响应,这些响应为“阻挡 Block”,“重叠 Overlap”和“忽略 Ignore”,接下来看看这些响应在碰撞过程中会产生什么效果:

  • 阻挡 Block:两个对象都将针对对方的碰撞响应设置为阻挡,才会互相阻挡。

    • 此时两个对象都会调用它们的OnHit()事件。如果其中的对象需要物理模拟,还需要将它的GeneratesHitEvents属性设置为true
    • 两个对象会在物理上阻止对方继续前进。

  • 重叠 Overlap:两个对象不互相阻挡,且均没有互相忽略,就会相互重叠。

    • 如果两个对象的GenerateOverlapEvents属性均为true,它们的OnBeginOverlap()OnEndOverlap()事件将被调用。
    • 两个对象会相互重叠;

  • 忽略 Ignore:两个对象中至少有一个对象忽略了对方,就会相互忽略。

    • 两个对象都不会调用任何事件;
    • 两个对象会相互重叠。

当碰撞发生时,主要有两个方面需要处理:

  1. 物理:所有与物理模拟相关的碰撞,例如球受到弹力从地板上反弹。
  2. 查询:调用和碰撞相关的事件,例如OnHit()OnBeginOverlap()OnEndOverlap()

碰撞对象通道

之前我们接触的是检测通道,实际上除了检测通道外还有碰撞对象通道。检测通道仅用于射线检测,而碰撞对象通道则用于对象间的碰撞:

同理,也能自定义碰撞对象通道:

给每个对象单独设置Custom的碰撞预设很麻烦,我们也能在这个设置界面去新建一个碰撞预设,然后去编辑他,这样这类对象都能用这个预设了,一劳永逸。

动态生成Actor

除了通过编辑器在关卡中放置创建的Actor外,还能在游戏运行时动态生成Actor。要在运行时生成Actor,需要在GetWorld()后调用SpawnActor()或者SpawnActorDeferred()函数。

接下来看看SpawnActor()函数需要传递的参数:

  • UClass*:让函数知道要生成对象的类。可以是一个C++类,通过类名::StaticClass()函数获得;也可以是一个蓝图类,通过TSubclassOf<它的父类>获得。一般推荐后者。
  • FTransform或者FVectorFRotator:想要生成对象的变换信息。
  • 可选的FActorSpawnParameters:允许我们指定更多特定于生成过程的属性。例如谁让Actor生成(Instigator),如果生成对象的位置被其他对象占用怎么处理等。

代码格式如下:

GetWorld()->SpawnActor<Actor类名>(classRef, SpawnLocation, SpawnRotation);

然后是SpawnDeferred()函数,和前者不同的是,调用该函数后对象不会马上被创建出来,只有调用它返回的Actor->FinishSpawning()才会生成该对象。

实践

实现反弹球类

下述代码是一个反弹球类DodgeballProjectile的实现:

// .h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "DodgeballProjectile.generated.h"

class UProjectileMovementComponent;
class USphereComponent;

UCLASS()
class DODGEBALL_API ADodgeballProjectile : public AActor
{
	GENERATED_BODY()

private:
	// 躲避球的碰撞包围球组件
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Dodgeball, meta = (AllowPrivateAccess = "true"))
	USphereComponent* SphereComponent;

	// 躲避球的投掷物移动组件, 给它一个投掷初速度
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Dodgeball, meta = (AllowPrivateAccess = "true"))
	UProjectileMovementComponent* ProjectileMovement;

public:
	// Sets default values for this actor's properties
	ADodgeballProjectile();

	// Called every frame
	virtual void Tick(float DeltaTime) override;

	// 躲避球发生刚体碰撞时触发此回调
	UFUNCTION()
	void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp,
	           FVector ImpulseNormal, const FHitResult& HitResult);

	// Getter & Setters
	FORCEINLINE UProjectileMovementComponent* GetProjectileMovementComponent() const
	{
		return ProjectileMovement;
	}

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;
};

在这个头文件中,我们给球声明了一个球体组件,用于充当碰撞发生的判定区域;接着给球声明了一个投掷物移动组件,给它一个投掷初速度以进行平抛运动;然后还声明了球发生碰撞时的OnHit()回调函数;以及给投掷手使用的Getter方法。

接下来看看充当判定区域的球体组件:

// 构造函数
// 初始化躲避球的网格体组件
SphereComponent = CreateDefaultSubobject<USphereComponent>(TEXT("Sphere Collision"));
SphereComponent->SetSphereRadius(35.f);
SphereComponent->SetCollisionProfileName(FName("DodgeBall"));                   // 设置碰撞预设
SphereComponent->SetSimulatePhysics(true);                                      // 需要物理模拟
SphereComponent->SetNotifyRigidBodyCollision(true);                             // 需要使用OnHit()回调
SphereComponent->OnComponentHit.AddDynamic(this, &ADodgeballProjectile::OnHit); // 设置OnHit回调

// 初始化根组件
RootComponent = SphereComponent;

这里创建球的网格体组件,设置它的大小、自定义碰撞预设,让它启用物理模拟,然后绑定了发生碰撞时候的回调函数OnHit(),最后将它作为根组件,以防止碰撞判定出错。

然后是投掷物移动组件的初始化:

// 构造函数
ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("Projectile Movement"));
ProjectileMovement->InitialSpeed = 1500.f; // 设置初速度

接下来是碰撞发生时的回调函数OnHit()

void ADodgeballProjectile::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp,
                                 FVector ImpulseNormal, const FHitResult& HitResult)
{
    // 击中玩家后的逻辑
    if (Cast<ADodgeballCharacter>(OtherActor) != nullptr)
    {
        Destroy();
    }
}

当球打到玩家时,它会消失。然后来看一下这个回调函数的参数:

  • UPrimitiveComponent* HitComp:自己被碰撞的组件;
  • AActor* OtherActor:碰撞中涉及到的对方;
  • UPrimitiveComponent* OtherComp:对方被碰撞的组件;
  • FVector ImpulseNormal:自己被击中后,应该移动的方向以及受到力的大小。只有启用物理模拟时这个量才非0。
  • FHitResult& HitResult:碰撞结果。

此外,重叠事件OnBeginOverlap()OnEndOverlap()的回调使用方法如下:

回调函数的格式如下:

>UFUNCTION()
>void OnBeginOverlap(UPrimitiveComponent* OverlappedComp,
                   AActor* OtherActor,
                   UPrimitiveComponent* OtherComp,
                   int32 OtherBodyIndex, bool bFromSweep,
                   const FHitResult& SweepResult);

>UFUNCTION()
>void OnEndOverlap(UPrimitiveComponent* OverlappedComp,
                 AActor* OtherActor,
                 UPrimitiveComponent* OtherComp,
                 int32 OtherBodyIndex);

参数含义如下:

  • UPrimitiveComponent* OverlappedComp:自身Actor发生重叠的组件;
  • AActor* OtherActor:重叠时另一个Actor;
  • UPrimitiveComponent* OtherComp:另一个Actor发生重叠的组件;
  • int32 OtherBodyIndex:被击中的图元索引(用于实例化静态网格体);
  • bool bFromSweep:重叠是否来自扫掠检测;
  • const FHitResult& SweepResult:重叠时产生的扫掠监测数据。

然后就能将其拓展为蓝图子类了,需要添加一个新的“球体”网格体,作为碰撞判定球体的子组件,方便玩家看到球本身:

然后设置该蓝图类的碰撞预设,将其拖到场景中就能看见一个遵循平抛运动且会反弹的球。

创建物理材质

如果我们想让球反弹更高,就该为它创建一个物理材质,它可以让你自定义物体在物理模拟时的表现。

在内容浏览器中新建一个物理材质(Physics Material),然后设定它的物理属性即可:

常用到的参数有:

  • 摩擦力 Friction:范围是[0, 1],越大物体越不容易滑动;
  • 恢复力/弹力 Restitution:范围是[0, 1],指定与另一个物体碰撞后保持的速度,越大物体反弹的次数越多。
  • 密度 Density:指定物体的密度,相同体积的网格体,密度越大受重力影响就越大。

为了让球能多弹几次,这里把弹力设置成0.95。别忘了将这个物理材质给反弹球蓝图类设定好。

使用定时器

定时器(Timer)是游戏引擎不可缺少的一项功能,用它可以让一件事情在一定延迟后发生,且有可能以一定的间隔重复发生。例如团队死斗中,人物被击杀后需要一定时间复活。

要想使用定时器,得先在类中声明如下成员变量:

FTimerHandle ThrowTimerHandle; // 计时器标识符
float ThrowingInterval = 2.f;  // 投掷躲避球之间的等待间隔(s)
float ThrowingDelay = 0.5f;    // 定时器开始循环前的初始延迟

然后需要包含TimerManager.h来获取世界的计时器管理者,通过它来设置计时器:

// 开始计时器
GetWorldTimerManager().SetTimer(ThrowTimerHandle, this, &AEnemyCharacter::ThrowDodgeball, ThrowingInterval, true, ThrowingDelay);

// 结束计时器
GetWorldTimerManager().ClearTimer(ThrowTimerHandle);

让角色扔球

接下来尝试让角色扔球,这需要我们动态生成反弹球。首先在这个角色的类中新增反弹球的TSubclassOf()属性:

// 要生成的投掷球蓝图类
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = Dodgeball)
TSubclassOf<ADodgeballProjectile> DodgeballClass;

接下来编写生成球的C++代码:

// 指定躲避球生成位置
FVector ForwardVector = GetActorForwardVector();
float SpawnDistance = 40.f;
FVector SpawnLocation = GetActorLocation() + (ForwardVector * SpawnDistance);
FTransform SpawnTransform(GetActorRotation(), SpawnLocation);

// 生成躲避球 - immediate
GetWorld()->SpawnActor<ADodgeballProjectile>(DodgeballClass, SpawnLocation, GetActorRotation());


// 生成躲避球 - Deferred
ADodgeballProjectile* DodgeballProjectile = GetWorld()->SpawnActorDeferred<ADodgeballProjectile>(
    DodgeballClass, SpawnTransform);
// 延迟生成的好处在于可以修改Actor的默认属性再生成
DodgeballProjectile->GetProjectileMovementComponent()->InitialSpeed = 2200.f;
DodgeballProjectile->FinishSpawning(SpawnTransform);

最后别忘了在角色的蓝图中设置投掷球属性:

参考资料

  • 中文翻译:《UE5 C++ 游戏开发完全学习教程》

  • 英文原版:《Elevating Game Experiences with UE5》