UE AI学习之路
本文最后更新于211 天前,其中的信息可能已经过时,如有错误请联系作者

UE AI学习之路

行为树学习

行为树简介

行为树控制着游戏中AI的行为,可以看成AI的大脑。它是一个自上而下运行的结构,从根节点开始向下一直到达任务节点。中间有一些改变执行路径的分支节点.

Bots行为与移动

  1. 创建新的类SAICharacter,SAIController

  1. 创建AI文件夹,新建SAICharacter蓝图

  1. 为蓝图选择Mesh,设置动画

  1. 修改蓝图,使动画正常运行(因为我们是从别的地方拿的资产,缺少相关的类)

  1. 将资产放入场景

  1. 在Volumes里面选择Nav Mesh Bounds Volume

  1. 在AI中选择Behavior Tree 和 Blackboard

8.在Behavior Tree中为ROOT选择Blackboard Asset,与我们建立的Blackboard绑定

  1. 在Blackboard中创建Key-选择Vector

  1. 完善Behavior Tree

  1. 修改C++代码,以SAIController为基类创建蓝图
//SAIController.h
#pragma once

#include "CoreMinimal.h"
#include "AIController.h"
#include "SAIController.generated.h"


class UBehaviorTree;

/**
 * 
 */
UCLASS()
class ACTIONROGUELIKE_API ASAIController : public AAIController
{
	GENERATED_BODY()

protected:

	UPROPERTY(EditDefaultsOnly,Category = "AI")
	UBehaviorTree* BehaviorTree;
	
	virtual void BeginPlay() override;
};


//SAIController.cpp
#include "AI/SAIController.h"

#include "BehaviorTree/BlackboardComponent.h"
#include "Kismet/GameplayStatics.h"

void ASAIController::BeginPlay()
{
	Super::BeginPlay();

	RunBehaviorTree(BehaviorTree);

	APawn* MyPawn =  UGameplayStatics::GetPlayerPawn(this,0);
	if(MyPawn != nullptr)
	{
		//使AI移动至角色初始位置
		GetBlackboardComponent()->SetValueAsVector("MoveToLocation",MyPawn->GetActorLocation());

		//使AI移动至角色当前位置
		GetBlackboardComponent()->SetValueAsObject("TargetActor",MyPawn);

		
	}
}

  1. 将蓝图与Behavior Tree绑定,修改AI Controller Class

攻击范围检测

  1. 创建新C++类,以BTService为基类

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

#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTService.h"
#include "SBTService_CheckAttackRange.generated.h"

/**
 * 
 */
UCLASS()
class ACTIONROGUELIKE_API USBTService_CheckAttackRange : public UBTService
{
	GENERATED_BODY()

protected:

	UPROPERTY(EditAnywhere,Category="AI")
	FBlackboardKeySelector AttackRangeKey;
	
	virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;

};

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


#include "AI/SBTService_CheckAttackRange.h"

#include "AIController.h"
#include "BehaviorTree/BlackboardComponent.h"

void USBTService_CheckAttackRange::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
	Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);

	//Check distance between ai pawn and target actor

	UBlackboardComponent* BlackboardComp = OwnerComp.GetBlackboardComponent();
	if(ensure(BlackboardComp))
	{
		AActor* TargetActor = Cast<AActor>(BlackboardComp->GetValueAsObject("TargetActor"));
		if(TargetActor)
		{
			AAIController* MyController =  OwnerComp.GetAIOwner();
			if(ensure(MyController))
			{
				APawn* AIPawn =  MyController->GetPawn();
				if(ensure(AIPawn))
				{
					float DistanceTo = FVector::Distance(TargetActor->GetActorLocation(),AIPawn->GetActorLocation());

					bool bWithinRange = DistanceTo < 2000.0f;

					/*bool bHasLOS = false;
					if(bWithinRange)
					{
						bHasLOS =  MyController->LineOfSightTo(TargetActor);
					}*/
					
					

					BlackboardComp->SetValueAsBool(AttackRangeKey.SelectedKeyName,(bWithinRange /*&& bHasLOS*/));

					
				}
			}
		}
	}
}
//未去掉注释的代码实现,NPC自动寻找玩家,如果范围小于2000.0f则继续寻找,否则原地停止
//去掉上述代码中的注释,上述代码实现Bot如果无法看到玩家,就自动寻找路径直到能够发现玩家
  1. 打开Blackboard,创建新Key,类型为bool,

  1. 打开Behavior Tree,修改Sequence,以及Move To Player

  1. 修改添加的Decorator,图中例子意味着当不在攻击范围时,会执行情况

  1. 添加Selector

