目录
作为一个工厂类模拟经营游戏,各个工厂之间的运输必不可少,本游戏采用的是按需进口的模式,工厂之间可以建立类似于传送带一样的直连道路,每个工厂根据自身当前缺少的所需物品,按照从近到远的顺序依次访问能够生产该物品的工厂,然后收到出口订单的工厂会发出包裹,沿着玩家建设的道路送达发出进口需求的工厂,玩家可以手动配置进出口清单,也就是工厂仓库中某类物品少于多少个就要进口,以及某类物品多于多少个才可以出口,效果如下:
一、进出口清单
玩家可以编辑每一个建筑的进出口清单实现对进出口的调控,即库存少于多少进口,多于多少出口。清单是一个数组,包括物品的种类和数量,同时还有自动和手动计算的功能切换,在自动模式下,清单中的数值即为生产时实际需求的原料数量,在改为手动模式后,对应物品的数量等于上次手动设置过的数量,清单数组中的数据结构如下:
USTRUCT(BlueprintType) struct FImportStardust { FImportStardust(const FName& StardustId, const int Quantity) : StardustId(StardustId), Quantity(Quantity) { } GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Import") FName StardustId{ "Empty" }; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Import") int Quantity{ 0 }; //是否手动更新数量 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Import") bool IsAuto{true}; //上一次手动设定的值 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Import") int LastManualSet{0}; FImportStardust()=default; };
设置清单中某类星尘的数量:
bool ABP_Asters::SetElementInImportingStardust(const int& Index, const int& Amount) { //检查索引是否合法 if(Index<0||Index>=ImportingStardust.Num()) { UE_LOG(LogTemp,Error,TEXT("SetElementInImportingStardust failed,invalid index:%d"),Index); return false; } ImportingStardust[Index].Quantity=Amount; //维护上一次手动设置的值 if(!ImportingStardust[Index].IsAuto) { ImportingStardust[Index].LastManualSet=Amount; } return true; }
设置某类星尘的计算是否手动:
void ABP_Asters::SetIsAutoInImportingStardust(const int& Index, const bool& IsAuto) { //检查索引是否合法 if(Index<0||Index>=ImportingStardust.Num()) { UE_LOG(LogTemp,Error,TEXT("SetIsAutoInImportingStardust failed,invalid index:%d"),Index); return; } ImportingStardust[Index].IsAuto=IsAuto; if(IsAuto) { ImportingStardust[Index].LastManualSet=ImportingStardust[Index].Quantity; //计算某类星尘的需求量 ImportingStardust[Index].Quantity=CalCulateReactionConsumption(ImportingStardust[Index].StardustId); } else { ImportingStardust[Index].Quantity=ImportingStardust[Index].LastManualSet; } }
二、路径计算
我们的物流是由进口需求引导的,所以寻路也是由某一个建筑出发,依次遍历连通的最近的建筑来尝试从其进口需要的物品,路径为从出口天体到该天体的路径
TArray<FStardustBasic> ATradingSystemActor::TriggerImport(const int& SourceAsterIndex, const TArray<FStardustBasic> ImportingStardust) {//输入进口源天体的索引和需求的星尘,返回有哪些进口需求未被满足 //检查输入索引是否合法 if(!DebugActor->AllAster.Find(SourceAsterIndex)) { UE_LOG(LogTemp,Error,TEXT("TriggerImport failed,invalid index:%d"),SourceAsterIndex); return TArray<FStardustBasic>(); } std::unordered_map<std::string,int>StardustNeed; for(const auto& it:ImportingStardust) { StardustNeed[TCHAR_TO_UTF8(*it.StardustId.ToString())]=it.Quantity; } //建立一个dijkstra算法使用的节点结构,包含点的ID和到起点距离 struct Node { Node(const int& ID, const long long& DIstance) : ID(ID), DIstance(DIstance) { } Node(const Node& Other):ID(Other.ID),DIstance(Other.DIstance){} int ID; long long DIstance; }; //重载优先队列排序规则 auto cmp{[](const TSharedPtr<Node>&a,const TSharedPtr<Node>& b){return a->DIstance>b->DIstance;}}; //储存当前待遍历的点的优先队列,按到起点路径长度从小到大排序 std::priority_queue<TSharedPtr<Node>,std::vector<TSharedPtr<Node>>,decltype(cmp)>Queue(cmp); //放入起点 Queue.push(MakeShared<Node>(SourceAsterIndex, 0)); //起点到每一个点的最短距离 std::map<int,long long>MinimumDistance; //每个点是否被处理完毕 std::map<int,bool>Done; //储存最短路径中每个点的父节点 std::map<int,int>Path; for(auto& it:DebugActor->AllAster) { //初始化最短距离为极大值 MinimumDistance[it.Key]=1e18; Done[it.Key]=false; } MinimumDistance[SourceAsterIndex]=0; while(!Queue.empty()) { auto Current{Queue.top()}; Queue.pop(); if(Done[Current->ID]) { continue; } if(Current->ID!=SourceAsterIndex) { if(!DebugActor->AllAster.Find(Current->ID)) { continue; } //当前遍历到的天体 auto FoundedAster{DebugActor->AllAster[Current->ID]}; TArray<FStardustBasic>PackgingStardust; //遍历出口清单 for(const auto&it:FoundedAster->GetExportingStardust()) { std::string IDString{TCHAR_TO_UTF8(*it.StardustId.ToString())}; if(StardustNeed.find(IDString)==StardustNeed.end()||!StardustNeed[IDString]) { continue; } //找到的天体可出口的星尘数量 int Available{FoundedAster->OutputInventory->CheckStardust(it.StardustId)-it.Quantity}; //实际出口的数量 if(int Transfered{std::max(0,std::min(StardustNeed[IDString],Available))}) { //维护当前包裹中的星尘和天体仓库中的星尘 PackgingStardust.Add(FStardustBasic(it.StardustId,Transfered)); FoundedAster->OutputInventory->RemoveStardust(it.StardustId,Transfered); StardustNeed[IDString]-=Transfered; if(!StardustNeed[IDString]) { StardustNeed.erase(IDString); } } } //该天体进行了出口 if(!PackgingStardust.IsEmpty()) { TArray<int>PassedAsters; int CurrentPosition{Current->ID}; //记录该天体到进口需求发出天体的路径 while (CurrentPosition!=SourceAsterIndex) { CurrentPosition=Path[CurrentPosition]; PassedAsters.Add(CurrentPosition); } TArray<int>PassedAsters2; //使路径从后往前为包裹要走过的天体 for(int i=PassedAsters.Num()-1;i>=0;i--) { PassedAsters2.Add(PassedAsters[i]); } //令目标天体发送包裹 SendPackage(FPackageInformation(Current->ID,PassedAsters2,PackgingStardust)); //所有进口需求都被满足,提前终止 if(StardustNeed.empty()) { return TArray<FStardustBasic>(); } } } //该天体处理完毕,防止被再次处理 Done[Current->ID]=true; //遍历该天体所有联通的天体 for(const auto&it:AsterGraph[Current->ID]) { if(Done[it->TerminalIndex]) continue; //这条路是最短路 if(MinimumDistance[it->TerminalIndex]>it->distance+Current->DIstance) { Path[it->TerminalIndex]=Current->ID; //更新最短路径 MinimumDistance[it->TerminalIndex]=it->distance+Current->DIstance; Queue.push(MakeShared<Node>(it->TerminalIndex,MinimumDistance[it->TerminalIndex])); } } } //返回未满足的进口需求 TArray<FStardustBasic> Result; if(!StardustNeed.empty()) { for(const auto&it:StardustNeed) { Result.Add(FStardustBasic(FName(UTF8_TO_TCHAR(it.first.c_str())),it.second)); } } return Result; }
重新寻路的逻辑与之类似,区别在于只是搜索确定的两点之间的最短路,不会发送包裹:
TArray<int> ATradingSystemActor::ReRoute(const int& Start, const int& end) { TArray<int>Result; struct Node { Node(const int ID, const int DIstance) : ID(ID), DIstance(DIstance) { } int ID; long long DIstance; }; auto cmp{[](const TSharedPtr<Node>&a,const TSharedPtr<Node>& b){return a->DIstance>b->DIstance;}}; std::priority_queue<TSharedPtr<Node>,std::vector<TSharedPtr<Node>>,decltype(cmp)>Queue(cmp); Queue.push(MakeShared<Node>(Start,0)); std::unordered_map<int,long long>MinimumDistance; std::unordered_map<int,bool>Done; std::map<int,int>Path; for(auto& it:DebugActor->AllAster) { MinimumDistance[it.Key]=1e18; Done[it.Key]=false; } MinimumDistance[0]=0; while(!Queue.empty()) { auto Current{Queue.top()}; Queue.pop(); if(Done[Current->ID]) { continue; } Done[Current->ID]=true; for(const auto&it:AsterGraph[Current->ID]) { //找到终点立刻终止运算 if(it->TerminalIndex==end) { TArray<int>PassedAsters; int CurrentPosition{Current->ID}; while (CurrentPosition!=Start) { CurrentPosition=Path[CurrentPosition]; PassedAsters.Add(CurrentPosition); } TArray<int>PassedAsters2; for(int i=PassedAsters.Num()-1;i>=0;i--) { PassedAsters2.Add(PassedAsters[i]); } return PassedAsters2; } if(Done[it->TerminalIndex]) continue; if(MinimumDistance[it->TerminalIndex]>it->distance+Current->DIstance) { Path[it->TerminalIndex]=Current->ID; MinimumDistance[it->TerminalIndex]=it->distance+Current->DIstance; Queue.push(MakeShared<Node>(it->TerminalIndex,MinimumDistance[it->TerminalIndex])); } } } //没找到路径返回的是空数组 return Result; }
三、包裹
1.包裹的数据结构
包裹的数据包裹发出该包裹的建筑的索引,计划要经过的所有建筑的索引,和携带的星尘
USTRUCT(BlueprintType) struct FPackageInformation { explicit FPackageInformation(const int SourceAsterIndex, const TArray<int>& ExpectedPath,const TArray<FStardustBasic>&ExpectedStardusts) : SourceAsterIndex(SourceAsterIndex), ExpectedPath(ExpectedPath),Stardusts(ExpectedStardusts) { } FPackageInformation() = default; GENERATED_BODY() //发出包裹的源天体 UPROPERTY(VisibleAnywhere,BlueprintReadWrite,Category="Package") int SourceAsterIndex{0}; //计划的路径,从后到前依次为即将走过的天体索引 UPROPERTY(VisibleAnywhere,BlueprintReadWrite,Category="Package") TArray<int> ExpectedPath; //包裹携带的星尘 UPROPERTY(VisibleAnywhere,BlueprintReadWrite,Category="Package") TArray<FStardustBasic>Stardusts; };
2.包裹在场景中的运动
每个包裹的路径是在其生成时就计算好的,数组中从后到前依次是其计划经过的建筑的索引,每到达一个建筑后将末尾的元素弹出,直到全部弹出即到达终点
bool APackageActor::AsterReached(const int& AsterIndex) { //检查输入的天体索引是否真实存在 if(!TradingSystem->DebugActor->AllAster.Find(AsterIndex)) { UE_LOG(LogTemp,Error,TEXT("AsterReached failed,invalid index:%d"),AsterIndex); return false; } //即将到达终点 if(PackgeInfo.ExpectedPath.Num()==1) { //送达包裹中的星尘 for(auto&it:PackgeInfo.Stardusts) { TradingSystem->DebugActor->AllAster[AsterIndex]->InputInventory->AddStardust(it.StardustId,it.Quantity); it.Quantity-=std::min(it.Quantity,TradingSystem->DebugActor->AllAster[AsterIndex]->InputInventory->CheckAddable(it.StardustId)); } //更新库存UI TradingSystem->DebugActor->AllAster[AsterIndex]->MCUpdateEvent(); TArray<FStardustBasic>LostStardust; //统计因终点库存已满而丢包的星尘 for(const auto&it:PackgeInfo.Stardusts) { if(it.Quantity) { LostStardust.Add(FStardustBasic(it.StardustId,it.Quantity)); UE_LOG(LogTemp,Error,TEXT("%d %s can't put in target aster"),it.Quantity,*it.StardustId.ToString()); } } return true; } //弹出路径中队尾的元素 PackgeInfo.ExpectedPath.Pop(); //更新包裹的路径 UpdatePathEvent(PackgeInfo.ExpectedPath); return false; }
我们使用时间轴和设置actor变换的方式来使包裹在场景中移动,也可以实现游戏暂停时停止移动和恢复移动
四、道路
1.道路的数据结构
在本游戏中,玩家可以建造多种道路,每种道路有不同的传输速度,最大建造距离和消耗,首先是数据表格的数据结构,这里和DataTable的互动可以看开发日志2(独立游戏《星尘异变》UE5 C++程序开发日志2——实现一个存储物品数据的c++类-CSDN博客)
USTRUCT(BlueprintType) struct FRoadDataTable:public FTableRowBase { FRoadDataTable() = default; FRoadDataTable(const FString& RoadName, ERoadType RoadType, int TransferSpeed, double MaximumLength) : RoadName(RoadName), RoadType(RoadType), TransferSpeed(TransferSpeed), MaximumLength(MaximumLength) { } GENERATED_USTRUCT_BODY() //道路名称 UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="RoadInfo") FString RoadName{"Empty"}; //道路种类 UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="RoadInfo") ERoadType RoadType{ERoadType::Empty}; //传输速度,单位距离/秒 UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="RoadInfo") int TransferSpeed{1}; //最大长度 UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="RoadInfo") double MaximumLength{1}; //道路建造消耗 UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="RoadInfo") TMap<FString,int>RoadConsumption; };
然后是每条建造出来的道路的数据结构,包括道路的起点和终点,用的是所连建筑物的全局索引,以及这条路建成的长度和表格数据。我们有一个数组维护着所有场上的建筑物的指针,通过这两个索引就可以访问到道路两端的建筑
USTRUCT(BlueprintType) struct FRoadInformation { friend bool operator<(const FRoadInformation& Lhs, const FRoadInformation& RHS) { return Lhs.distance > RHS.distance; } friend bool operator<=(const FRoadInformation& Lhs, const FRoadInformation& RHS) { return !(RHS < Lhs); } friend bool operator>(const FRoadInformation& Lhs, const FRoadInformation& RHS) { return RHS < Lhs; } friend bool operator>=(const FRoadInformation& Lhs, const FRoadInformation& RHS) { return !(Lhs < RHS); } friend bool operator==(const FRoadInformation& Lhs, const FRoadInformation& RHS) { return Lhs.TerminalIndex == RHS.TerminalIndex && Lhs.StartIndex==RHS.StartIndex; } friend bool operator!=(const FRoadInformation& Lhs, const FRoadInformation& RHS) { return !(Lhs == RHS); } FRoadInformation() = default; explicit FRoadInformation(const int& StartIndex,const int& TerminalIndex,const FVector&StartLocation,const FVector&EndLocation,const FRoadDataTable& Road) :StartIndex(StartIndex), TerminalIndex(TerminalIndex),distance(StartLocation.Distance(StartLocation,EndLocation)),RoadInfo(Road){ } GENERATED_USTRUCT_BODY() UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="Road") int StartIndex{0};//起点天体的索引 UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="Road") int TerminalIndex{0};//终点天体的索引 UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="Road") int distance{0};//两个天体之间的距离,取整 //道路的数据 UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="Road") FRoadDataTable RoadInfo; };
2.道路的建造
我们用一个红黑树来储存每个建筑都分别链接了哪些建筑
std::map<int,TArray<TSharedPtr<FRoadInformation>>> AsterGraph;//所有天体构成的图
在建造道路时传入起点和终点索引,以及道路类型的名称,将建造的道路存入上面存图的容器中
bool ATradingSystemActor::RoadBuilt(const int& Aster1, const int& Aster2,const FString& RoadName) { if(!DebugActor->IsValidLowLevel()) { UE_LOG(LogTemp,Error,TEXT("RoadBuild failed,invalid pointer:DebugActor")); return false; } //这两个建筑之间已存在道路,不可重复建造 if(AsterGraph[Aster1].FindByPredicate([Aster2](const TSharedPtr<FRoadInformation>& Road){return Road->TerminalIndex==Aster2;})) { return false; } //对应索引的天体不存在 if(!DebugActor->AllAster.Find(Aster1)||!DebugActor->AllAster.Find(Aster2)) { UE_LOG(LogTemp,Error,TEXT("RoadBuilt failed,invalid index :%d %d"),Aster1,Aster2); return false; } //数据表中存储的道路信息 auto RoadInfo{*Instance->RoadDataMap[TCHAR_TO_UTF8(*RoadName)]}; //存双向边 AsterGraph[Aster1].Add(MakeShared<FRoadInformation>(Aster1,Aster2,DebugActor->AllAster[Aster1]->AsterPosition,DebugActor->AllAster[Aster2]->AsterPosition,RoadInfo)); AsterGraph[Aster2].Add(MakeShared<FRoadInformation>(Aster2,Aster1,DebugActor->AllAster[Aster2]->AsterPosition,DebugActor->AllAster[Aster1]->AsterPosition,RoadInfo)); return true; }
3.道路的销毁
在销毁道路时,我们需要将存的图中的该道路删除,同时对于所有传输中的包裹,如果其原本的路径中包含这条道路,则重新计算路径,如果计算路径失败则将包裹送到下一个到达的建筑物处
void ATradingSystemActor::RoadDestructed(const int& Aster1, const int& Aster2) { if(!DebugActor->IsValidLowLevel()) { UE_LOG(LogTemp,Error,TEXT("RoadDestructed failed,invalid pointer:DebugActor")); return; } //两个方向都要删除 AsterGraph[Aster1].RemoveAll([Aster2](const TSharedPtr<FRoadInformation>& Road){return Road->TerminalIndex==Aster2;}); AsterGraph[Aster2].RemoveAll([Aster1](const TSharedPtr<FRoadInformation>& Road){return Road->TerminalIndex==Aster1;}); //遍历所有在路上的包裹 for(auto&it:TransferingPackage) { auto Temp{it->GetPackageInfo()}; //遍历其计划经过的天体 for(int i=Temp.ExpectedPath.Num()-1;i>=1;i--) { //是否经过该条道路 if(Temp.ExpectedPath[i]==Aster1&&Temp.ExpectedPath[i-1]==Aster2||Temp.ExpectedPath[i]==Aster2&&Temp.ExpectedPath[i-1]==Aster1) { //尝试重新计算路径 auto TempArray{ReRoute(Temp.ExpectedPath[Temp.ExpectedPath.Num()-1],Temp.ExpectedPath[0])}; //没有能到终点的道路了 if(TempArray.IsEmpty()) { UE_LOG(LogTemp,Error,TEXT("RerouteFailed")); //将终点改为下一个天体 TArray<int>Result; Result.Add(Temp.ExpectedPath[Temp.ExpectedPath.Num()-1]); Temp.ExpectedPath=Result; it->SetPackageInfo(Temp); it->UpdatePathEvent(Temp.ExpectedPath); break; } //应用新的路径 Temp.ExpectedPath=TempArray; it->SetPackageInfo(Temp); it->UpdatePathEvent(Temp.ExpectedPath); break; } } } }
4.某个有道路连接的建筑被删除
在有道路连接的建筑被删除后,所有路径中包含该建筑的包裹要重新寻路,如果不能到达终点,同样送到下一个建筑为止
void ABP_Asters::AsterDestructed() { //这里展示的仅是该函数中关于物流系统的部分 //删除以该天体为起点的道路 TradingSystem->AsterGraph.erase(AsterIndex); for(auto&it:TradingSystem->AsterGraph) { //删除以该天体为终点的道路 auto temp{AsterIndex}; it.second.RemoveAll([temp](const TSharedPtr<FRoadInformation>& Road){return Road->TerminalIndex==temp;}); } for(int i=0;i<TradingSystem->TransferingPackage.Num();i++) { auto it{TradingSystem->TransferingPackage[i]}; if(!IsValid(it)) { TradingSystem->TransferingPackage.RemoveAt(i); i--; continue; } auto Temp{it->GetPackageInfo()}; bool NeedReroute{false}; //计划路径中有该天体就需要重新寻路 for(auto& it2:Temp.ExpectedPath) { if(it2==AsterIndex) { NeedReroute=true; } } if(NeedReroute) { //下一个目的地就是该天体,直接删除 if(Temp.ExpectedPath.Num()==1) { it->Destroy(); continue; } //终点是该天体,那肯定找不到路了 if(Temp.ExpectedPath[0]==AsterIndex) { UE_LOG(LogTemp,Error,TEXT("Reroute failed")); TArray<int>Result; Result.Add(Temp.ExpectedPath[Temp.ExpectedPath.Num()-1]); Temp.ExpectedPath=Result; it->SetPackageInfo(Temp); it->UpdatePathEvent(Temp.ExpectedPath); continue; } //尝试重新寻路 auto TempArray{TradingSystem->ReRoute(Temp.ExpectedPath[Temp.ExpectedPath.Num()-1],Temp.ExpectedPath[0])}; //没找到合适的道路 if(TempArray.IsEmpty()) { UE_LOG(LogTemp,Error,TEXT("Reroute failed")); TArray<int>Result; Result.Add(Temp.ExpectedPath[Temp.ExpectedPath.Num()-1]); Temp.ExpectedPath=Result; it->SetPackageInfo(Temp); it->UpdatePathEvent(Temp.ExpectedPath); continue; } //应用新的路径 Temp.ExpectedPath=TempArray; it->SetPackageInfo(Temp); it->UpdatePathEvent(Temp.ExpectedPath); } } //蓝图实现的事件,因为道路的指针存在蓝图里,所以交给蓝图来删除对象 AsterDestructedEvent(this); }