UEC++ day7

news2024/11/15 16:01:18

敌人NPC机制

敌人机制分析与需求

  • 新建一个character类来作为敌人,直接建蓝图设置骨骼网格,因为敌人可能多种就不规定死,然后这个敌人肯定需要两个触发器,一个用于大范围巡逻,一个用于是否达到主角近点进行攻击
    在这里插入图片描述
  • 注意我们要避免摄像机被敌人阻挡
  • BaseEnemy.h
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "BaseEnemy.generated.h"

UCLASS()
class UEGAME_API ABaseEnemy : public ACharacter
{
	GENERATED_BODY()

public:
	// Sets default values for this character's properties
	ABaseEnemy();

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
	class USphereComponent* ChaseVolume;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
	USphereComponent* AttackVolume;
protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

	// Called to bind functionality to input
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

	UFUNCTION()
	virtual void OnChaseVolumeOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
	UFUNCTION()
	virtual void OnChaseVolumeOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
	UFUNCTION()
	virtual void OnAttackVolumeOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
	UFUNCTION()
	virtual void OnAttackVolumeOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
};
  • BaseEnemy.cpp
// Fill out your copyright notice in the Description page of Project Settings.


#include "BaseEnemy.h"
#include "Components/SphereComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "Components/CapsuleComponent.h"
// Sets default values
ABaseEnemy::ABaseEnemy()
{
 	// 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;
	
	//避免摄像机被敌人给阻挡
	GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
	GetCapsuleComponent()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);

	ChaseVolume = CreateDefaultSubobject<USphereComponent>(TEXT("ChaseVolume"));
	ChaseVolume->SetupAttachment(GetRootComponent());
	ChaseVolume->InitSphereRadius(800.f);
	ChaseVolume->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic);
	ChaseVolume->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
	ChaseVolume->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap);

	AttackVolume = CreateDefaultSubobject<USphereComponent>(TEXT("AttackVolume"));
	AttackVolume->SetupAttachment(GetRootComponent());
	AttackVolume->InitSphereRadius(100.f);
	AttackVolume->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic);
	AttackVolume->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
	AttackVolume->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap);
}

// Called when the game starts or when spawned
void ABaseEnemy::BeginPlay()
{
	Super::BeginPlay();

	ChaseVolume->OnComponentBeginOverlap.AddDynamic(this, &ABaseEnemy::OnChaseVolumeOverlapBegin);
	ChaseVolume->OnComponentEndOverlap.AddDynamic(this, &ABaseEnemy::OnChaseVolumeOverlapEnd);

	AttackVolume->OnComponentBeginOverlap.AddDynamic(this, &ABaseEnemy::OnAttackVolumeOverlapBegin);
	AttackVolume->OnComponentEndOverlap.AddDynamic(this, &ABaseEnemy::OnAttackVolumeOverlapEnd);
	
}

// Called every frame
void ABaseEnemy::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

// Called to bind functionality to input
void ABaseEnemy::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

}

void ABaseEnemy::OnChaseVolumeOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
}

void ABaseEnemy::OnChaseVolumeOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
}

void ABaseEnemy::OnAttackVolumeOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
}

void ABaseEnemy::OnAttackVolumeOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
}
  • 注意虚幻的visibility与camera默认是block
    在这里插入图片描述

部署导航网格

++ Nav Mesh Bounds Volume:导航网格
在这里插入图片描述

  • Nav Modifier Volume:修改导航网格

添加AI模块与创建敌人移动状态枚举

  • 有些组件的使用是需要添加依赖项的就像之前的UMG需要添加到自己的工程目录下的工程名.Build.cs里面,调用AI的模块就需要添加AIModule
    在这里插入图片描述
  • 添加敌人移动状态枚举变量与接近主角的函数
UENUM(BlueprintType)
enum class EEnemyMovementStatus :uint8
{
	EEMS_Idle			UMETA(DisplayName="Idle"),
	EEMS_MoveToTarget	UMETA(DisPlayName="MoveToTarget"),
	EEMS_Attacking		UMETA(DisPlayName="Attacking"),
	EEMS_Dead			UMETA(DisPlayName="Dead")
};

//--------------------------------------------------------------
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Enemy Stats")
	EEnemyMovementStatus EnemyMovementStatus;
//--------------------------------------------------------------
	void MoveToTarget(class AMainPlayer* Player);

获取AIController并持有敌人

  • AAIcontroller* AIController所需头文件:#include "AIController.h"
  • 声明AAIController类的指针
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
class AAIController* AIController;
  • 设置持有属性
// Sets default values
ABaseEnemy::ABaseEnemy()
{
 	// 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;
//-----------------省略----------------------------------------------
	//设置持有属性
	AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
	//初始化默认移动状态
	EnemyMovementStatus = EEnemyMovementStatus::EEMS_Idle;
}
  • 获取到Controller属性
// Called when the game starts or when spawned
void ABaseEnemy::BeginPlay()
{
	Super::BeginPlay();
//-----------------省略----------------------------------------------	
	//拿到Controller
	AIController = Cast<AAIController>(GetController());
}

调用MoveTo去追逐Player

  • 逻辑(一):首先在追逐事件中去判断是不是Player如果是就调用追逐函数MoveToTarget
void ABaseEnemy::OnChaseVolumeOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	if (OtherActor)
	{
		AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
		if (Player)
		{
			MoveToTarget(Player);
		}
	}
}
  • 逻辑(二):MoveToTarget函数中,首先将敌人的枚举移动状态变换为EEMS_MoveToTarget,然后判断AIController是否获取成功,成功就去调用MoveTo去追逐主角
  • FAIMoveRequest 是UE中的一个结构体,主要用于指定人工智能(AI)代理的移动请求。其内部包含了一系列参数,如目标位置、到达速度、抵达距离、是否允许短路等等,这些参数都可以用来控制 AI 代理的移动行为。
    当调用 AAIController::MoveTo() 或者 AAIController::SimpleMoveTo() 等函数时,会创建并返回一个 FAIMoveRequest 结构体实例。然后你可以设置它的参数,并将它传递给 AAIController::UpdateMoveStatus() 函数,从而让 AI 代理按照你的要求进行移动。
    总的来说,FAIMoveRequest 提供了一种方便的方式来指定和调整 AI 代理的移动请求,使得 AI 的行为更加灵活多样。
    FAIMoveRequest MoveRequest;
    MoveRequest.SetGoalActor(Player);//设置移动请求目标
    MoveRequest.SetAcceptanceRadius(10.f);	//设置移动半径
    
  • 在UE中,FNavPathSharedPtr NavPath 表示一个智能指针,用于存储和管理导航路径。智能指针是一个类模板,可以自动管理所指向对象的生命周期,避免了因忘记释放内存而导致的内存泄漏问题。
    具体来说,FNavPathSharedPtr 是一个 shared_ptr 类型的智能指针,它指向的是一个 FNavPath 对象。FNavPath 是 Unreal Engine 中的一个类,用于表示从起点到终点的一条导航路径。
    因此,在虚幻引擎中,NavPath 可以用来保存和操作导航路径。例如,可以使用它来获取路径的距离、方向等信息,或者更新路径以适应场景的变化。
    FNavPathSharedPtr NavPath;//会返回路径
    
  • 调用MoveTo去追逐主角