6.最终成果如下

使Bot根据范围进行攻击

  1. 创建新的类

  1. 在C++中完成下列代码
//SBTTask_RangedTask.cpp
// Fill out your copyright notice in the Description page of Project Settings.


#include "AI/SBTTask_RangedTask.h"

#include "AIController.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "GameFramework/Character.h"

EBTNodeResult::Type USBTTask_RangedTask::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("TargetActor"));
		if(TargetActor == nullptr)
		{
			return EBTNodeResult::Failed;

		}

		FVector Direction = TargetActor->GetActorLocation() - MuzzleLocation;
		FRotator MuzzleRotation = Direction.Rotation();

		FActorSpawnParameters Params;
		Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;

		AActor* NewProj = GetWorld()->SpawnActor<AActor>(ProjectileClass,MuzzleLocation,MuzzleRotation,Params);
		return NewProj ? EBTNodeResult::Succeeded : EBTNodeResult::Failed;
	}

	
	
	return EBTNodeResult::Failed;
}

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

#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "SBTTask_RangedTask.generated.h"

/**
 * 
 */
UCLASS()
class ACTIONROGUELIKE_API USBTTask_RangedTask : public UBTTaskNode
{
	GENERATED_BODY()


	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;


protected:

	UPROPERTY(EditAnywhere,Category="AI")
	TSubclassOf<AActor> ProjectileClass;
};

  1. 在BT Tree中如下勾选,发现Bot攻击没有间隔

  1. 添加Cooldown添加攻击间隔,设置Loop实现三连击

  1. 行为树观察中断操作(没有成功实现,目前不知道原因)

2024/5/9更新 将CoolDown的Observer aborts设置为Lower Priority即可解决

2024/5/14更新 ,其实根据下面的图片中的蓝图重新修改后就可完成效果,这里不成功是因为和教程中选择Blackboard Key还是不一样

  1. 使得Bot在攻击时焦点聚集于玩家身上,我们的Default Focus选择的TargetActor,也就是Bot瞄准的位置

让AI更聪明的移动

  1. 创建Environment Query

按照下图添加

  1. 在Behavior Tree中添加RunEQSQuery

  1. 修改Filter 确定最小范围

  1. 创建蓝图

在蓝图中创建函数 ProvideSingleActor

  1. 通过修改如图所指,可以实现影响ai的路径

  1. 继续修改

  1. 使用开发者工具中的 Visual Logger

  1. ai判断得分的标准

  1. 修改一下

为AI加上感测系统

  1. 修改下列代码
// SAICharacter.h

#pragma once

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


class UPawnSensingComponent;

UCLASS()
class ACTIONROGUELIKE_API ASAICharacter : public ACharacter
{
	GENERATED_BODY()

public:
	ASAICharacter();

protected:

	

	virtual void PostInitializeComponents() override;

	UPROPERTY(VisibleAnywhere,Category="Components")
	UPawnSensingComponent* PawnSensingComp;
	

	UFUNCTION()
	void OnPawnSeen(APawn* Pawn);
};
 
// SAICharacter.cpp


#include "AI/SAICharacter.h"

#include "AIController.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Perception/PawnSensingComponent.h"


// Sets default values
ASAICharacter::ASAICharacter()
{
 	PawnSensingComp =  CreateDefaultSubobject<UPawnSensingComponent>("PawnSensingComp");

}

void ASAICharacter::PostInitializeComponents()
{
	Super::PostInitializeComponents();
	PawnSensingComp->OnSeePawn.AddDynamic(this,&ASAICharacter::OnPawnSeen);
}

void ASAICharacter::OnPawnSeen(APawn* Pawn)
{
	AAIController* AIC = Cast<AAIController>(GetController());
	if(AIC)
	{
		UBlackboardComponent* BBComp =  AIC->GetBlackboardComponent();

		BBComp->SetValueAsObject("TargetActor",Pawn);

		DrawDebugString(GetWorld(),GetActorLocation(),"PlayerSpotted",nullptr,FColor::White,4.0f,true);

		
	}
}

