目录
作为一个工厂类模拟经营游戏,各个工厂之间的运输必不可少,本游戏采用的是按需进口的模式,工厂之间可以建立类似于传送带一样的直连道路,每个工厂根据自身当前缺少的所需物品,按照从近到远的顺序依次访问能够生产该物品的工厂,然后收到出口订单的工厂会发出包裹,沿着玩家建设的道路送达发出进口需求的工厂,玩家可以手动配置进出口清单,也就是工厂仓库中某类物品少于多少个就要进口,以及某类物品多于多少个才可以出口,效果如下:
一、进出口清单
玩家可以编辑每一个建筑的进出口清单实现对进出口的调控,即库存少于多少进口,多于多少出口。清单是一个数组,包括物品的种类和数量,同时还有自动和手动计算的功能切换,在自动模式下,清单中的数值即为生产时实际需求的原料数量,在改为手动模式后,对应物品的数量等于上次手动设置过的数量,清单数组中的数据结构如下:
cpp
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;
};
设置清单中某类星尘的数量:
cpp
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;
}
设置某类星尘的计算是否手动:
cpp
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;
}
}
二、路径计算
我们的物流是由进口需求引导的,所以寻路也是由某一个建筑出发,依次遍历连通的最近的建筑来尝试从其进口需要的物品,路径为从出口天体到该天体的路径
cpp
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;
}
重新寻路的逻辑与之类似,区别在于只是搜索确定的两点之间的最短路,不会发送包裹:
cpp
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.包裹的数据结构
包裹的数据包裹发出该包裹的建筑的索引,计划要经过的所有建筑的索引,和携带的星尘
cpp
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.包裹在场景中的运动
每个包裹的路径是在其生成时就计算好的,数组中从后到前依次是其计划经过的建筑的索引,每到达一个建筑后将末尾的元素弹出,直到全部弹出即到达终点
cpp
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博客)
cpp
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;
};
然后是每条建造出来的道路的数据结构,包括道路的起点和终点,用的是所连建筑物的全局索引,以及这条路建成的长度和表格数据。我们有一个数组维护着所有场上的建筑物的指针,通过这两个索引就可以访问到道路两端的建筑
cpp
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.道路的建造
我们用一个红黑树来储存每个建筑都分别链接了哪些建筑
cpp
std::map<int,TArray<TSharedPtr<FRoadInformation>>> AsterGraph;//所有天体构成的图
在建造道路时传入起点和终点索引,以及道路类型的名称,将建造的道路存入上面存图的容器中
cpp
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.道路的销毁
在销毁道路时,我们需要将存的图中的该道路删除,同时对于所有传输中的包裹,如果其原本的路径中包含这条道路,则重新计算路径,如果计算路径失败则将包裹送到下一个到达的建筑物处
cpp
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.某个有道路连接的建筑被删除
在有道路连接的建筑被删除后,所有路径中包含该建筑的包裹要重新寻路,如果不能到达终点,同样送到下一个建筑为止
cpp
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);
}