CoopGame06-AI基础

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

跟随B站up主“技术宅阿棍儿”的教程制作的笔记。教程链接

AI导航

资源准备:

导入最新工程的TrackerBot文件夹。工程地址

1.创建继承Pawn类的AI角色球STracerBot的C++类,。

STracerBot.h
UPROPERTY(VisibleAnywhere,BlueprintReadOnly,Category="Components")
class UStaticMeshComponent* StaticMeshComp;
STracerBot.cpp
ASTracerBot::ASTracerBot()
{
//...
    StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMeshComp"));
    RootComponent = StaticMeshComp;

        StaticMeshComp->SetCanEverAffectNavigation(false);//自身不影响导航
//...
}

2.创建继承STracerBotC++类的BP_TracerBot蓝图类。

3.给蓝图类设置静态网格体:复制默认的球体网格体到新建文件夹Meshes中,重命名为SM_TracerBot,打开静态网格体设置其编译设置,构建比例为0.2

4.创建材质M_TracerBot,先给个白色基础颜色,并设置网格体SM_TracerBot的材质为这个。

5.放大场地,摆放一些简单的阻挡物,做一个简单的场景,拖入BP_TracerBot.

6.在放置Actor窗口,选择体积,选择导航网格体边界体积拖入场景内,并调节大小覆盖场景,按p可显示导航面积。

7.Build.cs文件中添加导航系统

项目名.Build.cs
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" ,"NavigationSystem"});

8.获取导航路径的下一个点

STracerBot.h
FVector GetNextPathPoint();
STracerBot.cpp
FVector ASTracerBot::GetNextPathPoint()
{
    //拿到0号玩家对象
    APawn* PlayerPawn = UGameplayStatics::GetPlayerPawn(this, 0);
    //立即找到下一个路径(上下文,路径开始点,目标Actor)
    UNavigationPath* NavPath = UNavigationSystemV1::FindPathToActorSynchronously(this,GetActorLocation(),PlayerPawn);
    //如果路径点数量大于1返回下一个位置点
    if (NavPath->PathPoints.Num()>1)
    {
        return NavPath->PathPoints[1];
    }
    //否则返回初始位置
    return GetActorLocation();
}

9.用作用力移动AI角色球

STracerBot.h
    //添加力的大小
    UPROPERTY(EditDefaultsOnly,Category="TracerBot")
    float MovementForce;
    //是否使用改变速度
    UPROPERTY(EditDefaultsOnly,Category="TracerBot")
    bool bUseVelocityChange;
    //距离目标多少时判定为到达
    UPROPERTY(EditDefaultsOnly,Category="TracerBot")
    float RequiredDistanceToTarget;
    //下一个点的变量
    UPROPERTY()
    FVector NextPathPoint;
STracerBot.cpp
ASTracerBot::ASTracerBot()
{
//...
    //设置AI球网格体启用物理模拟
    StaticMeshComp->SetSimulatePhysics(true);
    //启用力改变速度
    bUseVelocityChange = true;
    //作用力大小
    MovementForce = 200;
    //判定到达目标的距离
    RequiredDistanceToTarget = 100;
//...
}

void ASTracerBot::BeginPlay()
{
    Super::BeginPlay();
    //获取到下一个导航点并赋值
    NextPathPoint = GetNextPathPoint();
}

void ASTracerBot::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    //获得两点向量差的大小,既是距离下一个点的距离
    float DistanceToTarget = (GetActorLocation() - NextPathPoint).Size();

    //距离下一个点的距离小于阈值100则继续获取下一个点,如果还没到则推进。
    if (DistanceToTarget <= RequiredDistanceToTarget)
    {
        NextPathPoint = GetNextPathPoint();
        DrawDebugSphere(GetWorld(), NextPathPoint, 20, 12, FColor::Yellow, 0.0f, 0, 1.0f);
    }
    else
    {
        //获得从AI球指向下一个点的向量。
        FVector ForceDirection = NextPathPoint - GetActorLocation();
        //获取方向,不受大小的影响。
        ForceDirection.Normalize();
        //方向向量 * 力 = 有方向的力,用来推动小球。
        ForceDirection *= MovementForce;
        //添加推力使小球朝目标滚动。同时改变小球的速度。
        StaticMeshComp->AddForce(ForceDirection, NAME_None, bUseVelocityChange);
        //画出AI球的运动方向
        DrawDebugDirectionalArrow(GetWorld(), GetActorLocation(), GetActorLocation() + ForceDirection, 32, FColor::Red,
                                  false, 0.0f,
                                  0, 1.0f);
    }
    ////画出下一个目标点位置
    DrawDebugSphere(GetWorld(), NextPathPoint, 20, 12, FColor::Yellow, false, 0.0f, 1.0f);
}