// 注释掉SAIController.cpp中的部分代码,这里原本是为之前ai寻找目标实现的,现在要让ai感知player的位置,这部分不需要了


#include "AI/SAIController.h"

#include "BehaviorTree/BlackboardComponent.h"
#include "Kismet/GameplayStatics.h"

void ASAIController::BeginPlay()
{
	Super::BeginPlay();

	RunBehaviorTree(BehaviorTree);

	/*APawn* MyPawn =  UGameplayStatics::GetPlayerPawn(this,0);
	if(MyPawn != nullptr)
	{
		//使AI移动至角色初始位置
		GetBlackboardComponent()->SetValueAsVector("MoveToLocation",MyPawn->GetActorLocation());

		//使AI移动至角色当前位置
		GetBlackboardComponent()->SetValueAsObject("TargetActor",MyPawn);

		
	}*/
	//上面注释的代码已经不需要了

	
}

  1. 查看我们设计的Character,可以看到绿线是ai的检测范围

  1. 修改蓝图,告知ai角色的位置

改善Bots动画

通过上一步,我们仍然发现bots的步伐在会在到达指定位置时卡顿,现在我们要使动画更加顺滑

添加代码

//SAIController.cpp

if(ensureMsgf(BehaviorTree,TEXT("Behavior Tree is a nullptr! Please assign BehaviorTree in Your AI Controller.")))
	{
		RunBehaviorTree(BehaviorTree);

	}

当行为树未部署时,会主动提示,但是在完成过程中并未发现提示

寻找机器人出生点

新建eqs

选择基础蓝图

在overvide函数中选择Actor Set重构

修改中心center

新建蓝图,进行eqs Testing

显示bot 生成位置

添加Distance Test检测距离上是否存在不能通过的位置

在这里我们很幸运,没有遇到点的位置出现在掩体内,不过我们还是要考虑这个问题

显示Navigation

我们在Volume中找到这个Nav Modifier Volume

然后在WireFrame模式下,将Nav部分消除

添加PathFinding Test,检测道路是否存在,这样做会增大开销,但是能避免路径出现在死点的情况。

自定义GameMode及实现Bots生成

创建新的类GameModeBase,在这里我们可以定义属于自己的游戏规则

//SAICharacter.cpp
ASAICharacter::ASAICharacter()
{
 	PawnSensingComp =  CreateDefaultSubobject<UPawnSensingComponent>("PawnSensingComp");

	AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
}

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

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "EnvironmentQuery/EnvQueryTypes.h"
#include "SGameModeBase.generated.h"

class UEnvQuery;
class UEnvQueryInstanceBlueprintWrapper;
class UCurveFloat;

/**
 * 
 */
UCLASS()
class ACTIONROGUELIKE_API ASGameModeBase : public AGameModeBase
{
	GENERATED_BODY()

protected:

	UPROPERTY(EditDefaultsOnly, Category = "AI")
	TSubclassOf<AActor> MinionClass;

	UPROPERTY(EditDefaultsOnly, Category = "AI")
	UEnvQuery* SpawnBotQuery;

	UPROPERTY(EditDefaultsOnly, Category = "AI")
	UCurveFloat* DifficultyCurve;

	FTimerHandle TimerHandle_SpawnBots;

	UPROPERTY(EditDefaultsOnly, Category = "AI")
	float SpawnTimerInterval;

	UFUNCTION()
	void SpawnBotTimerElapsed();

	UFUNCTION()
	void OnQueryCompleted(UEnvQueryInstanceBlueprintWrapper* QueryInstance, EEnvQueryStatus::Type QueryStatus);

public:

	ASGameModeBase();

	virtual void StartPlay() override;
};


////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 "AI/SAICharacter.h"
#include "SAttributeComponent.h"
#include "EngineUtils.h"



 

ASGameModeBase::ASGameModeBase()
{
	SpawnTimerInterval = 2.0f;
}


