前言

  • 输入系统,输入某个键,响应到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。有些浪费。

Unreal 输入系统 解析的更多相关文章

  1. Android系统--输入系统(十)Reader线程_核心类及配置文件深入分析

    Android系统--输入系统(十)Reader线程_核心类及配置文件深入分析 0. 前言 个人认为该知识点阅读Android源代码会不仅容易走进死胡同,并且效果并不好,前脚看完后脚忘记,故进行总结, ...

  2. Android系统--输入系统(十四)Dispatcher线程情景分析_dispatch前处理

    Android系统--输入系统(十四)Dispatcher线程情景分析_dispatch前处理 1. 回顾 我们知道Android输入系统是Reader线程通过驱动程序得到上报的输入事件,还要经过处理 ...

  3. 推文《阿里凑单算法首次公开!基于Graph Embedding的打包购商品挖掘系统解析》笔记

    推文<阿里凑单算法首次公开!基于Graph Embedding的打包购商品挖掘系统解析>笔记 从17年5月份开始接触Graph Embedding,学术论文读了很多,但是一直不清楚这技术是 ...

  4. I/O输入系统

    I/O输入系统 计算机有两个主要任务:I/O操作与计算处理.在许多情况下,主要任务是I/O操作.而计算处理只是附带的. 操作系统在计算机I/O方面的作用是管理和控制I/O操作和I/O设备. 概述 对与 ...

  5. Android核心分析之十五Android输入系统之输入路径详解

       Android用户事件输入路径 1 输入路径的一般原理 按键,鼠标消息从收集到最终将发送到焦点窗口,要经历怎样的路径,是Android GWES设计方案中需要详细考虑的问题.按键,鼠标等用户消息 ...

  6. Android核心分析之十四Android GWES之输入系统

          Android输入系统 依照惯例,在研究Android输入系统之前给出输入系统的本质描述:从哲学的观点来看,输入系统就是解决从哪里来又将到哪里去问题.输入的本质上的工作就是收集用户输入信息 ...

  7. [Android] 输入系统(一)

    Android输入系统是人与机器交互最主要的手段.我们通过按键或者触碰屏幕,会先经由linux产生中断,进行统一的处理过后,转换成Android能识别的事件信息,然后Android的输入系统去获取事件 ...

  8. Android系统--输入系统(五)输入系统框架

    Android系统--输入系统(五)输入系统框架 1. Android设备使用场景: 假设一个Android平板,APP功能.系统功能(开机关机.调节音量).外接设备功能(键盘.触摸屏.USB外接键盘 ...

  9. Android系统--输入系统(六)模拟输入驱动程序

    Android系统--输入系统(六)模拟输入驱动程序 1. 回顾输入子系统 简单字符设备驱动:应用程序通过调用驱动所实现的函数使能硬件. 输入子系统:由于有多个应用程序使用输入子系统,故肯定使用的是早 ...

随机推荐

  1. 我对arguments.callee的理解

    基本理解: 你怎么看待一个函数呢?又如何看待一个函数对象呢?函数和Function之间的关系到底是什么?我觉得理解这些对理解arguments.callee有所帮助. 先说说auguments.cal ...

  2. Redis的Jedis操作(五)

    需要把jedis依赖的jar包添加到工程中.Maven工程中需要把jedis的坐标添加到依赖. 推荐添加到服务层. 1.连接单机版 第一步:创建一个Jedis对象.需要指定服务端的ip及端口. 第二步 ...

  3. List和 Map区别?

    一个是存储单列数据的集合,另一个是存储键和值这样的双列数据的集合,List中存储的数据是有顺序,并且允许重复:Map中存储的数据是没有顺序的,其键是不能重复的,它的值是可以有重复的.

  4. 在 Spring 框架中如何更有效地使用 JDBC?

    使用 SpringJDBC 框架,资源管理和错误处理的代价都会被减轻.所以开发者只 需写 statements 和 queries 从数据存取数据,JDBC 也可以在 Spring 框架提供 的模板类 ...

  5. java-与文件相关

    java.nio.file 表示non-blocking 非阻塞io(输入和输出) 一个 Path 对象表示一个文件或者目录的路径,是一个跨操作系统(OS)和文件系统的抽象 java.nio.file ...

  6. jsp技术之JSLT技术<c:if text="">判断

    JSLT的c:if标签 作用:用来进行判断的 语法: <c:if test="判断条件,使用EL表达式进行判断"> 如果判断为true,这里的内容会生效:如果为fals ...

  7. #define的3种用法详解

    1.#define 的作用 在C或C++语言源程序中允许用一个标识符来表示一个字符串,称为"宏".被定义为"宏"的标识符称为"宏名".在编译 ...

  8. Altium design16设计技巧

    第一栏:共有界面 1.在原理图和PCB都打开的情况下,选中原理图可以对应到PCB界面元件里面 第二栏:原理图界面 1.批量改变元件属性 选择某一元件-查找相似对象-将其要改变的内容设置为same-点击 ...

  9. Socket.io+Notification实现浏览器消息推送

    前言 socket.io: 包含对websocket的封装,可实现服务端和客户端之前的通信.详情见官网(虽然是英文文档,但还是通俗易懂).Notification: Html5新特性,用于浏览器的桌面 ...

  10. Android Studio连接SQLite数据库与SQLite Studio实时同步的实现

    最近学习用到了android开发连接数据库这一块,发现连接成功后,都要先访问安卓项目的数据库路径data/data/项目/databases,然后把对应的db文件拷出来,再在SQLite的可视化工具中 ...