本游戏作为工厂游戏,任务系统的主要功能就是给玩家生产的目标和动力,也就是给玩家发布一个需要一定数量某星尘的订单,玩家提交需要的星尘后会获得奖励,游戏中实际的奖励机制略微有点复杂,这里直接简化为完成任务后就能获得随机的事件来给天体上buff,游戏内效果如下图:
目录
一、任务的数据结构
二、任务栏
1.任务栏数据结构
2.任务进度的更新
三、随机事件奖励
1.随机事件的结构
2.随机事件池的初始化
3.生成随机事件
一、任务的数据结构
先来看一下任务的结构体是怎么定义的:
USTRUCT(BlueprintType)
struct FQuest
{
GENERATED_BODY()
//任务名称
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Task")
FString QuestName{ "Empty" };
//任务类型
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Task")
ETaskTypes QuestType{ ETaskTypes::Empty };
//任务奖励
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Task")
int NovaValueReward{ 10 };
//任务描述
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Task")
FText QuestDescription;
//需求星尘
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Task/Stardust")
TArray<FStardustBasic>RequiredStardust;
//缺少的星尘
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Task/Stardust")
TArray<FStardustBasic>LackedStardust;
//任务当前是否满足完成条件
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Task")
bool QuestCanBeCompleted{ 0 };
//子任务
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Task")
TArray<FName>ChildTask;
FQuest() = default;
};
上面的结构中存了任务的名称,同时也是用来查找任务的索引
任务类型枚举决定了该任务完成条件,这里我们只介绍上面提到过的星尘订单这一类任务。
任务奖励在游戏中被称为星爆值,可以理解为玩家累积一定量的星爆值后就会获得上面提到过的随机事件奖励。
任务描述为玩家打开任务后看到的详细描述。
需求的星尘中存储的结构体中包含需求星尘的ID和需求星尘的数量,同时记录了当前要完成这个任务所缺少的星尘。
当缺少的星尘为空时,任务能否完成的bool值被置为真。
每个任务都会有一个或多个子任务,也就是完成该任务后,玩家就会获得该任务的子任务,使任务系统呈现一个树形结构。
二、任务栏
1.任务栏数据结构
玩家可能同时面对多个订单,所以需要一个任务栏容器存储当前玩家的任务,同样有增删查改功能,比库存系统实现起来要简单,就不多赘述了,看一下这些功能怎么定义的:
//返回包含该星尘的任务的索引
UFUNCTION(BlueprintCallable, Category = "QuestBoard")
TArray<int> GetQuestIndexFromStardustId(const FName StardustId)
//添加任务
UFUNCTION(BlueprintCallable,Category="QuestBoard")
bool AddQuest(const FQuest ExpectedQuest);
//删除任务
UFUNCTION(BlueprintCallable, Category = "QuestBoard")
bool RemoveQuest(const int Index);
//通过任务名称查找任务
UFUNCTION(BlueprintCallable, Category = "QuestBoard")
FQuest QueryQuestByName(const FString Name)const;
2.任务进度的更新
我们需要实现的是在玩家背包更新之后,更新所有相关任务的进度,在库存系统的蓝图中有这样一个事件:
该事件会在每个库存变更的函数后面调用,调用后首先会对我们在库存系统中定义的UI更新的动态多播委托进行广播,之后会计算任务进度,其中用到的主要函数就是下面这个计算并更新单个任务进度的函数:
bool UTaskComponent::CalculateLackStardust(UStarInventoryComponent* Inventory, const int Index)
{//计算任务进度,输入玩家背包库存对象,和要计算的任务的索引
if (Index < 0 || Index >= QuestArray.Num())
{//检查索引是否合法
UE_LOG(LogTemp, Error, TEXT("CalculateLackStardust at %d failed,invalid index"), Index);
return false;
}
if (!IsValid(Inventory)
{//检查对象是否有效
UE_LOG(LogTemp, Error, TEXT("CalculateLackStardust failed,invalid pointer:Inventory"));
return false;
}
bool flag = true;//返回该任务是否完成
for (int i = 0; i < QuestArray[Index]->TaskInformation.RequiredStardust.Num(); i++)
{//计算每个需要的星尘缺少的数量
int CurrentLack = std::max(0, QuestArray[Index]->TaskInformation.RequiredStardust[i].Quantity - Inventory->CheckStardust(QuestArray[Index]->TaskInformation.RequiredStardust[i].StardustId));//需要的减去库存中拥有的就是缺少的
QuestArray[Index]->TaskInformation.LackedStardust[i].Quantity = CurrentLack;
if (CurrentLack)
{//只要有一种星尘少了就不能完成任务
flag = false;
}
}
QuestArray[Index]->TaskInformation.QuestCanBeCompleted = flag;//更新任务是否能完成的标记
return flag;
}
三、随机事件奖励
1.随机事件的结构
USTRUCT(BlueprintType)
struct FNovaBurstEventInfo : public FTableRowBase
{
GENERATED_BODY()
// 事件名
UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="NovaBurstEventInfo")
FString EventName{ "Empty" };
// 事件类型
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NovaBurstEventInfo")
ENovaBurstEventType EventType{ ENovaBurstEventType::Empty };
//事件效果
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NovaBurstEventInfo")
ENovaBurstEventEffect Effect{ ENovaBurstEventEffect::Other };
// 事件等级
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NovaBurstEventInfo")
int EventLevel{ 0 };
// 默认数值
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NovaBurstEventInfo")
double DefaultValue{ 1 };
//是否添加到事件随机池里
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NovaBurstEventInfo")
bool Randomable{true};
FNovaBurstEventInfo() = default;
};
该结构也继承自FTableRowBase,所以也可以在数据表格中编辑。
事件名作为查找该事件的索引。
事件有多种类型,这里我们只介绍给天体添加buff的事件
效果枚举是用来决定事件具体该调用哪一个执行函数的
事件的等级决定了它在哪一个事件池中,在随机生成事件时,每个生成事件的等级是固定的,比如我们在玩家第一次星爆时,只会生成3个1级事件
默认数值是该事件的默认数值,表示事件的修正数值,例如加快天体的生产速度的事件,就会使产时间乘以这个默认值
是否可随机表示该事件是否会被添加到随机事件池,因为有一些事件是玩家到达某个阶段固定触发的,这类事件不用随机,这篇日志也不会细讲这类事件
2.随机事件池的初始化
和日志2中加载星尘数据类似,我们同样在游戏实例中加载并保存所有随机事件池中的事件信息:
void UAstromutateGameInstance::LoadPrimeEventDataTable()
{
UDataTable* EventTablePointer = LoadObject<UDataTable>(nullptr, UTF8_TO_TCHAR("DataTable'/Game/Data/NovaBurstEventInformation.NovaBurstEventInformation'"));//加载数据表
PrimeEventPool.Empty();
if (EventTablePointer == nullptr)
{//没找到表格
UE_LOG(LogTemp, Error, TEXT("Can't find PrimeEventInfo"));
return;
}
TArray<FName> RowNames {EventTablePointer->GetRowNames()};//将所有星尘的名字储存进数组里
for (const auto& it : RowNames)
{
FString ContextString;
FNovaBurstEventInfo* Row = EventTablePointer->FindRow<FNovaBurstEventInfo>(it, ContextString);//获取对应名字这一行的信息
if(!Row->Randomable)
continue;
PrimeEventPool.Add(*Row);
}
UE_LOG(LogTemp, Warning, TEXT("PrimeEventMap Loaded %d event"), PrimeEventPool.Num());//输出加载了多少个修正数据
}
3.生成随机事件
这里只展示天体加成的随机事件 ,输入参数为期望获得的事件的等级,例如[1,1,2]表示随机生成两个1级事件和一个2级事件,同时每个随机事件还会搭配生成一个随机的天体
TArray<FEventWithRandomAster> APrime::GetRandomEvent(TArray<int> ExpectedLevels)
{
srand(static_cast<unsigned int>(time(nullptr)));
std::unordered_map<int,TArray<FNovaBurstEventInfo>> CurrentEventPool;//事件等级到事件的映射
UAstromutateGameInstance::RandomShuffle(ExpectedLevels);//将期望数组洗牌
for(const auto&It:Instance->PrimeEventPool)
{//拷贝一份事件池
CurrentEventPool.emplace(It.first,TArray<FNovaBurstEventInfo>());
for(const auto&It2:*It.second)
{
CurrentEventPool[It.first].Add(It2);
}
}
auto Result{TArray<FEventWithRandomAster>()};//要返回的获取的随机事件
if (!Instance->IsValidLowLevel())
{
UE_LOG(LogTemp, Error, TEXT("GetRandomEvent failed,invalid pointer:Instance"));
return Result;
}
if (ExpectedLevels.IsEmpty())
{
UE_LOG(LogTemp, Error, TEXT("get random event failed, no expected levels,or game ended"));
return Result;
}
for(const auto&It:ExpectedLevels)
{
if(CurrentEventPool[It].IsEmpty())
{
UE_LOG(LogTemp,Error,TEXT("No valid event for level: %d"),It);
continue;
}
int RandomIndex{rand()%CurrentEventPool[It].Num()};
FName RandomAster{"Empty"};
if(CurrentEventPool[It][RandomIndex].EventType==ENovaBurstEventType::UpgradeAster)
RandomAster=GetRandomUnlockedAster();//获得随机的目标天体
Result.Add(FEventWithRandomAster(RandomAster,CurrentEventPool[It][RandomIndex]));
}
return Result;
}
上面用到的洗牌函数,可以确保数组中的每个数等概率的放到每个位置,因为是模板函数,所以对任意类型的数组都可使用:
template<typename T>
static void RandomShuffle(TArray<T>& Array)
{
if(Array.IsEmpty())
return;
for(int i=0;i<Array.Num();i++)
{
int j{rand()%Array.Num()};
std::swap(Array[i],Array[j]);
}
}
关于事件如何被执行,与游戏中的其他系统有关,没有太大的参考意义,这里就不过多赘述了
下一篇日志我将会介绍游戏中的物流系统是如何实现的