In-depth UE5 - GameFeatures architecture (4) state machine

introduction

Earlier I talked about the process of initializing the GF framework at startup, and also mentioned that the GF state machine is the core process and concept for managing the GF loading process. This article will explain this module in detail. Regarding how a GF plug-in is loaded and activated, the key to understanding GF is to understand the state machine of the GF plug-in. I hope everyone will sit down and listen carefully.

Big picture concept

Speaking of status, for each GF, there are only four GF statuses that we can use during use: Installed, Registered, Loaded, and Active. These four states can be converted in both directions for loading and unloading. I know that people may still be confused about the differences between these four states, and they can't figure out the difference between Installed and Loaded. Don’t worry, it will be explained later.

Continuing to talk about the transition between states, we should also note that the switching process of GF state is a two-way pipeline. It can go in the direction of loading and activation, which is represented by the black arrow in the figure below; it can also go in the direction of failure and unloading. , represented by the red line on the figure. The text on the arrow is actually the GF loading and unloading API provided in each GFS class. The rounded rectangle in the middle of the two-way arrow represents the state. The green state is what we can see, but in fact there are many transition states inside. The concept of transition states will also be explained later. It is worth noting that the UE5 preview version adds a Terminal state, which can release the memory state of the entire plug-in.

State machine creation

Looking back at the previous code, in the last step of LoadBuiltInGameFeaturePlugin, GFS will create a UGameFeaturePluginStateMachine object for each GF to manage internal GF state switching.

void UGameFeaturesSubsystem::LoadBuiltInGameFeaturePlugin(const TSharedRef<IPlugin>& Plugin, FBuiltInPluginAdditionalFilters AdditionalFilter)
{
    //...省略其他代码
    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));//更新到目标的初始状态
    }

    //...省略其他代码
}

Of course, the process of creating a state machine must also go through more detailed steps. The last step, InitStateMachine, is actually to create a state object for each element in the AllStates array. Friends who are interested can take a look at the code. It is relatively simple and will not be described in detail.

UGameFeaturePluginStateMachine* UGameFeaturesSubsystem::GetGameFeaturePluginStateMachine(const FString& PluginURL, bool bCreateIfItDoesntExist)
{
    UGameFeaturePluginStateMachine** ExistingStateMachine = GameFeaturePluginStateMachines.Find(PluginURL);
    if (ExistingStateMachine)
    {
        return *ExistingStateMachine;
    }

    if (!bCreateIfItDoesntExist)
    {
        return nullptr;
    }

    UGameFeaturePluginStateMachine* NewStateMachine = NewObject<UGameFeaturePluginStateMachine>(this);
    GameFeaturePluginStateMachines.Add(PluginURL, NewStateMachine); //GFS里保存了所有GF插件的每个状态机信息,这样你就能随时切换任一个了。
    NewStateMachine->InitStateMachine(PluginURL, FGameFeaturePluginRequestStateMachineDependencies::CreateUObject(this, &ThisClass::HandleRequestPluginDependencyStateMachines)); //初级化状态机

    return NewStateMachine;
}

The actual GF loading steps are performed by state objects in the three steps of BeginState, UpdateState, and EndState. There are more than a dozen internal states of the GF plug-in, but only the four in the picture are exposed to the outside world.

Each plugin state inherits from the following base classes. When you first switch to this state, you call BeginState to initialize the information inside the state. When this state is active, you call UpdateState to perform business operations so that you can switch to the next state. When you switch from this state, you call EndState to end it. Clean up your own internal information. At the same time, the virtual function CanBeDestinationState is also used to specify whether a state can be the target state. The so-called target state means that the UpdateState of this state will not automatically switch to the next state, that is, it is not an intermediate transition state. The target state will only be triggered to migrate to the next target state after the GFS state switching API is actively called.

struct FGameFeaturePluginState
{
    //...
    /** Called when this state becomes the active state */
    virtual void BeginState() {}

    /** Process the state's logic to decide if there should be a state transition. */
    virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) {}

    /** Called when this state is no longer the active state */
    virtual void EndState() {}

    /** Returns true if this state is allowed to be a destination (i.e. is not a transition state like 'Downloading') */
    virtual bool CanBeDestinationState() const { return false; }
};

State machine update process

After each GFS state API call, it will finally call UGameFeaturePluginStateMachine::SetDestinationStatethis function to specify the target state, and then call UpdateStateMachine to update the entire state machine to run the flow to the specified state.

