114. UE5 GAS RPG 实现配置怪物生成

之前,我们怪物生成是直接将怪物蓝图拖入到场景中,这样好处是清晰明了,坏处就是场景怪物过多会造成卡顿,并且地图加载时间过长。我们想做一个通用的生成工具,可以配置敌人使用的蓝图等级和职业,并且在角色进入到一定位置后,才会生成敌人。

实现原理

如果需要实现这个功能,我们需要一个在场景添加的Actor来定义相关内容。

我们可以打开放置Actor面板

找到目标点

它可以标识一个场景中的位置,我们想通过以此为基类创建一个派生类,可以在上面设置生成的怪物的数据来实现生成。

添加新类

首先,我们基于TargetPoint定义一个新类,用于设置目标生成位置

然后定义一个Actor类,用于触发激活对应位置的Actor生成。

在ARPGEnemySpawnPoint 里,我们需要添加生成敌人实现所需的数据,所以我们定义了一个敌人类设置,敌人等级,以及敌人类型的设置。

cpp 复制代码
// 版权归暮志未晚所有。

#pragma once

#include "CoreMinimal.h"
#include "AbilitySystem/Data/CharacterClassInfo.h"
#include "Engine/TargetPoint.h"
#include "RPGEnemySpawnPoint.generated.h"

/**
 * 
 */
UCLASS()
class RPG_API ARPGEnemySpawnPoint : public ATargetPoint
{
	GENERATED_BODY()

public:

	//生成敌人
	UFUNCTION(BlueprintCallable)
	void SpawnEnemy();

	//需要生成的敌人蓝图类,在类前面加class就不需要额外的前向申明
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Enemy Class")
	TSubclassOf<class ARPGEnemy> EnemyClass;

	//需要生成的敌人的等级
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Enemy Class")
	int32 EnemyLevel = 1;

	//敌人的职业类型
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Enemy Class")
	ECharacterClass CharacterClass = ECharacterClass::Warrior;
	
};

在生成敌人函数这里,我们采用延迟生成的方式,并设置Actor固定生成但可以调整位置

cpp 复制代码
// 版权归暮志未晚所有。


#include "Actor/RPGEnemySpawnPoint.h"

#include "Character/RPGEnemy.h"

void ARPGEnemySpawnPoint::SpawnEnemy()
{
	//延迟生成Actor,并设置其尝试调整位置但固定生成
	ARPGEnemy* Enemy = GetWorld()->SpawnActorDeferred<ARPGEnemy>(
		EnemyClass,
		GetActorTransform(),
		nullptr,
		nullptr,
		ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn);
	
	Enemy->SetLevel(EnemyLevel);
	Enemy->SetCharacterClass(CharacterClass);
	Enemy->FinishSpawning(GetActorTransform());
	Enemy->SpawnDefaultController();
}

接着在ARPGEnemySpawnVolume里,它作为ARPGEnemySpawnPoint 触发工具,在于此碰撞体积碰撞后,设置在此体积里的对应的怪物都将自动生成。

所以,我们在ARPGEnemySpawnVolume里增加一个碰撞体,并增加碰撞体对应的重叠函数,添加bReached 项,用于判断当前是否已经由玩家角色激活过,并添加SpawnPoints用于设置与此碰撞体重叠后,会生成多少敌人。

cpp 复制代码
// 版权归暮志未晚所有。

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Interaction/SaveInterface.h"
#include "RPGEnemySpawnVolume.generated.h"

class ARPGEnemySpawnPoint;
class UBoxComponent;

UCLASS()
class RPG_API ARPGEnemySpawnVolume : public AActor, public ISaveInterface
{
	GENERATED_BODY()
	
public:	
	ARPGEnemySpawnVolume();

	/* Save Interface */
	virtual void LoadActor_Implementation() override;
	/* Save Interface 结束 */

	//当前怪物生成体积是否已经生成过敌人
	UPROPERTY(BlueprintReadOnly, SaveGame)
	bool bReached = false;

protected:
	virtual void BeginPlay() override;
	
	UFUNCTION()
	virtual void OnBoxOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

	//配置当前的生成体积所需生成的敌人
	UPROPERTY(EditAnywhere)
	TArray<ARPGEnemySpawnPoint*> SpawnPoints;
private:

	UPROPERTY(VisibleAnywhere)
	TObjectPtr<UBoxComponent> Box;
};

在构造函数里,我们将创建一个碰撞盒子实例,并只会和角色触发重叠事件

cpp 复制代码
ARPGEnemySpawnVolume::ARPGEnemySpawnVolume()
{
	PrimaryActorTick.bCanEverTick = false;

	Box = CreateDefaultSubobject<UBoxComponent>("Box");
	SetRootComponent(Box);
	Box->SetCollisionEnabled(ECollisionEnabled::QueryOnly); //仅用于坚持
	Box->SetCollisionObjectType(ECC_WorldStatic); //设置物体类型
	Box->SetCollisionResponseToChannels(ECR_Ignore); //忽略所有检测通道
	Box->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap); //开启与pawn的重叠检测
}