void ABaseEnemy::MoveToTarget(AMainPlayer* Player)
{
	EnemyMovementStatus = EEnemyMovementStatus::EEMS_MoveToTarget;
	if (AIController)
	{
		FAIMoveRequest MoveRequest;
		MoveRequest.SetGoalActor(Player);//设置移动请求目标
		MoveRequest.SetAcceptanceRadius(10.f);	//设置移动半径

		FNavPathSharedPtr NavPath;//会返回路径

		AIController->MoveTo(MoveRequest, &NavPath);
	}
}
  • 逻辑(三):当主角出了追逐移动事件就停止移动
void ABaseEnemy::OnChaseVolumeOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	if (OtherActor)
	{
		AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
		if (Player)
		{
			if (AIController)
			{
				//停止移动
				AIController->StopMovement();
			}
		}
	}
}

创建敌人动画蓝图

  • 基本与创建MainPlayer动画蓝图差不多
  • EnemyAnimInstance,h
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "EnemyAnimInstance.generated.h"

/**
 * 
 */
UCLASS()
class UEGAME_API UEnemyAnimInstance : public UAnimInstance
{
	GENERATED_BODY()
public:
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Animation Properties")
	float Speed;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Animation Properties")
	class ABaseEnemy* Enemy;

	virtual void NativeInitializeAnimation() override;

	UFUNCTION(BlueprintCallable, Category = "Animaion Properties")
	void UpDataAnimationProperties();
};
  • EnemyAnimInstance.cpp
// Fill out your copyright notice in the Description page of Project Settings.


#include "EnemyAnimInstance.h"
#include "Animation/AnimInstance.h"
#include "Characters/Enemy/BaseEnemy.h"

void UEnemyAnimInstance::NativeInitializeAnimation()
{
	Enemy = Cast<ABaseEnemy>(TryGetPawnOwner());
}

void UEnemyAnimInstance::UpDataAnimationProperties()
{
	if (Enemy)
	{
		Enemy = Cast<ABaseEnemy>(TryGetPawnOwner());
	}
	if (Enemy)
	{
		FVector SpeedVector = Enemy->GetVelocity();
		FVector PlanarSpeed = FVector(SpeedVector.X, SpeedVector.Y, 0.f);
		Speed = PlanarSpeed.Size();
	}
}
  • 创建动画蓝图
    在这里插入图片描述

敌人行走的混合空间

  • 基本和当时创建Player的混合空间差不多
    在这里插入图片描述
  • 编写动画蓝图
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • 然后设置到蓝图上即可
    在这里插入图片描述

创建蒙太奇以及攻击编码

  • 创建蒙太奇
    在这里插入图片描述
  • 攻击逻辑:老规矩引用一个bool变量用于检测是否在攻击范围,新建Montage引用后续方便调用Montage功能,与两个函数用来攻击和攻击结束的逻辑编写,攻击结束要在蓝图中调用加上反射,在重叠事件中写是否检测到主角进入攻击范围如果是就执行攻击函数,攻击函数中首先关闭移动,然后判断是不是正在攻击(默认肯定没有攻击,所以判断完要设定为正在攻击),然后获取AnimInstance进行片段播放,攻击结束函数逻辑就先把状态变为待机状态,然后判断是否在攻击范围bool变量,如果为真就继续执行攻击函数形成闭环,最后离开了重叠事件范围就先把bool检测攻击变为false,判断攻击状态是否结束,如果结束就继续执行追逐主角函数,可能这个逻辑会导致bug,所以我们把追逐主角的函数MoveToTarget添加反射到时候在蓝图中完善
  • 需要的变量与函数
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Attack")
	bool bAttackVolumeOverlap;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attack")
	class UAnimMontage* AttackMontage;
	
UFUNCTION(BlueprintCallable)
void MoveToTarget(class AMainPlayer* Player);
void AttackBegin();
UFUNCTION(BlueprintCallable)
void AttackEnd();
  • 函数逻辑
void ABaseEnemy::OnChaseVolumeOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	if (OtherActor)
	{
		AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
		if (Player)
		{
			MoveToTarget(Player);
		}
	}
}

void ABaseEnemy::OnChaseVolumeOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	if (OtherActor)
	{
		AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
		if (Player)
		{
			if (AIController)
			{
				//停止移动
				AIController->StopMovement();
			}
		}
	}
}

void ABaseEnemy::OnAttackVolumeOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	if (OtherActor)
	{
		AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
		if (Player)
		{
			bAttackVolumeOverlap = true;
			AttackBegin();
		}
	}
}

void ABaseEnemy::OnAttackVolumeOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	if (OtherActor)
	{
		AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
		if (Player)
		{
			bAttackVolumeOverlap = false;
			if (EnemyMovementStatus!=EEnemyMovementStatus::EEMS_Attacking)
			{
				MoveToTarget(Player);
			}
		}
	}
}

void ABaseEnemy::MoveToTarget(AMainPlayer* Player)
{
	EnemyMovementStatus = EEnemyMovementStatus::EEMS_MoveToTarget;
	if (AIController)
	{
		FAIMoveRequest MoveRequest;
		MoveRequest.SetGoalActor(Player);//设置移动请求目标
		MoveRequest.SetAcceptanceRadius(10.f);	//设置移动半径

		FNavPathSharedPtr NavPath;//会返回路径

		AIController->MoveTo(MoveRequest, &NavPath);
	}
}

void ABaseEnemy::AttackBegin()
{
	//攻击中关闭移动
	if (AIController)
	{
		AIController->StopMovement();
	}
	if (EnemyMovementStatus != EEnemyMovementStatus::EEMS_Attacking)
	{
		EnemyMovementStatus = EEnemyMovementStatus::EEMS_Attacking;
		UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
		if (AnimInstance && AttackMontage)
		{
			float PlayRate = FMath::RandRange(0.9f, 1.1f);
			FString SectionName = FString::FromInt(FMath::RandRange(1, 3));
			AnimInstance->Montage_Play(AttackMontage, PlayRate);
			AnimInstance->Montage_JumpToSection(FName(*SectionName), AttackMontage);
		}
	}
}