void ASGameModeBase::StartPlay()
{
	Super::StartPlay();

	// Continuous timer to spawn in more bots.
	// Actual amount of bots and whether its allowed to spawn determined by spawn logic later in the chain...
	GetWorldTimerManager().SetTimer(TimerHandle_SpawnBots, this, &ASGameModeBase::SpawnBotTimerElapsed, SpawnTimerInterval, true);
}


void ASGameModeBase::SpawnBotTimerElapsed()
{
	UEnvQueryInstanceBlueprintWrapper* QueryInstance = UEnvQueryManager::RunEQSQuery(this, SpawnBotQuery, this, EEnvQueryRunMode::RandomBest5Pct, nullptr);
	if (ensure(QueryInstance))
	{
		QueryInstance->GetOnQueryFinishedEvent().AddDynamic(this, &ASGameModeBase::OnQueryCompleted);
	}
}


void ASGameModeBase::OnQueryCompleted(UEnvQueryInstanceBlueprintWrapper* QueryInstance, EEnvQueryStatus::Type QueryStatus)
{
	if (QueryStatus != EEnvQueryStatus::Success)
	{
		UE_LOG(LogTemp, Warning, TEXT("Spawn bot EQS Query Failed!"));
		return;
	}


	int32 NrOfAliveBots = 0;
	for (TActorIterator<ASAICharacter> It(GetWorld()); It; ++It)
	{
		ASAICharacter* Bot = *It;

		USAttributeComponent* AttributeComp = Cast<USAttributeComponent>(Bot->GetComponentByClass(USAttributeComponent::StaticClass()));
		if (ensure(AttributeComp) && AttributeComp->IsAlive())
		{
			NrOfAliveBots++;
		}
	}

	float MaxBotCount = 10.0f;

	if (DifficultyCurve)
	{
		MaxBotCount = DifficultyCurve->GetFloatValue(GetWorld()->TimeSeconds);
	}


	if (NrOfAliveBots >= MaxBotCount)
	{
		return;
	}



	TArray<FVector> Locations = QueryInstance->GetResultsAsLocations();

	if (Locations.IsValidIndex(0))
	{

		GetWorld()->SpawnActor<AActor>(MinionClass, Locations[0], FRotator::ZeroRotator);
	}
}

新建一个以SGameMode为基类的蓝图,设置MinionClass和eqs

在世界设计GameMode为GameModeBP

创建难度曲线,选择CurveFloat

在Curve中 在0s时,只允许存在一个敌人,到60秒后,最多存在10个

增加伤害,死亡,布娃娃功能

我们要新增三个新功能,对Bots造成伤害,使bots死亡并且呈现布娃娃的状态

首先新增代码

//SAICharacter.h
protected:
UPROPERTY(VisibleAnywhere,BlueprintReadOnly,Category="Components")
	USAttributeComponent* AttributeComp;

UFUNCTION()
	void OnHealthChanged(AActor* InstigatorActor, USAttributeComponent* OwningComp, float NewHealth,float Delta);

//SAICharacter.cpp
ASAICharacter::ASAICharacter()
{
	AttributeComp = CreateDefaultSubobject<USAttributeComponent>("AttributeComp");
}

void ASAICharacter::PostInitializeComponents()
{
	AttributeComp->OnHealthChanged.AddDynamic(this,&ASAICharacter::OnHealthChanged);
}

void ASAICharacter::OnHealthChanged(AActor* InstigatorActor, USAttributeComponent* OwningComp, float NewHealth,
	float Delta)
{
	if(Delta <0.0f)
	{


		if(NewHealth <= 0.0f)
		{
			//stop BT
			AAIController* AIC =  Cast<AAIController>(GetController());
			if(AIC )
			{
				AIC->GetBrainComponent()->StopLogic("Killed");
			}

			//ragdoll
			GetMesh()->SetAllBodiesSimulatePhysics(true);
			

			//set lifespan
			SetLifeSpan(10.0f);
		}
	}
}