void UGameFeaturePluginStateMachine::SetDestinationState(EGameFeaturePluginState InDestinationState, FGameFeatureStateTransitionComplete OnFeatureStateTransitionComplete)
{
    check(IsValidDestinationState(InDestinationState));
    StateProperties.DestinationState = InDestinationState;
    StateProperties.OnFeatureStateTransitionComplete = OnFeatureStateTransitionComplete;
    UpdateStateMachine();
}

void UGameFeaturePluginStateMachine::UpdateStateMachine()
{
    UE::GameFeatures::FResult TransitionResult(MakeValue());
    bool bKeepProcessing = false;
    EGameFeaturePluginState CurrentState = GetCurrentState();   //获取当前状态
    do
    {
        bKeepProcessing = false;

        FGameFeaturePluginStateStatus StateStatus;
        AllStates[(int32)CurrentState]->UpdateState(StateStatus);   //当前状态的更新

        TransitionResult = StateStatus.TransitionResult;
        if (StateStatus.TransitionToState != EGameFeaturePluginState::Uninitialized) //需要切换到下一个状态
        {
            AllStates[(int32)CurrentState]->EndState(); //先退出当前状态
            CurrentStateInfo = FGameFeaturePluginStateInfo(StateStatus.TransitionToState);
            CurrentState = StateStatus.TransitionToState; //指定下个状态为当前状态

            AllStates[(int32)CurrentState]->BeginState(); //开始当前状态
            OnStateChangedEvent.Broadcast(this);
            bKeepProcessing = true;
        }

        if (!TransitionResult.HasValue())
        {
            StateProperties.DestinationState = CurrentState;
            break;
        }
    } while (bKeepProcessing);

    if (CurrentState == StateProperties.DestinationState)
    {
        StateProperties.OnFeatureStateTransitionComplete.ExecuteIfBound(this, TransitionResult);//触发状态切换的回调
        StateProperties.OnFeatureStateTransitionComplete.Unbind();
    }
}

In the code, I have omitted a lot of code that prints logs to check for errors, leaving only the bare bones.

1. Check the existence of

There are so many states, and if you want to sort them out, you have to trace them back to their original source. Now I will start to walk you through the loading process of GF state machine step by step. First of all, the initial state of the GF plug-in is Uninitialized, and it soon enters UnknownStatus, indicating that the status of the plug-in is not yet known. Then it enters the CheckingStatus stage and starts to see what protocol it is based on the PluginURL. If it is a file (for example: file:../../../../../Workspace/LearnGF/Plugins/GameFeatures/MyFeature/MyFeature.uplugin), check the local disk path to see if the file exists. If it's the web, you have to try to download it first. Web protocol support may not be improved until the official version of UE5. The main purpose of this stage is to check whether the uplugin file exists. Generally, GF plug-ins are already local and can pass detection relatively quickly. Each state logic can be viewed in the UpdateState method in "GameFeaturePluginStateMachine.cpp".

Since my last speech at UOD2021, two new states have been added to the UE5 preview version: One is Terminal , which means that the plug-in has been terminated and is triggered by the new method TerminateGameFeaturePlugin in UGameFeaturesSubsystem. This is also a target state, which will eventually cause the state machine associated with the plug-in to be released. Therefore, if you really want to no longer need to activate the plug-in, you can terminate it. Of course, after termination, you can still continue to reload to load the trigger.

void UGameFeaturesSubsystem::TerminateGameFeaturePlugin(const FString& PluginURL, const FGameFeaturePluginUninstallComplete& CompleteDelegate)
{
    if (UGameFeaturePluginStateMachine* StateMachine = FindGameFeaturePluginStateMachine(PluginURL))
    {
        if (StateMachine->GetCurrentState() > EGameFeaturePluginState::Terminal)
        {   
            //设定目标终结状态
            StateMachine->SetDestinationState(EGameFeaturePluginState::Terminal, FGameFeatureStateTransitionComplete::CreateUObject(this, &ThisClass::TerminateGameFeaturePluginComplete, CompleteDelegate));
        }
        //....
    }
}
void UGameFeaturesSubsystem::TerminateGameFeaturePluginComplete(UGameFeaturePluginStateMachine* Machine, const UE::GameFeatures::FResult& Result, FGameFeaturePluginUninstallComplete CompleteDelegate)
{
    if (Result.HasValue())
    {
        GameFeaturePluginStateMachines.Remove(Machine->GetPluginURL()); //删除状态机
        Machine->MarkAsGarbage();
    }

    CompleteDelegate.ExecuteIfBound(Result);
}