与玩家互动

1.添加造成伤害支持

STracerBot.h
UPROPERTY(VisibleAnywhere,BlueprintReadOnly,Category="Conmponents")
class USHealthComponent* HealthComp;

//生命值委托函数?
UFUNCTION()
void HandleTakeDamage(class USHealthComponent* OwningHealthComp,float Health,float HealthDelta,const class UDamageType* DamageType, class AController*InstigatedBy, AActor* DamageCauser);
STracerBot.cpp
ASTracerBot::ASTracerBot()
{
//...
    HealthComp = CreateDefaultSubobject<USHealthComponent>(TEXT("HealthComp"));
    HealthComp->OnHealthChanged.AddDynamic(this,&ASTracerBot::HandleTakeDamage);
//...
}

void ASTracerBot::HandleTakeDamage(USHealthComponent* OwningHealthComp, float Health, float HealthDelta,
    const UDamageType* DamageType, AController* InstigatedBy, AActor* DamageCauser)
{
    UE_LOG(LogTemp,Log,TEXT("Health %s of %s"),*FString::SanitizeFloat(Health),*GetName);
}

2.受伤闪烁,通过控制材质参数来改变材质。

M_TracerBot材质文件修改。

CoopGame06-AI基础

STracerBot.h
class UMaterialInstanceDynamic* MatInstance;
STracerBot.cpp
void ASTracerBot::HandleTakeDamage(USHealthComponent* OwningHealthComp, float Health, float HealthDelta,
    const UDamageType* DamageType, AController* InstigatedBy, AActor* DamageCauser)
{
    if (MatInstance ==nullptr)
    {
                //拿到材质实例
        MatInstance = StaticMeshComp->CreateAndSetMaterialInstanceDynamicFromMaterial(0,StaticMeshComp->GetMaterial(0));
    }
    if (MatInstance)
    {
                //受伤时,通过改变材质中的参数,这时这个参数几乎等于输入Timer,所以会亮一下。
        MatInstance->SetScalarParameterValue("LastTimeDamageTaken",GetWorld()->TimeSeconds);
    }

    UE_LOG(LogTemp,Log,TEXT("Health %s of %s"),*FString::SanitizeFloat(Health),*GetName());
}

3.AI球生命值为0时解体爆炸,记得给BP_TracerBot蓝图类的类默认值指定爆炸特效。

STracerBot.h
    void SelfDestruct();//自毁函数,需要生成爆炸特效,造成范围伤害。

    UPROPERTY(EditDefaultsOnly,Category="TracerBot")
    class UParticleSystem* ExplosionEffect;//爆炸特效

    bool bExploded;//是否爆炸

    UPROPERTY(EditDefaultsOnly,Category="TracerBot")
    float ExplosionRadius;//爆炸范围

    UPROPERTY(EditDefaultsOnly,Category="TracerBot")
    float ExplosionDamage;//爆炸伤害
STracerBot.cpp
ASTracerBot::ASTracerBot()
{
//...
    ExplosionRadius = 200;
    ExplosionDamage = 100;
//...
}

void ASTracerBot::HandleTakeDamage(USHealthComponent* OwningHealthComp, float Health, float HealthDelta,
    const UDamageType* DamageType, AController* InstigatedBy, AActor* DamageCauser)
{
//...
    if (Health <= 0)
    {
        SelfDestruct();
    }
//...
}

