In-depth UE5 - GameFeatures architecture (3) Initialization

introduction

The last basic usage article was actually a bit confusing. Of course, this is definitely not because I am lazy, but because the usage of GameFeatures is so simple. Well, it must be like this! My personal habit is to spend a lot of time trying to explain the concept and design motivation behind a thing, and then try to deduce how to use it correctly and what the best posture is. If the article only describes what the API is used for, without explaining the Why behind it, then it is only a superficial understanding. And I believe that only with a deep understanding of the mechanism and principles can we have enough confidence to use a module well. I hope to learn and encourage together.

Core idea

Before explaining the various mechanisms, it is still necessary to briefly and concisely sort out the various key categories and terminology abbreviations that you will encounter. First, to avoid ambiguity, and secondly, of course, to save trouble, abbreviations are more convenient.

  • GameFeature , abbreviated as GF , represents a GameFeature plug-in.
  • CoreGame , specifically adding Core, refers to the separation of the game's ontology Module and GameFeature, that is, the game's ontology without GF taking effect.
  • UGameFeatureData , abbreviated as GFD , is a pure data configuration asset of the game function, which is used to describe the actions to be performed by the GF.

  • UGameFeatureAction , a single action, abbreviated GFA . The engine has several built-in Actions, and we can also extend them ourselves.
  • UGameFeatureSubsystem , abbreviated as GFS , the management class of the GF framework, and the global API can be found here.
  • UGameFeaturePluginStateMachine , abbreviated as GFSM , each GF plug-in is associated with a state machine to manage its own loading and unloading logic.
  • UGameFrameworkComponentManager , abbreviated as GFCM , is a management class that supports the AddComponent Action function. It records which components are added to which Actors so that they can be removed when GF is uninstalled.

It doesn’t matter if you are confused about these concepts now, I will explain them in detail later. At this point just remember what each abbreviation stands for.

GameFeatures easy to understand

I know that at this point, some friends may still be a little vague about the operating mechanism of this framework. Here I drew a simple diagram. In simple terms, it means creating different GFs. Each GF has a GFD with the same name. Each GFD describes a list of actions to be performed by the GF. We can extend this Action to suit our project needs. Our operation is to configure various Actions in the GFD of each GF. When GF is activated, Action::OnGameFeatureActivating() will be executed; when GF is deactivated, Action::OnGameFeatureDeactivating() will be executed. Therefore, a typical GFA action occurs on the two callbacks OnGameFeatureActivating and OnGameFeatureDeactivating, just like BeginPlay and EndPlay have the same effect on Actor. As mentioned before, we can inherit Action to customize extensions. The most important one in GFA is AddComponents. Its importance accounts for about half of the entire GFA system, so it will take more chapters to explain later.

GameFeatures initialization

Every time I explain a new module, I always feel that it is a little confusing at the beginning and it is difficult to get started. But after a long illness, he became a doctor and gradually found some ways. I have said in previous articles that a system can be understood from the perspective of time and space, from the two dimensions of time and space. In terms of time, we care about what it looks like at the beginning, what steps it goes through, and what the final result is; in terms of space, we care about what data is defined, how the data is configured and modified, and where the data flows. If you can clarify the interaction and reaction process between the two, you can almost understand its structure and logical flow. Therefore, the same is true for GameFeatures. If you want to understand its mechanism, you must first understand how it is initialized step by step from scratch.

1. Initial initialization

The process of using the GameFeature framework actually involves several classes. First, the core management class of the framework is UGameFeaturesSubsystem, which is inherited from EngineSubsystem, which means that it is started along with the engine. It doesn't matter at runtime, but in the case of Editor, we have to pay attention to that the GF status managed internally by UGameFeaturesSubsystem follows the engine editor. Therefore, even if you stop PIE playback, the GF plug-in will still be loaded. Therefore, in practice, I would recommend that everyone manually activate or deactivate GF through the API in the game instead of using the UI buttons on the editor.

Of course, the first thing to start running is the call to the overloaded Subsystem's Initialize method.

void UGameFeaturesSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
    const FSoftClassPath& PolicyClassPath = GetDefault<UGameFeaturesSubsystemSettings>()->GameFeaturesManagerClassName;
    UClass* SingletonClass = nullptr;
    if (!PolicyClassPath.IsNull())
    {
        SingletonClass = LoadClass<UGameFeaturesProjectPolicies>(nullptr, *PolicyClassPath.ToString());
    }

    if (SingletonClass == nullptr)
    {
        SingletonClass = UDefaultGameFeaturesProjectPolicies::StaticClass();
    }

    GameSpecificPolicies = NewObject<UGameFeaturesProjectPolicies>(this, SingletonClass);//创建策略对象

    //注册UAssetManager回调,以便之后配置GFD的PrimaryAssetType
    UAssetManager::CallOrRegister_OnAssetManagerCreated(FSimpleMulticastDelegate::FDelegate::CreateUObject(this, &ThisClass::OnAssetManagerCreated));

    //注册控制台命令,有:istGameFeaturePlugins,LoadGameFeaturePlugin,DeactivateGameFeaturePlugin,UnloadGameFeaturePlugin
    IConsoleManager::Get().RegisterConsoleCommand(...);
}

Note: GF status still exists in editor mode, so it may persist across games. Remember to deactivate the status yourself.

2. Create a GF loading strategy

As mentioned in the code above, Subsystem will create a Policy policy object internally to determine which GFs should be loaded and which ones are disabled. It can also be used to decide whether certain plug-ins should be allowed to be loaded on the server and client. The engine has built-in implementation of a default policy object UDefaultGameFeaturesProjectPolicies, which loads all GF plug-ins by default. If we need to customize our own strategy, for example, some GF plug-ins are only for testing and need to be closed later, we can inherit and overload one of our own strategy objects, such as UMyGameFeaturesProjectPolicies. You can implement your own filters and judgment logic in it.

3. Configure GF loading strategy

The general application scenario is to flexibly define additional configuration items according to the needs of your own game, such as game versions, patch repairs, game activities, AB testing, etc. For example, assuming that version 2.0 of the game wants to disable a GF plug-in that was used in the previous version 1.0 for activities, we can inherit and define our own Policy object, and then implement our own filtering logic in the filter in Init, such as in the screenshot This is an example of specifying the version number based on the MyGameVersion key in uplugin, and then comparing it. What should be noted here is that you must first configure the Additional Plugin Metadata Keys in the project settings before the custom keys in the uplugin file can be identified and parsed into PluginDetails.AdditionalMetadata before subsequent judgments can be made. As for what keys to add, it depends on your own project needs.

Custom items can be added to the GF.uplugin file:

Get the value of the item in the code for judgment:

UCLASS()
class LEARNGF_API UMyGameFeaturesProjectPolicies : public UGameFeaturesProjectPolicies
{
    GENERATED_BODY()
public:
    //~UGameFeaturesProjectPolicies interface
    virtual void InitGameFeatureManager() override;
    virtual void GetGameFeatureLoadingMode(bool& bLoadClientData, bool& bLoadServerData) const override;

    virtual TArray<FPrimaryAssetId> GetPreloadAssetListForGameFeature(const UGameFeatureData* GameFeatureToLoad) const { return TArray<FPrimaryAssetId>(); }
    virtual bool IsPluginAllowed(const FString& PluginURL) const { return true; }
    //~End of UGameFeaturesProjectPolicies interface
};
//自定义的写法,和UDefaultGameFeaturesProjectPolicies的默认写法差不多
void UMyGameFeaturesProjectPolicies::InitGameFeatureManager()
{
    auto AdditionalFilter = [&](const FString& PluginFilename, const FGameFeaturePluginDetails& PluginDetails, FBuiltInGameFeaturePluginBehaviorOptions& OutOptions) -> bool
    {   //可以自己写判断逻辑
        if (const FString* myGameVersion = PluginDetails.AdditionalMetadata.Find(TEXT("MyGameVersion")))
        {
            float verison = FCString::Atof(**myGameVersion);
            if (verison > 2.0)
            {
                return true;
            }
        }
        return false;
    };

    UGameFeaturesSubsystem::Get().LoadBuiltInGameFeaturePlugins(AdditionalFilter);  //加载内建的所有可能为GF的插件
}