//SGameModeBase.cpp 
//仅仅添加一些调试的内容
void ASGameModeBase::OnQueryCompleted(UEnvQueryInstanceBlueprintWrapper* QueryInstance, EEnvQueryStatus::Type QueryStatus)
{
	if (QueryStatus != EEnvQueryStatus::Success)
	{
		UE_LOG(LogTemp, Warning, TEXT("Spawn bot EQS Query Failed!"));
		return;
	}


	UE_LOG(LogTemp,Log,TEXT("Found %i alive Bots"),NrOfAliveBots);

	if (NrOfAliveBots >= MaxBotCount)
	{
		UE_LOG(LogTemp,Log,TEXT("At maximum bot capacity.Skipping bot spawn"));
		return;
	}

	TArray<FVector> Locations = QueryInstance->GetResultsAsLocations();

	if (Locations.IsValidIndex(0))
	{

		GetWorld()->SpawnActor<AActor>(MinionClass, Locations[0], FRotator::ZeroRotator);

		DrawDebugSphere(GetWorld(),Locations[0],50.0f,20,FColor::Blue,false,60.0f);
	}
    
}

修改魔法子弹的伤害后,射击机器人,发生机器人会停止行为,掉入虚空

为魔法子弹建立子蓝图,因为我们不需要bot能够打出那么多伤害,然后在子蓝图中我们修改伤害为5点

我们中RangedTask的ProjectileClass设置为刚刚的子蓝图

新增ragdoll的代码

////SAICharacter.cpp
//ragdoll
			GetMesh()->SetAllBodiesSimulatePhysics(true);
			GetMesh()->SetCollisionProfileName("Ragdoll");

现在击中后,bot不会直接掉入虚空,而是Ragdoll的碰撞条件的效果

这是因为我们bots的Mesh是由胶囊和球型组合在一起的,他们之间的空隙存在一定的约束

为AI添加更多感觉

我们希望当我们攻击ai时,他们不会傻傻的仍然往前走,而是会立刻反击

//SAICharacter.h
void SetTargetActor(AActor* NewTarget);

UFUNCTION()
	void OnHealthChanged(AActor* InstigatorActor, USAttributeComponent* OwningComp, float NewHealth,float Delta);

//SAICharacter.cpp
void ASAICharacter::OnPawnSeen(APawn* Pawn)
{
	SetTargetActor(Pawn);

	DrawDebugString(GetWorld(),GetActorLocation(),"PlayerSpotted",nullptr,FColor::White,4.0f,true);
	
}


void ASAICharacter::OnHealthChanged(AActor* InstigatorActor, USAttributeComponent* OwningComp, float NewHealth,
	float Delta)
{
	if(Delta <0.0f)
	{
		if(InstigatorActor != this)
		{
			SetTargetActor(InstigatorActor);
		}

		if(NewHealth <= 0.0f)
		{
			//stop BT
			AAIController* AIC =  Cast<AAIController>(GetController());
			if(AIC )
			{
				AIC->GetBrainComponent()->StopLogic("Killed");
			}

			//ragdoll
			GetMesh()->SetAllBodiesSimulatePhysics(true);
			GetMesh()->SetCollisionProfileName("Ragdoll");
			

			//set lifespan
			SetLifeSpan(10.0f);
		}
	}
}

//SAttributeComponent.h
UFUNCTION(BlueprintCallable,Category="Attributes")
	bool ApplyHealthChange(AActor* InstigatorActor,float Delta);


//SAttributeComponent.cpp
bool USAttributeComponent::ApplyHealthChange(AActor* InstigatorActor,float Delta)
{

	float OldHealth = Health;

	Health = FMath::Clamp(Health+Delta,0.0f,HealthMax);

	float ActualDelta = Health - OldHealth;
	OnHealthChanged.Broadcast(InstigatorActor,this,Health,ActualDelta);
	return ActualDelta != 0;
}

//SMagicProjectile.cpp
void ASMagicProjectile::OnActorOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
	UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	if(OtherActor && OtherActor != GetInstigator())
	{
		USAttributeComponent* AttributeComp = Cast<USAttributeComponent>(OtherActor->GetComponentByClass(USAttributeComponent::StaticClass()));
		if(AttributeComp)
		{
			AttributeComp->ApplyHealthChange(GetInstigator(),-DamageAmount);

			Destroy();
		}
	}
}

//SPowerup_HealthPotion.cpp
if (ensure(AttributeComp) && !AttributeComp->IsFullHealth())
	{
		// Only activate if healed successfully
		if (AttributeComp->ApplyHealthChange(this,AttributeComp->GetHealthMax()))
		{
			HideAndCooldownPowerup();
		}
	}

