前言
- 輸入系統,輸入某個鍵,響應到GamePlay層做對應的事。例如 點選滑鼠,前進還是開槍之類,是如何響應的。這裡只說應用層邏輯,硬體層邏輯不講述。
詳解
1.問題來源
先看下面一個例子:跳躍的事件響應堆疊
從上述堆疊我們不難發現,疑惑點主要集中於 APlayerController::ProcessPlayerInput 和 UPlayerInput::ProcessInputStack.
(APlayerController::PlayerTick之前的堆疊可以忽略)
2.簡要分析
先檢視 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即可。
3.UPlayerInput::ProcessInputStack 解析
因為原始碼過大,為了不影響閱讀,下方給出的均是虛擬碼,對於一些次要的的特殊邏輯也拋除了。主要是圍繞一個普通按鍵的邏輯程式碼。
I.TArray<TPair<FKey, FKeyState*>> KeysWithEvents;
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,因為後面會說到,存在一個鍵按了,後面的按鍵是響應還是不響應,出於滿足這種需求的原因。
II.核心邏輯
下述虛擬碼中文是我給出的解釋,英文是原始碼註釋。
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被吃掉。
- 如果想實現只在某個UI中響應輸入,其他介面,或者PlayerController中的都不響應,可以使用bBlockInput搭配Priority實現。也就是對應UserWidget中的常見的
缺陷
- 不能自定義組合鍵。
- 對同一個Action註冊了多個事件,順序不能自定義。
- 同一個InputComponent的多個相同的鍵註冊的Action不能被吃掉。
- Unreal 中 ListenForInputAction 介面,每個UserWidget生成一個新的InputComponent,而玩家的PlayerController用的是一個InputComponent。有些浪費。