Let’s talk about some of the more important methods in the Policy object:

  • GetPreloadAssetListForGameFeature returns a list of assets to be preloaded by GF when entering Loading, which is convenient for preloading some assets, such as data configuration tables.
  • IsPluginAllowed, this function can be overloaded to further determine whether a plug-in is allowed to be loaded, and more detailed judgment can be made.

4. GFS settings-UGameFeaturesSubsystemSettings

Regarding the GF settings in the project settings, they are defined by the UGameFeaturesSubsystemSettings object. There are currently two keys in it that are more important:

  • GameFeaturesManagerClassName, you can customize the Policy subclass, which has been described above.
  • AdditionalPluginMetadataKeys, only the configuration items defined here will be parsed from uplugin.

Of course, there are also other keys that you can configure yourself, and you can even expand new configuration items by yourself.

5. GFD Asset Manager configuration

After creating the Policy object, Subsystem will begin to configure the AssetManager internally to identify the GFD object. GameFeature strongly relies on AM to identify loaded assets, so the main asset type of GFD must be added to the AssetManager configuration. This item is generally configured by default after creating the GF. But if you manually migrate the GF plug-in to a new project, you need to check it carefully.

Only when the above options are configured can the GFD asset be loaded correctly in the future:

If you delve deeper into the code, this is where the logic is reflected. However, please also note that LoadGameFeatureData is loaded when the subsequent GF state machine is Registered. The early appearance at this time is just to illustrate the connection with it later. And only the GF plug-in that passes the Filters of the Policy will have a chance to be loaded later.

TSharedPtr<FStreamableHandle> UGameFeaturesSubsystem::LoadGameFeatureData(const FString& GameFeatureToLoad)
{
    UAssetManager& LocalAssetManager = UAssetManager::Get();
    IAssetRegistry& LocalAssetRegistry = LocalAssetManager.GetAssetRegistry();

    FAssetData GameFeatureAssetData = LocalAssetRegistry.GetAssetByObjectPath(FName(*GameFeatureToLoad));
    if (GameFeatureAssetData.IsValid())
    {
        FPrimaryAssetId AssetId = GameFeatureAssetData.GetPrimaryAssetId();
        // Add the GameFeatureData itself to the primary asset list
        LocalAssetManager.RegisterSpecificPrimaryAsset(AssetId, GameFeatureAssetData);  //如果之前没配置,就会失败

        // LoadPrimaryAsset will return a null handle if the AssetID is already loaded. Check if there is an existing handle first.
        TSharedPtr<FStreamableHandle> ReturnHandle = LocalAssetManager.GetPrimaryAssetHandle(AssetId);
        if (ReturnHandle.IsValid())
        {
            return ReturnHandle;
        }
        else
        {
            return LocalAssetManager.LoadPrimaryAsset(AssetId);
        }
    }
    return nullptr;
}

6. Load and parse GF.uplugin

UDefaultGameFeaturesProjectPolicies::InitGameFeatureManager()The last step above is UGameFeaturesSubsystem::Get().LoadBuiltInGameFeaturePlugins(AdditionalFilter);to traverse all plug-ins in the current project's Plugins directory, and then try to load the GF plug-in:

void UGameFeaturesSubsystem::LoadBuiltInGameFeaturePlugins(FBuiltInPluginAdditionalFilters AdditionalFilter)
{
    TArray<TSharedRef<IPlugin>> EnabledPlugins = IPluginManager::Get().GetEnabledPlugins();
    for (const TSharedRef<IPlugin>& Plugin : EnabledPlugins)//遍历所有插件
    {
        LoadBuiltInGameFeaturePlugin(Plugin, AdditionalFilter);
    }
}

The LoadBuiltInGameFeaturePlugin is the most critical function for loading the GF plug-in:

