首先给一直在等待这个系列的各位道个歉,因为期末一直在准备答辩,所以鸽了很久很久。。。还是先上教程链接吧,给第一次点进来的朋友指个路:https://www.bilibili.com/video/BV1nU4y1X7iQ
这个是AI这一章节的最后一个部分了,说句实话,我当时看AI的这个部分的时候人有点懵,因为毕竟不是我们传统意义上的玩家输入游戏内的人物作出回应的模式了,行为树的概念,每个节点的意义我如今也还在慢慢摸索,看别的游戏的敌人AI,只会感叹,感叹这个AI做的有多么的好,这些是我还是玩家的时候从未想过的问题。
好了,言归正传,这一段教程主要讲了这几件事:
- AI的受伤相关事件,包括:受伤,受伤导致的死亡,布娃娃系统,收到伤害后知道是谁在打它,受伤的视觉反馈
- AI的攻击相关事件,包括:攻击的随机性(总不能让电脑跟个开挂的一样准心锁着你走吧?)
- 游戏框架相关,包括:静态函数
还是老规矩,上代码,在代码内用注释来讲怎么做这些改变,然后在后面做个总结
SAICharacter.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "SAICharacter.generated.h"
UCLASS()
class ACTIONROGUELIKE_API ASAICharacter : public ACharacter
{
GENERATED_BODY()
protected:
UPROPERTY(EditAnywhere,Category="Component")
class UPawnSensingComponent* PawnSensingComp;
//回忆一下你当初在SCharacter类里头怎么加上这个组件的?还有就是你的血量是怎么存储的,你是怎么"死"的?
//对于SAICharacter和SCharacter来说,这些都是大差不差的。
UPROPERTY(EditDefaultsOnly,Category="Component")
class USAttributeComponent* AttributeComp;
UFUNCTION()
void OnPawnSee(APawn * Pawn);
public:
// Sets default values for this character's properties
ASAICharacter();
protected:
virtual void PostInitializeComponents() override;
void SetTargetActor(AActor * NewTarget);
UFUNCTION()
void OnHealthChanged(AActor* InstigatorActor, USAttributeComponent* OwningComp, float NewHealth, float Delta);
};
SAICharacter.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "AI/SAICharacter.h"
#include "AIModule/Classes/AIController.h"
#include "AIModule/Classes/BehaviorTree/BlackboardComponent.h"
#include "AIModule/Classes/Perception/PawnSensingComponent.h"
#include "Delegates/Delegate.h"
#include "SAttributeComponent.h"
#include "BrainComponent.h"
#include "GameFramework/Actor.h"
void ASAICharacter::OnPawnSee(APawn* Pawn)
{
SetTargetActor(Pawn);
}
// Sets default values
ASAICharacter::ASAICharacter()
{
AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
PawnSensingComp = CreateDefaultSubobject<UPawnSensingComponent>("PawnSensingComp");
//创建组件
AttributeComp = CreateDefaultSubobject<USAttributeComponent>("AttributeComp");
}
void ASAICharacter::PostInitializeComponents()
{
Super::PostInitializeComponents();
//函数绑定
FScriptDelegate OnSee;
OnSee.BindUFunction(this, STATIC_FUNCTION_FNAME(TEXT("ASAICharacter::OnPawnSee")));
PawnSensingComp->OnSeePawn.Add(OnSee);
AttributeComp->OnHealthChanged.AddDynamic(this, &ASAICharacter::OnHealthChanged);
}
void ASAICharacter::SetTargetActor(AActor* NewTarget)
{
AAIController* AIC = Cast<AAIController>(GetController());
if (AIC)
{
AIC->GetBlackboardComponent()->SetValueAsObject("TargectActor", NewTarget);
}
}
//受击函数
void ASAICharacter::OnHealthChanged(AActor* InstigatorActor, USAttributeComponent* OwningComp, float NewHealth, float Delta)
{
if (Delta < 0.0f)
{
//在AI被攻击后,如果发现InstigatorActor不是自己的话,那么就进行赋值,也就是说,挨打之后知道是谁打的它
if (InstigatorActor != this)
{
SetTargetActor(InstigatorActor);
}
//下面这一行语句你能在SCharacter内找到,作用就是被打的时候就闪一下
this->GetMesh()->SetScalarParameterValueOnMaterials("TimeToHit", GetWorld()->TimeSeconds);
//当已有的生命值等于或小于0的时候(其实不会小于0,在我的改版后的项目内,生命值只有0到100,不会有超出100或小于0的情况),执行下面的语句,也就是执行死亡的语句
if (NewHealth<=0.0f)
{
//stop BT
AAIController* AIC = Cast<AAIController>(GetController());
if (AIC)
{
AIC->GetBrainComponent()->StopLogic("Killed");
}
//rag doll
//开启布娃娃系统,同时我们要更改网格的碰撞通道文件,改成Ragdoll。
GetMesh()->SetAllBodiesSimulatePhysics(true);
GetMesh()->SetCollisionProfileName("Ragdoll");
//set lifespan
//设置留存时间,这里的意思是,会留存10s,然后会被垃圾回收
SetLifeSpan(10.0f);
}
}
}
受击这块整体没有什么大问题,因为我们以前在SCharacter类里头是做过的。主要在工作的还是我们所写的Attribute组件。这里就可以看到我们写组件的好处了,如今有了两个不同的类在使用一段相同的代码,如果以后需要维护也只需要更改Attribute类里面的东西就可以了,而不需要两个类往返跑。
接下来是AI的攻击相关的事件了,还记得我们的攻击函数写在哪里吗?我们的攻击函数是写在了行为树里面,也就是说,在UE的角度看来,攻击并不是玩家的属性,而是一个行为,所以会被划分到行为树里面。
SBTTask_RangedAttack.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "SBTTask_RangedAttack.generated.h"
/**
*
*/
UCLASS()
class ACTIONROGUELIKE_API USBTTask_RangedAttack : public UBTTaskNode
{
GENERATED_BODY()
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
//子弹散布的最大值
UPROPERTY(EditAnywhere,Category="AI")
float MaxBulletSpread;
protected:
UPROPERTY(EditAnywhere,Category="AI")
TSubclassOf<AActor>ProjectileClass;
public:
USBTTask_RangedAttack();
};
SBTTask_RangedAttack.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "AI/SBTTask_RangedAttack.h"
#include "AIModule/Classes/AIController.h"
#include "GameFramework/Character.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "SAttributeComponent.h"
EBTNodeResult::Type USBTTask_RangedAttack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
AAIController* MyController = OwnerComp.GetAIOwner();
if (ensure(MyController))
{
ACharacter* MyPawn = Cast<ACharacter>(MyController->GetPawn());
if (MyPawn == nullptr)
{
return EBTNodeResult::Failed;
}
FVector MuzzleLocation = MyPawn->GetMesh()->GetSocketLocation("Muzzle_01");
AActor* TargetActor = Cast<AActor>(OwnerComp.GetBlackboardComponent()->GetValueAsObject("TargectActor"));
if (TargetActor == nullptr)
{
return EBTNodeResult::Failed;
}
if (!USAttributeComponent::IsActorAlive(TargetActor))
{
return EBTNodeResult::Failed;
}
FVector Direction = TargetActor->GetActorLocation() - MuzzleLocation;
FRotator MuzzleRotation = Direction.Rotation();
//在这里进行插入,上面的代码已经获得了Rotation的数值,我们所说的散布其实就是对Rotation做手脚罢了,让Rotation并不是指向我们的玩家,而是指向我们玩家所在范围的一片区域内
MuzzleRotation.Pitch += FMath::RandRange(0.0f, MaxBulletSpread);
MuzzleRotation.Yaw += FMath::RandRange(-MaxBulletSpread, MaxBulletSpread);
FActorSpawnParameters Params;
Params.Instigator = MyPawn;
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
AActor* NewProj = GetWorld()->SpawnActor<AActor>(ProjectileClass, MuzzleLocation, MuzzleRotation, Params);
return NewProj ? EBTNodeResult::Succeeded : EBTNodeResult::Failed;
}
return EBTNodeResult::Failed;
}
USBTTask_RangedAttack::USBTTask_RangedAttack()
{
//初始化的时候我们设置最大散布为7,当然你也可以调整
MaxBulletSpread = 7.0f;
}
再到最后的静态函数部分,不知道各位有没有用过函数库,我觉得静态函数和函数库并没有什么区别,都是随时随地都可以调用的函数,对于我们的开发很方便。复用的时候不需要重复写过多的函数。
此处就拿SAttributeComponent举例
SAttributeComponent.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "SAttributeComponent.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FOnHealthChange, AActor*,InstigatorActor,USAttributeComponent*,OwingComp,float,NewHealth,float,Delta );
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class ACTIONROGUELIKE_API USAttributeComponent : public UActorComponent
{
GENERATED_BODY()
protected:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Attributes")
float Health;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Attributes")
float HealthMax;
public:
// Sets default values for this component's properties
USAttributeComponent();
public:
UFUNCTION(BlueprintCallable)
bool IsAlive() const;
public:
UFUNCTION(BlueprintCallable)
float GetHealth() const;
UPROPERTY(BlueprintAssignable)
FOnHealthChange OnHealthChanged;
public:
UFUNCTION(BlueprintCallable,Category="Attributes")
bool ApplyHealthChange(AActor*InstigatorActor,float Delta);
//静态函数声明
UFUNCTION(BlueprintCallable,Category="Attributes")
static USAttributeComponent* GetArrtibutes(AActor* FromActor);
//静态函数声明
UFUNCTION(BlueprintCallable, Category = "Attributes")
static bool IsActorAlive(AActor* Actor);
};
SAttributeComponent.cpp
#include "SAttributeComponent.h"
#include "SCharacter.h"
USAttributeComponent::USAttributeComponent()
{
PrimaryComponentTick.bCanEverTick = true;
Health = 100.0f;
HealthMax = 100.0f;
}
bool USAttributeComponent::IsAlive() const
{
return Health > 0.0f;
}
float USAttributeComponent::GetHealth() const
{
return Health;
}
bool USAttributeComponent::ApplyHealthChange(AActor* InstigatorActor,float Delta)
{
Health = FMath::Clamp(Health+Delta, 0, HealthMax);
OnHealthChanged.Broadcast(InstigatorActor,this,Health,Delta);
return true;
}
//静态函数实现
USAttributeComponent* USAttributeComponent::GetArrtibutes(AActor* FromActor)
{
if (FromActor)
{
return Cast<USAttributeComponent>(FromActor->GetComponentByClass(USAttributeComponent::StaticClass()));
}
return nullptr;
}
//静态函数实现
bool USAttributeComponent::IsActorAlive(AActor* Actor)
{
USAttributeComponent* AttributeComp = GetArrtibutes(Actor);
if (AttributeComp)
{
return AttributeComp->IsAlive();
}
return false;
}
既然我们声明并实现了静态函数了,那么我们该如何调用呢?
用SGameModeBase.cpp做示例
SGameModeBase.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "SGameModeBase.h"
#include "EnvironmentQuery/EnvQueryManager.h"
#include "EnvironmentQuery/EnvQueryTypes.h"
#include "EnvironmentQuery/EnvQueryInstanceBlueprintWrapper.h"
#include "../Public/AI/SAICharacter.h"
#include "../Public/SAttributeComponent.h"
#include "../../../../../UE_5.1/Engine/Source/Runtime/Engine/Public/EngineUtils.h"
void ASGameModeBase::SpawnBotTimerElapsed()
{
int32 NrOfAliveBots = 0;
for (TActorIterator<ASAICharacter>It(GetWorld()); It; ++It)
{
ASAICharacter* Bot = *It;
//看到了下面这行代码的GetArrtibutes么?这就是我们刚刚在上面的代码中声明并实现了的静态函数
//也就是说,调用方法为 类名::静态函数名(参数)
USAttributeComponent* AttributeComp = USAttributeComponent::GetArrtibutes(Bot);
if (ensure(AttributeComp) && AttributeComp->IsAlive())
{
NrOfAliveBots++;
}
}
float MaxBotCount = 10.0f;
if (DifficultyCurve)
{
MaxBotCount = DifficultyCurve->GetFloatValue(GetWorld()->TimeSeconds);
}
if (NrOfAliveBots >= MaxBotCount)
{
return;
}
UEnvQueryInstanceBlueprintWrapper* QuetyInstance = UEnvQueryManager::RunEQSQuery(this,SpawnBotQuery,this,EEnvQueryRunMode::RandomBest5Pct,nullptr);
if (ensure(QuetyInstance))
{
FScriptDelegate QueryCompleted;
QueryCompleted.BindUFunction(this, STATIC_FUNCTION_FNAME(TEXT("&ASGameModeBase::OnQueryCompleted")));
QuetyInstance->GetOnQueryFinishedEvent().Add(QueryCompleted);
}
}
void ASGameModeBase::OnQueryCompleted(UEnvQueryInstanceBlueprintWrapper* QueryInstance, EEnvQueryStatus::Type QueryStatus)
{
if (QueryStatus != EEnvQueryStatus::Success)
{
UE_LOG(LogTemp,Warning,TEXT("Spawn EQS Failed!!!"))
return;
}
TArray<FVector> Locations = QueryInstance->GetResultsAsLocations();
if (Locations.IsValidIndex(0))
{
GetWorld()->SpawnActor<AActor>(MinionClass,Locations[0],FRotator::ZeroRotator);
}
}
ASGameModeBase::ASGameModeBase()
{
SpawnTimerInterval = 2.0f;
}
void ASGameModeBase::StartPlay()
{
Super::StartPlay();
GetWorldTimerManager().SetTimer(TimerHandle_SpawnBots, this, &ASGameModeBase::SpawnBotTimerElapsed, SpawnTimerInterval, true);
}
以上就是第13章的内容了,总体来说没有很困难的地方,有新的概念但是比较好消化,如果有什么问题的话记得私信我,我看到了的话会回你们的。