【UE4C++-ActionRougelike-13】AI优化


【UE4C++-ActionRougelike-13】AI优化

一、AI角色再生

1、创建生成点EQS

新建EQS文件Query_FindBotSpawn,该EQS的目的是从所有的玩家身边的环形区域随机生成AI机器人。(具体节点设置有些还是不太清楚为什么,以后系统学习AI部分后再回来看看是否能够理解吧)

Donut节点设置:中心点设置为一个自定义情景(找到所有的玩家)

Donut节点设置

Distance节点设置:

Distance节点设置

PathFinding节点设置:

PathFinding节点设置

QueryContext_FindAllPlayers蓝图:找到所有的玩家类

QueryContext蓝图

2、自定义GameMode

以GameModeBase为父类新建一个GameMode类

class ACTIONROGUELIKE_API AMyGameModeBase : public AGameModeBase
{
	GENERATED_BODY()
protected:
	//指定要生成的AI角色的类
	UPROPERTY(EditDefaultsOnly, Category = "AI")
	TSubclassOf<AActor> MinionClass;
	//指定用于生成AI角色的环境查询系统(EQS)查询
	UPROPERTY(EditDefaultsOnly, Category = "AI")
	UEnvQuery* SpawnBotQuery;
	//指定游戏的难度曲线(AI角色生成数量)
	UPROPERTY(EditDefaultsOnly, Category = "AI")
	UCurveFloat* DifficultyCurve;
	//管理定时生成AI角色的计时器
	FTimerHandle TimerHandle_SpawnBots;
	//指定生成AI角色的时间间隔
	UPROPERTY(EditDefaultsOnly, Category = "AI")
	float SpawnTimerInterval;
	//计时器回调函数,用于生成AI角色
	UFUNCTION()
	void SpawnBotTimerElapsed();
	//EQS查询完成的回调函数,用于处理查询结果
	UFUNCTION()
	void OnQueryCompleted(UEnvQueryInstanceBlueprintWrapper* QueryInstance, EEnvQueryStatus::Type QueryStatus);
public:
	AMyGameModeBase();
	virtual void StartPlay() override;
};

在MyGameModeBase.cpp中给出GameMode类的实现,主要思路是:

  • 游戏开始后,设置定时器来循环执行AI机器人生成函数
  • 生成函数会首先检测当前游戏中AI机器人数量是否达到上限,如果达到上限则进入下一次定时器。
  • 如果没有则会运行先前创建的EQS来寻找生成位置
  • 在EQS查询结束后执行与查询结束实践绑定的 OnQueryCompleted() 函数
AMyGameModeBase::AMyGameModeBase()
{
	SpawnTimerInterval = 2.0f;
}

void AMyGameModeBase::StartPlay()
{
	Super::StartPlay();
	//设置了一个定时器TimerHandle_SpawnBots,定时调用SpawnBotTimerElapsed()函数,时间间隔为SpawnTimerInterval秒,循环执行
	GetWorldTimerManager().SetTimer(TimerHandle_SpawnBots, this, &AMyGameModeBase::SpawnBotTimerElapsed, SpawnTimerInterval, true);
}

void AMyGameModeBase::SpawnBotTimerElapsed()
{
	int32 NrOfAliveBots = 0;
	//统计当前世界中存活的 AMyAICharacter 类型的角色数量
	for (TActorIterator<AMyAICharacter> It(GetWorld()); It; ++It)
	{
		AMyAICharacter* Bot = *It;
		UMyAttributeComponent* AttributeComp = Cast<UMyAttributeComponent>(Bot->GetComponentByClass(UMyAttributeComponent::StaticClass()));
        
        //这里目前会报Bug,因为还没有给MyAICharacter添加MyAttributeComponent组件
		if (ensure(AttributeComp) && AttributeComp->IsAlive())
		{
			NrOfAliveBots++;
		}
	}
	UE_LOG(LogTemp, Log, TEXT("Found %i alive bots"), NrOfAliveBots);
	float MaxBotCount = 10.0f;
	//根据 DifficultyCurve 曲线的值计算出最大的 bot 数量
	if (DifficultyCurve)
	{
		MaxBotCount = DifficultyCurve->GetFloatValue(GetWorld()->TimeSeconds);
	}
	//与当前存活的 bot 数量进行比较。如果存活的 bot 数量小于最大数量,则从查询结果中获取一个位置
	if (NrOfAliveBots >= MaxBotCount)
	{
		UE_LOG(LogTemp, Log, TEXT("At maximum bot capacity. Skipping bot spawn."));
		return;
	}
	//运行一个环境查询(Environment Query System, EQS)并获取查询实例 QueryInstance。
	UEnvQueryInstanceBlueprintWrapper* QueryInstance = UEnvQueryManager::RunEQSQuery(this, SpawnBotQuery, this, EEnvQueryRunMode::RandomBest5Pct, nullptr);
	if (ensure(QueryInstance))
	{
		//将当前对象绑定到查询完成事件,查询完成后会调用 OnQueryCompleted() 函数。
		QueryInstance->GetOnQueryFinishedEvent().AddDynamic(this, &AMyGameModeBase::OnQueryCompleted);
	}
}

