热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Unreal输入系统解析

前言输入系统,输入某个键,响应到GamePlay层做对应的事。例如点击鼠标,前进还是开枪之类,是如何响应的。这里只说应用层逻辑,硬件层逻辑不讲述。详解1.问题来源先看下面一个例子:

前言



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


详解


1.问题来源

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

节点

从上述堆栈我们不难发现,疑惑点主要集中于 APlayerController::ProcessPlayerInput 和 UPlayerInput::ProcessInputStack.

(APlayerController::PlayerTick之前的堆栈可以忽略)


2.简要分析

先查看 APlayerController::ProcessPlayerInput 源码

void APlayerController::ProcessPlayerInput(const float DeltaTime, const bool bGamePaused)
{
static TArray 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> KeysWithEvents;

ConditionalBuildKeyMappings();
static TArray NonAxisDelegates;
static TArray KeysToConsume;
static TArray FoundChords;
static TArray> KeysWithEvents;
static TArray> PotentialActions;
// copy data from accumulators to the real values
for (TMap::TIterator It(KeyStateMap); It; ++It)
{
bool bKeyHasEvents = false;
FKeyState* const KeyState = &It.Value();
const FKey& Key = It.Key();
for (uint8 EventIndex = 0; 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& KeyWithEvent : KeysWithEvents)
{
if (!KeyWithEvent.Value->bConsumed)//被Consume的按键,不会被响应
{
FGetActionsBoundToKey::Get(IC, this, KeyWithEvent.Key, PotentialActions);
//根据Key找出当前InputComponent中所需要响应的事件集合 PotentialActions(就是通过BindAction绑定的那些事件)
}
}
for (const TSharedPtr& ActionBinding : PotentialActions)
{
GetChordsForAction(*ActionBinding.Get(), bGamePaused, FoundChords, KeysToConsume);
//根据KeyState 检测该键是否是组合键,是否需要按Alt/Ctrl/Shift...,如果达成组合键则返回FoundChords
//PS:这边代码写的有点烂,写死的组合键判断
}
PotentialActions.Reset();
for (int32 ChordIndex=0; 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 {
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。有些浪费。



推荐阅读
  • 本文讨论了如何使用IF函数从基于有限输入列表的有限输出列表中获取输出,并提出了是否有更快/更有效的执行代码的方法。作者希望了解是否有办法缩短代码,并从自我开发的角度来看是否有更好的方法。提供的代码可以按原样工作,但作者想知道是否有更好的方法来执行这样的任务。 ... [详细]
  • 如何自行分析定位SAP BSP错误
    The“BSPtag”Imentionedintheblogtitlemeansforexamplethetagchtmlb:configCelleratorbelowwhichi ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • 前景:当UI一个查询条件为多项选择,或录入多个条件的时候,比如查询所有名称里面包含以下动态条件,需要模糊查询里面每一项时比如是这样一个数组条件:newstring[]{兴业银行, ... [详细]
  • 开发笔记:Java是如何读取和写入浏览器Cookies的
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了Java是如何读取和写入浏览器Cookies的相关的知识,希望对你有一定的参考价值。首先我 ... [详细]
  • 本文介绍了如何使用Express App提供静态文件,同时提到了一些不需要使用的文件,如package.json和/.ssh/known_hosts,并解释了为什么app.get('*')无法捕获所有请求以及为什么app.use(express.static(__dirname))可能会提供不需要的文件。 ... [详细]
  • 本文介绍了在iOS开发中使用UITextField实现字符限制的方法,包括利用代理方法和使用BNTextField-Limit库的实现策略。通过这些方法,开发者可以方便地限制UITextField的字符个数和输入规则。 ... [详细]
  • VueCLI多页分目录打包的步骤记录
    本文介绍了使用VueCLI进行多页分目录打包的步骤,包括页面目录结构、安装依赖、获取Vue CLI需要的多页对象等内容。同时还提供了自定义不同模块页面标题的方法。 ... [详细]
  • baresip android编译、运行教程1语音通话
    本文介绍了如何在安卓平台上编译和运行baresip android,包括下载相关的sdk和ndk,修改ndk路径和输出目录,以及创建一个c++的安卓工程并将目录考到cpp下。详细步骤可参考给出的链接和文档。 ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • SpringMVC接收请求参数的方式总结
    本文总结了在SpringMVC开发中处理控制器参数的各种方式,包括处理使用@RequestParam注解的参数、MultipartFile类型参数和Simple类型参数的RequestParamMethodArgumentResolver,处理@RequestBody注解的参数的RequestResponseBodyMethodProcessor,以及PathVariableMapMethodArgumentResol等子类。 ... [详细]
author-avatar
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有