void ABaseEnemy::AttackEnd()
{
	EnemyMovementStatus = EEnemyMovementStatus::EEMS_Idle;
	if (bAttackVolumeOverlap)
	{
		AttackBegin();
	}
}

攻击动画的编写以及连续追逐

  • 首先把Montage的攻击通知加上,然后在动画蓝图里面添加蒙太奇
    在这里插入图片描述
    在这里插入图片描述
  • 然后在事件图表里面进行编辑,调用通知AttackEnd事件执行AttackEnd函数,然后进行判断主角离开了攻击重叠事件的判断,如果离开了,就又继续执行追逐主角函数
    在这里插入图片描述

敌人更新攻击目标

  • 思想:近点跟随,谁离得近就先攻击谁
  • 在MainPlayer中新建一个敌人类的指针引用与一个模版敌人类,一个更新攻击目标的函数
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Attack")
	class ABaseEnemy* AttackTarget;
		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attack")
	TSubclassOf<ABaseEnemy> EnemyFilter;
//-----------------------------------------------------------------
void UpdataAttackTarget();
  • 定义一个模版敌人类就是因为要使用这个函数GetOverlappingActors:它能够获取与指定组件相交的所有 Actors 列表,在使用时记得加上敌人类头文件,避免不知道EnemyFilter
void AMainPlayer::UpdataAttackTarget()
{

	TArray<AActor*> OVerlappingActors;
	GetOverlappingActors(OVerlappingActors,EnemyFilter);
}
  • 在返回的相交所有Actor列表里面进行选择最近的那个,逻辑是:遍历OverlapingActors数组进行比较,近的就替换远的。新建一个用于交换的敌人类引用,新建一个最小范围,然后获取当前位置,然后开始变量数组,将数组里面的单位全部转换为敌人类,判断敌人是否存在是否死亡,如果存在无死亡就当前距离主角的位置记录下来,如果距离主角位置小于最小范围那么就将距离主角位置赋值最小范围,将当前敌人类给交换敌人的引用,循环结束就将当前距离主角位置赋值给AttackTarget
void AMainPlayer::UpdataAttackTarget()
{

	TArray<AActor*> OVerlappingActors;
	GetOverlappingActors(OVerlappingActors,EnemyFilter);

	//判断列表里面是否为空,为空就无攻击目标
	if (OVerlappingActors.Num() == 0)
	{
		AttackTarget = nullptr;
		return;
	}

	ABaseEnemy* ClosestDistance = nullptr;
	float MinDistance = 1000.f;
	FVector Loation = GetActorLocation();

	for (auto Actor : OVerlappingActors)
	{
		ABaseEnemy* Enemy = Cast<ABaseEnemy>(Actor);
		if (Enemy && Enemy->EnemyMovementStatus != EEnemyMovementStatus::EEMS_Dead)
		{
			float DistanceToActor = (Enemy->GetActorLocation() - Loation).Size();//记录当前位置Enemy距离MainPlayer位置
			if (DistanceToActor < MinDistance)
			{
				MinDistance = DistanceToActor;
				ClosestDistance = Enemy;
			}
		}
	}
	AttackTarget = ClosestDistance;
}
  • 然后在BaseEnemy类的攻击碰撞事件中调用此函数
void ABaseEnemy::OnAttackVolumeOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	if (OtherActor)
	{
		AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
		if (Player)
		{
			Player->UpdataAttackTarget();
			bAttackVolumeOverlap = true;
			AttackBegin();
		}
	}
}

玩家自动面向攻击目标

  • 采用插值的思想方法,当我们需要攻击转向时就进行插值转向
  • 在MainPlayer类中新建一个插值速度变量与一个bool是否进行插值变量并赋初值
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attack")
	float InterpSpeed;

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Attack")
	bool bInterpToEnemy;


InterpSpeed = 15.f;
bInterpToEnemy = false;
  • 然后在攻击函数中将bool插值变量赋为true,攻击结束函数中设为false
void AMainPlayer::AttackBegin()
{
	if (!bIsAttacking)
	{
		bIsAttacking = true;
		bInterpToEnemy = true;
		//拿到动画
		UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();

		if (AnimInstance && AttackMontage)
		{
			float PlayRate = FMath::RandRange(1.25f, 1.75f);
			FString SectionName = FString::FromInt(FMath::RandRange(1, 2));
			//指定片段播放
			AnimInstance->Montage_Play(AttackMontage, PlayRate);
			AnimInstance->Montage_JumpToSection(FName(*SectionName), AttackMontage);
		}
	}
}

void AMainPlayer::AttackEnd()
{
	bIsAttacking = false;
	bInterpToEnemy = false;
	//形成闭环
	if (bAttackKeyDown)
	{
		AttackKeyDown();
	}
}
  • 然后去Tick里面进行转向逻辑编写,使用RInterpTo要头文件:#include “Kismet/KismetMathLibrary.h”
//进行转向插值
	if (bInterpToEnemy && AttackTarget)
	{
		//只需要AttackTarget的Yaw转向
		FRotator LookAtYaw(0.f, UKismetMathLibrary::FindLookAtRotation(GetActorLocation(), AttackTarget->GetActorLocation()).Yaw, 0.f);
		FRotator InterpRotation = FMath::RInterpTo(GetActorRotation(), LookAtYaw, DeltaTime, InterpSpeed);
		SetActorRotation(InterpRotation);
	}
  • 将Enemy给过滤器,必须是C++类
    在这里插入图片描述

敌人自动面向玩家攻击目标

  • 基本与玩家面向攻击目标的编写差不多
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attack")
	float InterpSpeed;

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Attack")
	bool bInterpToPlayer;


InterpSpeed = 15.f;
bInterpToPlayer = false;
  • 然后在攻击函数中将bool插值变量赋为true,攻击结束函数中设为false
void ABaseEnemy::AttackBegin()
{
	//攻击中关闭移动
	if (AIController)
	{
		AIController->StopMovement();
	}
	if (EnemyMovementStatus != EEnemyMovementStatus::EEMS_Attacking)
	{
		EnemyMovementStatus = EEnemyMovementStatus::EEMS_Attacking;

		bInterpToPlayer = true;

		UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
		if (AnimInstance && AttackMontage)
		{
			float PlayRate = FMath::RandRange(0.9f, 1.1f);
			FString SectionName = FString::FromInt(FMath::RandRange(1, 3));
			AnimInstance->Montage_Play(AttackMontage, PlayRate);
			AnimInstance->Montage_JumpToSection(FName(*SectionName), AttackMontage);
		}
	}
}

