之前,我们怪物生成是直接将怪物蓝图拖入到场景中,这样好处是清晰明了,坏处就是场景怪物过多会造成卡顿,并且地图加载时间过长。我们想做一个通用的生成工具,可以配置敌人使用的蓝图等级和职业,并且在角色进入到一定位置后,才会生成敌人。
实现原理
如果需要实现这个功能,我们需要一个在场景添加的Actor来定义相关内容。
我们可以打开放置Actor面板
找到目标点
它可以标识一个场景中的位置,我们想通过以此为基类创建一个派生类,可以在上面设置生成的怪物的数据来实现生成。
添加新类
首先,我们基于TargetPoint定义一个新类,用于设置目标生成位置
然后定义一个Actor类,用于触发激活对应位置的Actor生成。
在ARPGEnemySpawnPoint 里,我们需要添加生成敌人实现所需的数据,所以我们定义了一个敌人类设置,敌人等级,以及敌人类型的设置。
// 版权归暮志未晚所有。
#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固定生成但可以调整位置
// 版权归暮志未晚所有。
#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用于设置与此碰撞体重叠后,会生成多少敌人。
// 版权归暮志未晚所有。
#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;
};
在构造函数里,我们将创建一个碰撞盒子实例,并只会和角色触发重叠事件
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。
void ARPGEnemySpawnVolume::LoadActor_Implementation()
{
if(bReached)
{
Destroy();
}
}
在事件开始时,我们绑定Box碰撞体的重叠事件
void ARPGEnemySpawnVolume::BeginPlay()
{
Super::BeginPlay();
Box->OnComponentBeginOverlap.AddDynamic(this, &ThisClass::OnBoxOverlap);
}
在重叠事件里,判断重叠的Actor是否继承玩家接口,玩家角色都会继承此接口(或者通过Actor标签也可以)。随后我们将bReached 设置为true,并将添加到SpawnPoints里的所有敌人调用SpawnEnemy函数生成,最后将此Box设置为无碰撞来节约性能。
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的全部代码。
// 版权归暮志未晚所有。
#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++类,接着编译打开蓝图,我们要基于类创建对应的蓝图,并应用到关卡中。
首先创建一个敌人生成体积蓝图
然后创建一个敌人位置蓝图
定义好名称
在点位里,我们定义好使用的敌人类,等级和职业类型
在敌人生成体积里,我们可以设置碰撞体盒子线条宽度,以及在场景中显示,方便测试。
接着,我们在关卡中拖入一个体积和两个位置节点,拖入场景相当于生成了对应蓝图的实例。
接着,我们在体积上添加把所需的位置节点添加,并进行测试即可。
位置节点和体积分离,可以让位置节点复用,它可以添加到多个体积里。