TopMark 发表于 2020-11-25 09:08

Unreal源码学习(二)用户输入

本篇文章主要明确从用户触发输入到引擎触发指定事件的主要逻辑。


为了跨平台,在虚幻中定义了一个通用的事件处理接口:
class FGenericApplicationMessageHandler
{
public:
      ...
      
        virtual bool OnKeyChar( const TCHAR Character, const bool IsRepeat )
        virtual bool OnKeyDown( const int32 KeyCode, const uint32 CharacterCode, const bool IsRepeat )
        virtual bool OnKeyUp( const int32 KeyCode, const uint32 CharacterCode, const bool IsRepeat )
        virtual void OnInputLanguageChanged()
        virtual bool OnMouseDown( const TSharedPtr< FGenericWindow >& Window, const EMouseButtons::Type Button )

      ...
};篇幅原因就不全贴出来了。
在这当中定义了键盘,鼠标,屏幕触摸,Gamepad等一系列的事件。我们以键盘输入为例:
在引擎初始化时会为我们创建 FSceneViewport 和 UGameViewportClient:
..void UGameEngine::Init(IEngineLoop* InEngineLoop)
{
        ...

        // Initialize the viewport client.
        UGameViewportClient* ViewportClient = NULL;
        if(GIsClient)
        {
                ViewportClient = NewObject<UGameViewportClient>(this, GameViewportClientClass);
                ViewportClient->Init(*GameInstance->GetWorldContext(), GameInstance);
                GameViewport = ViewportClient;
                GameInstance->GetWorldContext()->GameViewport = ViewportClient;
        }

        LastTimeLogsFlushed = FPlatformTime::Seconds();

        // Attach the viewport client to a new viewport.
        if(ViewportClient)
        {
                // This must be created before any gameplay code adds widgets
                bool bWindowAlreadyExists = GameViewportWindow.IsValid();
                if (!bWindowAlreadyExists)
                {
                        UE_LOG(LogEngine, Log, TEXT("GameWindow did not exist.Was created"));
                        GameViewportWindow = CreateGameWindow();
                }

                CreateGameViewport( ViewportClient );

                if( !bWindowAlreadyExists )
                {
                        SwitchGameWindowToUseGameViewport();
                }

                FString Error;
                if(ViewportClient->SetupInitialLocalPlayer(Error) == NULL)
                {
                        UE_LOG(LogEngine, Fatal,TEXT("%s"),*Error);
                }

                UGameViewportClient::OnViewportCreated().Broadcast();
        }

        UE_LOG(LogInit, Display, TEXT("Game Engine Initialized.") );

        // for IsInitialized()
        bIsInitialized = true;
}当我们按下键盘的某个按键,会触发 FSceneViewport 的 OnKeyDown 的代码,这里面会把我们输入事件经由 UGameViewportClient 和 PlayerController 的 InputKey 方法最后传递到UplayerInput::InputKey 方法中:
bool UPlayerInput::InputKey(FKey Key, EInputEvent Event, float AmountDepressed, bool bGamepad)
{
        // first event associated with this key, add it to the map
        FKeyState& KeyState = KeyStateMap.FindOrAdd(Key);
        UWorld* World = GetWorld();
        check(World);

        switch(Event)
        {
        case IE_Pressed:
        case IE_Repeat:
                KeyState.RawValueAccumulator.X = AmountDepressed;
                KeyState.EventAccumulator.Add(++EventCount);
                if (KeyState.bDownPrevious == false)
                {
                        // check for doubleclick
                        // note, a tripleclick will currently count as a 2nd double click.
                        const float WorldRealTimeSeconds = World->GetRealTimeSeconds();
                        if ((WorldRealTimeSeconds - KeyState.LastUpDownTransitionTime) < GetDefault<UInputSettings>()->DoubleClickTime)
                        {
                                KeyState.EventAccumulator.Add(++EventCount);
                        }

                        // just went down
                        KeyState.LastUpDownTransitionTime = WorldRealTimeSeconds;
                }
                break;
        case IE_Released:
                KeyState.RawValueAccumulator.X = 0.f;
                KeyState.EventAccumulator.Add(++EventCount);
                break;
        case IE_DoubleClick:
                KeyState.RawValueAccumulator.X = AmountDepressed;
                KeyState.EventAccumulator.Add(++EventCount);
                KeyState.EventAccumulator.Add(++EventCount);
                break;
        }
      ...
        return true;
}可以看到,这里面并不会直接调用具体事件,而是维护了一个 KeyStateMap 的状态集。
具体的触发事件的逻辑是在 PlayerInput::ProcessInputStack 中每帧处理:
void UPlayerInput::ProcessInputStack(const TArray<UInputComponent*>& InputComponentStack, const float DeltaTime, const bool bGamePaused)
{
        ...

        static TArray<FAxisDelegateDetails> AxisDelegates;
        static TArray<FVectorAxisDelegateDetails> VectorAxisDelegates;
        static TArray<FDelegateDispatchDetails> NonAxisDelegates;
        static TArray<FKey> KeysToConsume;
        static TArray<FDelegateDispatchDetails> FoundChords;
        static TArray<TPair<FKey, FKeyState*>> KeysWithEvents;
        static TArray<TSharedPtr<FInputActionBinding>> PotentialActions;

        // must be called non-recursively and on the game thread
        check(IsInGameThread() && !AxisDelegates.Num() && !VectorAxisDelegates.Num() && !NonAxisDelegates.Num() && !KeysToConsume.Num() && !FoundChords.Num() && !EventIndices.Num() && !KeysWithEvents.Num() && !PotentialActions.Num());

        // @todo, if input is coming in asynchronously, we should buffer anything that comes in during playerinput() and not include it
        // in this processing batch

        APlayerController* PlayerController = GetOuterAPlayerController();

        PlayerController->PreProcessInput(DeltaTime, bGamePaused);

        // copy data from accumulators to the real values
        for (TMap<FKey,FKeyState>::TIterator It(KeyStateMap); It; ++It)
        {
                bool bKeyHasEvents = false;
                FKeyState* const KeyState = &It.Value();
                const FKey& Key = It.Key();

                for (uint8 EventIndex = 0; EventIndex < IE_MAX; ++EventIndex)
                {
                        KeyState->EventCounts.Reset();
                        Exchange(KeyState->EventCounts, KeyState->EventAccumulator);

                        if (!bKeyHasEvents && KeyState->EventCounts.Num() > 0)
                        {
                                KeysWithEvents.Emplace(Key, KeyState);
                                bKeyHasEvents = true;
                        }
                }

                if ( (KeyState->SampleCountAccumulator > 0) || Key.ShouldUpdateAxisWithoutSamples() )
                {
                        if (KeyState->PairSampledAxes)
                        {
                                // Paired keys sample only the axes that have changed, leaving unaltered axes in their previous state
                                for (int32 Axis = 0; Axis < 3; ++Axis)
                                {
                                        if (KeyState->PairSampledAxes & (1 << Axis))
                                        {
                                                KeyState->RawValue = KeyState->RawValueAccumulator;
                                        }
                                }
                        }
                        else
                        {
                                // Unpaired keys just take the whole accumulator
                                KeyState->RawValue = KeyState->RawValueAccumulator;
                        }

                        // if we had no samples, we'll assume the state hasn't changed
                        // except for some axes, where no samples means the mouse stopped moving
                        if (KeyState->SampleCountAccumulator == 0)
                        {
                                KeyState->EventCounts.Add(++EventCount);
                                if (!bKeyHasEvents)
                                {
                                        KeysWithEvents.Emplace(Key, KeyState);
                                        bKeyHasEvents = true;
                                }
                        }
                }
                ...
        }
        EventCount = 0;

        struct FDelegateDispatchDetailsSorter
        {
                bool operator()( const FDelegateDispatchDetails& A, const FDelegateDispatchDetails& B ) const
                {
                        return (A.EventIndex == B.EventIndex ? A.FoundIndex < B.FoundIndex : A.EventIndex < B.EventIndex);
                }
        };

        int32 StackIndex = InputComponentStack.Num()-1;

        // Walk the stack, top to bottom
        for ( ; StackIndex >= 0; --StackIndex)
        {
                UInputComponent* const IC = InputComponentStack;
                if (IC)
                {
                        check(!KeysToConsume.Num() && !FoundChords.Num() && !EventIndices.Num() && !PotentialActions.Num());

                        IC->ConditionalBuildKeyMap(this);

                        for (const TPair<FKey,FKeyState*>& KeyWithEvent : KeysWithEvents)
                        {
                                if (!KeyWithEvent.Value->bConsumed)
                                {
                                        FGetActionsBoundToKey::Get(IC, this, KeyWithEvent.Key, PotentialActions);
                                }
                        }

                        for (const TSharedPtr<FInputActionBinding>& ActionBinding : PotentialActions)
                        {
                                GetChordsForAction(*ActionBinding.Get(), bGamePaused, FoundChords, KeysToConsume);
                        }

                        PotentialActions.Reset();

                        for (const FInputKeyBinding& KeyBinding : IC->KeyBindings)
                        {
                                GetChordForKey(KeyBinding, bGamePaused, FoundChords, KeysToConsume);
                        }

                        FoundChords.Sort(FDelegateDispatchDetailsSorter());

                        ...

                }
        }

       

        // Dispatch the delegates in the order they occurred
        NonAxisDelegates.Sort(FDelegateDispatchDetailsSorter());
        for (const FDelegateDispatchDetails& Details : NonAxisDelegates)
        {
                if (Details.ActionDelegate.IsBound())
                {
                        Details.ActionDelegate.Execute(Details.Chord.Key);
                }
                else if (Details.TouchDelegate.IsBound())
                {
                        Details.TouchDelegate.Execute(Details.FingerIndex, Details.TouchLocation);
                }
                else if (Details.GestureDelegate.IsBound())
                {
                        Details.GestureDelegate.Execute(Details.GestureValue);
                }
        }
        ...

        PlayerController->PostProcessInput(DeltaTime, bGamePaused);

        FinishProcessingPlayerInput();
        AxisDelegates.Reset();
        VectorAxisDelegates.Reset();
        NonAxisDelegates.Reset();
        TouchEventLocations.Reset();
        KeysWithEvents.Reset();
}参与的核心对象就是前文提到的 KeyStateMap 和 InputComponent。其中 InputComponent 保存着我们具体的输入配置。经过一系列的数据过滤操作,选出要触发的具体事件,在方法的最后统一触发。
总结

引擎自己定义了一套平台无关的系统信息回调接口。我们通过输入设备触发的输入事件会由此进入引擎并将触发状态维护在 PlayerInput 的 KeyStateMap 状态集中。引擎每帧依据 InputComponent 的输入绑定信息与 KeyStateMap 状态集进行数据对比,并触发具体的输入事件。
页: [1]
查看完整版本: Unreal源码学习(二)用户输入