开始之前
确保你已完成上一节装备角色中的以下目标:
创建了一个可重新生成的拾取物并将其添加到了你的关卡中
创建了一个可装备的飞镖发射器,供你的角色持有和使用
基本发射物
你的角色可以持有飞镖发射器,你的工具已设置控制绑定来使用它,但尚不具备发射飞镖的能力。 在本节中,你将实现发射物逻辑,使飞镖能从装备的物品中发射出去。
虚幻引擎提供一个发射物移动(Projectile Movement)组件类,你可以将其添加到Actor中以将其转换为发射物。 该组件包含许多实用变量,例如发射物速度、弹力和重力缩放等。
要处理用于实现发射物的数学计算,你需要考虑以下几个方面:
发射物的初始变换、速度和方向。
你希望从角色的中心还是从其装备的工具生成发射物。
你希望发射物与其他对象碰撞时表现出何种行为。
创建发射物基类
你将首先创建一个发射物基类,然后从该类派生出子类,以便为工具创建独特的发射物。
要开始设置发射物基类,请执行以下步骤:
在虚幻编辑器中,转到工具(Tools)> 新建C++类(New C++ Class)。 选择Actor作为父类,并将类命名为
FirstPersonProjectile。 点击创建类(Create Class)。在VS中,转到
FirstPersonProjectile.h。 在文件顶部,前置声明UProjectileMovementComponent和USphereComponent。你将使用球体组件模拟发射物与其他对象之间的碰撞。
C++// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "FirstPersonProjectile.generated.h" class UProjectileMovementComponent; class USphereComponent;添加
BlueprintType和Blueprintable说明符,以使此类向蓝图公开:C++UCLASS(BlueprintType, Blueprintable) class FIRSTPERSON_API AFirstPersonProjectile : public AActor打开
FirstPersonProjectile.cpp,在文件顶部,添加用于"GameFramework/ProjectileMovementComponent.h"的include语句,以包含发射物移动组件类。
#include "FirstPersonProjectile.h"
#include "GameFramework/ProjectileMovementComponent.h"
#include "Components/SphereComponent.h"
// Sets default values
AFirstPersonProjectile::AFirstPersonProjectile()实现发射物击中对象时的行为
为了使发射物更逼真,你可以让其对击中的对象施加一定的力(冲量)。 例如,如果你射击一个启用了物理模拟的方块,发射物会推动方块在地面上移动。 然后,在碰撞后移除发射物,而不是让它继续存在直至默认生命周期结束。 创建一个OnHit()函数来实现此行为。
要实现发射物击中行为,请执行以下步骤:
在
FirstPersonProjectile.h的public部分,定义一个名为PhysicsForce的浮点属性。为其添加
UPROPERTY()宏,并设EditAnywhere、BlueprintReadOnly、Category = "Projectile | Physics"。这是发射物击中对象时施加的力度。
C++// The amount of force this projectile imparts on objects it collides with UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Projectile | Physics") float PhysicsForce = 100.0f;定义一个
void函数OnHit()。 这是AActor中的一个函数,当该Actor与另一个组件或Actor发生碰撞时被调用。 该函数接受以下参数:HitComp:被击中的组件。OtherActor:被击中的Actor。OtherComp:造成碰撞的组件(在本例中为发射物的碰撞组件)。NormalImpulse:碰撞的法线冲量。Hit:一个FHitResult引用,包含有关碰撞事件的更多数据,如时间、距离和位置。
C++// Called when the projectile collides with an object UFUNCTION() void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);在
FirstPersonProjectile.cpp中,实现在头文件中定义的OnHit()函数。 在OnHit()中,通过if语句检查:OtherActor不为空且不是发射物自身。OtherComp不为空且正在模拟物理。
C++void AFirstPersonProjectile::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit) { // Only add impulse and destroy projectile if we hit a physics if ((OtherActor != nullptr) && (OtherActor != this) && (OtherComp != nullptr) && OtherComp->IsSimulatingPhysics()) { } }这将检查发射物是否击中既非自身且参与物理模拟的其他对象。
在
if语句内部,使用AddImpulseAtLocation()函数向OtherComp组件添加冲量。将发射物的速度乘以
PhysicsForce作为参数传入该函数,并在发射物Actor的位置处应用。C++if ((OtherActor != nullptr) && (OtherActor != this) && (OtherComp != nullptr) && OtherComp->IsSimulatingPhysics()) { OtherComp->AddImpulseAtLocation(GetVelocity() * PhysicsForce, GetActorLocation()); }AddImpulseAtLocation()是虚幻引擎中的物理函数,用于在特定世界空间位置向模拟物理的对象施加瞬时力(冲量)。 当需要模拟爆炸推动物体、子弹击中对象或门被撞开等冲击效果时,该函数非常有用。最后,由于此发射物已击中其他Actor,通过调用
Destroy()将其从场景中移除。
完整的OnHit()函数应如下所示:
void AFirstPersonProjectile::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
// Only add impulse and destroy projectile if we hit a physics
if ((OtherActor != nullptr) && (OtherActor != this) && (OtherComp != nullptr) && OtherComp->IsSimulatingPhysics())
{
OtherComp->AddImpulseAtLocation(GetVelocity() * PhysicsForce, GetActorLocation());
Destroy();
}
}添加发射物的网格体、移动和碰撞组件
接下来,为发射物添加静态网格体、发射物移动逻辑和碰撞球体,并定义发射物的移动方式。
要将这些组件添加到发射物,请执行以下步骤:
在
FirstPersonProjectile.h的public部分,声明一个指向名为ProjectileMesh的UStaticMeshComponent的TObjectPtr。 这是发射物在世界中的静态网格体。为其添加
UPROPERTY()宏,并设EditAnywhere、BlueprintReadOnly、Category = "Projectile | Mesh"。C++// Mesh of the projectile in the world UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Projectile | Mesh") TObjectPtr<UStaticMeshComponent> ProjectileMesh;在
protected部分,声明:一个指向名为
CollisionComponent的USphereComponent的TObjectPtr。一个指向名为
ProjectileMovement的UProjectileMovementComponent的TObjectPtr。
为这两个添加
UPROPERTY()宏,并设VisibleAnywhere、BlueprintReadOnly和Category = "Projectile | Components"。C++// Sphere collision component UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Projectile | Components") TObjectPtr<USphereComponent> CollisionComponent; // Projectile movement component UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Projectile | Components") TObjectPtr<UProjectileMovementComponent> ProjectileMovement;ProjectileMovementComponent将为你处理移动逻辑。 它将根据速度、重力和其他变量计算其父级Actor的位置。 然后,在
Tick时将移动应用到发射物上。在
FirstPersonProjectile.cpp的AFirstPersonProjectile()构造函数中,为发射物的网格体、碰撞和发射物移动组件创建默认子对象。 然后,将发射物网格体附加到碰撞组件上。C++// Sets default values AFirstPersonProjectile::AFirstPersonProjectile() { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; // Use a sphere as a simple collision representation CollisionComponent = CreateDefaultSubobject<USphereComponent>(TEXT("CollisionComponent")); check(CollisionComponent != nullptr);调用
InitSphereRadius()以5.0f的半径初始化CollisionComponent。使用
BodyInstance.SetCollisionProfileName()将碰撞组件的碰撞配置文件名称设置为"Projectile"。在虚幻编辑器中,碰撞配置文件的存储位置为项目设置(Project Settings) > 引擎(Engine) > 碰撞(Collision)。你可以在代码中定义最多18个自定义碰撞配置文件以供使用。 此"发射物(Projectile)"碰撞配置文件的默认行为是 阻挡(Block),并在与任何对象碰撞时生成碰撞事件。
C++CollisionComponent->InitSphereRadius(5.0f); // Set the collision profile to the "Projectile" collision preset CollisionComponent->BodyInstance.SetCollisionProfileName("Projectile");你之前定义了
OnHit()函数,用于在发射物击中对象时激活,但还需要想办法在发生碰撞时发出通知。 为此,使用AddDynamic()宏将一个函数订阅到CollisionComponent中的OnComponentHitEvent。 将此宏传递给OnHit()函数。C++// Set up a notification for when this component hits something blocking CollisionComponent->OnComponentHit.AddDynamic(this, &AFirstPersonProjectile::OnHit);将
CollisionComponent设置为发射物的RootComponent以及移动组件要追踪的UpdatedComponent。C++// Set as root component RootComponent = CollisionComponent; ProjectileMovement->UpdatedComponent = CollisionComponent;使用以下值初始化
ProjectileMovement组件:InitialSpeed:发射物生成时的初始速度。 将此值设置为3000.0f。MaxSpeed:发射物的最大速度。 将此值设置为3000.0f。bRotationFollowVelocity:对象是否应旋转以匹配速度的方向。 例如,纸飞机上升和下降时的俯仰方式。 将此值设置为true。bShouldBounce:发射物是否应从障碍物弹开。 将此值设置为true。
C++ProjectileMovement->UpdatedComponent = CollisionComponent; ProjectileMovement->InitialSpeed = 3000.f; ProjectileMovement->MaxSpeed = 3000.f; ProjectileMovement->bRotationFollowsVelocity = true; ProjectileMovement->bShouldBounce = true;
设置发射物的生命周期
默认情况下,你需要让发射物在发射后的几秒内消失。 不过,当你在编辑器中派生发射物蓝图后,可以尝试更改或移除该默认时间,例如让关卡中充满泡沫飞镖!
要让发射物在几秒之后,请执行以下步骤:
在
FirstPersonProjectile.h的public部分,声明一个名为ProjectileLifespan的浮点。为其添加
UPROPERTY()宏,并设EditAnywhere、BlueprintReadOnly、Category = "Projectile | Lifespan"。它是发射物的生命周期(以秒为单位)。
C++// Despawn after 5 seconds by default UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Projectile | Lifespan") float ProjectileLifespan = 5.0f;在
FirstPersonProjectile.cpp中,在AFirstPersonProjectile()构造函数的末尾,将发射物的InitialLifeSpan设置为ProjectileLifespan,使其在五秒后消失。C++// Disappear after 5.0 seconds by default. InitialLifeSpan = ProjectileLifespan;InitialLifeSpan是从AActor继承的属性。 它是一个用于设置Actor在销毁前存活时长的浮点。 值为0表示Actor会一直存在,直到游戏停止。
完整的FirstPersonProjectile.h应如下所示:
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "FirstPersonProjectile.generated.h"
class UProjectileMovementComponent;
class USphereComponent;
完整的AFirstPersonProjectile构造函数函数应如下所示:
// Sets default values
AFirstPersonProjectile::AFirstPersonProjectile()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
// Use a sphere as a simple collision representation
CollisionComponent = CreateDefaultSubobject<USphereComponent>(TEXT("CollisionComponent"));
check(CollisionComponent != nullptr);
获取角色摄像机方向
发射物应从飞镖发射器本身生成,因此你需要通过计算获取飞镖发射器的位置和朝向。 由于发射器附着在玩家角色上,这些值将随角色位置和视角的变化而变化。
你的第一人称角色包含发射飞镖所需的部分位置信息,因此首先修改你的角色类,通过线迹捕获这些信息并返回结果。
如果要使用追踪从角色获取所需信息,请执行以下步骤:
在VS中,打开你的角色的
.h和.cpp文件。在
.h文件的public部分,声明一个名为GetCameraTargetLocation()的新函数,该函数返回一个FVector。 此函数将返回角色在世界中视线所指向的位置。C++// Returns the location in the world the character is looking at UFUNCTION() FVector GetCameraTargetLocation();在角色的
.cpp文件中,实现GetCameraTargetLocation()函数。 首先声明一个名为TargetPosition的FVector用于返回。通过调用
GetWorld()创建UWorld的指针。C++// The target position to return FVector TargetPosition; UWorld* const World = GetWorld();添加
if语句检查World是否为空。 在if语句中,声明一个名为Hit的FHitResult。C++if (World != nullptr) { // The result of the line trace FHitResult Hit;FHitResult是虚幻引擎中的结构体,用于存储碰撞查询结果的信息,包括被击中的Actor或组件以及击中位置。要确定角色注视的点,你需要沿角色的视线向量模拟一条连接到远处某点的线迹。 如果线迹与对象碰撞,即可确定角色在世界中注视的位置。
声明两个名为TraceStart和TraceEnd的常量FVector值:
将
TraceStart设置为FirstPersonCameraComponent的位置。将
TraceEnd设置为TraceStart,加上摄像机组件前向向量乘以极大值。 这可确保追踪线足够长,能与世界中的大多数对象碰撞,除非角色正看向天空。 (如果角色正看向天空,TraceEnd将作为线迹的终点。)C++// Simulate a line trace from the character along the vector they're looking down const FVector TraceStart = FirstPersonCameraComponent->GetComponentLocation(); const FVector TraceEnd = TraceStart + FirstPersonCameraComponent->GetForwardVector() * 10000.0;
通过
UWorld调用LineTraceSingleByChannel()模拟追踪。 向它传递Hit、TraceStart、TraceEnd和ECollisionChannel::ECC_Visibility。这模拟了从
TraceStart到TraceEnd的线迹,与可见对象碰撞并将追踪结果存储在Hit中。ECollisionChannel::ECC_Visibility是用于追踪的通道,这些通道定义了你的追踪应尝试命中的对象类型。 使用ECC_Visibility进行视线摄像机检查。C++World->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECollisionChannel::ECC_Visibility);现在,
Hit值包含关于命中结果的信息,例如撞击的位置和法线。 它也知道命中是否是对象碰撞的结果。 撞击位置(或追踪线的终点)是要返回的摄像机目标位置。使用三元运算符将
TargetPosition设置为Hit.ImpactPoint(如果命中是阻挡命中),否则设置为Hit.TraceEnd。 然后,返回TargetPosition。C++TargetPosition = Hit.bBlockingHit ? Hit.ImpactPoint : Hit.TraceEnd; } return TargetPosition;
完整的GetCameraTargetLocation()函数应如下所示:
FVector AADventureCharacter::GetCameraTargetLocation()
{
// The target position to return
FVector TargetPosition;
UWorld* const World = GetWorld();
if (World != nullptr)
{
// The result of the line trace
FHitResult Hit;
使用DartLauncher::Use()生成发射物
现在你已经知道角色的注视点,你可以在飞镖发射器的 Use()函数中实现发射物逻辑的其余部分。 你将获得发射发射物的位置和方向,然后生成发射物。
要获取发射物应生成的位置和旋转,请执行以下步骤:
在
DartLauncher.h中,在文件顶部,添加AFirstPersonProjectile的前置声明。在
public部分,声明一个名为ProjectileClass的TSubclassOf<AFirstPersonProjectile>属性。 这将是飞镖发射器生成的发射物。 为其添加UPROPERTY()宏,设置为EditAnywhere和Category = Projectile。DartLauncher.h现在应如下所示:C++// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "AdventureGame/EquippableToolBase.h" #include "AdventureGame/FirstPersonProjectile.h" #include "DartLauncher.generated.h" class AFirstPersonProjectile;DartLauncher.cpp,添加"Kismet/KismetMathLibrary.h"的include语句。 发射物数学运算可能很复杂,此文件包含你将用于发射发射物的实用函数。C++#include "DartLauncher.h" #include "Kismet/KismetMathLibrary.h" #include "AdventureGame/AdventureCharacter.h"开始实现DartLauncher的
Use()函数:通过调用
GetWorld()获取UWorld。添加
if语句检查World和ProjectileClass是否为空。在
if语句中,通过调用OwningCharacter->GetCameraTargetLocation()获取角色正在注释的位置。
C++void ADartLauncher::Use() { UWorld* const World = GetWorld(); if (World != nullptr && ProjectileClass != nullptr) { FVector TargetPosition = OwningCharacter->GetCameraTargetLocation(); } }虽然你想从角色持有的工具生成发射物,但你可能不想直接从对象的中心生成发射物。 飞镖发射器的
SKM_Pistol网格体有一个"枪口(Muzzle)"插槽,可以用来为你的飞镖设置精确的生成点。声明一个名为
SocketLocation的新FVector,并将其设置为在ToolMeshComponent上调用GetSocketLocation("Muzzle")的结果。C++// Get the correct socket to spawn the projectile from FVector SocketLocation = ToolMeshComponent->GetSocketLocation("Muzzle");接下来,声明一个名为
SpawnRotation的FRotator。 这是发射物生成时的旋转(俯仰、偏转和滚动值)。将其设置为从kismet数学库调用
FindLookAtRotation()的结果,传递你从玩家角色获得的SocketLocation和TargetPosition。C++FRotator SpawnRotation = UKismetMathLibrary::FindLookAtRotation(SocketLocation, TargetPosition);FindLookAtRotation会计算并返回在SocketLocation处面对TargetPosition所需的旋转。声明一个名为
SpawnLocation的FVector,并将其设置为SocketLocation加上SpawnRotation的前向向量乘以10.0的结果。枪口插槽并不完全位于发射器的前端,因此你需要将向量乘以一个偏移量,以使发射物从正确位置发射。
C++FVector SpawnLocation = SocketLocation + UKismetMathLibrary::GetForwardVector(SpawnRotation) * 10.0;
现在你已经获得位置和旋转,可以准备生成发射物。
要生成发射物,请执行以下步骤:
仍在
Use()函数中,声明一个名为ActorSpawnParams的FActorSpawnParameters。 该类包含关于在何处以及如何生成Actor的信息。C++//Set Spawn Collision Handling Override FActorSpawnParameters ActorSpawnParams;将
ActorSpawnParams中的SpawnCollisionHandlingOverride值设置为ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButDontSpawnIfColliding。这会尝试找到一个发射物不与其他Actor碰撞的生成位置(例如墙内),如果未找到合适位置,则不生成。
C++ActorSpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButDontSpawnIfColliding;使用
SpawnActor()在飞镖发射器的枪口处生成发射物,传入ProjectileClass、SpawnLocation、SpawnRotation和ActorSpawnParams。C++// Spawn the projectile at the muzzle World->SpawnActor<AFirstPersonProjectile>(ProjectileClass, SpawnLocation, SpawnRotation, ActorSpawnParams);
完整的Use()函数现在应如下所示:
void ADartLauncher::Use()
{
GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Yellow, TEXT("Using the dart launcher!"));
UWorld* const World = GetWorld();
if (World != nullptr && ProjectileClass != nullptr)
{
FVector TargetPosition = OwningCharacter->GetCameraTargetLocation();
// Get the correct socket to spawn the projectile from
派生泡沫飞镖类和蓝图
现在所有生成逻辑已完成,该编译真实的发射物了! 你的飞镖发射器类需要一个AFirstPersonProjectile的子类来发射,因此你需要在代码中编译一个子类,以便在关卡中使用。
要派生一个供游戏中使用的泡沫飞镖发射物类,请执行以下步骤:
在虚幻编辑器中,转到工具(Tools)> 新建C++类(New C++ Class)。
转到所有类(All Classes),搜索并选择 第一人称发射物(First Person Projectile) 作为父类,将类命名为
FoamDart。 点击 创建类(Create Class)。在VS中,保持这些文件不变,保存代码并编译。
在本教程中,除了在FirstPersonProjectile中定义的内容外,你无需实现任何自定义发射物代码,但你可以自行修改FoamDart类以满足项目需求。 例如,你可以尝试让飞镖发射物粘在对象上,而不是消失或弹开。
要创建泡棉飞镖蓝图,请按以下步骤:
在虚幻编辑器中,创建一个基于FoamDart的蓝图类,命名为
BP_FoamDart。 将其保存在FirstPerson/Blueprints/Tools文件夹中。打开蓝图后,选择发射物网格体(Projectile Mesh)组件,将静态网格体(Static Mesh)设置为SM_FoamBullet。
保存并编译你的蓝图。
在内容浏览器(Content Browser)中,打开
BP_DartLauncher。转到其细节(Details)面板的发射物(Projectile)分段,将 发射物类(Projectile Class) 设为
BP_FoamDart,然后保存并编译蓝图。如果你未能在列表中看到
BP_FoamDart,则请转到内容浏览器,选择BP_FoamDart,然后回到发射物类(Projectile Class)属性,并点击使用来自内容浏览器的选定资产(Use Selected Asset from Content Browser)。
现在是见证成果的时刻。 保存你的资产并点击播放(Play)。 游戏开始时,你可以跑到飞镖发射器旁拾取它。 按下鼠标左键会从飞镖发射器的枪口生成一个发射物,并在关卡中弹跳! 这些发射物应在五秒后消失,并对碰撞的对象(包括你自己!)施加一个小的物理力。
下一步
祝贺你! 你已完成第一人称程序员路径教程,并在此过程中学到了很多知识!
你已实现了自定义角色和移动、创建拾取物和数据资产,甚至还制作了可装备的工具及其发射物。 现在,你掌握了所有必要的知识,可以将此项目打造成完全属于你自己的作品。
以下是一些建议:
能否扩展玩家的物品栏以容纳不同类型的物品? 要不要实现物品堆叠功能?
能否将拾取物与发射物结合,创建可拾取的弹药? 要不要在飞镖发射器中实现此弹药系统?
能否将消耗品发展成可装备的消耗品? 比如玩家手持的医疗包,或是可拾取并投掷的球?
完整代码
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "FirstPersonProjectile.generated.h"
class UProjectileMovementComponent;
class USphereComponent;
// Fill out your copyright notice in the Description page of Project Settings.
#include "FirstPersonProjectile.h"
#include "GameFramework/ProjectileMovementComponent.h"
#include "Components/SphereComponent.h"
// Sets default values
AFirstPersonProjectile::AFirstPersonProjectile()
{
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Camera/CameraComponent.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "GameFramework/Character.h"
#include "InputActionValue.h"
// Copyright Epic Games, Inc. All Rights Reserved.
#include "AdventureCharacter.h"
#include "EquippableToolBase.h"
#include "EquippableToolDefinition.h"
#include "ItemDefinition.h"
#include "InventoryComponent.h"
// Sets default values
AAdventureCharacter::AAdventureCharacter()
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "AdventureGame/EquippableToolBase.h"
#include "AdventureGame/FirstPersonProjectile.h"
#include "DartLauncher.generated.h"
class AFirstPersonProjectile;
// Copyright Epic Games, Inc. All Rights Reserved.
#include "DartLauncher.h"
#include "Kismet/KismetMathLibrary.h"
#include "AdventureGame/AdventureCharacter.h"
void ADartLauncher::Use()
{
UWorld* const World = GetWorld();