一些对建设游戏框架有用的技巧(Static Function)

//SAttributeComponent.h
public:
	UFUNCTION(BlueprintCallable,Category="Attributes")
	static USAttributeComponent* GetAttributes(AActor* FromActor);

	UFUNCTION(BlueprintCallable, Category = "Attributes",meta = (DisplayName = "Alive"))
	static bool IsActorAlive(AActor* Actor);

//SAttributeComponent.cpp
USAttributeComponent* USAttributeComponent::GetAttributes(AActor* FromActor)
{
	if (FromActor)
	{
		return Cast<USAttributeComponent>(FromActor->GetComponentByClass(USAttributeComponent::StaticClass()));
	}

	return nullptr;
}

bool USAttributeComponent::IsActorAlive(AActor* Actor)
{
	 USAttributeComponent* AttributeComp = GetAttributes((Actor));
	if(AttributeComp)
	{
		return AttributeComp->IsAlive();
	}

	return false;
}

改善Bots的开火逻辑

//SBTTask_RangedTask.cpp
USBTTask_RangedTask::USBTTask_RangedTask()
{
	MaxBulletSpread = 2.0f;
}
 
EBTNodeResult::Type USBTTask_RangedTask::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("TargetActor"));
		if(TargetActor == nullptr)
		{
			return EBTNodeResult::Failed;
 
		}

		if(!USAttributeComponent::IsActorAlive(TargetActor))
		{
			return EBTNodeResult::Failed;
		}
  
		FVector Direction = TargetActor->GetActorLocation() - MuzzleLocation;
		FRotator MuzzleRotation = Direction.Rotation();

		MuzzleRotation.Pitch += FMath::RandRange(0.0f,MaxBulletSpread);
		MuzzleRotation.Yaw += FMath::RandRange(-MaxBulletSpread,MaxBulletSpread);
 
		FActorSpawnParameters Params;
		Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
		Params.Instigator = MyPawn;
 
		AActor* NewProj = GetWorld()->SpawnActor<AActor>(ProjectileClass,MuzzleLocation,MuzzleRotation,Params);
		return NewProj ? EBTNodeResult::Succeeded : EBTNodeResult::Failed;
	}
 
	
	
	return EBTNodeResult::Failed;
}
 

//SBTTask_RangedTask.h
protected:
	UPROPERTY(EditAnywhere,Category="AI")
	float MaxBulletSpread;

public:
	USBTTask_RangedTask();

如果我们将弹丸的尺寸变大,在发生时会直接与bots发生碰撞,这时候我们在//SBTTask_RangedTask.cpp 中的

Params.Instigator = MyPawn;就起到了作用,防止与bots自身碰撞

控制台

slomo 5顿帧,可以加速运行时的速度

在行为树中,将最大偏移再次修改为8

为Bots添加被击效果(失败)

在之前的一次为角色添加被击效果时我就没搞定

//SAICharacter.cpp
ASAICharacter::ASAICharacter()
{
	TimeToHitParamName = "TimeOfHit";
}

void ASAICharacter::OnHealthChanged(AActor* InstigatorActor, USAttributeComponent* OwningComp, float NewHealth,
	float Delta)
{
    GetMesh()->SetScalarParameterValueOnMaterials(TimeToHitParamName, GetWorld()->TimeSeconds);
}


//SAICharacter.h
protected:
	UPROPERTY(VisibleAnywhere,Category="Effects")
	FName TimeToHitParamName;

进入到Minion的材质中

更改连线如下图

Bots残血逃跑寻找掩体回血

//SBTService_CheckAttackRange.h
UPROPERTY(EditAnywhere, Category = "AI")
	float MaxAttackRange;
	
