这一篇,我们实现敌人被击败后,掉落战利品的功能。首先,我们将创建一个新的结构体,用于定义掉落体的内容,方便我们设置掉落物。然后,我们实现敌人死亡时的掉落函数,并在蓝图里实现对应的逻辑,在场景里生成掉落物。最后,让掉落物动起来,显得掉落物需要玩家赶紧去拾取的感觉。
添加新的资产结构体
为了实现对敌人掉落战利品的配置,我们需要创建一个新的类,作为配置掉落物的新的资产类。
命名为战利品类
在类里,我们首先添加一个结构体,用于设置一种物品的掉落内容和几率。
cpp
USTRUCT(BlueprintType)
struct FLootItem
{
GENERATED_BODY()
//战利品在场景中的显示效果
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="LootTiers|Spawning")
TSubclassOf<AActor> LootClass;
//战利品生成几率
UPROPERTY(EditAnywhere, Category="LootTiers|Spawning")
float ChanceToSpawn = 0.f;
//物品生成的最大数量
UPROPERTY(EditAnywhere, Category="LootTiers|Spawning")
int32 MaxNumberToSpawn = 0.f;
//修改物品生成等级,false则使用敌人等级
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="LootTiers|Spawning")
bool bLootLevelOverride = true;
};
然后在类里,我们增加一个数组,开发者可以配置多个掉落物,然后添加一个函数,用于获取当前可以生成的战利品数组。
cpp
UCLASS()
class RPG_API ULootTiers : public UDataAsset
{
GENERATED_BODY()
public:
//获取需要生成的战利品数据
UFUNCTION(BlueprintCallable)
TArray<FLootItem> GetLootItems();
UPROPERTY(EditDefaultsOnly, Category="LootTiers|Spawning")
TArray<FLootItem> LootItems;
};
在函数实现这里,我们创建了一个新的数组,用于根据概率计算每个物品的是否可掉落,物品可以设置多个,所以,我们需要两层嵌套循环,如果当前可掉落,那么我们将其添加到返回数组里。最后返回。
cpp
TArray<FLootItem> ULootTiers::GetLootItems()
{
TArray<FLootItem> ReturnItems;
for(const FLootItem Item : LootItems)
{
for(int32 i=0; i<Item.MaxNumberToSpawn; ++i)
{
if(FMath::RandRange(1.f, 100.f) < Item.ChanceToSpawn)
{
FLootItem NewItem;
NewItem.LootClass = Item.LootClass;
NewItem.bLootLevelOverride = Item.bLootLevelOverride;
ReturnItems.Add(NewItem);
}
}
}
return ReturnItems;
}
实现配置和触发掉落逻辑
然后,我们需要一个能够去获取配置好的数据资产的地方,最方便的就是放到GameMode里,我们在GameMode类里添加一个配置,可以在蓝图配置使用哪一个数据资产
cpp
//战利品数据配置
UPROPERTY(EditDefaultsOnly, Category="Loot Tiers")
TObjectPtr<ULootTiers> LootTiers;
为了方便获取数据资产,我们在蓝图函数库里增加一个函数,用于获取数据资产
cpp
/**
* 获取生成的战利品数据资产,此数据会配置到GameMode上
* @param WorldContextObject 一个世界场景的对象,用于获取当前所在的世界
* @return 战利品数据
*
* @note 敌人死亡后,所需生成的战利品
*/
UFUNCTION(BlueprintCallable, Category="RPGAbilitySystemLibrary|CharacterClassDefaults", meta=(DefaultToSelf = "WorldContextObject"))
static ULootTiers* GetLootTiers(const UObject* WorldContextObject);
函数实现,我们将获取到GameMode,并从GameMode身上获取到数据资产
cpp
ULootTiers* URPGAbilitySystemLibrary::GetLootTiers(const UObject* WorldContextObject)
{
//获取到当前关卡的GameMode实例
const ARPGGameMode* GameMode = Cast<ARPGGameMode>(UGameplayStatics::GetGameMode(WorldContextObject));
if(GameMode == nullptr) return nullptr;
//返回敌人战利品配置,需要设置到GameMode上
return GameMode->LootTiers;
}
最后,我们要在敌人类里实现掉落逻辑,我们增加一个函数,这个函数需要在蓝图里实现逻辑
cpp
//生成战利品
UFUNCTION(BlueprintImplementableEvent)
void SpawnLoot();
然后在触发死亡逻辑时,我们调用此函数来生成战利品
添加蓝图
代码方便,我们完成了,接着,我们要在编辑器里,添加一个新的数据资产
资产类型使用我们创建的战利品资产
我们将其和之前的资产放到一块
然后,在资产里,我们将之前制作的药瓶和水晶(持续回血)添加到战利品里,掉落概率设置为100,并且要使用敌人等级掉落。
接着将其设置到GameMode蓝图里
我们在敌人蓝图基类里实现统一的掉落战利品机制,首先通过蓝图库函数获取到数据资产实例,然后通过GetLootItems获取到需要生成的掉落物
然后创建一个蓝图函数,用于通过蓝图函数库,来设置掉落物的转向。
然后我们将其设置为纯函数,这样,不需要通过执行箭头调用。
然后将转向存储起来方便后续使用
然后,我们通过一个定时器,让物品掉落持续生成,这样可以防止卡顿,并且物品有一个个生成的效果,我们在设置定时器时,直接调用一次此事件。
接着就是生成战利品的自定义事件,我们通过索引,从返回的数组里获取到需要生成的对应的数据,并计算出物品掉落位置,最后生成Actor
我们创建获取变换的纯函数,通过转向对敌人位置进行一个朝向在一定范围内随机偏移,并使用旋转的朝向。
在创建Actor后,我们需要更新掉落物的等级,如果此物品需要修改等级,那么我们将通过此函数内的逻辑进行修改
函数内,我们首先判断是否需要修改,然后将其转换为场景物品的基类,然后判断当前物品是否存在,最后将敌人等级应用到掉落物身上。
最后一步,就是修改索引,每调用一次,我们将索引+1,下次再调用此事件时,将会生成下一个坐标的物品,如果索引超过或等于数组长度时,我们将结束定时器,完成掉落物的生成。
最后展示一下完成的蓝图连线
然后进入关卡打怪测试效果
关于掉落物的一些扩展
如果后续扩展的话,我考虑对于每个大关卡创建单独的一套掉落,然后在数组资产里增加几个数组,比如效果,使用小怪的一套掉落,而精英怪使用精英怪的一套掉落,最后是boss的掉落,使用boss的一套掉落逻辑。
然后获取掉落时,可以根据敌人品质,去获取不同的掉落,我们当然没必须单独为敌人去配置掉落。BOSS除外,我们当然可以对一些特殊BOSS去配置单独的掉落,比如关卡的最终BOSS掉落,以及一些特殊怪物掉落。
实现掉落物自动旋转和悬浮效果
为了实现这个效果,我们要在掉落物的基类RPGEffectActor里增加一些属性和函数,用于实现此效果
要实现这个功能,我们增加一批函数,用于实现此功能
cpp
// 计算后的Actor所在的位置
UPROPERTY(BlueprintReadWrite)
FVector CalculatedLocation;
// 计算后的Actor的旋转
UPROPERTY(BlueprintReadWrite)
FRotator CalculatedRotation;
// Actor是否帧更新旋转
UPROPERTY(BlueprintReadWrite, Category="Pickup Movement")
bool bRotates = false;
// Actor每秒旋转的角度
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Pickup Movement")
float RotationRate = 45.f;
// Actor是否更新位置
UPROPERTY(BlueprintReadWrite, Category="Pickup Movement")
bool bSinusoidalMovement = false;
// 正弦值-1到1,此值为调整更新移动范围
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Pickup Movement")
float SineAmplitude = 1.f;
// 此值参与正弦运算,默认值为1秒一个循环(2PI走完一个正弦的循环,乘以时间,就是一秒一个循环,可用于调整位置移动速度)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Pickup Movement")
float SinePeriod = 1.f; //2 * PI
//调用此函数,Actor开始自动更新上下位置
UFUNCTION(BlueprintCallable)
void StartSinusoidalMovement();
//调用此函数,Actor开始自动旋转
UFUNCTION(BlueprintCallable)
void StartRotation();
private:
//当前掉落物的存在时间,可以通过此时间实现动态效果
float RunningTime = 0.f;
// Actor生成的默认初始位置,在Actor动态浮动时,需要默认位置作为基础位置
FVector InitialLocation;
// 每一帧更新Actor的位置和转向
void ItemMovement(float DeltaSeconds);
我们还需要用到帧更新函数
cpp
virtual void Tick(float DeltaSeconds) override;
在事件开始时,我们将掉落物的默认位置和旋转保存,并设置计算后的属性,我们需要在后续使用它更新Actor
cpp
void ARPGEffectActor::BeginPlay()
{
Super::BeginPlay();
//设置初始位置
InitialLocation = GetActorLocation();
CalculatedLocation = InitialLocation;
CalculatedRotation = GetActorRotation();
}
我们需要在帧更新了去保存当前效果的执行时间,并调用更新函数
cpp
void ARPGEffectActor::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
//更新当前Actor的存在时间
RunningTime += DeltaSeconds;
ItemMovement(DeltaSeconds);
}
在更新函数里,我们根据变量判断是否需要更新,来对转向和位置更新,这里需要提到的是,位置更新用到了正弦三角函数进行更新
cpp
void ARPGEffectActor::ItemMovement(float DeltaSeconds)
{
//更新转向
if(bRotates)
{
const FRotator DeltaRotation(0.f, DeltaSeconds * RotationRate, 0.f);
CalculatedRotation = UKismetMathLibrary::ComposeRotators(CalculatedRotation, DeltaRotation);
}
//更新位置
if(bSinusoidalMovement)
{
const float Sine = SineAmplitude * FMath::Sin(RunningTime * SinePeriod * 6.28318f);
CalculatedLocation = InitialLocation + FVector(0.f, 0.f, Sine);
}
}
你会发现上面的变量无法在蓝图面板直接设置,那么如何将其设置为true呢,我们通用函数将其设置为true
cpp
void ARPGEffectActor::StartSinusoidalMovement()
{
bSinusoidalMovement = true;
InitialLocation = GetActorLocation();
CalculatedLocation = InitialLocation;
}
void ARPGEffectActor::StartRotation()
{
bRotates = true;
CalculatedRotation = GetActorRotation();
}
后续效果我们需要在蓝图里实现,所以,我们创建一个拾取的基类,然后将所有可掉落物都继承此蓝图,没必要在每个蓝图里实现一遍
我们在拾取物基类里,创建一个时间轴,来实现掉落物的从无到有的效果,并通过时间轴的更新实现一些位置更新,和缩放效果。在时间轴播放完成后,我们调用开始旋转和开始移动的默认效果。
时间轴里,我们增加了两个轨道,用于分别更新位置和缩放使用
在帧更新里,我们通过计算后的位置和旋转更新Actor即可
以下是拾取物的表现效果
添加音效
最后一个功能,我们在Actor里添加一些音效,来实现一些点缀效果。
首先在Actor增加一个变量,设置音效基础类型
然后找到对应的音效
设置给变量
在更新位置和缩放后(我折叠成了一个函数),我们通过修改位置的值,判断如果大于0.3时,执行一次模拟触碰到地面的音效
然后在事件开始时增加一个生成的音效
在销毁时,播放一个拾取的音效。