void UGameFeaturesSubsystem::LoadBuiltInGameFeaturePlugin(const TSharedRef<IPlugin>& Plugin, FBuiltInPluginAdditionalFilters AdditionalFilter)
{
    const FString& PluginDescriptorFilename = Plugin->GetDescriptorFileName();
    if (!PluginDescriptorFilename.IsEmpty() && FPaths::ConvertRelativePathToFull(PluginDescriptorFilename).StartsWith(GetDefault<UGameFeaturesSubsystemSettings>()->BuiltInGameFeaturePluginsFolder) && FPaths::FileExists(PluginDescriptorFilename))//写死了GF必须待在“GameFeatures”目录下
    {
        const FString PluginURL = TEXT("file:") + PluginDescriptorFilename;
        if (GameSpecificPolicies->IsPluginAllowed(PluginURL))   //Policy是否允许该插件
        {
            FGameFeaturePluginDetails PluginDetails;
            if (GetGameFeaturePluginDetails(PluginDescriptorFilename, PluginDetails))//读取json的uplugin文件信息
            {
                FBuiltInGameFeaturePluginBehaviorOptions BehaviorOptions;
                bool bShouldProcess = AdditionalFilter(PluginDescriptorFilename, PluginDetails, BehaviorOptions);//进行过滤器判定
                if (bShouldProcess)
                {
                    UGameFeaturePluginStateMachine* StateMachine = GetGameFeaturePluginStateMachine(PluginURL, true);//创建状态机
                    const EBuiltInAutoState InitialAutoState = (BehaviorOptions.AutoStateOverride != EBuiltInAutoState::Invalid) ? BehaviorOptions.AutoStateOverride : PluginDetails.BuiltInAutoState;
                    const EGameFeaturePluginState DestinationState = ConvertInitialFeatureStateToTargetState(InitialAutoState);

                    if (StateMachine->GetCurrentState() >= DestinationState)
                    {
                        // If we're already at the destination or beyond, don't transition back
                        LoadGameFeaturePluginComplete(StateMachine, MakeValue());
                    }
                    else
                    {
                        StateMachine->SetDestinationState(DestinationState, FGameFeatureStateTransitionComplete::CreateUObject(this, &ThisClass::LoadGameFeaturePluginComplete));//更新到目标的初始状态
                    }

                    if (!GameFeaturePluginNameToPathMap.Contains(Plugin->GetName()))
                    {
                        GameFeaturePluginNameToPathMap.Add(Plugin->GetName(), PluginURL);
                    }
                }
            }
        }
    }
}

After passing the initial determination of the policy based only on the file name, you can proceed to the next step of loading and parsing the plug-in information. Only after parsing GF.uplugin, will there be enough key value information to determine whether the GF plug-in should be loaded or disabled. The identification of GF plug-ins starts from parsing the .uplugin file. We can manually edit this json file to describe the GF plug-in in detail. Here are a few keys worth mentioning:

  • BuiltInAutoState, the default initial state after this GF is recognized, there are 4 types in total. If you set it to Active, it means that this GF is activated by default. We will talk more about the status of GF later.
  • AdditionalMetadata, which has been discussed just now and is used for Policy identification.
  • PluginDependencies, we can still set up references to other plug-ins in the GF plug-in. Other plug-ins can be ordinary plug-ins or other GF plug-ins. These dependent plug-ins will be loaded recursively. This recursive loading mechanism is also consistent with the ordinary plug-in mechanism.
  • ExplicitlyLoaded=true, must be true, because the GF plug-in is loaded explicitly, unlike other plug-ins that may be enabled by default.
  • CanContainContent=true must be true, because the GF plug-in must have at least GFD assets after all.

This json file can be edited manually:

Initialization process overview

The logic of the entire GF framework initialization code is summarized as follows:

Thinking: Why does the Policy object need to be filtered twice by IsPluginAllowed and AdditionalFilter?

This problem is relatively simple, because the initial IsPluginAllowed determination is only based on the PluginURL, that is, the uplugin file path of the plug-in, and the information based on it is just a simple file name. If you can filter out the GF plug-ins you want to disable at this step, you can avoid subsequent loading and parsing of the GF.uplugin file and improve performance a little bit. Of course, for more detailed GF plug-in management strategies (such as game versions, event information, etc.), this information can only be stored in the custom key-value pairs of uplugin. You can only wait until the plug-in information is read before making the judgment of AdditionalFilter.

Summarize

This article mainly describes some initialization process steps when the GF framework is started, and the strategy for determining whether a GF plug-in should be loaded. We also noticed that the last step in LoadBuiltInGameFeaturePlugin is to create a state machine associated with each GF plug-in. The GF state machine is the core process and concept for managing the GF loading process. We will describe it in the next article.

Guess you like

Origin blog.csdn.net/ttod/article/details/133265931