• Unreal 输入系统 解析


    前言

    • 输入系统,输入某个键,响应到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。有些浪费。
  • 相关阅读:
    Ubuntu上搭建Watir-Webdriver与Cucumber环境
    使--no-ri --no-rdoc成为gem安装的默认选项
    Ruby require 路径问题
    【原创】LoadRunner Java Vuser脚本的配置和调试指南
    【原创】LoadRunner Java Vuser开发环境配置指南
    【原创】使用Nmon_Analyzer处理较大nmon文件的方法
    2014年,马上要上班啦,希望一切顺利
    关于.jar的文件在cmd中无法连接数据库的问题
    ios 的通知机制
    ios开发小技巧-用宏化简代码
  • 原文地址:https://www.cnblogs.com/u3ddjw/p/16149880.html
Copyright © 2020-2023  润新知