CoopGame03-武器2
type: Post
status: Published
date: 2022/10/03
slug: CoopGame03
summary: UE4 C++多人游戏入门
category: Unreal
本小节介绍
1.实现三个功能
开镜狙击
- 优化武器开火代码
- 武器开火时反作用力照成的
摄像机抖动
- 设置对多表面类型的支持,比如击中
木质表面
,金属表面
或玩家
时生成不同的效果。 - 优化开火时播放特效的代码
狙击开镜
添加操作映射
编写开镜逻辑
在
SCharacter
角色类中添加开镜相关函数/*SCharacter.h*/ //开镜后的视角大小 UPROPERTY(EditDefaultsOnly,Category="Player") float ZoomedFOV; //是否开镜 bool bWantsToZoom; //默认视角大小 float DefaultFOV; //开始开镜函数 void BeginZoom(); //结束开镜函数 void EndZoom();
/*SCharacter.cpp*/ /*在ASCharacter()构造函数中设置开镜后的视场角*/ ZoomedFOV = 65; /*在BeginPlay()函数中设置默认视场角*/ //默认视角等于相机组件的视场 DefaultFOV = CameraComp->FieldOfView; /*在Tick()函数中判断是否开镜并处理开镜逻辑*/ //不断判断是否开镜,是则设置当前视场为开镜后的视场,否者默认。 float CurrentFOV = bWantsToZoom?ZoomedFOV:DefaultFOV; CameraComp->SetFieldOfView(CurrentFOV); /*在SetupPlayerInputComponent()函数中绑定鼠标右键按下和松开要调用的函数*/ PlayerInputComponent->BindAction("Zoom",IE_Pressed,this,&ASCharacter::BeginZoom); PlayerInputComponent->BindAction("Zoom",IE_Released,this,&ASCharacter::EndZoom); /*定义开镜和结束开镜函数*/ void ASCharacter::BeginZoom() { bWantsToZoom = true; } void ASCharacter::EndZoom() { bWantsToZoom = false; }
优化开镜平滑效果(使用插值)
/*SCharacter.h*/
//视场平滑速度
UPROPERTY(EditDefaultsOnly,Category="Player")
float ZoomInterpSpeed;
/*SCharacter.cpp*/
/*在ASCharacter()构造函数中初始化视场平滑速度*/
//设置默认插值速度
ZoomInterpSpeed = 20;
/*在Tick()函数中修改设置开镜后的视场角为插值后的视场角NewFOV*/
//不断判断是否开镜,是则设置当前视场为开镜后的视场,否者默认。
float CurrentFOV = bWantsToZoom ? ZoomedFOV : DefaultFOV;
//使用数学函数插值浮点从当前值到目标值,既默认视角到目标视角过度。
float NewFOV = FMath::FInterpTo(DefaultFOV, CurrentFOV, DeltaTime, ZoomInterpSpeed);
CameraComp->SetFieldOfView(NewFOV);
优化武器开火代码
之前角色开火是在角色蓝图中使用自带的鼠标左键事件,调用CurrentWeapon引用的武器类的
Fire()
函数进行开火.现在我们需要删除这些蓝图,改成在C++中通过输入中的操作映射来开火.
在角色类SCharacter中编写开火函数
ToFire();
/*SCharacter.h*/ //角色去开枪函数 void ToFire();
/*SCharacter.cpp*/ /*在SetupPlayerInputComponent()函数中绑定鼠标左键按下的开火函数*/ PlayerInputComponent->BindAction("ToFire",IE_Pressed,this,&ASCharacter::ToFire); /*定义开火函数ToFire()*/ void ASCharacter::ToFire() { //调用武器类的开火函数 if (CurrentWeapon) CurrentWeapon->Fire(); }
- 运行测试是否能正常开火.
开火镜头震动
- 在武器类
SWeapon
中声明和使用相机震动类
/*SWeapon.h*/
protected:
//镜头震动类
UPROPERTY(EditDefaultsOnly,Category="Weapon")
TSubclassOf<class UCameraShakeBase> FireCamShake;
/*SWeapon.cpp*/
/*在Fire()开火函数的if(WeaponOwner)判断后面添加震动代码*/
//拿到武器的拥有者转成Pawn类型,再拿到控制器,需要确保拥有者是Pawn类型
APawn* OwnerPawn = Cast<APawn>(GetOwner());
if (OwnerPawn)
{
APlayerController* PlayerController = Cast<APlayerController>(OwnerPawn->GetController());
//该方法过时
// if (PlayerController) PlayerController->ClientPlayCameraShake(FireCamShake);
//拿到玩家控制器播放相机抖动
if (PlayerController) PlayerController->ClientStartCameraShake(FireCamShake);
}
创建继承
MatineeCameraShake
相机震动类的蓝图类,命名为CamShake_RifleFire
,- 设置相机震动类
MatineeCameraShake
的属性
- 设置相机震动类
- 在武器蓝图类
BP_SWeapon
的类默认值中设置FireCamShake
的属性值为CamShake_RifleFire
。
不同物体不同击中效果
设定自定义表面实现不同击中特效。
在
CoopGame.Build.cs
文件中添加PhysicsCore
依赖。/*CoopGamePlus.Build.cs*/ PrivateDependencyModuleNames.AddRange(new string[] { "PhysicsCore" });
在
项目设置-物理-物理表面
中添加两个物理表面FlashDefault
,FlashVulnerable
新建Corn文件夹,右键创建两个物理材质继承
PhysicalMaterial
类。FlashDefault
,FlashVulnerable
在项目头文件中定义表面对应类型(注意定义的结尾没有分号)
/*CoopGame.h*/ #define SURFACE_FLESHDEFAULT SurfaceType1 #define SURFACE_FLESHVULNERABLE SurfaceType2
在武器C++类
SWeapon
中修改击中特效为两个不同的特效。/*SWeapon.h*/ //注释之前的击中特效 // UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Weapon") // UParticleSystem* ImpactEffect; //击中特效——击中东西的默认特效 UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Weapon") UParticleSystem* DefaultImpactEffect; //击中特效——击中人的飙血特效 UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Weapon") UParticleSystem* FleshImpactEffect;
/*SWeapon.cpp*/ /*引入物理材质类,以及需要项目.h文件中的自定义物理类型*/ #include "PhysicalMaterials/PhysicalMaterial.h" #include "CoopGame/CoopGame.h" /*修改之前Fire()函数中的逻辑*/ //查询参数添加,设置碰到的物体是否返回物理材质,利用返回的物理材质判断使用哪个击中特效。 QueryParams.bReturnPhysicalMaterial = true;//返回击中的物理材质 //获取击中的物体的物理表面材质类型 EPhysicalSurface HitSurfaceType = UPhysicalMaterial::DetermineSurfaceType(Hit.PhysMaterial.Get()); //根据表面材质选择后最终要使用的例子特效 UParticleSystem* SelectedEffect = nullptr; //根据物理表面设置要使用的特效 switch (HitSurfaceType) { case SURFACE_FLESHDEFAULT: case SURFACE_FLESHVULNERABLE: SelectedEffect = FleshImpactEffect; break; default: SelectedEffect = DefaultImpactEffect; break; } // if (ImpactEffect) // { // //在生成特效在某个位置,参数为(生成的世界,粒子特效,世界位置,世界旋转)——SpawnEmitterAtLocation有三个重载。 // UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ImpactEffect, Hit.ImpactPoint, // Hit.ImpactPoint.Rotation()); // } if (SelectedEffect) { //枪口位置 FVector MuzzleLocation = SKMeshComp->GetSocketLocation(MuzzleSocketName); //受击方向 FVector ShotDirection = Hit.ImpactPoint - MuzzleLocation; //归一化矢量 ShotDirection.Normalize(); //位置处生成击中特效 UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), SelectedEffect, Hit.ImpactPoint, ShotDirection.Rotation()); }
- 在武器蓝图
BP_SWeapon
中设置两个特效变量的类默认值。
给新建的Corn文件夹下的两个物理材质
FlashDefault
,FlashVulnerable
分别设置对应的表面类型FleshDefault
和FleshVulnerable
设置人物骨骼物理资产,
添加自定义通道。
创建检测通道
Weapon
,默认响应为阻挡。项目类头文件中定义第一个追踪通道为COLLISION_WEAPON
/*CoopGame.h*/ #define COLLISION_WEAPON ECC_GameTraceChannel1
武器类
SWeapon
开火函数Fire()
中使用自己的碰撞通道COLLISION_WEAPON
/*SWeapon.cpp*/ //判断是否弹道射线打到东西,单射线查询通道(打击结果,射线开始位置,射线结束位置,碰撞通道,查询参数) bool bHit = GetWorld()->LineTraceSingleByChannel(Hit, EyeLocation, TraceEnd, COLLISION_WEAPON, QueryParams);
角色类
SCharacter
中设置胶囊体的碰撞通道忽略COLLISION_WEAPON
,因为要让子弹打到网格体才能有不同的特效,而不能打到胶囊体./*SCharacter.cpp*/ /*引入项目.h头文件,才能拿到自定义碰撞,以及胶囊体组件*/ #include "CoopGame/CoopGame.h" #include "Components/CapsuleComponent.h" /*在ASCharacter()构造函数中设置胶囊体忽略武器碰撞通道*/ //角色胶囊体忽略武器碰撞 GetCapsuleComponent()->SetCollisionResponseToChannel(COLLISION_WEAPON,ECR_Ignore);
然后确保角色蓝图
BP_SCharacter
中胶囊体的碰撞通道忽略了Weapon
。将碰撞预设设置为自定义并设置Weapon通道为忽略
播放开火特效代码优化
合并后的代码
/*SWeapon.h*/ //声明两个合并开火特效的特效播放函数。 void PlayFireEffects(FVector TraceEnd);//枪口和弹道特效 void PlayImpactEffects(FHitResult Hit);//受击特效
/*SWeapon.cpp*/ /*移动Fire()函数中的代码到新的两个函数*/ void ASWeapon::Fire() Fire() if (bHit) { UGameplayStatics::ApplyPointDamage(Hit.GetActor(), 20, EyeRotator.Vector(), Hit,WeaponOwner->GetInstigatorController(), this, DamageType); PlayImpactEffects(Hit);//播放受击特效 } PlayFireEffects(TraceEnd); //播放弹道特效和枪口 } } /*播放枪口特效和弹道特效*/ void ASWeapon::PlayFireEffects(FVector TraceEnd) { //开火枪口特效。 if (MuzzleEffect) { //播放附加到指定组件并跟随指定组件的指定效果(粒子特效,要依附的组件,生成的命名点)。 UGameplayStatics::SpawnEmitterAttached(MuzzleEffect, SKMeshComp, MuzzleSocketName); } //弹道特效。这里没有搞懂 if (TracerEffect) { //获得枪口插槽位置。 FVector MuzzleSocketLocation = SKMeshComp->GetSocketLocation(MuzzleSocketName); //枪口生成特效并拿到特效实例。 UParticleSystemComponent* TracerComp = UGameplayStatics::SpawnEmitterAtLocation( GetWorld(), TracerEffect, MuzzleSocketLocation); if (TracerComp) { //在此粒子组件上设置命名矢量实例参数。 TracerComp->SetVectorParameter(TracerTargetName, TraceEnd); } } /*相机抖动*/ APawn* MyOwner = Cast<APawn>(GetOwner()); if (MyOwner) { APlayerController* PlayerController = Cast<APlayerController>(MyOwner->GetController()); //该方法过时 // if (PlayerController) PlayerController->ClientPlayCameraShake(FireCamShake);.................. //拿到玩家控制器播放相机抖动 if (Pl.ayerController) PlayerController->ClientStartCameraShake(FireCamShake); } } /*播放受击特效*/ void ASWeapon::PlayImpactEffects(FHitResult Hit) { //获取击中的物体的物理表面材质类型 EPhysicalSurface HitSurfaceType = UPhysicalMaterial::DetermineSurfaceType(Hit.PhysMaterial.Get()); //根据表面材质选择后最终要使用的例子特效 UParticleSystem* SelectedEffect = nullptr; //根据物理表面设置要使用的特效 switch (HitSurfaceType) { case SURFACE_FLESHDEFAULT: case SURFACE_FLESHVULNERABLE: SelectedEffect = FleshImpactEffect; break; default: SelectedEffect = DefaultImpactEffect; break; } if (SelectedEffect) { //枪口位置 FVector MuzzleLocation = SKMeshComp->GetSocketLocation(MuzzleSocketName); //受击方向 FVector ShotDirection = Hit.ImpactPoint - MuzzleLocation; //归一化矢量 ShotDirection.Normalize(); //位置处生成击中特效 UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), SelectedEffect, Hit.ImpactPoint, ShotDirection.Rotation()); } }
击中头部伤害暴击
修改武器类SWeapon代码。
/*SWeapon.h*/ //基础伤害值 UPROPERTY(EditDefaultsOnly,Category="Weapon") float BaseDamage;
/*SWeapon.cpp*/ /*在ASWeapon()构造函数中初始化基础伤害值*/ ASWeapon::ASWeapon() { //... //基础伤害值。 BaseDamage = 20; } void ASWeapon::Fire() { AActor* WeaponOwner = GetOwner(); if (WeaponOwner) { //... //如果射线打到东西 if (bHit) { float ActualDamage = BaseDamage; //在应用点状伤害前判断打到的物理表面是不是头部,是则伤害翻4倍。 if (UPhysicalMaterial::DetermineSurfaceType(Hit.PhysMaterial.Get()) == SURFACE_FLESHVULNERABLE) { ActualDamage *= 4.0f; } //应用点状伤害(被伤害的Actor,要应用的基础伤害,受击方向,描述命中的碰撞或跟踪结果,照成伤害的控制器(例如射击武器的玩家的控制器),实际造成伤害的Actor,伤害类型) UGameplayStatics::ApplyPointDamage(Hit.GetActor(), ActualDamage, EyeRotator.Vector(), Hit,WeaponOwner->GetInstigatorController(), this, DamageType); //播放受击特效 PlayImpactEffects(Hit); } //... } }
- 绘制伤害调试球。
鼠标长按连发开火
SWeapon.h
/*连发开火*/
//连发间隔时间句柄
FTimerHandle TimerHandle_TimeBetweenShots;
//上次开火时间
float LastFireTime;
//开火频率——每分钟多少枪
UPROPERTY(EditDefaultsOnly, Category="Weapon")
float RateOfFire;
//两枪之前的时间——分钟
float TimeBetweenShots;
//开始和停止开火函数
void StartFire();
void StopFire();
SWeapon.cpp
ASWeapon::ASWeapon()
{
PrimaryActorTick.bCanEverTick = true;
//...
//每分钟开600枪
RateOfFire = 600;
}
void ASWeapon::BeginPlay()
{
Super::BeginPlay();
//两枪之间最小时间0.1
TimeBetweenShots = 60 / RateOfFire;
}
void ASWeapon::StartFire()
{
//设置定时器到执行定时器的间隔。相当于第一次开枪后到当前开枪的时间间隔?
float FirstDelay = FMath::Max(0.0f, LastFireTime + TimeBetweenShots - GetWorld()->TimeSeconds);
//设置连续开火的定时器绑定函数(定时器绑定的句柄,调用执行函数的对象,定时器所执行的函数,函数执行的时间间隔[如果<=0,则清除现存定时器,即 InOutHandle 所绑定的定时器],是否循环,从设置定时器到执行定时器的时间间隔[若<0,则使用 InRate代替])
GetWorldTimerManager().SetTimer(TimerHandle_TimeBetweenShots, this, &ASWeapon::Fire, TimeBetweenShots, true,
FirstDelay);
}
void ASWeapon::StopFire()
{
//清除连续开火的时间句柄
GetWorldTimerManager().ClearTimer(TimerHandle_TimeBetweenShots);
}
SCharacter.h
//角色去开枪函数
void ToFire();
//角色停止开枪函数
void StopFire();
SCharacter.cpp(改变按键绑定的开火函数)
void ASCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
//...
PlayerInputComponent->BindAction("ToFire",IE_Pressed,this,&ASCharacter::ToFire);
PlayerInputComponent->BindAction("ToFire",IE_Released,this,&ASCharacter::StopFire);
}
void ASCharacter::ToFire()
{
//调用武器的开火函数
if (CurrentWeapon) CurrentWeapon->StartFire();
}
void ASCharacter::StopFire()
{
if (CurrentWeapon) CurrentWeapon->StopFire();
}