void ABaseEnemy::AttackEnd()
{
	EnemyMovementStatus = EEnemyMovementStatus::EEMS_Idle;
	bInterpToPlayer = false;
	if (bAttackVolumeOverlap)
	{
		AttackBegin();
	}
}
  • 然后去Tick里面进行转向逻辑编写,注意的事FindLookAtRotation中的目标位置是Player,得去获取位置,获取位置要加头文件:#include "Kismet/GameplayStatics.h",使用RInterpTo也要头文件:#include "Kismet/KismetMathLibrary.h"
void ABaseEnemy::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	if (bInterpToPlayer)
	{
		FRotator LookYaw(0.f, UKismetMathLibrary::FindLookAtRotation(GetActorLocation(), UGameplayStatics::GetPlayerPawn(this, 0)->GetActorLocation()).Yaw, 0.f);
		FRotator InterpRotation = FMath::RInterpTo(GetActorRotation(), LookYaw, DeltaTime, InterpSpeed);
		SetActorRotation(InterpRotation);
	}
}

BaseEnemy.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "BaseEnemy.generated.h"

UENUM(BlueprintType)
enum class EEnemyMovementStatus :uint8
{
	EEMS_Idle			UMETA(DisplayName="Idle"),
	EEMS_MoveToTarget	UMETA(DisPlayName="MoveToTarget"),
	EEMS_Attacking		UMETA(DisPlayName="Attacking"),
	EEMS_Dead			UMETA(DisPlayName="Dead")
};

UCLASS()
class UEGAME_API ABaseEnemy : public ACharacter
{
	GENERATED_BODY()

public:
	// Sets default values for this character's properties
	ABaseEnemy();

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
	class USphereComponent* ChaseVolume;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
	USphereComponent* AttackVolume;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
	class AAIController* AIController;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Enemy Stats")
	EEnemyMovementStatus EnemyMovementStatus;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Attack")
	bool bAttackVolumeOverlap;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attack")
	class UAnimMontage* AttackMontage;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attack")
	float InterpSpeed;

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Attack")
	bool bInterpToPlayer;
protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

	// Called to bind functionality to input
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

	UFUNCTION()
	virtual void OnChaseVolumeOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
	UFUNCTION()
	virtual void OnChaseVolumeOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
	UFUNCTION()
	virtual void OnAttackVolumeOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
	UFUNCTION()
	virtual void OnAttackVolumeOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
	
	UFUNCTION(BlueprintCallable)
	void MoveToTarget(class AMainPlayer* Player);

	void AttackBegin();

	UFUNCTION(BlueprintCallable)
	void AttackEnd();
};

BaseEnemy.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "BaseEnemy.h"
#include "Components/SphereComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "Components/CapsuleComponent.h"
#include "AIController.h"
#include "Characters/Player/MainPlayer.h"
#include "Animation/AnimInstance.h"
#include "Kismet/KismetMathLibrary.h"
#include "Kismet/GameplayStatics.h"
// Sets default values
ABaseEnemy::ABaseEnemy()
{
 	// 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;
	
	ChaseVolume = CreateDefaultSubobject<USphereComponent>(TEXT("ChaseVolume"));
	ChaseVolume->SetupAttachment(GetRootComponent());
	ChaseVolume->InitSphereRadius(800.f);
	ChaseVolume->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic);
	ChaseVolume->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
	ChaseVolume->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap);

	AttackVolume = CreateDefaultSubobject<USphereComponent>(TEXT("AttackVolume"));
	AttackVolume->SetupAttachment(GetRootComponent());
	AttackVolume->InitSphereRadius(100.f);
	AttackVolume->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic);
	AttackVolume->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
	AttackVolume->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap);
	
	//避免摄像机被敌人给阻挡
	GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
	GetCapsuleComponent()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
	//设置持有属性
	AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;

	//初始化默认移动状态
	EnemyMovementStatus = EEnemyMovementStatus::EEMS_Idle;

	InterpSpeed = 15.f;
	bInterpToPlayer = false;
}

// Called when the game starts or when spawned
void ABaseEnemy::BeginPlay()
{
	Super::BeginPlay();

	ChaseVolume->OnComponentBeginOverlap.AddDynamic(this, &ABaseEnemy::OnChaseVolumeOverlapBegin);
	ChaseVolume->OnComponentEndOverlap.AddDynamic(this, &ABaseEnemy::OnChaseVolumeOverlapEnd);

	AttackVolume->OnComponentBeginOverlap.AddDynamic(this, &ABaseEnemy::OnAttackVolumeOverlapBegin);
	AttackVolume->OnComponentEndOverlap.AddDynamic(this, &ABaseEnemy::OnAttackVolumeOverlapEnd);
	
	//拿到Controller
	AIController = Cast<AAIController>(GetController());
}

// Called every frame
void ABaseEnemy::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	if (bInterpToPlayer)
	{
		FRotator LookYaw(0.f, UKismetMathLibrary::FindLookAtRotation(GetActorLocation(), UGameplayStatics::GetPlayerPawn(this, 0)->GetActorLocation()).Yaw, 0.f);
		FRotator InterpRotation = FMath::RInterpTo(GetActorRotation(), LookYaw, DeltaTime, InterpSpeed);
		SetActorRotation(InterpRotation);
	}
}

// Called to bind functionality to input
void ABaseEnemy::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

}

void ABaseEnemy::OnChaseVolumeOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	if (OtherActor)
	{
		AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
		if (Player)
		{
			MoveToTarget(Player);
		}
	}
}

void ABaseEnemy::OnChaseVolumeOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	if (OtherActor)
	{
		AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
		if (Player)
		{
			if (AIController)
			{
				//停止移动
				AIController->StopMovement();
			}
		}
	}
}

void ABaseEnemy::OnAttackVolumeOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	if (OtherActor)
	{
		AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
		if (Player)
		{
			Player->UpdataAttackTarget();
			bAttackVolumeOverlap = true;
			AttackBegin();
		}
	}
}

void ABaseEnemy::OnAttackVolumeOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	if (OtherActor)
	{
		AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
		if (Player)
		{
			bAttackVolumeOverlap = false;
			if (EnemyMovementStatus!=EEnemyMovementStatus::EEMS_Attacking)
			{
				MoveToTarget(Player);
			}
		}
	}
}

void ABaseEnemy::MoveToTarget(AMainPlayer* Player)
{
	EnemyMovementStatus = EEnemyMovementStatus::EEMS_MoveToTarget;
	if (AIController)
	{
		FAIMoveRequest MoveRequest;
		MoveRequest.SetGoalActor(Player);//设置移动请求目标
		MoveRequest.SetAcceptanceRadius(10.f);	//设置移动半径

		FNavPathSharedPtr NavPath;//会返回路径

		AIController->MoveTo(MoveRequest, &NavPath);
	}
}