The second is various Error states. Often after an error is encountered in the previous state detection or loading, it will be transferred to the error state and specify more specific error information. Of course, there are two ways to go from these Error states. One is to try again, return to the previous state and try again, maybe this time it will succeed; the other is to simply forget it, give up, and the world will be destroyed. At this time You can use Terminate to terminate this plug-in.

2. Load the CF C++ module

After finding the uplugin stage, the next stage is to start trying to load GF's C++ module. Although we only had the ContentOnly option in NewPlugin earlier, GF plug-ins can still contain C++ code like ordinary plug-ins, so you can implement some GF logic in C++.

The initial state of this stage is Installed, which I indicate in green, indicating that it is a target state and is different from the transition state. The target state means that you can stay in this state until you manually call the API to trigger the migration to the next state. For example, if you want to register or activate this plug-in, the Installed state will be converted to the next state. When unloading, go to the red line in the opposite direction. Then go down:

  • The Mounting phase will trigger the plug-in manager to explicitly load this module, so the dll will be loaded and StartupModule will be triggered. In the previous Unmounting phase, C++ was not unloaded, so ShutdownModule was not called. This means that once the C++ module is loaded, it resides in memory. But in the UE5 preview version, this step has been added, so Unmounting can now uninstall the plug-in dll.
  • WaitingForDependencies will load other plug-in modules that were previously relied on in uplugin. The recursive loading will wait for all other dependencies to be completed before entering the next stage. This is actually consistent with the ordinary plug-in loading strategy, so the GF plug-in essentially operates as a plug-in mechanism, but there are some special places.

3. Load GameFeatureData

After the C++ module is loaded, the next step is to register GF itself into GFS. The Registering step actually starts loading some GF plug-in configuration ini files. The key step is to load the GFD asset file and trigger the OnGameFeatureRegistering that defines the Action in it. Generally, there is no need to do anything in this callback because the Action has not been activated at this time. . But if you want to do some static pre-logic before the Action is activated, you can do it here. In addition, in GFD, you can actually add additional main asset types for each GF separately, and the Registering step will also be added.

After these are completed, the target state of Registered will be entered. Indicates that the engine already knows what the structure of this GF plug-in is and what the asset types are. The next step is to load them.

The most important step is to load the GFD asset during Registering, which will trigger a UGameFeaturesSubsystem::LoadGameFeatureData(BackupGameFeatureDataPath);call to complete the GFD loading.

4. Preload assets and configurations

The next stage is loading. The Loading phase will begin to load two things. One is the runtime ini of the plug-in (such as.../LearnGF/Saved/Config/WindowsEditor/MyFeature.ini). The other is to preload some assets. The asset list can be determined by the Policy object. Obtained according to the GFD file of each GF plugin, so we can also overload GFD to add the list of assets we want to preload. The Loaded status indicates that the GF configuration and preloaded assets have been loaded. The next step is to activate it. Note here that the Loaded state does not mean that loading all the assets in the GF plug-in into the memory at once is not that stupid. Let's recall that what has been loaded so far in the Loaded state is: C++ module dll, configuration ini file, GFD assets that define the Action list, and some custom assets to be preloaded. Other assets are loaded on demand during the activation phase based on the execution of the Action.

5. Activation takes effect

After loading is complete, we can activate this GF. The logic here is also very simple and clear. Activating will trigger OnGameFeatureActivating for each Action defined in GFD, and Deactivating will trigger OnGameFeatureDeactivating. Activation and deactivation are the times when an Action actually does its work. When activated, you can register some engine event callbacks, add components to Actors, or generate Actors in the scene, etc. Naturally, when deactivated, you can perform some cleaning work, remove event callbacks, delete previously added components, etc. The final status is Active, which means that the GF is functioning.

Overall process review

Finally, let's sort out the entire process. A GF plug-in will roughly go through these stages of loading. There are 4 target states in it. We can switch between these 4 target states according to our logical needs. GFS also provides a public API to allow you to switch. So you can switch dynamically at runtime based on your logic.

Status monitoring

In order to expand our needs, GFS also provides callback objects for status monitoring to facilitate our expansion needs. The usage is also very simple. Just customize a class to inherit from UGameFeatureStateChangeObserver, then overload the four methods, and then register the object to GFS through AddObserver. It is a very simple observer pattern.

Summarize

After the lengthy description of GF's state machine, I don't know if you still have the patience to read this. It doesn’t matter if you don’t have it. It will be useful later and you can study it again when you need to have a deeper understanding. I hope that the GF framework will have received many upgrades and iterations by then. When I was learning the GF framework myself, the process I wanted to understand most was how a GF plug-in was loaded to released, and how it was applied to other aspects of the system.

Guess you like

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