由于此类继承了Save接口,我们将bReached属性保存到了存档,当进入此地图时,将读取此属性,并根据此属性进行处理,当前属性会在玩家角色与碰撞体接触后设置为true,在玩家再次进入场景时,我们不期望再触发一次,如果值为true我们将直接销毁此Actor。

cpp 复制代码
void ARPGEnemySpawnVolume::LoadActor_Implementation()
{
	if(bReached)
	{
		Destroy();
	}
}

在事件开始时,我们绑定Box碰撞体的重叠事件

cpp 复制代码
void ARPGEnemySpawnVolume::BeginPlay()
{
	Super::BeginPlay();

	Box->OnComponentBeginOverlap.AddDynamic(this, &ThisClass::OnBoxOverlap);
}

在重叠事件里,判断重叠的Actor是否继承玩家接口,玩家角色都会继承此接口(或者通过Actor标签也可以)。随后我们将bReached 设置为true,并将添加到SpawnPoints里的所有敌人调用SpawnEnemy函数生成,最后将此Box设置为无碰撞来节约性能。

cpp 复制代码
void ARPGEnemySpawnVolume::OnBoxOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	if(!OtherActor->Implements<UPlayerInterface>()) return;
	
	bReached = true;

	//在设置的所有点位生成敌人
	for(ARPGEnemySpawnPoint* SpawnPoint : SpawnPoints)
	{
		if(IsValid(SpawnPoint))
		{
			SpawnPoint->SpawnEnemy();
		}
	}

	//设置不在产生物理查询,直接销毁无法保存到存档
	Box->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}

以下是cpp的全部代码。

cpp 复制代码
// 版权归暮志未晚所有。


#include "Actor/RPGEnemySpawnVolume.h"

#include "Actor/RPGEnemySpawnPoint.h"
#include "Components/BoxComponent.h"
#include "Interaction/PlayerInterface.h"

// Sets default values
ARPGEnemySpawnVolume::ARPGEnemySpawnVolume()
{
	PrimaryActorTick.bCanEverTick = false;

	Box = CreateDefaultSubobject<UBoxComponent>("Box");
	SetRootComponent(Box);
	Box->SetCollisionEnabled(ECollisionEnabled::QueryOnly); //仅用于坚持
	Box->SetCollisionObjectType(ECC_WorldStatic); //设置物体类型
	Box->SetCollisionResponseToChannels(ECR_Ignore); //忽略所有检测通道
	Box->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap); //开启与pawn的重叠检测
}

void ARPGEnemySpawnVolume::LoadActor_Implementation()
{
	if(bReached)
	{
		Destroy();
	}
}

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

	Box->OnComponentBeginOverlap.AddDynamic(this, &ThisClass::OnBoxOverlap);
}

void ARPGEnemySpawnVolume::OnBoxOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	if(!OtherActor->Implements<UPlayerInterface>()) return;
	
	bReached = true;

	//在设置的所有点位生成敌人
	for(ARPGEnemySpawnPoint* SpawnPoint : SpawnPoints)
	{
		if(IsValid(SpawnPoint))
		{
			SpawnPoint->SpawnEnemy();
		}
	}

	//设置不在产生物理查询,直接销毁无法保存到存档
	Box->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}

创建蓝图

我们创建了对应的c++类,接着编译打开蓝图,我们要基于类创建对应的蓝图,并应用到关卡中。

首先创建一个敌人生成体积蓝图

然后创建一个敌人位置蓝图

定义好名称

在点位里,我们定义好使用的敌人类,等级和职业类型

在敌人生成体积里,我们可以设置碰撞体盒子线条宽度,以及在场景中显示,方便测试。

接着,我们在关卡中拖入一个体积和两个位置节点,拖入场景相当于生成了对应蓝图的实例。

接着,我们在体积上添加把所需的位置节点添加,并进行测试即可。

位置节点和体积分离,可以让位置节点复用,它可以添加到多个体积里。

相关推荐
darkb1rd13 分钟前
五、PHP类型转换与类型安全
android·安全·php
lang2015092817 分钟前
Java JSR 250核心注解全解析
java·开发语言
czhc114007566328 分钟前
协议 25
java·开发语言·算法
逆光的July29 分钟前
如何解决超卖问题
java
落花流水 丶34 分钟前
Java 集合框架完全指南
java
gjxDaniel35 分钟前
Kotlin编程语言入门与常见问题
android·开发语言·kotlin
csj5036 分钟前
安卓基础之《(22)—高级控件(4)碎片Fragment》
android
lang201509281 小时前
Java WebSocket API:JSR-356详解
java·python·websocket
jiang_changsheng1 小时前
环境管理工具全景图与深度对比
java·c语言·开发语言·c++·python·r语言
计算机学姐1 小时前
基于SpringBoot的民宿预定管理系统【三角色+个性化推荐算法+数据可视化统计】
java·vue.js·spring boot·mysql·信息可视化·intellij-idea·推荐算法