void ABaseEnemy::AttackBegin()
{
	//攻击中关闭移动
	if (AIController)
	{
		AIController->StopMovement();
	}
	if (EnemyMovementStatus != EEnemyMovementStatus::EEMS_Attacking)
	{
		EnemyMovementStatus = EEnemyMovementStatus::EEMS_Attacking;

		bInterpToPlayer = true;

		UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
		if (AnimInstance && AttackMontage)
		{
			float PlayRate = FMath::RandRange(0.9f, 1.1f);
			FString SectionName = FString::FromInt(FMath::RandRange(1, 3));
			AnimInstance->Montage_Play(AttackMontage, PlayRate);
			AnimInstance->Montage_JumpToSection(FName(*SectionName), AttackMontage);
		}
	}
}

void ABaseEnemy::AttackEnd()
{
	EnemyMovementStatus = EEnemyMovementStatus::EEMS_Idle;
	bInterpToPlayer = false;
	if (bAttackVolumeOverlap)
	{
		AttackBegin();
	}
}

MainPlayer.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "MainPlayer.generated.h"

//声明移动状态枚举
UENUM(BlueprintType)
enum class EPlayerMovementStatus :uint8
{
	EPMS_Normal UMETA(DisplayName = "Normal"),
	EPMS_Sprinting UMETA(DisplayName = "Sprinting"),
	EPMS_Dead UMETA(DisplayName = "Dead")
};

UENUM(BlueprintType)
enum class EPlayerStaminaStatus :uint8
{
	EPSS_Normal UMETA(DisplayName = "Normal"),
	EPSS_Exhausted UMETA(DisplayName = "Exhausted"),
	EPSS_ExhaustedRecovering UMETA(DisplayName = "ExhaustedRecovering")
};



UCLASS()
class UEGAME_API AMainPlayer : public ACharacter
{
	GENERATED_BODY()

public:
	// Sets default values for this character's properties
	AMainPlayer();

	//新建一个SpringArm
	UPROPERTY(visibleAnywhere,BlueprintReadOnly)
	class USpringArmComponent* SpringArm;
	//新建一个Camera
	UPROPERTY(visibleAnywhere, BlueprintReadOnly)
	class UCameraComponent* FollowCamera;

	
	float BaseTurnRate;		//使用键盘X转向的速率
	float BaseLookUpRate;	//使用键盘Y转向的速率

	//主角状态
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Playe State")
	float Health;
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playe State")
	float MaxHealth;
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Playe State")
	float Stamina;
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playe State")
	float MaxStamina;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Playe State")
	float StaminaConsumeRate;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Playe State", meta = (ClampMin = 0, ClampMax = 1))
	float ExhaustedStamina;
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playe State")
	int Coins;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Player State")
	float RunningSpeed;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Player State")
	float SprintSpeed;
	UPROPERTY(VisibleAnywhere,BlueprintReadWrite,Category="Player State")
	EPlayerMovementStatus MovementStatus;
	UPROPERTY(VisibleAnywhere,BlueprintReadWrite,Category="Player State")
	EPlayerStaminaStatus StaminaStatus;

	bool bLeftShiftDown;
	
	//检测是否持剑
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Weapon")
	bool bIsWeapon;

	//正在装备的武器
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Weapon")
	class AWeaponItem* EquipWeapon;

	//正在重叠的武器
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Weapon")
	AWeaponItem* OverlapWeapon;

	bool bAttackKeyDown;//是否按下攻击键

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Animation")
	bool bIsAttacking;

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Animation")
	class UAnimMontage* AttackMontage;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Attack")
	class ABaseEnemy* AttackTarget;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attack")
	TSubclassOf<ABaseEnemy> EnemyFilter;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attack")
	float InterpSpeed;

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Attack")
	bool bInterpToEnemy;
protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

	// Called to bind functionality to input
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

	//重新Character类中的Jump方法
	void Jump() override;

	void MoveForward(float value);
	void MoveRight(float value);

	void Turn(float Value);
	void LookUp(float Value);

	void TurnRate(float Rate);
	void LookUpRate(float Rate);

	//改变状态
	UFUNCTION(BlueprintCallable,Category="Player|State")
	void AddHealth(float value);
	UFUNCTION(BlueprintCallable, Category = "Player|State")
	void AddStamina(float value);
	UFUNCTION(BlueprintCallable, Category = "Player|State")
	void AddCoin(float value);

	//重写TakeDamage方法
	float TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;

	//短小精悍
	FORCEINLINE void LeftShiftDown() { bLeftShiftDown = true; }
	FORCEINLINE void LeftShiftUp() { bLeftShiftDown = false; }

	void SetMovementStatus(EPlayerMovementStatus Status);

	void InteractKeyDown();

	void AttackKeyDown();

	FORCEINLINE void AttackKeyUp() { bAttackKeyDown = false; }

	void AttackBegin();
	
	UFUNCTION(BlueprintCallable)
	void AttackEnd();

	void UpdataAttackTarget();
};

MainPlayer.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "MainPlayer.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/InputComponent.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GamePlay/WeaponItem.h"
#include "Animation/AnimInstance.h"
#include "Characters/Enemy/BaseEnemy.h"
#include "Kismet/KismetMathLibrary.h"
// Sets default values
AMainPlayer::AMainPlayer()
{
 	// 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;

	SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
	SpringArm->SetupAttachment(GetRootComponent());
	//设置SPringArm无碰撞臂长
	SpringArm->TargetArmLength = 600.f;
	SpringArm->bUsePawnControlRotation = true;//硬编码SpringArm继承controlller旋转为真


	FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
	FollowCamera->SetupAttachment(SpringArm, NAME_None);
	FollowCamera->bUsePawnControlRotation = false;//硬编码FollowCamera继承controlller旋转为假

	//设置胶囊体的默认宽高
	GetCapsuleComponent()->SetCapsuleSize(35.f, 100.f);

	//对Character的Pawn进行硬编码
	bUseControllerRotationPitch = false;
	bUseControllerRotationYaw = false;
	bUseControllerRotationRoll = false;

	//硬编码orient Rotation to Movement,给个默认转向速率
	GetCharacterMovement()->bOrientRotationToMovement = true;
	GetCharacterMovement()->RotationRate = FRotator(0.f, 500.f, 0.f);

	//设置跳跃初始值与在空中的坠落时横向运动控制量
	GetCharacterMovement()->JumpZVelocity = 400.f;
	GetCharacterMovement()->AirControl = 0.15f;

	//给键盘控制转向的速率变量赋初值
	BaseTurnRate = 21.f;
	BaseLookUpRate = 21.f;

	//初始化角色状态
	MaxHealth = 100.f;
	Health = MaxHealth;
	MaxStamina = 200.f;
	Stamina = MaxStamina;
	StaminaConsumeRate = 20.f;
	ExhaustedStamina = 0.167f;
	Coins = 0;
	RunningSpeed = 600.f;
	SprintSpeed = 900.f;
	MovementStatus = EPlayerMovementStatus::EPMS_Normal;
	StaminaStatus = EPlayerStaminaStatus::EPSS_Normal;

	//默认没有按下shift
	bLeftShiftDown = false;


	InterpSpeed = 15.f;
	bInterpToEnemy = false;
}

