前面介绍了武器基类的实现,现在在这个武器基类的基础上进行扩展,实现其派生类——射弹类武器和射线类武器。
射弹类武器
.h文件:
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Weapon.h"
#include "ProjectileWeapon.generated.h"
/**
*
*/
UCLASS()
class TPS_API AProjectileWeapon : public AWeapon
{
GENERATED_BODY()
public:
virtual void Fire(const FVector& HitTarget) override;
private:
UPROPERTY(EditAnywhere)
TSubclassOf<class AProjectile> ProjectileClass;
};
.cpp文件:
// Fill out your copyright notice in the Description page of Project Settings.
#include "ProjectileWeapon.h"
#include "Projectile.h"
#include "Engine/SkeletalMeshSocket.h"
void AProjectileWeapon::Fire(const FVector& HitTarget)
{
Super::Fire(HitTarget);
if(!HasAuthority()) return;
APawn* InstigatorPawn = Cast<APawn>(GetOwner());
const USkeletalMeshSocket* MuzzleFlashSocket = GetWeaponMesh()->GetSocketByName(FName("MuzzleFlash"));
if(MuzzleFlashSocket)
{
//获取枪口插槽的Transform,也就是子弹射出的点
FTransform SocketTransform = MuzzleFlashSocket->GetSocketTransform(GetWeaponMesh());
//枪口指向射线检测的目标
FVector ToTarget = HitTarget - SocketTransform.GetLocation();
FRotator TargetRotation = ToTarget.Rotation();
if(ProjectileClass && InstigatorPawn)
{
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = GetOwner();
SpawnParams.Instigator = InstigatorPawn;
UWorld* World = GetWorld();
if(World)
{
World->SpawnActor<AProjectile>(
ProjectileClass,
SocketTransform.GetLocation(),
TargetRotation,
SpawnParams
);
}
}
}
}
射弹类武器需要进行的改动并不多,大多功能直接使用武器基类的方法就可以。在武器基类中,Fire函数实现了武器开火动画、弹壳弹出和子弹计算,因此射弹类武器在此基础上仅需添加实例化子弹并发射功能。
在.h文件中声明重写Fire函数,并声明一个子弹类成员。.cpp文件中对Fire函数进行重写,因为需要武器基类中Fire函数的功能,因此使用Super::Fire(HitTarget)进行父类函数的调用。后续获取该武器对象的拥有者以及枪口插槽并保存为变量,获取枪口插槽的Transform和枪口指向射线检测的目标,以此来确定子弹生成时的位置和旋转,之后用SpawnActor方法生成子弹即可。子弹的相关功能由子弹类去实现。
射线类武器
.h文件:
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Weapon.h"
#include "HitScanWeapon.generated.h"
/**
*
*/
UCLASS()
class TPS_API AHitScanWeapon : public AWeapon
{
GENERATED_BODY()
public:
virtual void Fire(const FVector& HitTarget) override;
protected:
FVector TraceEndWithScatter(const FVector& TraceStart, const FVector& HitTarget);
void WeaponTraceHit(const FVector& TraceStart, const FVector& HitTarget, FHitResult& OutHit);
UPROPERTY(EditAnywhere)
class UParticleSystem* ImpactParticles;
UPROPERTY(EditAnywhere)
USoundCue* HitSound;
UPROPERTY(EditAnywhere)
float Damage = 20.f;
private:
UPROPERTY(EditAnywhere)
UParticleSystem* BeamParticles;
UPROPERTY(EditAnywhere, Category = "Weapon Scatter")
float DistanceToSphere = 800.f;
UPROPERTY(EditAnywhere, Category = "Weapon Scatter")
float SphereRadius = 75.f;
UPROPERTY(EditAnywhere, Category = "Weapon Scatter")
bool bUseScatter = false;
};
.cpp文件:
// Fill out your copyright notice in the Description page of Project Settings.
#include "HitScanWeapon.h"
#include "DrawDebugHelpers.h"
#include "Engine/SkeletalMeshSocket.h"
#include "Kismet/GameplayStatics.h"
#include "Kismet/KismetMathLibrary.h"
#include "Particles/ParticleSystemComponent.h"
#include "PhysicalMaterials/PhysicalMaterial.h"
#include "Sound/SoundCue.h"
#include "TPS/TPS.h"
#include "TPS/BlasterComponents/CombatComponent.h"
#include "TPS/Character/BlasterCharacter.h"
#include "TPS/Enemy/Enemy.h"
void AHitScanWeapon::Fire(const FVector& HitTarget)
{
Super::Fire(HitTarget);
APawn* OwnerPawn = Cast<APawn>(GetOwner());
if(OwnerPawn == nullptr) return;
AController* InstigatorController = OwnerPawn->GetController();
const USkeletalMeshSocket* MuzzleFlashSocket = GetWeaponMesh()->GetSocketByName("MuzzleFlash");
if(MuzzleFlashSocket && InstigatorController)
{
FTransform SocketTransform = MuzzleFlashSocket->GetSocketTransform(GetWeaponMesh());
FVector Start = SocketTransform.GetLocation();
FHitResult FireHit;
WeaponTraceHit(Start, HitTarget, FireHit);
if(FireHit.GetActor() && HasAuthority() && InstigatorController && !Cast<ABlasterCharacter>(FireHit.GetActor()))
{
const float FinalDamage = FireHit.BoneName.ToString() == FString("head") ? Damage * 1.4 : Damage;
UGameplayStatics::ApplyDamage(
FireHit.GetActor(),
FinalDamage,
InstigatorController,
this,
UDamageType::StaticClass()
);
}
if(ImpactParticles)
{
UGameplayStatics::SpawnEmitterAtLocation(
GetWorld(),
ImpactParticles,
FireHit.ImpactPoint,
FireHit.ImpactNormal.Rotation()
);
}
if(HitSound)
{
UGameplayStatics::PlaySoundAtLocation(
this,
HitSound,
FireHit.ImpactPoint
);
}
}
}
void AHitScanWeapon::WeaponTraceHit(const FVector& TraceStart, const FVector& HitTarget, FHitResult& OutHit)
{
UWorld* World = GetWorld();
if (World)
{
FVector End = bUseScatter ? TraceEndWithScatter(TraceStart, HitTarget) : TraceStart + (HitTarget - TraceStart) * 1.25f;
World->LineTraceSingleByChannel(
OutHit,
TraceStart,
End,
ECollisionChannel::ECC_Visibility
);
FVector BeamEnd = End;
if (OutHit.bBlockingHit)
{
BeamEnd = OutHit.ImpactPoint;
}
if (BeamParticles)
{
UParticleSystemComponent* Beam = UGameplayStatics::SpawnEmitterAtLocation(
World,
BeamParticles,
TraceStart,
FRotator::ZeroRotator,
true
);
if (Beam)
{
Beam->SetVectorParameter(FName("Target"), BeamEnd);
}
}
}
}
FVector AHitScanWeapon::TraceEndWithScatter(const FVector& TraceStart, const FVector& HitTarget)
{
FVector ToTargetNormalized = (HitTarget - TraceStart).GetSafeNormal();
FVector SphereCenter = TraceStart + ToTargetNormalized * DistanceToSphere;
FVector RandVec = UKismetMathLibrary::RandomUnitVector() * FMath::FRandRange(0.f, SphereRadius);
FVector EndLoc = SphereCenter + RandVec;
FVector ToEndLoc = EndLoc - TraceStart;
return FVector(TraceStart + ToEndLoc * TRACE_LENGTH / ToEndLoc.Size());
}
射线类武器看起来改动比较多,主要是因为它没有实体子弹,因此也就不需要子弹类,与命中相关的功能函数和变量就放到武器类本身中来了。.cpp文件中实现三个方法,接下来依次介绍。
TraceEndWithScatter:传入一个起始位置向量和目标点向量,使用FMath::FRandRange生成一个随机偏移,加到终点位置上,实现子弹轨迹在圆形范围内随机落点的效果。
WeaponTraceHit:传入起始位置向量和目标点向量,以及一个打击点引用(OutHit),先判断武器bUseScatter是否为true,为true说明是适用子弹偏移的武器,就使用TraceEndWithScatter计算偏移后的射线终点,否则直接以HitTarget延长作为射线终点,接下来使用LineTraceSingleByChannel方法检测射线碰撞到的第一个可视物体,并赋给OutHit,以此获取打击点,之后就是拖尾特效的生成。
Fire:同射弹类武器一样,射线类武器也需要开火动画、弹壳弹出和子弹计算等功能,因此调用Super::Fire(HitTarget)。接下来的实现与射弹类武器类似,获取武器持有者和枪口插槽,只不过接下来不是实例化子弹,而是调用WeaponTraceHit方法获取打击点,FireHit变量中存储了打击点的信息,可通过FireHit.GetActor()方法获取打击物体的Actor,如果能够转换为角色类以外的类型,那就适用接下来的一系列命中方法(因为不想造成打击队友的效果,所以如果能够将打击物体转换为角色类型,就忽略)。满足命中条件后,判断命中部位是否为头部,并直接使用ApplyDamage方法应用伤害,后续就是粒子效果、打击音效的生成了。