void ASTracerBot::SelfDestruct()
{
    //如果爆炸过直接返回
    if (bExploded) return;
    //设置爆炸
    bExploded = true;
    //播放爆炸特效
    UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ExplosionEffect, GetActorLocation());
    //添加忽略的Actor,自身
    TArray<AActor*> IgnoredActors;
    IgnoredActors.Add(this);
    //造成范围伤害ApplyRadialDamage(上下文,原点的基础伤害,原点位置,伤害半径,伤害类型类,要忽略的Actor列表,造成伤害的人,负责造成伤害的控制器,伤害是否根据原点缩放)
    UGameplayStatics::ApplyRadialDamage(this, ExplosionDamage, GetActorLocation(), ExplosionRadius, nullptr,
                                        IgnoredActors, this, GetInstigatorController(), true);
    DrawDebugSphere(GetWorld(), GetActorLocation(), ExplosionRadius, 12, FColor::Green, false, 2.0f, 0, 1.0f);
    //设置这个Actor的寿命。当寿命结束就会销毁。
    SetLifeSpan(2.0f);
}

4.靠近玩家时倒计时自爆。

STracerBot.h
    //球形组件,用来判断和玩家重叠
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
    class USphereComponent* SphereComp;
    //时间句柄
    FTimerHandle TimerHandle_SelfDamage;
    //伤害自己的函数
    void DamageSelf();
    //是否开始伤害自己
    bool bStartSelfDestruction;
    //重叠事件
    virtual void NotifyActorBeginOverlap(AActor* OtherActor) override;
STracerBot.cpp
ASTracerBot::ASTracerBot()
{
//...
    SphereComp = CreateDefaultSubobject<USphereComponent>(TEXT("SphereComp"));
    //设置半径
    SphereComp->SetSphereRadius(200);
    //设置碰撞类型为只查询
    SphereComp->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
    //设置所有碰撞通道为忽略
    SphereComp->SetCollisionResponseToChannels(ECR_Ignore);
    //只开启和Pawn类型的重叠
    SphereComp->SetCollisionResponseToChannel(ECC_Pawn,ECR_Overlap);
    SphereComp->SetupAttachment(StaticMeshComp);
}

void ASTracerBot::DamageSelf()
{
    //对自己造成20点伤害,ApplyDamage(被伤害的Actor,基础伤害,造成伤害的控制器,造成伤害的Actor,描述造成伤害的类)
    UGameplayStatics::ApplyDamage(this,20,GetInstigatorController(),this,nullptr);
}

void ASTracerBot::NotifyActorBeginOverlap(AActor* OtherActor)
{
    Super::NotifyActorBeginOverlap(OtherActor);
    if (!bStartSelfDestruction)
    {
        ASCharacter* MyCharacter = Cast<ASCharacter>(OtherActor);
        //如果碰到的是玩家,就用定时器每0.5秒伤害自己一次,一次20滴血
        if (MyCharacter)
        {
            //SetTimer(时间句柄,调用者,调用的方法,调用间隔,是否循环,离第一次调用的延迟)
            GetWorldTimerManager().SetTimer(TimerHandle_SelfDamage,this,&ASTracerBot::DamageSelf,0.5f,true,0.0f);
            //设置为开始自爆
            bStartSelfDestruction = true;
        }
    }
}

5.AI球的音效。(滚动,警告,爆炸),记得在蓝图类选择音效。

STracerBot.h
    //倒计时自爆音效
    UPROPERTY(EditDefaultsOnly,Category="TracerBot")
    class USoundBase *SelfDestructSound;
    //爆炸音效
    UPROPERTY(EditDefaultsOnly,Category="TracerBot")
    class USoundBase* ExplodeSound;
STracerBot.cpp
   void ASTracerBot::NotifyActorBeginOverlap(AActor* OtherActor)
{
    Super::NotifyActorBeginOverlap(OtherActor);
    if (!bStartSelfDestruction)
    {
    //...
        if (MyCharacter)
        {
        //...
        //播放爆炸特效
        UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ExplosionEffect, GetActorLocation());
        //播放爆炸音效
        UGameplayStatics::SpawnSoundAtLocation(this, ExplodeSound, GetActorLocation());
        }

    }
}

void ASTracerBot::SelfDestruct()
{
//...
    UGameplayStatics::SpawnSoundAtLocation(this,ExplodeSound,GetActorLocation());//播放爆炸音效
    Destroy();
}

6.两种设置声音衰减的方法。

1.直接在声音Cue上设置,如图:

CoopGame06-AI基础

2.创建声音衰减文件,如图:

CoopGame06-AI基础

7.设置滚动的声音随速度变化。

1.蓝图BP_TracerBot添加音频组件,选择滚动音效。
2.写蓝图,如图:

CoopGame06-AI基础

联机AI

1.只在服务端执行寻路逻辑。

TracerBot.cpp
void ASTracerBot::BeginPlay()
{
    Super::BeginPlay();
    //只在服务端执行寻路逻辑
    if (GetLocalRole() == ROLE_Authority)
    {
        NextPathPoint = GetNextPathPoint(); //获取到下一个导航点
    }

}

void ASTracerBot::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
        //只在服务端执行寻路逻辑,且AI球没爆炸
    if (GetLocalRole() == ROLE_Authority && !bExploded )
    {
        //...
    }
}

2.修改生命值组件和AI球联机逻辑。

SHealthComponent.h
    UPROPERTY(ReplicatedUsing=OnRep_Health,BlueprintReadOnly,Category="HealthComponent")
    float Health;

    UFUNCTION()
    void OnRep_Health(float OldHealth);
SHealthComponent.cpp

void USHealthComponent::OnRep_Health(float OldHealth)
{
    OnHealthChanged.Broadcast(this,Health,Health - OldHealth,nullptr,nullptr,nullptr);
}
STracerBot.cpp
void ASTracerBot::SelfDestruct()
{
    //...
    //静态网格体设置可见性和关闭碰撞
    StaticMeshComp->SetVisibility(false, true);
    StaticMeshComp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
    //AI球造成范围伤害也在服务端上做
    if (GetLocalRole() == ROLE_Authority)
    {
    //添加忽略的Actor,自身
        TArray<AActor*> IgnoredActors;
        IgnoredActors.Add(this);
        //造成范围伤害ApplyRadialDamage(上下文,原点的基础伤害,原点位置,伤害半径,伤害类型类,要忽略的Actor列表,造成伤害的人,负责造成伤害的控制器,伤害是否根据原点缩放)
        UGameplayStatics::ApplyRadialDamage(this, ExplosionDamage, GetActorLocation(), ExplosionRadius, nullptr,
                                            IgnoredActors, this, GetInstigatorController(), true);
        DrawDebugSphere(GetWorld(), GetActorLocation(), ExplosionRadius, 11, FColor::Green, false, 2.0f, 0, 1.0f);
        //设置这个Actor的寿命。当寿命结束就会销毁。
        SetLifeSpan(1.0f);
    }
}

void ASTracerBot::NotifyActorBeginOverlap(AActor* OtherActor)
{
    Super::NotifyActorBeginOverlap(OtherActor);
    //如果没有开始倒计时自爆,且没有爆炸
    if (!bStartSelfDestruction && !bExploded)
    {
        //...
        //如果碰到的是玩家,就用定时器每0.5秒伤害自己一次,一次20滴血
        if (MyCharacter)
        {
            //设置自爆的定时器也要在服务器上
            if (GetLocalRole() == ROLE_Authority)
            {
                //SetTimer(时间句柄,调用者,调用的方法,调用间隔,是否循环,离第一次调用的延迟)
                GetWorldTimerManager().SetTimer(TimerHandle_SelfDamage, this, &ASTracerBot::DamageSelf, 0.5f, true, 0.0f);
            }
            //...
        }
    }
}

3.运行结果:两端的角色都能被AI球炸死,特效音效正常。

挑战:AI群体Buff

1.AI球附近有多个同类时,闪烁并增加伤害。

StracerBot.h
    //检测附近同类的函数
    UFUNCTION()
    void OnCheckNearbyBots();
    //伤害等级
    int32 PowerLevel;
StracerBot.cpp
void ASTracerBot::BeginPlay()
{
    Super::BeginPlay();
    //只有在服务端上的AI球才寻路
    if (GetLocalRole() == ROLE_Authority)
    {
        //获取到下一个导航点并赋值
        NextPathPoint = GetNextPathPoint();
        //设置每秒调用检测附近AI球同类的OnCheckNearbyBots函数
        FTimerHandle TimerHandle;
        GetWorldTimerManager().SetTimer(TimerHandle, this, &ASTracerBot::OnCheckNearbyBots, 1.0f, true);
    }
}