// Called when the game starts or when spawned
void AMainPlayer::BeginPlay()
{
	Super::BeginPlay();
}

// Called every frame
void AMainPlayer::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	switch (StaminaStatus)
	{
	case EPlayerStaminaStatus::EPSS_Normal:
		//当Shift按下
		if (bLeftShiftDown)
		{
			if (Stamina - StaminaConsumeRate * DeltaTime <= MaxStamina * ExhaustedStamina)
			{
				StaminaStatus = EPlayerStaminaStatus::EPSS_Exhausted;
			}
			//无论是不是精疲力尽状态都要减去当前帧冲刺消耗的耐力
			Stamina -= StaminaConsumeRate * DeltaTime;
			SetMovementStatus(EPlayerMovementStatus::EPMS_Sprinting);
		}
		else
		{
			//当Shift没有按下,恢复耐力
			Stamina = FMath::Clamp(Stamina + StaminaConsumeRate * DeltaTime, 0.f, MaxStamina);
			SetMovementStatus(EPlayerMovementStatus::EPMS_Normal);
		}
		break;
	case EPlayerStaminaStatus::EPSS_Exhausted:
		if (bLeftShiftDown)
		{
			//如果耐力已经为0
			if (Stamina - StaminaConsumeRate * DeltaTime <= 0.f)
			{
				//么我们需要内部编码把shift抬起,此时StaminaStatus状态转换为ExhaustedRecovering状态,然后设置移动状态为Normal
				LeftShiftUp();
				StaminaStatus = EPlayerStaminaStatus::EPSS_ExhaustedRecovering;	
				SetMovementStatus(EPlayerMovementStatus::EPMS_Normal);
			}
			else
			{
				Stamina -= StaminaConsumeRate * DeltaTime;
			}
		}
		else
		{
			StaminaStatus = EPlayerStaminaStatus::EPSS_ExhaustedRecovering;
			Stamina = FMath::Clamp(Stamina + StaminaConsumeRate * DeltaTime, 0.f, MaxStamina);
			SetMovementStatus(EPlayerMovementStatus::EPMS_Normal);
		}
		break;
	case EPlayerStaminaStatus::EPSS_ExhaustedRecovering:
		//当恢复大于疲劳区时,StaminaStatus状态为Normal
		if (Stamina + StaminaConsumeRate * DeltaTime >= MaxStamina * ExhaustedStamina)
		{
			StaminaStatus = EPlayerStaminaStatus::EPSS_Normal;
		}
		//这状态值肯定是加定了
		Stamina += StaminaConsumeRate * DeltaTime;

		//抬起shift
		LeftShiftUp();
		SetMovementStatus(EPlayerMovementStatus::EPMS_Normal);
		break;
	default:
		break;
	}

	//进行转向插值
	if (bInterpToEnemy && AttackTarget)
	{
		//只需要AttackTarget的Yaw转向
		FRotator LookAtYaw(0.f, UKismetMathLibrary::FindLookAtRotation(GetActorLocation(), AttackTarget->GetActorLocation()).Yaw, 0.f);
		FRotator InterpRotation = FMath::RInterpTo(GetActorRotation(), LookAtYaw, DeltaTime, InterpSpeed);
		SetActorRotation(InterpRotation);
	}
}

// Called to bind functionality to input
void AMainPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
	//检查PlayerInputComponent指针,check函数只能在这使用
	check(PlayerInputComponent);

	//绑定跳跃轴映射事件
	PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &AMainPlayer::Jump);//按下空格
	PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);//抬起空格

	PlayerInputComponent->BindAction("Sprint", IE_Pressed, this, &AMainPlayer::LeftShiftDown);//按下shift
	PlayerInputComponent->BindAction("Sprint", IE_Released, this, &AMainPlayer::LeftShiftUp);//抬起shift

	//拾取剑
	PlayerInputComponent->BindAction("Interact", IE_Pressed, this, &AMainPlayer::InteractKeyDown);//按下F

	//攻击
	PlayerInputComponent->BindAction("Attack", IE_Pressed, this, &AMainPlayer::AttackKeyDown);
	PlayerInputComponent->BindAction("Attack", IE_Released, this, &AMainPlayer::AttackKeyUp);

	//绑定移动轴映射事件
	PlayerInputComponent->BindAxis("MoveForward", this, &AMainPlayer::MoveForward);
	PlayerInputComponent->BindAxis("MoveRight", this, &AMainPlayer::MoveRight);

	//绑定Controller控制器去管理视角旋转
	PlayerInputComponent->BindAxis("Turn", this, &AMainPlayer::Turn);
	PlayerInputComponent->BindAxis("LookUp", this, &AMainPlayer::LookUp);

	//绑定键盘鼠标轴映射事件
	PlayerInputComponent->BindAxis("TurnRate", this, &AMainPlayer::TurnRate);
	PlayerInputComponent->BindAxis("LookUpRate", this, &AMainPlayer::LookUpRate);

}

void AMainPlayer::Jump()
{
	//继承父类的方法
	Super::Jump();
}

void AMainPlayer::MoveForward(float value)
{
	if (Controller != nullptr && value != 0.f && !(bIsAttacking))
	{
		//获取到Control旋转
		FRotator Rotation = Controller->GetControlRotation();
		//转向只关注水平Yaw方向,因此置0防止影响
		FRotator YowRotation = FRotator(0.0f, Rotation.Yaw, 0.0f);
		//获取相机(鼠标控制器的朝向),并且朝这个轴的方向移动
		FVector Direction = FRotationMatrix(YowRotation).GetUnitAxis(EAxis::X);
		AddMovementInput(Direction, value);
	}

}

void AMainPlayer::MoveRight(float value)
{
	if (Controller != nullptr && value != 0.f && !(bIsAttacking))
	{
		//获取到Controller旋转
		FRotator Rotation = Controller->GetControlRotation();
		//转向只关注水平Yaw方向,因此置0防止影响
		FRotator YowRotation = FRotator(0.0f, Rotation.Yaw, 0.0f);
		//获取相机(鼠标控制器的朝向),并且朝这个轴的方向移动
		FVector Direction = FRotationMatrix(YowRotation).GetUnitAxis(EAxis::Y);
		AddMovementInput(Direction, value);
	}
}