//SBTService_CheckAttackRange.cpp
void USBTService_CheckAttackRange::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
	Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);

	//Check distance between ai pawn and target actor
	
	UBlackboardComponent* BlackboardComp = OwnerComp.GetBlackboardComponent();
	if(ensure(BlackboardComp))
	{
		AActor* TargetActor = Cast<AActor>(BlackboardComp->GetValueAsObject("TargetActor"));
		if(TargetActor)
		{
			AAIController* MyController =  OwnerComp.GetAIOwner();

			APawn* AIPawn = MyController->GetPawn();
			if (ensure(AIPawn))
			{
				float DistanceTo = FVector::Distance(TargetActor->GetActorLocation(), AIPawn->GetActorLocation());

				bool bWithinRange = DistanceTo < MaxAttackRange;
				
				bool bHasLOS = false;
				if (bWithinRange)
				{
					bHasLOS = MyController->LineOfSightTo(TargetActor);
				}
				BlackboardComp->SetValueAsBool(AttackRangeKey.SelectedKeyName, (bWithinRange && bHasLOS));
			}
		}
	}
}

USBTService_CheckAttackRange::USBTService_CheckAttackRange()
{
	MaxAttackRange = 2000.f;
}

//SBTService_CheckHealth.h

#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTService.h"
#include "SBTService_CheckHealth.generated.h"

/**
 * 
 */
UCLASS()
class ACTIONROGUELIKE_API USBTService_CheckHealth : public UBTService
{
	GENERATED_BODY()


protected:
	UPROPERTY(EditAnywhere,Category="AI")
	FBlackboardKeySelector LowHealthKey;


	UPROPERTY(EditAnywhere, Category = "AI", meta = (ClampMin="0.0", ClampMax="1.0"))
	float LowHealthFraction;

	virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;


public:
	USBTService_CheckHealth();
};


//SBTService_CheckHealth.cpp

#include "AI/SBTService_CheckHealth.h"
#include "SAttributeComponent.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "AIController.h"

USBTService_CheckHealth::USBTService_CheckHealth()
{
	LowHealthFraction = 0.3f;
}


void USBTService_CheckHealth::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
	Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);

	APawn* AIPawn = OwnerComp.GetAIOwner()->GetPawn();
	if (ensure(AIPawn))
	{
		USAttributeComponent* AttributeComp = USAttributeComponent::GetAttributes(AIPawn);
		if (ensure(AttributeComp))
		{
			bool bLowHealth = (AttributeComp->GetHealth() / AttributeComp->GetHealthMax()) < LowHealthFraction;

			UBlackboardComponent* BlackBoardComp = OwnerComp.GetBlackboardComponent();
			BlackBoardComp->SetValueAsBool(LowHealthKey.SelectedKeyName, bLowHealth);
		}
	}
}


//SBTTask_HealSelf.h
#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "SBTTask_HealSelf.generated.h"

/**
 * 
 */
UCLASS()
class ACTIONROGUELIKE_API USBTTask_HealSelf : public UBTTaskNode
{
	GENERATED_BODY()

	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
	
};

//SBTTask_HealSelf.cpp

#include "AI/SBTTask_HealSelf.h"

#include "AIController.h"
#include "SAttributeComponent.h"

EBTNodeResult::Type USBTTask_HealSelf::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	APawn* MyPawn = Cast<APawn>(OwnerComp.GetAIOwner()->GetPawn());
	if(MyPawn == nullptr)
	{
		return EBTNodeResult::Failed;
	}

	USAttributeComponent* AttributeComp = USAttributeComponent::GetAttributes(MyPawn);
	if(ensure((AttributeComp)))
	{
		AttributeComp->ApplyHealthChange(MyPawn,AttributeComp->GetHealthMax());
	}
	

	return EBTNodeResult::Succeeded;
}

安装图下内容添加EQS 以及修改BTtree

优化Bots死亡布娃娃效果

//SGameplayFunctionLibrary.cpp
bool USGameplayFunctionLibrary::ApplyDirctionalDamage(AActor* DamageCauser, AActor* TargetActor, int DamageAmount,
	const FHitResult& HitResult)
{
	if(ApplyDamage(DamageCauser,TargetActor,DamageAmount))
	{

		UPrimitiveComponent* HitComp =  HitResult.GetComponent();
		if(HitComp && HitComp->IsSimulatingPhysics(HitResult.BoneName))
		{
			// Direction = Target - Origin
			FVector Direction = HitResult.TraceEnd - HitResult.TraceStart;
			Direction.Normalize();

			HitComp->AddImpulseAtLocation(Direction * 300000.f, HitResult.ImpactPoint, HitResult.BoneName);
		}

		return true;
	}

	return false;
}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