CoopGame03-武器2

type: Post
status: Published
date: 2022/10/03
slug: CoopGame03
summary: UE4 C++多人游戏入门
category: Unreal

本小节介绍

1.实现三个功能

  • 开镜狙击
  • 优化武器开火代码
  • 武器开火时反作用力照成的摄像机抖动
  • 设置对多表面类型的支持,比如击中木质表面金属表面玩家时生成不同的效果。
  • 优化开火时播放特效的代码

狙击开镜

添加操作映射

  1. 鼠标右键绑定Zoom操作(原理按下右键瞄准时视野减小)

    CoopGame03-武器2

编写开镜逻辑

  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);

优化武器开火代码

  1. 之前角色开火是在角色蓝图中使用自带的鼠标左键事件,调用CurrentWeapon引用的武器类的Fire()函数进行开火.

    CoopGame03-武器2

  2. 现在我们需要删除这些蓝图,改成在C++中通过输入中的操作映射来开火.

    1. 添加ToFire操作映射

      CoopGame03-武器2

  3. 在角色类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();
    }
  4. 运行测试是否能正常开火.

开火镜头震动

  1. 在武器类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);
        }
  1. 创建继承MatineeCameraShake相机震动类的蓝图类,命名为CamShake_RifleFire,

    1. 设置相机震动类MatineeCameraShake的属性

    CoopGame03-武器2

  2. 在武器蓝图类BP_SWeapon的类默认值中设置FireCamShake的属性值为CamShake_RifleFire

不同物体不同击中效果

设定自定义表面实现不同击中特效。
  1. CoopGame.Build.cs文件中添加PhysicsCore依赖。

    /*CoopGamePlus.Build.cs*/
    PrivateDependencyModuleNames.AddRange(new string[] { "PhysicsCore" });
    
  2. 项目设置-物理-物理表面中添加两个物理表面FlashDefault,FlashVulnerable

    CoopGame03-武器2

  3. 新建Corn文件夹,右键创建两个物理材质继承PhysicalMaterial类。FlashDefault,FlashVulnerable

    CoopGame03-武器2

  4. 在项目头文件中定义表面对应类型(注意定义的结尾没有分号)

    /*CoopGame.h*/
    #define SURFACE_FLESHDEFAULT SurfaceType1
    #define SURFACE_FLESHVULNERABLE SurfaceType2
  5. 在武器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());
            }
  6. 在武器蓝图BP_SWeapon中设置两个特效变量的类默认值。

CoopGame03-武器2

  1. 给新建的Corn文件夹下的两个物理材质FlashDefault,FlashVulnerable分别设置对应的表面类型FleshDefaultFleshVulnerable

    !CoopGame03-武器2

  2. 设置人物骨骼物理资产,

    1. 设置除了头部之外的形体物理材质重载为FleshDefault,头部物理重载设置为FleshVulnerable

      CoopGame03-武器2

  3. 添加自定义通道。

    1. 创建检测通道Weapon,默认响应为阻挡。

      CoopGame03-武器2

    2. 项目类头文件中定义第一个追踪通道为COLLISION_WEAPON

      /*CoopGame.h*/
      #define COLLISION_WEAPON ECC_GameTraceChannel1
    3. 武器类SWeapon开火函数Fire()中使用自己的碰撞通道COLLISION_WEAPON

      /*SWeapon.cpp*/
      
         //判断是否弹道射线打到东西,单射线查询通道(打击结果,射线开始位置,射线结束位置,碰撞通道,查询参数)
              bool bHit = GetWorld()->LineTraceSingleByChannel(Hit, EyeLocation, TraceEnd, COLLISION_WEAPON, QueryParams);
  4. 角色类SCharacter中设置胶囊体的碰撞通道忽略COLLISION_WEAPON,因为要让子弹打到网格体才能有不同的特效,而不能打到胶囊体.

    /*SCharacter.cpp*/
    
    /*引入项目.h头文件,才能拿到自定义碰撞,以及胶囊体组件*/
    #include "CoopGame/CoopGame.h"
    #include "Components/CapsuleComponent.h"
    
    /*在ASCharacter()构造函数中设置胶囊体忽略武器碰撞通道*/
      //角色胶囊体忽略武器碰撞
        GetCapsuleComponent()->SetCollisionResponseToChannel(COLLISION_WEAPON,ECR_Ignore);
    
  5. 然后确保角色蓝图BP_SCharacter中胶囊体的碰撞通道忽略了Weapon。将碰撞预设设置为自定义并设置Weapon通道为忽略

    !CoopGame03-武器2

播放开火特效代码优化

  1. 合并后的代码

    /*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());
        }
    }

击中头部伤害暴击

  1. 修改武器类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);
            }
            //...
    
        }
    }
    
  2. 绘制伤害调试球。

CoopGame03-武器2

鼠标长按连发开火

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();
}
文章目录