void AMainPlayer::Turn(float Value)
{
	if (Value != 0.f)
	{
		AddControllerYawInput(Value);
	}
	
}

void AMainPlayer::LookUp(float Value)
{
	//UE_LOG(LogTemp, Warning, TEXT("%f"), GetControlRotation().Pitch);

	//控制视角
	if (GetControlRotation().Pitch < 270.f && GetControlRotation().Pitch >180.f && Value > 0.f)
	{
		return;
	}
	else if (GetControlRotation().Pitch < 180.f && GetControlRotation().Pitch >45.f && Value < 0.f)
	{
		return;
	}
	AddControllerPitchInput(Value);
}

void AMainPlayer::TurnRate(float Rate)
{
	//要乘以一个DeltaTime这样就可以避免高帧底帧差值问题
	float Value = Rate * BaseTurnRate * GetWorld()->GetDeltaSeconds();
	if (Value != 0.f)
	{
		AddControllerYawInput(Value);
	}
}

void AMainPlayer::LookUpRate(float Rate)
{
	//要乘以一个DeltaTime这样就可以避免高帧底帧差值问题
	float Value = Rate * BaseLookUpRate * GetWorld()->GetDeltaSeconds();
	//控制视角
	if (GetControlRotation().Pitch < 270.f && GetControlRotation().Pitch >180.f && Value > 0.f)
	{
		return;
	}
	else if (GetControlRotation().Pitch < 180.f && GetControlRotation().Pitch >45.f && Value < 0.f)
	{
		return;
	}
	AddControllerPitchInput(Value);

}

void AMainPlayer::AddHealth(float value)
{
	Health = FMath::Clamp(Health + value, 0.f, MaxHealth);
}

void AMainPlayer::AddStamina(float value)
{
	Stamina = FMath::Clamp(Stamina + value, 0.f, MaxStamina);
}

void AMainPlayer::AddCoin(float value)
{
	Coins += value;
}

float AMainPlayer::TakeDamage(float Damage, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	if (Health - Damage <= 0.f)
	{
		Health = FMath::Clamp(Health - Damage, 0.f, MaxHealth);
		//TODO Die();
	}
	else
	{
		Health -= Damage;
	}
	return Health;
}

void AMainPlayer::SetMovementStatus(EPlayerMovementStatus Status)
{
	MovementStatus = Status;
	//切换状态的时候改变移动速度
	switch (MovementStatus)
	{
	case EPlayerMovementStatus::EPMS_Sprinting:
		GetCharacterMovement()->MaxWalkSpeed = SprintSpeed;
		break;
	default:
		GetCharacterMovement()->MaxWalkSpeed = RunningSpeed;
		break;
	}
}

void AMainPlayer::InteractKeyDown()
{
	if (OverlapWeapon)
	{
		if (EquipWeapon)
		{
			//交换武器
			EquipWeapon->UnEuip(this);
			OverlapWeapon->Equip(this);
		}
		else
		{
			//装备武器
			OverlapWeapon->Equip(this);
		}
	}
	else
	{
		if (EquipWeapon)
		{
			//卸载武器
			EquipWeapon->UnEuip(this);
		}
	}
}

void AMainPlayer::AttackKeyDown()
{
	bAttackKeyDown = true;
	if (bIsWeapon)
	{
		AttackBegin();
	}

}


void AMainPlayer::AttackBegin()
{
	if (!bIsAttacking)
	{
		bIsAttacking = true;
		bInterpToEnemy = true;
		//拿到动画
		UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();

		if (AnimInstance && AttackMontage)
		{
			float PlayRate = FMath::RandRange(1.25f, 1.75f);
			FString SectionName = FString::FromInt(FMath::RandRange(1, 2));
			//指定片段播放
			AnimInstance->Montage_Play(AttackMontage, PlayRate);
			AnimInstance->Montage_JumpToSection(FName(*SectionName), AttackMontage);
		}
	}
}

void AMainPlayer::AttackEnd()
{
	bIsAttacking = false;
	bInterpToEnemy = false;
	//形成闭环
	if (bAttackKeyDown)
	{
		AttackKeyDown();
	}
}

