炮弹类

在之前的文章中,我们已经实现了玩家坦克和敌方炮塔的开火逻辑。但是我们只是调试绘制了炮弹的初始生成位置。在这一篇文章中,我们将实现:在开火时显示炮弹 Mesh,并发射移动。

和坦克和炮塔类似,我们也要创建内置类承载炮弹的 Mesh。这正好让我们重新复习一下之前的创建流程。

如图 1 所示,我们选择菜单 工具/新建C++类。然后此处父类选择 Actor 就足够了,因为我们的炮弹不需要接控制器。

图1 Actor

我们将炮弹类命名为 Projectile,具体的实现如代码清单 1 和清单 2 所示,我们使用 UStaticMeshComponent 组件承载 Mesh,并使用 CreateDefaultSubobject() 函数创建它。然后将其设置为根组件。

此处我们关闭 Actor 内每帧 Tick() 函数的执行。因为我们后续会引入其他组件控制炮弹的移动,关闭可以提高性能。

代码清单 1 Projectile.h
  1. UCLASS()
  2. class TOONTANKS_API AProjectile : public AActor
  3. {
  4.     GENERATED_BODY()
  5.  
  6. private:
  7.     UPROPERTY(EditDefaultsOnly, Category = "Combat")
  8.     UStaticMeshComponent* ProjectileMesh;
  9. };
代码清单 2 Projectile.cpp
  1. AProjectile::AProjectile()
  2. {
  3.     // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
  4.     // PrimaryActorTick.bCanEverTick = true;
  5.     PrimaryActorTick.bCanEverTick = false;
  6.  
  7.     ProjectileMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Projectile Mesh"));
  8.     RootComponent = ProjectileMesh;
  9. }

接着,如图 2 所示,我们可以基于创建的炮弹 C++ 类,再创建一个蓝图类。然后在蓝图类的 UStaticMeshComponent 组件里指定 Mesh。

图2 蓝图

完成好炮弹类之后,我们需要在游戏运行时动态创建这个 Actor 实例。我们需要使用 SpawnActor() 函数来创建。其函数原型为:

  • template< class T >
  • T* SpawnActor(
  •     UClass* Class,
  •     FVector const& Location,
  •     FRotator const& Rotation,
  •     const FActorSpawnParameters& SpawnParameters = FActorSpawnParameters() );

其中,Class 参数指定实际要生成的类对象;Location 参数指定生成的位置;Rotation 参数指定生成的旋转角度;SpawnParameters 参数指定其他生成细节。

因为我们要生成的炮弹类是蓝图类,所以我们需要在编辑器中指定。如代码清单 3 所示,我们创建变量供编辑器里指定。

TSubclassOf 是带类型检查的 UClass 类。UClass 类是其他类的“说明书”。

代码清单 3 BasePawn.h
  1. UCLASS()
  2. class TOONTANKS_API ABasePawn : public APawn
  3. {
  4.     GENERATED_BODY()
  5.  
  6. private:
  7.     UPROPERTY(EditDefaultsOnly, Category = "Combat")
  8.     TSubclassOf<AProjectile> ProjectileClass;
  9. };

在编辑器中,我们将 ProjectileClass 指定为我们创建好的炮弹蓝图类。接着如代码清单 4 所示,我们修改原先的开火函数,使其使用 SpawnActor() 函数生成炮弹。

代码清单 4 BasePawn.cpp
  1. void ABasePawn::Fire()
  2. {
  3.     FVector Location = ProjectileSpawnPoint->GetComponentLocation();
  4.     FRotator Rotation = ProjectileSpawnPoint->GetComponentRotation();
  5.  
  6.     GetWorld()->SpawnActor<AProjectile>(ProjectileClass, Location, Rotation);
  7. }

现在生成的炮弹是静止不动的,我们还需要使其运动起来。我们可以自己在 Tick 函数里指定炮弹的位置,或者开启物理模拟作用冲量。但还有一种很方便的方法,就是使用 UProjectileMovementComponent 组件,它是 Unreal Engine 中专门用于处理投射物运动逻辑的组件。

如代码清单 5 和清单 6 所示,我们添加并创建 UProjectileMovementComponent 组件。然后初始化一些参数:InitialSpeed 指定投射物生成时的初始速度;MaxSpeed 指定投射物允许达到的最大速度。

代码清单 5 Projectile.h
  1. UCLASS()
  2. class TOONTANKS_API AProjectile : public AActor
  3. {
  4.     GENERATED_BODY()
  5.  
  6. private:
  7.     UPROPERTY(VisibleAnywhere, Category = "Movement")
  8.     UProjectileMovementComponent* ProjectileMovementComponent;
  9. };
代码清单 6 Projectile.cpp
  1. AProjectile::AProjectile()
  2. {
  3.     // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
  4.     // PrimaryActorTick.bCanEverTick = true;
  5.     PrimaryActorTick.bCanEverTick = false;
  6.  
  7.     ProjectileMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Projectile Mesh"));
  8.     RootComponent = ProjectileMesh;
  9.  
  10.     ProjectileMovementComponent = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("Projectile Movement Component"));
  11.     ProjectileMovementComponent->MaxSpeed = 1300.0f;
  12.     ProjectileMovementComponent->InitialSpeed = 1300.0f;
  13. }

目前炮弹运行的逻辑是,遇到碰撞就停止不动了,后续我们会添加爆炸特效,并使导弹消失。在此之前,我们需要知道炮弹何时发生了碰撞。

我们可以按照代码清单 7 第 5 行的方式添加碰撞的“回调函数”。

代码清单 7 OnComponentHit
  1. void AProjectile::BeginPlay()
  2. {
  3.     Super::BeginPlay();
  4.    
  5.     ProjectileMesh->OnComponentHit.AddDynamic(this, &AProjectile::OnHit);
  6. }
  7.  
  8. void AProjectile::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
  9. {
  10.     UE_LOG(LogTemp, Warning, TEXT("OnHit"));
  11.     UE_LOG(LogTemp, Warning, TEXT("HitComp: %s"), *HitComp->GetName());
  12.     UE_LOG(LogTemp, Warning, TEXT("OtherActor: %s"), *OtherActor->GetName());
  13.     UE_LOG(LogTemp, Warning, TEXT("OtherComp: %s"), *OtherComp->GetName());
  14. }

注意响应的函数必须添加 UFUNCTION 宏声明。

  1. UFUNCTION()
  2. void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);

这块机制比较复杂,看着和反射机制有关,留作后续研究。

最终的效果如下方视频所示。可以在控制台中看到碰撞响应函数对各个参数的打印:

HitComp 参数是自己身上发生碰撞的组件。因为一个 Actor 下不止一个组件,各个组件都有可能发生碰撞。

OtherActor 和 OtherComp 参数同理,是碰到的那个物体所在的 Actor 和组件。

NormalImpulse 参数是碰撞产生的冲量;Hit 参数是具体的碰撞结果信息。