1. What is One File Per Actor?
World Partition is a new tool for large open-world map development in Unreal Engine 5, in traditional method, building a large open-world map requests for dividing the whole map into several sub-levels, then use level streaming system to load or unload them. This method causes issuse on streaming management tool, multiple users cooperation etc.
World Partition is an automatic data management and distance-based level streaming system that provides a complete solution for large world management. The system removes the previous need to divide large levels into sublevels by storing your world in a single persistent level separated into grid cells, and provides you with an automatic streaming system to load and unload those cells based on distance from a streaming source.
However, World Partition also bring up some new problems, in this post, wo focus on an issue about it: When multiple users are workding on a map, how to manage the source control?
As is known to us, a level is stored in a UPackage traditionally, but in World Partition, this is not a good idea. Some level designers may focus on the north of the map, while some are the south of the map, although there is no conflict on there jobs, but there do have confilicts on the source control. They just using the same UPackage file, which will eventually cause conflicts. To avoid that, sometimes the level designer may lock the file which slows down the development speed.
One File Per Actor (OFPA) reduces overlap between users by saving data for instances of Actors in external files, removing the need to save the main Level file when making changes to its Actors.
Note that OFPA only works in Editor, All Actors are embedded in their respective Level files when cooked.
To learn this seires of posts, you can create a new project in UE5.1.1, the default map is a large open world map with world partition enabled.
2. Enable OFPA
OFPA is an expenrimental feature by now(UE 5.1.1), it is disabled by default even if you are working with a map with world partition. Follow these steps to enable the OFPA:
- From the main menu, go to Edit > Editor Preferences.
- From the sidebar, select Experimental.
- Locate the Tools section and enable the check box for One File Per Actor.
In some cases, you may want to enable OFPA for one Actor at a time. To do this, search for the Packaging Mode option in the Details panel, and select External from the dropdown menu.
After selecting this option, you will be asked if you want to enable the feature for all the Actors in the Level. Select Yes to convert all the Actors in the Level to OFPA or No to only enable it for the selected Actor.
You will need to save the newly created external packages for the Actor and the Level. Afterward, you won’t need to save the Level again if you modify the Actor.
Enable OFPA by manually enabling this check box on every actor is boring, especially when your team are using PCG to generated the world, the second method is to enable One File Per Actor for use with the entire Level by enabling “Use External Actors” in World Setting:
3. Using OFPA for Source Control
To be honest, although I use Git while working for my company, PlasticSCM is my favourite source control solution, in this post, I shall use Plastic as an example. First, enable source control in Unreal Engine Editor, note that the OFPA only works correctly in the submission by the Uneal Engine Editor except Perforce for now.
I just added some StaticMeshActors to the world:
Submitting by source control software maybe weird to read, it decribes the internal implemenation of OFPA rather than the encapsulation form:
While working in your source control application, you will notice that external Actor file names are encoded. To address this issue, you can view and validate the contents of a changelist before you submit it using the View Changelist window.
Viewing the the editor of Unreal Engine provides a readable method for OFPA.
OFPA is still an experimental version, can’t wait for the future version.
4. Going a bit further
Let’s focusing on the implementation of the OFPA, first is FWorldPartitionActorSecInitData, which describes the init data for actors in World Partition, some of them are used for the OFPA:
struct FWorldPartitionActorDescInitData { UClass* NativeClass; FName PackageName; FSoftObjectPath ActorPath; TArray<uint8> SerializedData; };
However, these data may not be the final data of OFPA, FWorldPartitionActorDescInitData are used to init:
class ENGINE_API FWorldPartitionActorDesc { friend class AActor; friend class UWorldPartition; friend class FActorDescContainerCollection; friend struct FWorldPartitionHandleImpl; friend struct FWorldPartitionReferenceImpl; friend struct FWorldPartitionActorDescUtils; public: ... ... ... }
Get a package of an actor marked with external package mode is different with those normal package:
FSceneOutlinerTreeItemSCC::FSceneOutlinerTreeItemSCC(FSceneOutlinerTreeItemPtr InTreeItemPtr) { TreeItemPtr = InTreeItemPtr; if (TreeItemPtr.IsValid()) { if (FActorTreeItem* ActorItem = TreeItemPtr->CastTo<FActorTreeItem>()) { if (AActor* Actor = ActorItem->Actor.Get()) { if (Actor->IsPackageExternal()) { ExternalPackageName = USourceControlHelpers::PackageFilename(Actor->GetExternalPackage()); ExternalPackage = Actor->GetExternalPackage(); } ActorPackingModeChangedDelegateHandle = Actor->OnPackagingModeChanged.AddLambda([this](AActor* InActor, bool bExternal) { if (bExternal) { ExternalPackageName = USourceControlHelpers::PackageFilename(InActor->GetExternalPackage()); ExternalPackage = InActor->GetExternalPackage(); ConnectSourceControl(); } else { ExternalPackageName = FString(); ExternalPackage = nullptr; DisconnectSourceControl(); } }); } } else if (FActorFolderTreeItem* ActorFolderItem = TreeItemPtr->CastTo<FActorFolderTreeItem>()) { if (const UActorFolder* ActorFolder = ActorFolderItem->GetActorFolder()) { if (ActorFolder->IsPackageExternal()) { ExternalPackageName = USourceControlHelpers::PackageFilename(ActorFolder->GetExternalPackage()); ExternalPackage = ActorFolder->GetExternalPackage(); } } } else if (FActorDescTreeItem* ActorDescItem = TreeItemPtr->CastTo<FActorDescTreeItem>()) { if (const FWorldPartitionActorDesc* ActorDesc = ActorDescItem->ActorDescHandle.Get()) { ExternalPackageName = USourceControlHelpers::PackageFilename(ActorDesc->GetActorPackage().ToString()); ExternalPackage = FindPackage(nullptr, *ActorDesc->GetActorPackage().ToString()); } } if (!ExternalPackageName.IsEmpty()) { ConnectSourceControl(); } } }
Which finally calls:
UPackage* UObjectBase::GetExternalPackage() const { // if we have no outer, consider this a package, packages returns themselves as their external package if (OuterPrivate == nullptr) { return CastChecked<UPackage>((UObject*)(this)); } UPackage* ExternalPackage = nullptr; if ((GetFlags() & RF_HasExternalPackage) != 0) { ExternalPackage = GetObjectExternalPackageThreadSafe(this); // if the flag is set there should be an override set. ensure(ExternalPackage); } return ExternalPackage; }
After going over a few of the articles on your website, I truly appreciate your technique of writing a blog. I book-marked it to my bookmark webpage list and will be checking back soon. Please visit my website too and tell me what you think.