void AMyGameModeBase::OnQueryCompleted(UEnvQueryInstanceBlueprintWrapper* QueryInstance, EEnvQueryStatus::Type QueryStatus)
{
	if (QueryStatus != EEnvQueryStatus::Success)
	{
		UE_LOG(LogTemp, Warning, TEXT("Spawn bot EQS Query Failed!"));
		return;
	}
	TArray<FVector> Locations = QueryInstance->GetResultsAsLocations();
	//通过 GetWorld()->SpawnActor<AActor>() 函数在该位置生成一个 MinionClass 类型的角色
	if (Locations.IsValidIndex(0))
	{
		GetWorld()->SpawnActor<AActor>(MinionClass, Locations[0], FRotator::ZeroRotator);
	}
}

3、创建Curve & 分配资产

创建Curve可以自定义随时间变化的最大AI机器人数量,以MyGameMode为父类创建蓝图类GameMode_BP,并分配资产。最后要在WorldSetting里的GameMode选用GameMode_BP。

GameMode蓝图

如果生成的AI机器人不会动,要在MyAICharacter.cpp的构造函数中添加下面代码,表示生成的AI也可以执行各种行为。

AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;

4、效果演示

效果演示这里已经为MyAICharacter添加了Attribute组件,不添加的话会在保错。

生成AI机器人效果演示

二、AI角色增加血量属性

1、添加属性组件

在MyAICharacter中添加属性组件

class ACTIONROGUELIKE_API AMyAICharacter : public ACharacter
{
protected:
	//属性组件
	UPROPERTY(VisibleAnywhere,BlueprintReadOnly, Category = "Components")
	UMyAttributeComponent* AttributeComp;	
    
	UFUNCTION()
	void OnHealthChanged(AActor* InstigatorActor, UMyAttributeComponent* OwningComp, float NewHealth, float Delta);

在MyAICharacter.cpp中初始化属性组件,并绑定血量变化事件函数

AMyAICharacter::AMyAICharacter()
{
	AttributeComp = CreateDefaultSubobject<UMyAttributeComponent>("AttributeComp");
}

void AMyAICharacter::PostInitializeComponents()
{
	Super::PostInitializeComponents();
    //血量变化事件触发时执行OnHealthChanged函数
	AttributeComp->OnHealthChange.AddDynamic(this, &AMyAICharacter::OnHealthChanged);
}

2、设置AI死亡

在MyAICharacter.cpp的OnHealthChanged中可以设置AI何时死亡,主要思路:

