先看下面一个例子:跳跃的事件响应堆栈
从上述堆栈我们不难发现,疑惑点主要集中于 APlayerController::ProcessPlayerInput 和 UPlayerInput::ProcessInputStack.
(APlayerController::PlayerTick之前的堆栈可以忽略)
先查看 APlayerController::ProcessPlayerInput 源码
void APlayerController::ProcessPlayerInput(const float DeltaTime, const bool bGamePaused) { static TArray<UInputComponent*> InputStack; // must be called non-recursively and on the game thread check(IsInGameThread() && !InputStack.Num()); // process all input components in the stack, top down { SCOPE_CYCLE_COUNTER(STAT_PC_BuildInputStack); BuildInputStack(InputStack); } // process the desired components { SCOPE_CYCLE_COUNTER(STAT_PC_ProcessInputStack); PlayerInput->ProcessInputStack(InputStack, DeltaTime, bGamePaused); } InputStack.Reset(); }
查看上述BuildInputStack的源码也比较简单,这里不贴了,大概的意思是把当前PlayerPawn的InputComponent组件和当前地图的InputComponent和PlayerController栈上的InputComponent组件。总之,大概意思就是把当前世界的所有打开的InputComponent全部获取。
传入到PlayerInput处理。
也就是说问题,只要弄明白UPlayerInput::ProcessInputStack即可。
因为源码过大,为了不影响阅读,下方给出的均是伪代码,对于一些次要的的特殊逻辑也抛除了。主要是围绕一个普通按键的逻辑代码。
ConditionalBuildKeyMappings(); static TArray<FDelegateDispatchDetails> NonAxisDelegates; static TArray<FKey> KeysToConsume; static TArray<FDelegateDispatchDetails> FoundChords; static TArray<TPair<FKey, FKeyState*>> KeysWithEvents; static TArray<TSharedPtr<FInputActionBinding>> PotentialActions; // 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[EventIndex].Reset(); Exchange(KeyState->EventCounts[EventIndex], KeyState->EventAccumulator[EventIndex]); if (!bKeyHasEvents && KeyState->EventCounts[EventIndex].Num() > 0) { KeysWithEvents.Emplace(Key, KeyState); bKeyHasEvents = true; } } }
从源码最上方查看,ConditionalBuildKeyMappings,这个比较简单,就是检测是否需要把ProjectSetting->Engine->Input中预先绑定的值初始化到PlayerInput.
然后主要是根据KeyStateMap的数据转换成KeysWithEvents。KeyStateMap 即会记录当前局内按下的键位的状态,KeysWithEvents就是当前哪些键需要处理。为什么KeyStateMap不是直接的一个Key的结构,而是Map,因为后面会说到,存在一个键按了,后面的按键是响应还是不响应,出于满足这种需求的原因。
下述伪代码中文是我给出的解释,英文是源码注释。
int32 StackIndex = InputComponentStack.Num()-1; for ( ; StackIndex >= 0; --StackIndex) { UInputComponent* const IC = InputComponentStack[StackIndex]; if (IC) { for (const TPair<FKey,FKeyState*>& KeyWithEvent : KeysWithEvents) { if (!KeyWithEvent.Value->bConsumed)//被Consume的按键,不会被响应 { FGetActionsBoundToKey::Get(IC, this, KeyWithEvent.Key, PotentialActions); //根据Key找出当前InputComponent中所需要响应的事件集合 PotentialActions(就是通过BindAction绑定的那些事件) } } for (const TSharedPtr<FInputActionBinding>& ActionBinding : PotentialActions) { GetChordsForAction(*ActionBinding.Get(), bGamePaused, FoundChords, KeysToConsume); //根据KeyState 检测该键是否是组合键,是否需要按Alt/Ctrl/Shift...,如果达成组合键则返回FoundChords //PS:这边代码写的有点烂,写死的组合键判断 } PotentialActions.Reset(); for (int32 ChordIndex=0; ChordIndex < FoundChords.Num(); ++ChordIndex) { const FDelegateDispatchDetails& FoundChord = FoundChords[ChordIndex]; bool bFireDelegate = true; // If this is a paired action (implements both pressed and released) then we ensure that only one chord is // handling the pairing if (FoundChord.SourceAction && FoundChord.SourceAction->IsPaired()) { FActionKeyDetails& KeyDetails = ActionKeyMap.FindChecked(FoundChord.SourceAction->GetActionName()); if (!KeyDetails.CapturingChord.Key.IsValid() || KeyDetails.CapturingChord == FoundChord.Chord || !IsPressed(KeyDetails.CapturingChord.Key)) { if (FoundChord.SourceAction->KeyEvent == IE_Pressed) { KeyDetails.CapturingChord = FoundChord.Chord; } else { KeyDetails.CapturingChord.Key = EKeys::Invalid; } } else { bFireDelegate = false; } } if (bFireDelegate && FoundChords[ChordIndex].ActionDelegate.IsBound()) { FoundChords[ChordIndex].FoundIndex = NonAxisDelegates.Num(); NonAxisDelegates.Add(FoundChords[ChordIndex]); } } //上述这段,就是判断是否是成对出现的事件,如果是成对出现的,只会被添加一条进NonAxisDelegates. if (IC->bBlockInput) { // stop traversing the stack, all input has been consumed by this InputComponent --StackIndex; KeysToConsume.Reset(); FoundChords.Reset(); break; } //上述这段,是判断是否bBlockInput,如果这个为true,则这个之后的InputComponent都会被吃掉,就是不会执行。 // we do this after finishing the whole component, so we don't consume a key while there might be more bindings to it for (int32 KeyIndex=0; KeyIndex<KeysToConsume.Num(); ++KeyIndex) { ConsumeKey(KeysToConsume[KeyIndex]); } //上述这段,最为重要,根据当前InputComponent中的KeysToConsume,对KeyStateMap中的键Consume掉,这样在之后的InputComponent的键,可以被吃掉,不会被执行。 KeysToConsume.Reset(); FoundChords.Reset(); } }
一个PlayerInput在Tick中不断执行,这个PlayerInput中存了一个包含当前世界所拥的InputComponent的栈。根据传来的当前响应的键,在这个栈中依次进行计算。根据Consume这个字段来判断之后的InputComonent中的相同的键是否被吃掉。每个InputComponent根据bBlockInput 这个字段来决定之后的InputComponent所有键被吃掉。这个一般应用搭配层级,低于这个层级的InputComponent被吃掉。