• Welcome to the world's largest Chinese hacker forum

    Welcome to the world's largest Chinese hacker forum, our forum registration is open! You can now register for technical communication with us, this is a free and open to the world of the BBS, we founded the purpose for the study of network security, please don't release business of black/grey, or on the BBS posts, to seek help hacker if violations, we will permanently frozen your IP and account, thank you for your cooperation. Hacker attack and defense cracking or network Security

    business please click here: Creation Security  From CNHACKTEAM

Recommended Posts

前言

输入系统,输入某个键,响应到游戏部分层做对应的事。例如点击鼠标,前进还是开枪之类,是如何响应的。这里只说应用层逻辑,硬件层逻辑不讲述。

详解

1.问题来源

先看下面一个例子:跳跃的事件响应堆栈

节点

从上述堆栈我们不难发现,疑惑点主要集中于一个播放控制器:进程播放输入和uplayer input :3360 processinputstack .

(播放器控制器:播放器勾选之前的堆栈可以忽略)

2.简要分析

先查看一个播放控制器:进程播放输入源码

void a player controller : processplayerinput(const float delta time,const bool bGamePaused)

{

静态TArrayUInputComponent *输入堆栈;

//必须在游戏线程上以非递归方式调用

check(IsInGameThread()!输入堆栈. num());

//自上而下处理堆栈中的所有输入组件

{

SCOPE _ CYCLE _ COUNTER(STAT _ PC _ BuildInputStack);

BuildInputStack(输入堆栈);

}

//处理所需的组件

{

SCOPE _ CYCLE _ COUNTER(STAT _ PC _ ProcessInputStack);

播放器input-ProcessInputStack(输入栈,DeltaTime,bgame暂停);

}

输入堆栈. reset();

}

查看上述BuildInputStack的源码也比较简单,这里不贴了,大概的意思是把当前PlayerPawn的输入组件组件和当前地图的输入组件和播放器控制器栈上的输入组件组件。总之,大概意思就是把当前世界的所有打开的输入组件全部获取。

传入到播放输入处理。

也就是说问题,只要弄明白uplayer input :3360 processinputstack即可。

3.UPlayerInput::ProcessInputStack 解析

因为源码过大,为了不影响阅读,下方给出的均是伪代码,对于一些次要的的特殊逻辑也抛除了。主要是围绕一个普通按键的逻辑代码。

I.TArrayTPairFKey, FKeyState* KeysWithEvents;

ConditionalBuildKeyMappings();

静态TArrayFDelegateDispatchDetails非轴委托;

静态TArrayFKey消费

静态TArrayFDelegateDispatchDetails找到了和弦;

静态TArrayTPairFKey,FKeyState * KeysWithEvents

静态TArrayTSharedPtrFInputActionBinding potential ations;

//将累加器中的数据复制到实数值

for (TMapFKey,fkeystate :迭代器It(key state map);它;它)

{

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。有些浪费。
Link to comment
Share on other sites