void AMainPlayer::UpdataAttackTarget()
{

	TArray<AActor*> OVerlappingActors;
	GetOverlappingActors(OVerlappingActors,EnemyFilter);

	//判断列表里面是否为空,为空就无攻击目标
	if (OVerlappingActors.Num() == 0)
	{
		AttackTarget = nullptr;
		return;
	}

	ABaseEnemy* ClosestDistance = nullptr;
	float MinDistance = 1000.f;
	FVector Loation = GetActorLocation();

	for (auto Actor : OVerlappingActors)
	{
		ABaseEnemy* Enemy = Cast<ABaseEnemy>(Actor);
		if (Enemy && Enemy->EnemyMovementStatus != EEnemyMovementStatus::EEMS_Dead)
		{
			float DistanceToActor = (Enemy->GetActorLocation() - Loation).Size();//记录当前位置Enemy距离MainPlayer位置
			if (DistanceToActor < MinDistance)
			{
				MinDistance = DistanceToActor;
				ClosestDistance = Enemy;
			}
		}
	}
	AttackTarget = ClosestDistance;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1234720.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

matlab-BP神经网络的训练参数大全

本文部分图文来自《老饼讲解-BP神经网络》bp.bbbdata.com 本文列兴趣MATLAB神经网络工具箱中&#xff0c;训练参数trainParam的各个参数与意义 以方便在使用matlab工具箱时&#xff0c;用于查阅 一、matlab神经网络工具箱trainParam的参数列表 trainParam中的各个具体参数如下…

函数有返回类型,但函数体未返回类型,程序崩溃问题记录

问题 使用类指针调用函数时&#xff0c;程序崩溃。 问题定位&#xff1a; name new nameSetting;name->setName("helloworld");qDebug().noquote() << name->getName();原因 class nameSetting { public:nameSetting();QString setName(const QStri…

【大数据开发】FineReport报表基础入门

博主&#xff1a;&#x1f44d;不许代码码上红 欢迎&#xff1a;&#x1f40b;点赞、收藏、关注、评论。 格言&#xff1a; 大鹏一日同风起&#xff0c;扶摇直上九万里。 文章目录 一 登录账号二 创建一个新的表格三 单元格扩展3.1 无扩展3.2 纵向扩展3.3 横向扩展 四 父子格…

Redis事务+秒杀案例

Redis事务是一个单独的隔离操作&#xff0c;是指将多条命令放在一个命令队列当中&#xff0c;按顺序执行&#xff0c;保证多个命令在同一个事务中执行而不受其他客户端的影响。 通俗来说就是&#xff1a;串联多个命令防止别的命令插队。 1.Multi、Exec、discard 在输入Multi命…

SSM框架(二):AOP和事物

文章目录 一、AOP的介绍1.1 基本概念1.2 AOP入门1.3 AOP工作流程1.4 切入点表达式1.5 AOP的通知类型1.6 ProceedingJoinPoint1.7 AOP通知获取参数数据 二、事物2.1 基本介绍2.2 事物角色2.3 事物属性2.4 事物的传播行为 一、AOP的介绍 1.1 基本概念 1.2 AOP入门 导入坐标 <…

【数值计算方法】矩阵特征值与特征向量的计算(一):Jacobi 旋转法及其Python实现

文章目录 一、Jacobi 旋转法1. 基本思想2. 计算过程演示3. 注意事项 二、Python实现迭代过程&#xff08;调试&#xff09; 矩阵的特征值&#xff08;eigenvalue&#xff09;和特征向量&#xff08;eigenvector&#xff09;在很多应用中都具有重要的数学和物理意义。Jacobi 旋转…

Linux 环境配置小白入门

Linux从 全栈开发centOS 7 到 运维 一 Linux 入门概述1.1 操作系统1.2 Linux 简介1.3 Linux 系统组成1.4 Linux 发行版1.5 Linux 应用领域1.6 Linux vs Windows 二 虚拟机2.1 虚拟机介绍2.2 VMware WorkStation 安装2.3 VMware WorkStation 配置检查2.3 安装 CentOS 72.3.1 安装…

Git 远程仓库(Github)

目录 添加远程库 查看当前的远程库 提取远程仓库 推送到远程仓库 删除远程仓库 Git 并不像 SVN 那样有个中心服务器。 目前我们使用到的 Git 命令都是在本地执行&#xff0c;如果你想通过 Git 分享你的代码或者与其他开发人员合作。 你就需要将数据放到一台其他开发人员…

2021年03月 Scratch(二级)真题解析#中国电子学会#全国青少年软件编程等级考试

Scratch等级考试(1~4级)全部真题・点这里 一、单选题(共25题,每题2分,共50分) 第1题 小猫在沙漠中旅行好不容易找到了一杯水,初始位置如下图所示,下面哪个程序可以帮助它成功喝到水? A: B: C: D:

LR学习笔记——基本面板

文章目录 面板介绍色彩调整区域明暗调整区域纹理及质感色彩饱和 面板介绍 面板如上图所示 基本可分为几个板块&#xff1a;色彩、明暗、纹理及质感、色彩饱和 色彩调整区域 色温&#xff1a;由蓝色和黄色控制色调&#xff1a;由绿色和洋红控制 互补色&#xff1a;蓝色对黄色&…

opencv-形态学处理

通过阈值化分割可以得到二值图&#xff0c;但往往会出现图像中物体形态不完整&#xff0c;变的残缺&#xff0c;可以通过形态学处理&#xff0c;使其变得丰满&#xff0c;或者去除掉多余的像素。常用的形态学处理算法包括&#xff1a;腐蚀&#xff0c;膨胀&#xff0c;开运算&a…

Altium Designer学习笔记7

PCB封装库的制作&#xff1a; 距离的测量&#xff1a; 各个焊盘的位置&#xff1a; 直插元件选择Multi-Layer。如果贴片元件的则选择顶层Top-Layer&#xff0c;或者Bottom-Layer。 形状是方形&#xff0c;尺寸是2mm*2mm。 孔的尺寸是1.4mm。 则该器件就制作完成。 TSSOP28封装…

Java精品项目源码基于SpringBoot的樱花短视频平台(v66)

Java精品项目源码基于SpringBoot的樱花短视频平台(v66) 大家好&#xff0c;小辰今天给大家介绍一个樱花短视频平台&#xff0c;演示视频公众号&#xff08;小辰哥的Java&#xff09;对号查询观看即可 文章目录 Java精品项目源码基于SpringBoot的樱花短视频平台(v66)难度指数&…

Python3,必备数据可视化之:数据交互可视化

数据可视化之交互可视化 1、引言2、交互可视化介绍2.1 Bokeh2.1.1 基本定义2.1.2 常用功能2.1.3 安装2.1.4 代码示例 2.2 Plotly2.2.1 基本定义2.1.2 常用功能2.1.3 安装2.2.4 代码示例 2.3 Bokeh与Plotly 差异点 3、总结 1、引言 小屌丝&#xff1a;鱼哥&#xff0c;我发现一…

详解Python安装requests库的实例代码

文章目录 前言基本用法基本的get请求带参数的GET请求解析json关于Python技术储备一、Python所有方向的学习路线二、Python基础学习视频三、精品Python学习书籍四、Python工具包项目源码合集①Python工具包②Python实战案例③Python小游戏源码五、面试资料六、Python兼职渠道 前…

腾讯云COS+picgo+typora 图床搭建与自动上传

1、腾讯云 COS 腾讯云活动 COS新用户专享 COS 操作步骤 1、点击 创建桶&#xff0c;完善信息 点击下一步&#xff0c;剩下的配置可自己配置 2、picgo 官网地址 2.3.1版本下载地址 现在稳定版本是2.3.1 相关连接 腾讯云密钥设置地址picgo官网地址2.3.1版本下载地址

4-11 四个数排序

#include<stdio.h> int main(){int t,a,b,c,d;printf("请输入四个数&#xff1a;");scanf("%d %d %d %d",&a,&b,&c,&d);printf("a%d,b%d,c%d,d%d\n",a,b,c,d);if(a>b){ta;ab;bt;}if(a>c){ta;ac;ct;}if(a>d){ta;a…

注册亚马逊美国买家号需要些什么资料?

注册亚马逊美国买家号需要准备邮箱、美国手机号、美国地址及能支付的支付卡。准备好之后进入亚马逊美国站进行点击注册&#xff0c;按照格式填写好之后即可注册成功了。 而如果想要注册大量买家号&#xff0c;可以使用亚马逊鲲鹏系统进行自动化操作&#xff0c;想要自动化更顺畅…

【STM32】TF卡FTA32文件系统

一、SD卡介绍 1.SD简介 本质&#xff1a;NandFlash控制芯片 2.SD卡存储容量等级 3.FAT文件系统的使用 4.SD卡速度等级 5.SD卡驱动方式 1.SDIO&&SD 1&#xff09;SDIO接口通信线&#xff1a;CLK/CMD/DAT0-3&#xff08;数据传输线4根&#xff09; 2&#xff09;SPI接口…