  • 当AI生命值变化时,根据Delta判断是否收到伤害
  • 如果收到伤害且来源不是自身,则将伤害来源角色设为TargetActor
  • 如果生命值降至0以下,则AI角色死亡并执行死亡后的逻辑
    • 停止角色的行为树逻辑
    • 设置角色Mesh的所有物体模拟物理效果
    • 设置角色的寿命,定时销毁角色
void AMyAICharacter::OnHealthChanged(AActor* InstigatorActor, UMyAttributeComponent* OwningComp, float NewHealth, float Delta)
{
	if (Delta < 0.0f)
	{
		if (InstigatorActor != this)
		{
			SetTargetActor(InstigatorActor);
		}
		GetMesh()->SetScalarParameterValueOnMaterials(TimeToHitParamName, GetWorld()->TimeSeconds);
		//AI角色死亡
		if (NewHealth <= 0.0f)
		{
			//停止行为树
			AAIController* AIC = Cast<AAIController>(GetController());
			if (AIC)
			{
				AIC->GetBrainComponent()->StopLogic("Killed");
			}
            //设置模拟物理效果
			GetMesh()->SetAllBodiesSimulatePhysics(true);
			GetMesh()->SetCollisionProfileName("Ragdoll");

			//设置尸体存留时间
			SetLifeSpan(10.0f);
		}
	}
}
void AMyAICharacter::SetTargetActor(AActor* NewTarget)
{
	AAIController* AIC = Cast<AAIController>(GetController());
	if (AIC)
	{
		AIC->GetBlackboardComponent()->SetValueAsObject("TargetActor", NewTarget);
	}
}

3、效果演示

AI机器人死亡效果演示

三、射击逻辑优化

1、静态函数

在MyAttributeComponent中声明两个静态函数,分别用于获取某个对象的属性组件和判断对象是否存活

class ACTIONROGUELIKE_API UMyAttributeComponent : public UActorComponent
{
public:	
	UFUNCTION(BlueprintCallable, Category = "Attributes")
	static UMyAttributeComponent* GetAttributes(AActor* FromActor);

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

MyAttributeComponent.cpp中给出函数的实现

UMyAttributeComponent* UMyAttributeComponent::GetAttributes(AActor* FromActor)
{
	if (FromActor)
	{
		return Cast<UMyAttributeComponent>(FromActor->GetComponentByClass(UMyAttributeComponent::StaticClass()));
	}

	return nullptr;
}

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

	return false;
}

这种静态函数的设计模式可以提供一些全局的工具函数或实用功能,并且不需要创建类的实例就可以调用这些函数。静态函数在这种情况下可以作为一个工具函数库,提供方便的方法来处理属性组件和判断 Actor 存活状态

//UMyAttributeComponent* AttributeComp = Cast<UMyAttributeComponent>(Bot->GetComponentByClass(UMyAttributeComponent::StaticClass()));
UMyAttributeComponent* AttributeComp = UMyAttributeComponent::GetAttributes(Bot);

2、玩家死亡后停止射击 & 射击弹道偏移

修改MyBTTask_RangedAttack类,添加弹道偏移范围属性

class ACTIONROGUELIKE_API UMyBTTask_RangedAttack : public UBTTaskNode
{
public:
	UMyBTTask_RangedAttack();
protected:
	//弹道波动范围
	UPROPERTY(EditAnywhere, Category = "AI")
	float MaxBulletSpread;
};

在MyBTTask_RangedAttack.cpp中添加玩家死亡后停止射击逻辑和弹道偏移

EBTNodeResult::Type UMyBTTask_RangedAttack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	//获取当前任务节点所属的 AI 控制器
	AAIController* MyAIController = OwnerComp.GetAIOwner();
	if (ensure(MyAIController))
	{
		//获取 AI 控制器的控制角色
		ACharacter* MyPawn = Cast<ACharacter>(MyAIController->GetPawn());
		//如果控制角色为空,则返回 EBTNodeResult::Failed 表示任务失败。
		if (MyPawn == nullptr)
		{
			return EBTNodeResult::Failed;
		}
		//获取枪口位置(MuzzleLocation)
		FVector MuzzleLocation = MyPawn->GetMesh()->GetSocketLocation("Muzzle_01");

		//获取黑板中保存的目标角色(TargetActor)
		AActor* TargetActor = Cast<AActor>(OwnerComp.GetBlackboardComponent()->GetValueAsObject("TargetActor"));
		//如果目标角色为空,则返回 EBTNodeResult::Failed 表示任务失败
		if (TargetActor == nullptr)
		{
			return EBTNodeResult::Failed;
		}
        //如果角色死亡,停止射击
		if (!UMyAttributeComponent::IsActorAlive(TargetActor))
		{
			return EBTNodeResult::Failed;
		}
		//计算从枪口位置到目标角色位置的方向向量(Direction),并将其转换为旋转
		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);

		//如果新生成的投射物实例不为空,则返回 EBTNodeResult::Succeeded 表示任务成功,否则返回 EBTNodeResult::Failed 表示任务失败
		return NewProj ? EBTNodeResult::Succeeded : EBTNodeResult::Failed;
	}	
	return EBTNodeResult::Failed;
}

3、效果演示

3.1 弹道偏移
射击偏移效果
3.2 玩家死亡后停止射击
玩家死亡

文章作者: Woilin
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Woilin !
评论
  目录