今天做一个怪物追踪与攻击的功能
敌人追踪和攻击
敌人的自动追踪功能涉及AI的机制与功能,因此先在C++工程中的Build.cs文件中添加AIModule如下
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore","UMG", "AIModule" });
然后创建一个敌人的动画实例EnemyAnimInstance继承于AnimInstance
敌人的动画实例准备
覆写动画蓝图初始化函数,以及声明更新蓝图动画的函数 声明运动速度以及蓝图动画的拥有者
public:
//声明一个初始化函数
virtual void NativeInitializeAnimation() override;
//声明一个用于更新动画状态的函数
UFUNCTION(BlueprintCallable,Category = AnimationProperty)
void UpdateAnimationProperties();
//运动速度
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Movement)
float MovementSpeed;
//动画实例的拥有者--敌人
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Movement)
class AEnemy *Enemy;
//动画实例的拥有者--人类
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Movement)
class APawn *Pawn;
敌人的动画实例函数
这里和人物动画那里的配置是类似的
初始化函数中将动画实例的拥有者转换为Enemy
void UEnemyAnimInstance::NativeInitializeAnimation()
{
if (!Pawn)
{
Pawn = TryGetPawnOwner();
}
//把敌人拥有者初始化
Enemy = Cast<AEnemy>(Pawn);
}
因为移动速度是一个我们需要用在状态机的变量,
因此定义一个状态更新函数,实时进行参数的更新和传递
void UEnemyAnimInstance::UpdateAnimationProperties()
{
if (!Pawn)
{
Pawn = TryGetPawnOwner();
}
//更新敌人的速率
if (Pawn)
{
//获取Pawn的速度
FVector Speed = Pawn->GetVelocity();
//获取Pawn的横向速度
FVector LateralSpeed = FVector(Speed.X, Speed.Y, 0);
MovementSpeed = LateralSpeed.Size();
}
}
敌人类
有了敌人的动画类,然后我们需要一个敌人类去设定追踪目标以及攻击目标的各种条件
敌人的状态目前来说是分攻击、追踪和静止三个状态的,所以我们定义一个枚举变量表示状态
//定义枚举类,三个敌人的状态
UENUM(BlueprintType)
enum class EMoveStatus :uint8
{
//站立追逐和攻击状态
MS_Idle UMETA(DisplayName = "Idle"),
MS_MoveToTarget UMETA(DisplayName = "MoveToTarget"),
MS_Attacking UMETA(DisplayName = "Attacking")
};
该枚举状态需要在类中声明
其攻击状态对应着其攻击范围、攻击目标以及开始和结束攻击的函数
同理,其追踪状态对应着其追踪范围、追踪目标以及追踪开始和结束的函数
另外,敌人是通过AI模块追踪的,所以也需要声明AI控制器以及追踪函数
public:
// Sets default values for this character's properties
AEnemy();
//声明敌人的侦查范围和攻击范围
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
class USphereComponent * DetectSphere;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
class USphereComponent * AttackSphere;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
EMoveStatus MoveStatus;
//AI控制器,因为追踪玩家时需要导航
class AAIController *AIController;
//记录敌人追踪时的目标
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
class AMan * TargetMan;
//记录敌人攻击时的目标
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
class AMan * HittingMan;
//检测时的重合和离开函数
UFUNCTION()
virtual void OnDetectOverlapBegin(UPrimitiveComponent* OverlappedComponent,
AActor* OtherActor, UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex, bool bFromSweep,
const FHitResult & SweepResult);
UFUNCTION()
virtual void OnDetectOverlapEnd(UPrimitiveComponent* OverlappedComponent,
AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
//攻击时的重合和离开函数
UFUNCTION()
virtual void OnAttackOverlapBegin(UPrimitiveComponent* OverlappedComponent,
AActor* OtherActor, UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex, bool bFromSweep,
const FHitResult & SweepResult);
UFUNCTION()
virtual void OnAttackOverlapEnd(UPrimitiveComponent* OverlappedComponent,
AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
//向目标移动的函数
UFUNCTION(BlueprintCallable)
void MoveToTarget();
敌人类实现
首先在初始化函数中对两个范围初始化
AEnemy::AEnemy()
{
// Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
//侦测范围初始化
DetectSphere = CreateDefaultSubobject<USphereComponent>(TEXT("DetectSphere"));
DetectSphere->SetupAttachment(RootComponent);
DetectSphere->SetSphereRadius(600);
//攻击范围初始化
AttackSphere = CreateDefaultSubobject<USphereComponent>(TEXT("AttackSphere"));
AttackSphere->SetupAttachment(RootComponent);
AttackSphere->SetSphereRadius(300);
}
然后设置追击开始函数,目前的追踪函数只追踪一个目标;
将进入范围Actor进行类型转换为Man,如果成功,则将Man设为TargetMan;
另外设置在攻击时不能追踪,如果没有在攻击状态,则将状态设置为追击,
然后调用追击函数,追击函数只在状态为追击时才有效
void AEnemy::OnDetectOverlapBegin(UPrimitiveComponent * OverlappedComponent, AActor * OtherActor, UPrimitiveComponent * OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult & SweepResult)
{
//如果没有追踪对象,则获取;有追踪对象则不执行,相当于只追踪一个目标
if (!TargetMan)
{
if (OtherActor)
{
AMan *Man = Cast<AMan>(OtherActor);
if (Man)
{
TargetMan = Man;
//攻击状态时播放动画的状态,在播放状态不可以追踪
if (MoveStatus != EMoveStatus::MS_Attacking)
{
//如果不是攻击状态,进行追击
MoveStatus = EMoveStatus::MS_MoveToTarget;
}
MoveToTarget();
}
}
}
}
然后是离开追击范围的函数,这里的逻辑做了简化,并没有设置追踪第二个人;
如果离开范围的是追踪目标且没有攻击,则将状态设置为站立
并停止AI控制器的自动追踪
void AEnemy::OnDetectOverlapEnd(UPrimitiveComponent * OverlappedComponent, AActor * OtherActor, UPrimitiveComponent * OtherComp, int32 OtherBodyIndex)
{
if (OtherActor)
{
AMan *Man = Cast<AMan>(OtherActor);
//如果离开的对象是被追踪的对象
if (Man == TargetMan)
{
//清空被追踪的对象
TargetMan = nullptr;
if (MoveStatus != EMoveStatus::MS_Attacking)
{
//如果不是在攻击,就变成站立
MoveStatus = EMoveStatus::MS_Idle;
}
if (AIController)
{
//AI控制器停止追踪
AIController->StopMovement();
}
}
}
}
攻击范围在追击范围之内,当进入攻击范围时,敌人状态进入攻击状态;
开始攻击时停止追击
void AEnemy::OnAttackOverlapBegin(UPrimitiveComponent * OverlappedComponent, AActor * OtherActor, UPrimitiveComponent * OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult & SweepResult)
{
//如果没有攻击对象,则获取;有追踪对象则不执行,相当于只追踪一个目标
if (!HittingMan)
{
if (OtherActor)
{ //如果没有攻击的目标,把当前进入范围的Pawn转化为HittingMan
AMan *Man = Cast<AMan>(OtherActor);
if (Man)
{
HittingMan = Man;
//如果有玩家则无条件得进入攻击状态
MoveStatus = EMoveStatus::MS_Attacking;
//开始攻击则停止追踪
if (AIController)
{
AIController->StopMovement();
}
}
}
}
}
离开攻击范围时,主角会先进入追踪范围,因此将HittingMan赋值给TargetMan,HittingMan清空;
void AEnemy::OnAttackOverlapEnd(UPrimitiveComponent * OverlappedComponent, AActor * OtherActor, UPrimitiveComponent * OtherComp, int32 OtherBodyIndex)
{
if (OtherActor)
{
AMan *Man = Cast<AMan>(OtherActor);
if (Man == HittingMan)
{
//离开了攻击范围,把攻击者变成追踪者
//这里简单处理,不会从攻击状态停止就在攻击范围内找另一个玩家
TargetMan = HittingMan;
HittingMan = nullptr;
}
}
}
然后是追踪时调用的函数MoveToTarget,当有TargetMan时调用AIController模块追踪,否则状态为站立
AIController调用MoveTo函数有两个关键参数,一个是移动请求,一个是路线;
移动请求通过FAIMoveRequest定义,并设置追踪对象为TargetMan,且在TargetMan为圆心半径10范围内,判定为到达,
FNavPathSharedPtr定义追踪路线,配合追踪盒子使用,
需要在场景中设置追踪盒子的范围,且敌人在范围内才能追踪,通常追踪盒子下边缘要在地面之下才能完全包住敌人,敌人才能进行追踪
void AEnemy::MoveToTarget()
{
if (TargetMan)
{
if (AIController)
{
UE_LOG(LogTemp, Warning, TEXT("%s"), *FString(__FUNCTION__));
//设置AI移动请求
FAIMoveRequest MoveRequest;
//AI移动追踪的目标是TargetMan
MoveRequest.SetGoalActor(TargetMan);
//在TargetMan半径10范围内算是追到了
MoveRequest.SetAcceptanceRadius(10);
//追踪经过的路径,作为输出可以告诉我们,AI是用什么路径去追踪的
FNavPathSharedPtr NavPath;
//AI控制器,施加移动请求
AIController->MoveTo(MoveRequest, &NavPath);
//将路径上所有的点用球画出来
#if 0
auto PathPoints = NavPath->GetPathPoints();
for (auto Point:PathPoints)
{
FVector Location = Point.Location;
UKismetSystemLibrary::DrawDebugSphere(this, Location);
}
#endif
}
}
else
{
MoveStatus = EMoveStatus::MS_Idle;
}
}
BeginPlay中获取AAIController,并将AI追踪和触发开始结束的函数进行绑定
void AEnemy::BeginPlay()
{
Super::BeginPlay();
//获取AI控制器
AIController = Cast<AAIController>(GetController());
//将AI追踪触发进行函数绑定
DetectSphere->OnComponentBeginOverlap.AddDynamic(this, &AEnemy::OnDetectOverlapBegin);
DetectSphere->OnComponentEndOverlap.AddDynamic(this, &AEnemy::OnDetectOverlapEnd);
//将AI攻击触发进行函数绑定
AttackSphere->OnComponentBeginOverlap.AddDynamic(this, &AEnemy::OnAttackOverlapBegin);
AttackSphere->OnComponentEndOverlap.AddDynamic(this, &AEnemy::OnAttackOverlapEnd);
}
敌人动画
代码部分完成后,需要对敌人类设置一个动画状态机,以敌人是否在攻击状态为变换条件;
idel/run中播放以速度变量为参考的1维混合动画;Attack中播放攻击动画
除此之外,程序里并没有攻击结束的逻辑,和之前一样,在程序中判断动画的结束比较困难;
因此在动画中添加一个结束事件,并在动画蓝图中以攻击结束写蓝图逻辑