一、创建C++类
创建武器子弹的类,创建生产武器子弹的类,创建弹壳的类,生产武器子弹的类的父类是武器的类
![](https://i-blog.csdnimg.cn/direct/82b446de1f5a4c04982e8e5bbd4c7357.png)
创建后如图,ProjectileMyWeapon类(产生子弹的类)继承自weapon类,Projectile(子弹的类),Casing(弹壳声音的类)
![](https://i-blog.csdnimg.cn/direct/9f098e3d85d2470f94d9440975d30f14.png)
在子弹的类中添加如下代码
cpp
//头文件中添加
private:
UPROPERTY(EditAnywhere)
class UBoxComponent* CollisionBox;// 碰撞盒的类
//构造中添加
CollisionBox = CreateDefaultSubobject<UBoxComponent>(TEXT("CollisionBox"));
SetRootComponent(CollisionBox);
CollisionBox->SetCollisionObjectType(ECollisionChannel::ECC_WorldStatic); //设置自身的碰撞类型
CollisionBox->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); //启动碰撞,启动触发器
CollisionBox->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore); //设置对其他类型的碰撞
/* 第一个参数对角色 第二个参数是对角色是哪种碰撞 */
CollisionBox->SetCollisionResponseToChannel(ECollisionChannel::ECC_Visibility, ECollisionResponse::ECR_Block);
CollisionBox->SetCollisionResponseToChannel(ECollisionChannel::ECC_WorldStatic, ECollisionResponse::ECR_Block);
二、武器蒙太奇(开火动画),添加开火功能
1.将动画设置为加性的(让动作连贯),将瞄准和不瞄准的动画找到设置如图
![](https://i-blog.csdnimg.cn/direct/33c82c5d44804181adeb40877732b111.png)
2.创建武器蒙太奇动画,在对应动画右键->创建->创建动画蒙太奇 (我将创建好的蒙太奇动画放到了其他文件夹中)
![](https://i-blog.csdnimg.cn/direct/ec8d1e6a43a34e0cb99cb31fe4601c31.png)
3.新建蒙太奇片段,添加插槽,将瞄准的动画拖拽到蒙太奇中
![](https://i-blog.csdnimg.cn/direct/9858a39255b941f6b878ac16f44a970c.png)
![](https://i-blog.csdnimg.cn/direct/4cf47810a60a40e59339589ba5247131.png)
将之前的default片段名删除
![](https://i-blog.csdnimg.cn/direct/112281f3141641e18bd1a993f0432f13.png)
添加另一端动画
![](https://i-blog.csdnimg.cn/direct/54dbecb10caf44b0aaa5ebf33a821af1.png)
将之前默认跳转的动画清空变成单独的动画
![](https://i-blog.csdnimg.cn/direct/97c874c9d3be40b3b6c68603f9e94879.png)
选择插槽
![](https://i-blog.csdnimg.cn/direct/4b0bcb79a1e743e29c108a98a9614a85.png)
最后样子
![](https://i-blog.csdnimg.cn/direct/21eb845108ce442992b12c325a1baf67.png)
![](https://i-blog.csdnimg.cn/direct/2e89804d33694cd59f904130d9fa4816.png)
三、绑定开火按键
1.编辑->项目设置->输入->操作映射->添加fire鼠标左键
![](https://i-blog.csdnimg.cn/direct/33aca78bba2a437fba078cacfbfbca67.png)
2.在角色类中添加绑定
cpp
//角色类头文件
/* 发射子弹函数 */
void FireButtonPressed();
void FireButtonRelease();
/* 发射子弹函数 */
//角色类源文件
void ABlasterCharacter::FireButtonPressed()
{
if (Combat)
{
Combat->FireButtonPressed(true);
}
}
void ABlasterCharacter::FireButtonRelease()
{
if (Combat)
{
Combat->FireButtonPressed(false);
}
}
//函数SetupPlayerInputComponent中
PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &ABlasterCharacter::FireButtonPressed);
PlayerInputComponent->BindAction("Fire", IE_Released, this, &ABlasterCharacter::FireButtonRelease);
3.声明一个蒙太奇动画类指针
cpp
//角色头文件
// 当前类指针在界面中赋值 EditAnywhere可编辑 Combat在细节中找到对应设置
UPROPERTY(EditAnywhere , Category = Combat)
class UAnimMontage* FireWeaponMontage; // 动画蒙太奇类
4.编译后在角色蓝图中设置对应的蒙太奇动画
![](https://i-blog.csdnimg.cn/direct/8eaaaa4612fc4dc0b33d92741cddf697.png)
5.定义播放动画的函数,FName中的名字是蒙太奇动画中的名字
cpp
//角色类头文件
void PlayFireMontage(bool bAiming);
//角色类源文件
void ABlasterCharacter::PlayFireMontage(bool bAiming)
{
if (Combat == nullptr || Combat->EquippedWeapon == nullptr) return;
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (AnimInstance && FireWeaponMontage)
{
AnimInstance->Montage_Play(FireWeaponMontage);
/* 找到播放哪段动画 名字是动画中新建蒙太奇片段的名字 */
FName SectionName;
SectionName = bAiming ? FName("RifleAim") : FName("RifleHip");
/* 找到播放哪段动画 */
AnimInstance->Montage_JumpToSection(SectionName);
}
}
6.动画蓝图中的改变,武器开火实在装备武器后才可以所以如图,动画蓝图类中添加
![](https://i-blog.csdnimg.cn/direct/767061d05a3742478250b3b625580b90.png)
slot中右侧细节可以选择槽
![](https://i-blog.csdnimg.cn/direct/ce34e13897c74ce69e62382dd8a75047.png)
new一个姿势
![](https://i-blog.csdnimg.cn/direct/7b8a8b51f953405a9c393d1bd69871d0.png)
7.在aimoffset中使用6中的姿势(在动作偏移中使用对应姿势)
![](https://i-blog.csdnimg.cn/direct/ec5406088ead413db8515a31f6f49240.png)
- 在战斗组件类中添加代码(class ABlasterCharacter* Character指针我在角色类中的PostInitializeComponents函数赋值),使用了RPC函数多播功能让动画在每个客户端都能看见
cpp
void ABlasterCharacter::PostInitializeComponents()
{
Super::PostInitializeComponents();
if (Combat)
{
Combat->Character = this;
}
}
cpp
FVector_NetQuantize是FVector的网络传输的序列化的结构,减少网络带宽
//战斗组件类头文件
// 开火函数
void FireButtonPressed(bool bPressed);
/* Server RPC函数 */
UFUNCTION(Server, Reliable)
void ServerFire(const FVector_NetQuantize& TraceHitTarget);
/* Server RPC函数 */
/* 多播函数 */
UFUNCTION(NetMulticast , Reliable)
void MulticastFire(const FVector_NetQuantize& TraceHitTarget);
/* 多播函数 */
/* 命中线 */
void TraceUnderCrosshairs(FHitResult& TraceHitResult);
/* 命中线 */
class ABlasterCharacter* Character;
bool bFireButtonPressed;
//战斗组件类源文件
void UCombatComponent::FireButtonPressed(bool bPressed)
{
bFireButtonPressed = bPressed;
if (bFireButtonPressed)
{
FHitResult HitResult;
TraceUnderCrosshairs(HitResult);
ServerFire(HitResult.ImpactPoint);
}
}
void UCombatComponent::TraceUnderCrosshairs(FHitResult& TraceHitResult)
{
/* 屏幕中心为瞄准点 */
/* 获得视口大小 */
FVector2D ViewportSize;
if (GEngine && GEngine->GameViewport)
{
GEngine->GameViewport->GetViewportSize(ViewportSize);
}
/* 获得屏幕中心坐标 */
FVector2D CrosshaurLocation(ViewportSize.X / 2.f, ViewportSize.Y / 2.f);
FVector CrosshairWorldPosition; //世界空间中的相应 3D 位置
FVector CrosshairWorldDirection; //在给定的 2d 点处远离摄像机的世界空间方向矢量
bool bScreenToWorld = UGameplayStatics::DeprojectScreenToWorld(
UGameplayStatics::GetPlayerController(this, 0),
CrosshaurLocation,
CrosshairWorldPosition,
CrosshairWorldDirection
);
if (bScreenToWorld)
{
FVector Start = CrosshairWorldPosition;
TRACE_LENGTH 我设置为8000 在世界坐标的长度可以理解成武器的射程
FVector End = Start + CrosshairWorldDirection * TRACE_LENGTH;
/*
* bool LineTraceSingleByChannel(
FHitResult& OutHit, // 输出的碰撞信息
const FVector& Start, // 射线的起点
const FVector& End, // 射线的终点
ECollisionChannel TraceChannel, // 碰撞通道
const FCollisionQueryParams& Params = FCollisionQueryParams::DefaultQueryParam, // 可选的额外查询参数
const FCollisionResponseParams& ResponseParam = FCollisionResponseParams::DefaultResponseParam // 可选的碰撞响应参数
);
*/
//检查射线与场景中的物体是否有交点,并返回相关的碰撞信息
GetWorld()->LineTraceSingleByChannel(
TraceHitResult,
Start,
End,
ECollisionChannel::ECC_Visibility
);
#if 0
if (!TraceHitResult.bBlockingHit)
{
TraceHitResult.ImpactPoint = End;
HitTarget = End;
}
else
{
HitTarget = TraceHitResult.ImpactPoint;
/*DrawDebugSphere(
const UWorld* World, // 表示你要在哪个世界中绘制球体
FVector Center, // 球体的中心位置
float Radius, // 球体的半径
int32 Segments, // 球体的分段数,影响球体的平滑度
FColor Color, // 球体的颜色
bool bPersistentLines, // 是否为持久化的调试线条(场景切换后是否还存在)
float LifeTime, // 调试球体的生存时间,0 为永久存在
uint8 DepthPriority, // 渲染优先级(影响是否被遮挡)
float Thickness // 球体线条的厚度
)*/
DrawDebugSphere(
GetWorld(),
TraceHitResult.ImpactPoint,
12.f, //半径
12, //
FColor::Red
);
}
#endif
}
}
void UCombatComponent::ServerFire_Implementation(const FVector_NetQuantize& TraceHitTarget)
{
MulticastFire(TraceHitTarget);
}
void UCombatComponent::MulticastFire_Implementation(const FVector_NetQuantize& TraceHitTarget)
{
if (EquippedWeapon == nullptr) return;
if (Character)
{
//UE_LOG(LogTemp, Warning, TEXT("FireButtonPressed"));
Character->PlayFireMontage(bAiming);
EquippedWeapon->Fire(TraceHitTarget);
}
}
9.武器类添加代码
cpp
//武器类头文件
/* 开火功能 */
virtual void Fire(const FVector& HitTaget);
UPROPERTY(EditAnywhere , Category = "Weapon Properties")
class UAnimationAsset* FireAnimation; //动画资产类
UPROPERTY(EditAnywhere)
TSubclassOf<class ACasing> CasingClass; // 监视类 -- 监视蛋壳弹出
//武器类源文件
void AWeapon::Fire(const FVector& HitTaget)
{
if (FireAnimation)
{
WeapomMesh->PlayAnimation(FireAnimation, false);
}
if (CasingClass)
{
const USkeletalMeshSocket* AmmoEjectSocket = WeapomMesh->GetSocketByName(FName("AmmoEject"));
if (AmmoEjectSocket)
{
FTransform SocketTransform = AmmoEjectSocket->GetSocketTransform(GetWeaponMesh());
UWorld* World = GetWorld();
if (World)
{
World->SpawnActor<ACasing>(
CasingClass,
SocketTransform.GetLocation(),
SocketTransform.GetRotation().Rotator()
);
}
}
}
}
10.创建子弹蓝图类
![](https://i-blog.csdnimg.cn/direct/84dabec25770466a8602a5baf2a0756d.png)
11.设置蓝图
1.打开蓝图设置对用的武器网格体,WeaponMesh细节中网格体的骨骼网格体资产选择对于你武器资产
![](https://i-blog.csdnimg.cn/direct/468c9b8acd2b4cdb9a0afea01fb64cd8.png)
2.设置pickwidget(没有可以不用设置)细节中用户界面的空间选择屏幕空间类选择对饮蓝图类
![](https://i-blog.csdnimg.cn/direct/b6076c303e984e709dff3f5d2b6e053c.png)
3.设置动画(武器开火动画)该蓝图的类是projectileweapon,它的父类是weapon,父类中有
UPROPERTY(EditAnywhere , Category = "Weapon Properties")
class UAnimationAsset* FireAnimation; //动画资产类
所以一在细节中可以找到(C++类的继承)
![](https://i-blog.csdnimg.cn/direct/cb655eecd3f648139c2dcbeff3548f8a.png)
4.将11中的创建武器蓝图拖拽到地图中
- 摄像机的偏移(可选,若想看见角色前方的可以设置)
![](https://i-blog.csdnimg.cn/direct/093af55f61e4457e91e86da13df6ae5c.png)
13.生产子弹类中代码(GetSocketName中的名字是对应武器网格体中枪口的插槽的名字)
cpp
//子弹类头文件
public:
virtual void Fire(const FVector& HitTaget) override;
protected:
UPROPERTY(EditAnywhere)
TSubclassOf<class AProjectile> ProjectileClass;
//子弹类源文件
void AProjectileMyWeapon::Fire(const FVector& HitTaget)
{
Super::Fire(HitTaget);
if (!HasAuthority()) return;
APawn* InstigatorPawn = Cast<APawn>(GetOwner());
const USkeletalMeshSocket* MuzzleFlashSocket = GetWeaponMesh()->GetSocketByName(FName("MuzzleFlash"));
if (MuzzleFlashSocket)
{
FTransform SocketTransform = MuzzleFlashSocket->GetSocketTransform(GetWeaponMesh());
// 从枪口闪光插座到开火位置 获得尖端的位置
FVector ToTarget = HitTaget - 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
);
}
}
}
}
14.创建子弹类蓝图
![](https://i-blog.csdnimg.cn/direct/c3bd8e9806924d228f221b7195c5e86b.png)
15.打开子弹类蓝图设置(中间黄色的是碰撞盒)
15.1 设置碰撞盒大小
![](https://i-blog.csdnimg.cn/direct/cf90b040567648f287c141c64a3ccdcb.png)
16.定义粒子特效类和声音类
cpp
//子弹类头文件
virtual void Destroyed() override;
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
UFUNCTION()
virtual void OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);
public:
private:
UPROPERTY(VisibleAnywhere)
class UProjectileMovementComponent* ProjectileMovementComponent; //子弹运动的类
UPROPERTY(EditAnywhere)
class UParticleSystem* Tracer; //粒子系统类
class UParticleSystemComponent* TracerComponent; //粒子系统组件类
UPROPERTY(EditAnywhere)
class UParticleSystem* ImpactParticals; //粒子系统类
UPROPERTY(EditAnywhere)
class USoundCue* ImpactSound;//声音提示类
//子弹类源文件
//构造中添加
ProjectileMovementComponent = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileMovementComponent"));
ProjectileMovementComponent->bRotationFollowsVelocity = true; //如果为 true,则此射弹将在每一帧更新其旋转以匹配其速度方向
// Called when the game starts or when spawned
void AProjectile::BeginPlay()
{
Super::BeginPlay();
if (Tracer)
{
//播放附加到指定组件并跟随指定组件的指定效果。当效果完成时,系统将消失。不复制
TracerComponent = UGameplayStatics::SpawnEmitterAttached(
Tracer,//粒子系统创建
CollisionBox,//要附加到的组件。
FName(),//AttachComponent 中的可选命名点,用于生成发射器
GetActorLocation(),// 位置 -- 根据 LocationType 的值,这是与附加组件/点的相对偏移量,或者是将转换为相对偏移量的绝对世界位置(如果 LocationType 为 KeepWorldPosition)。
GetActorRotation(),//旋转 -- 根据 LocationType 的值,这是与附加组件/点的相对偏移量,或者是将转换为相对偏移量的绝对世界旋转(如果 LocationType 为 KeepWorldPosition)
EAttachLocation::KeepWorldPosition//根据 LocationType 的值,这是附加组件中的相对缩放,或者是将转换为相对缩放的绝对世界缩放(如果 LocationType 为 KeepWorldPosition)。
//指定 Location 是相对偏移还是绝对世界位置
//当粒子系统完成播放时,组件是否会自动销毁,或者是否可以重新激活
//用于池化此组件的方法。默认为 none。
//组件是否在创建时自动激活。
);
}
if (HasAuthority())
{
CollisionBox->OnComponentHit.AddDynamic(this,&AProjectile::OnHit);
}
}
void AProjectile::OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
Destroy();
}
void AProjectile::Destroyed()
{
Super::Destroyed();
if (ImpactParticals)
{
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ImpactParticals, GetActorTransform());
}
if (ImpactSound)
{
UGameplayStatics::PlaySoundAtLocation(this, ImpactSound, GetActorLocation());
}
}
17.设置声音和粒子特效设置速度
- 若想发射子弹时在其他客户端也可以显示在子弹类的构造中将bReplicates = true;即可
19.弹壳类
cpp
//弹壳头文件
UFUNCTION()
virtual void OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);
#if 0
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
#endif
private:
UPROPERTY(VisibleAnywhere)
UStaticMeshComponent* CasingMesh;//静态网格体类 武器开火时弹出的子弹的网格体
UPROPERTY(EditAnywhere)
float ShellEjectionImpulse;// 弹壳初速度
UPROPERTY(EditAnywhere)
class USoundCue* ShellSound;// 弹壳弹出时的声音
//弹壳源文件
ACasing::ACasing()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = false;
CasingMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("CasingMesh"));
SetRootComponent(CasingMesh);
CasingMesh->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera,ECollisionResponse::ECR_Ignore);
CasingMesh->SetSimulatePhysics(true); // 物理
CasingMesh->SetEnableGravity(true); // 重力
CasingMesh->SetNotifyRigidBodyCollision(true); //通知
ShellEjectionImpulse = 10.f;
}
// Called when the game starts or when spawned
void ACasing::BeginPlay()
{
Super::BeginPlay();
CasingMesh->OnComponentHit.AddDynamic(this, &ACasing::OnHit);
// 给弹壳添加初始速度
CasingMesh->AddImpulse(GetActorForwardVector() * ShellEjectionImpulse);
}
void ACasing::OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
if (ShellSound)
{
UGameplayStatics::PlaySoundAtLocation(this, ShellSound, GetActorLocation());
}
Destroy();
}
#if 0
// Called every frame
void ACasing::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
#endif
20.弹壳类蓝图
![](https://i-blog.csdnimg.cn/direct/9815a741323543c580f28fb980b3fc16.png)