void ASTracerBot::SelfDestruct()
{
//...
    if (GetLocalRole() == ROLE_Authority)
    {
    //...
        //照成的伤害翻倍取决于附近的同类数量
        float Damage = ExplosionDamage + (ExplosionDamage * PowerLevel);
        //造成范围伤害ApplyRadialDamage(上下文,原点的基础伤害,原点位置,伤害半径,伤害类型类,要忽略的Actor列表,造成伤害的人,负责造成伤害的控制器,伤害是否根据原点缩放)
        UGameplayStatics::ApplyRadialDamage(this, Damage, GetActorLocation(), ExplosionRadius, nullptr,IgnoredActors, this, GetInstigatorController(), true);
        DrawDebugSphere(GetWorld(), GetActorLocation(), ExplosionRadius, 11, FColor::Green, false, 2.0f, 0, 1.0f);
    //...
    }
//...
}

FVector ASTracerBot::GetNextPathPoint()
{
    //拿到0号玩家对象
    APawn* PlayerPawn = UGameplayStatics::GetPlayerPawn(this, 0);
    //拿到0号玩家才继续寻路
    if (PlayerPawn)
    {
        //立即找到下一个路径(上下文,路径开始点,目标Actor)
        UNavigationPath* NavPath = UNavigationSystemV1::FindPathToActorSynchronously(this, GetActorLocation(), PlayerPawn);
        //如果路径点数量大于1返回下一个位置点,且路径存在
        if (NavPath && NavPath->PathPoints.Num() > 1)
        {
            return NavPath->PathPoints[1];
        }
    }
//否则返回初始位置
        return GetActorLocation();
}

void ASTracerBot::OnCheckNearbyBots()
{
    //声明碰撞球,用于检测球体内有多少AI同类。
    FCollisionShape CollisionShape;
    //设置碰撞球半径。
    CollisionShape.SetSphere(600);
    //重叠结果的数组,用来存放所有重叠到的东西
    TArray<FOverlapResult> OverlapResults;
    //碰撞对象查询参数
    FCollisionObjectQueryParams QueryParams;
    //添加需要查询的两种碰撞通道类型
    QueryParams.AddObjectTypesToQuery(ECC_PhysicsBody);
    QueryParams.AddObjectTypesToQuery(ECC_Pawn);
    //按对象类型的重叠检测,将检测到的东西放进OverlapResults数组中
    GetWorld()->OverlapMultiByObjectType(OverlapResults, GetActorLocation(), FQuat::Identity, QueryParams,
                                         CollisionShape);
    //画出用于检测的碰撞球
    DrawDebugSphere(GetWorld(), GetActorLocation(), CollisionShape.GetSphereRadius(), 12, FColor::Blue, false, 1.0f);
    //声明其他同类AI球的数量
    int32 NrOfBots = 0;
    //遍历所有重叠检测到的东西,如果是同类就把同类数量+1
    for (FOverlapResult Result : OverlapResults)
    {
        ASTracerBot* Bot = Cast<ASTracerBot>(Result.GetActor());
        //重叠检测到的是AI球,且不是自己,就计数
        if (Bot && Bot != this)
        {
            NrOfBots++;
        }
    }
    //定义常量最大伤害等级
    const int32 MaxPowerLevel = 4;
    //限制伤害等级范围0-4
    PowerLevel = FMath::Clamp(NrOfBots, 0, MaxPowerLevel);
    if (MatInst == nullptr)
    {
        //拿到AI球的材质实例
        MatInst = StaticMeshComp->
            CreateAndSetMaterialInstanceDynamicFromMaterial(0, StaticMeshComp->GetMaterial(0));
    }
    if (MatInst)
    {
        //通过设置参数来改变材质闪烁,更详细的材质介绍请看视频
        float Alpha = PowerLevel / (float)MaxPowerLevel;
        MatInst->SetScalarParameterValue("PowerLevelAlpha", Alpha);
    }
    //打印伤害等级
    DrawDebugString(GetWorld(), FVector(), FString::FromInt(PowerLevel), this, FColor::White, 1.0f, true);
}

2.材质修改,参数PowerLevelAlpha控制材质闪烁。

蓝图如图:

CoopGame06-AI基础

文章目录