虚幻中实现本地双人的输入设备分别控制需要的Pawn

news/2025/3/16 6:18:22/文章来源:https://www.cnblogs.com/hellogiao1/p/18233815

想要实现双人成行游戏中的双输入设备(双输入设备指的是一个键鼠和一个手柄,或者两个手柄)分别控制玩家1和玩家2,同时可以动态插拔设备切换对应的Pawn的控制权;本文是对探索并实现此功能的一个解决思路记录。

1、前期准备和知识点梳理

1.1 本地多玩家 LocalPlayer

平常我们运行游戏的时候,引擎会默认创建一个LocalPlayer,不同于PlayerController概念,LocalPlayer更能代表的是当前客户端所存在的一个玩家,而当我们需要另一个玩家的时候,就可以通过CreateLocalPlayer蓝图静态函数,再创建一个LocalPlayer玩家,此时就会存在LocalPlayer0和LocalPlayer1两个玩家。

1.2 分屏 Splitscreen

打开项目设置,Maps&Modes分类下的本地多人LocalMultiplayer,有一个勾选项UseSplitscreen,勾选上即可分屏,而且这个分类下可为多玩家分屏布局进行相关配置;当要实现一些分屏布局的动态效果(比如双屏合为一屏、两个屏交换等),可以通过这些配置去追溯相关逻辑代码,然后写定制功能,简单来说呢就是修改布局大小和位置然后进行插值融合,后面功能会详细说明原理和接口配置,以供蓝图自定义调用。

1.3 输入 Input(Enhance Input)

在UE中当需要输入时,可以通过UE的增强输入EnhanceInput,配置相关按键映射,比如按键盘空格或手柄A键触发角色Jump起跳,若你创建的第三称角色项目,项目默认已经帮你配好小白人相关输入,可以对比这篇文章进行相关学习了解UE的增强输入用人话讲!虚幻引擎 UE5 增强输入系统(蓝图篇)-CSDN博客。

2、开始前的预期以及可能会遇到的问题

  • 默认运行游戏的时候会发现键盘和手柄都控制的是同一个角色,期望键盘控制角色1,手柄控制角色2
  • 插入第二个手柄设备的时候,发现按键没什么反应,没有达到预期我们希望的第二个手柄会自动控制第二个角色
  • 若希望能动态切换需要控制的Pawn,比如我当前控制的角色1,我希望能互相交换控制,我控制角色2,他控制角色1

3、方案一《通过增强输入配置不同InputAction去绑定Event》蓝图可实现

此方案可以在不动代码的情况上,纯蓝图下实现

3.1 当存在一个键盘和插入一个手柄分别控制(需要插入大量消息分发逻辑胶水)

举个例子,我们知道InputAction可以去绑定事件,同时对应多个按键映射,在虚幻项目的第三人称项目中是把键盘的空格键和手柄A键同时绑定了一个InputAction,这时候我们可以再创建一个InputAction,只绑定一个按键,这样就分开了两个按键和事件,这里值得注意的是,键盘和手柄的按键响应都会传到第一个控制器内,因此,需要去蓝图中写分发逻辑,键盘的Jump事件调用第一个角色,手柄的Jump调用第二个角色。

3.2 当插入两个手柄分别控制

上面例子是一个控制器集中收发消息然后写蓝图逻辑分发按键消息出去,此时当我们插入两个手柄的时候,发现触发手柄2的时候角色2没有任何动静,而且和上一个例子矛盾,下面这种情况我期望第一个手柄1控制角色1,第二个手柄2控制角色2,而不是键盘控制角色1,手柄1控制角色2;插入第二个手柄为啥没响应呢?难道是按键消息被吃了吗?没办法,这种情况只能通过去查源码断点,追溯一下,输入消息哪里被断掉了,通过第一个手柄的输入消息对比和输入流程的梳理(关于输入代码追溯逻辑这里先跳过,下文中会有详细记录),发现是没按键绑定委托回调,这里只需要在创建第二个LocalPlayer的时候,重新添加一个输入映射上下文(InputMappingContexts),如下图,会发现第二个手柄2能控制第二个角色2了; 而且不需要写蓝图逻辑去分发输入,多舒服,之前一个Controller中写输入逻辑分发,胶水代码一多就很容易写出Bug呢,现在分开,一套输入配置大家都可以共用。

4、方案二《通过代码》

4.1 当存在一个键盘和插入一个手柄分别控制(不需要写大量的分发逻辑胶水,只需要修改一个值即可)

这里其实只需要把第一个手柄的按键消息全部转发到第二个玩家中,就如第二个手柄的消息都到第二个玩家中一样,如何实现呢?

首先,我们通过代码断点了解到,所有的按键都会走到UGameViewportClient类中的bool UGameViewportClient::InputKey(const FInputKeyEventArgs& InEventArgs)函数中,而且会发现这个函数中的下面一段代码是实现分发按键消息给对应的Player玩家的,

bool UGameViewportClient::InputKey(const FInputKeyEventArgs& InEventArgs)
{//...略if (!bResult){ULocalPlayer* const TargetPlayer = GEngine->GetLocalPlayerFromInputDevice(this, EventArgs.InputDevice);if (TargetPlayer && TargetPlayer->PlayerController){bResult = TargetPlayer->PlayerController->InputKey(FInputKeyParams(EventArgs.Key, EventArgs.Event, static_cast<double>(EventArgs.AmountDepressed), EventArgs.IsGamepad(), EventArgs.InputDevice));}// A gameviewport is always considered to have responded to a mouse buttons to avoid throttlingif (!bResult && EventArgs.Key.IsMouseButton()){bResult = true;}}//...略
}

通过GEngine->GetLocalPlayerFromInputDevice()函数并传入参数EventArgs.InputDevice获取到对应的LocalPlayer,而这里的LocalPlayer就是上面提到的玩家,然后把按键消息传入到TargetPlayer->PlayerController->InputKey()中,就可以调用对应的按键响应,关于里面的输入响应等事件在初始化的时候输入组件都已经处理好了,我们不需要进去处理,只要在外部把这个参数EventArgs.InputDevice修改我们期望的值即可。

那如何拿到第二个LocalPlayer?对应的EventArgs.InputDevice如何修改呢?

我们发现键盘按键的InputDevice的ID为0,而第一个手柄的InputDevice的ID也为0,这也就是为什么键盘和第一个手柄控制的都是第一个玩家的原因,而第二个手柄的InputDevice的ID为1,因此只需要修改EventArgs.InputDevice.GetId()为1即可,然后在哪修改比较好呢?直接动引擎源码也不太好,写一个虚函数,子类继承然后修改逻辑是最好的,就算这样,也需要动引擎代码,不急,我们继续来看这个函数bool UGameViewportClient::InputKey(const FInputKeyEventArgs& InEventArgs),在这个逻辑中发现了一个函数RemapControllerInput(EventArgs);

bool UGameViewportClient::InputKey(const FInputKeyEventArgs& InEventArgs)
{FInputKeyEventArgs EventArgs = InEventArgs;if (TryToggleFullscreenOnInputKey(EventArgs.Key, EventArgs.Event)){return true;}if (EventArgs.Key == EKeys::LeftMouseButton && EventArgs.Event == EInputEvent::IE_Pressed){GEngine->SetFlashIndicatorLatencyMarker(GFrameCounter);}RemapControllerInput(EventArgs);//..略
}

RemapControllerInput(EventArgs),看函数名“重新映射控制器输入”,也许就是我们想要的函数,同时他还是一个虚函数,给我们在子类中插入逻辑的一个机会,而且看这个函数的逻辑,

void UGameViewportClient::RemapControllerInput(FInputKeyEventArgs& InOutEventArgs)
{const int32 NumLocalPlayers = World ? World->GetGameInstance()->GetNumLocalPlayers() : 0;if (NumLocalPlayers > 1 && InOutEventArgs.Key.IsGamepadKey() && GetDefault<UGameMapsSettings>()->bOffsetPlayerGamepadIds){//...略// We still want to increment the controller ID in case there is any legacy code listening for itInOutEventArgs.ControllerId++;}
}

简单来说里面其实也是引擎自己做的一些输入逻辑的再次处理,处理当有两个LocalPlayer时,且是Gampad即手柄按键时,项目配置中的bOffsetPlayerGamepadIds为True,修改DesiredInputDeviceId,这里逻辑我们参照一下,然后继承一个子类重写这个函数;

同时,我们发现UGameViewportClient这个类是可以配置我们自己的子类,在项目设置中搜索GameViewportClient配置一下即可,在void UGameEngine::Init(IEngineLoop* InEngineLoop)中引擎初始化的时候会根据我们的配置动态生成我们的GameViewport子类;

最后来到这个问题:对应的EventArgs.InputDevice如何修改呢?

下面代码是我们自己写一个子类函数逻辑,我在下面注释中解释一下代码逻辑,只用看中文注释的解释

void UDemoGameViewportClient::RemapControllerInput(FInputKeyEventArgs& InOutKeyEvent)
{Super::RemapControllerInput(InOutKeyEvent);const int32 NumLocalPlayers = World ? World->GetGameInstance()->GetNumLocalPlayers() : 0;// 当有两个玩家的时,且输入时来之手柄if (NumLocalPlayers > 1 && InOutKeyEvent.Key.IsGamepadKey()){if (bDisableSwapGamepadDevice){return;}// 我们知道第一个手柄的ID为0if (InOutKeyEvent.InputDevice.GetId() == 0){// 修改ID为1InOutKeyEvent.InputDevice = FInputDeviceId::CreateFromInternalId(1);IPlatformInputDeviceMapper& PlatformInputDeviceMapper = IPlatformInputDeviceMapper::Get();// Determine the owning user for this input deviceFPlatformUserId OwningUser = PlatformInputDeviceMapper.GetUserForInputDevice(InOutKeyEvent.InputDevice);// 当只插入一个手柄的时候,ID为1的设备是不存在的,因此需要我们手动去创建一个,并映射到第二个LocalPlayer上,下次就不需要创建了if (!OwningUser.IsValid()){UE_LOG(LogTemp, Warning, TEXT("No second gamepad input device detected, attempting to generate a placeholder for the second device for switching."))OwningUser = FPlatformUserId::CreateFromInternalId(1);PlatformInputDeviceMapper.Internal_MapInputDeviceToUser(InOutKeyEvent.InputDevice, OwningUser, EInputDeviceConnectionState::Connected);}}else if (InOutKeyEvent.InputDevice.GetId() == 1){InOutKeyEvent.InputDevice = FInputDeviceId::CreateFromInternalId(0);}}
}

到这里,其实就已经可以实现键盘控制第一个玩家1,手柄1控制第二个玩家2,而且不需要动源码,只需要重写一个函数,最小化的修改,同时给一个开关bDisableSwapGamepadDevice,是否需要切换,不需要切换就走引擎以前的输入转发逻辑,大大自定义化;

3.2 当插入两个手柄分别交换控制,一个断开连接等

场景题

不想当美术的程序不是一个好策划:我希望 键盘控制角色1,第一个插入的手柄控制角色2,当然我肯定看懂了你写的代码逻辑,我自己都能蓝图写出来,没什么了不起,是实现了键盘和手柄的分开控制,如果啊,当插入第二个手柄,第二个手柄控制角色1,而不是控制角色2,因为我不希望手柄1切换控制的角色2;最后我希望第一个手柄断开连接的时候,键盘控制角色1,第二个手柄切换到控制角色2;

答:

细心的玩家大大会发现,,对比上文代码的最后一段,在我重写的void UDemoGameViewportClient::RemapControllerInput(FInputKeyEventArgs& InOutKeyEvent)函数里当InOutKeyEvent.InputDevice.GetId() == 1时,逻辑中把ID设为了0,这是因为我希望第二个控制器的消息应该发给第一个玩家,这样就实现了两个控制器的输入交换;

而关于手柄输入设备的连接和断开,是有一个委托通知的,进行绑定,监听消息,然后处理即可,如下代码解释

UDemoGameViewportClient::UDemoGameViewportClient()
{// 我在构造函数中绑定一个手柄设备的连接改变的回调函数const IPlatformInputDeviceMapper& PlatformInputMapper = IPlatformInputDeviceMapper::Get();PlatformInputMapper.GetOnInputDeviceConnectionChange().AddUObject(this, &UDemoGameViewportClient::HandleInputDeviceConnectionChange);CachedSplitscreenInfo = SplitscreenInfo;
}void UDemoGameViewportClient::HandleInputDeviceConnectionChange(EInputDeviceConnectionState NewConnectionState, FPlatformUserId PlatformUserId, FInputDeviceId InputDeviceId)
{// 当第一个手柄设备连接或者断开,我设置是否启用交换Device的IDif (InputDeviceId.GetId() == 0){// When the first gamepad input device is not in a connected state, it indicates the need to disable device switching.bDisableSwapGamepadDevice = (NewConnectionState != EInputDeviceConnectionState::Connected);}
}

总而言之,言而总之,It works.

5、最后

我在实现这个功能的当中,了解到UE源码的一些输入流程,这也正好查缺补漏;但是却在实现这个功能很久之后,我才迟迟写一个笔记,记录一下,当时很多遇到的有趣的新手问题,都比较模糊,就没记录下来,不过,却乘此机会,对照网上的源码文章,去学习了解UE的输入框架,才能对UE的输入系统有个大概理解,然后实现对应的输入功能,因此强烈推荐看看UE的输入系统的源码,这是我当时看的,我有机会还是要去复习一下,现在全没印象了,凎

UE 输入系统之:概览 及 WindowApplication - 知乎 (zhihu.com)

UE 输入系统之:PlayerController - 知乎 (zhihu.com)

UE 输入系统之:InputComponent - 知乎 (zhihu.com)

UE 输入系统之:PlayerInput - 知乎 (zhihu.com)

祝你好运。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/720531.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HiPPO: Recurrent Memory with Optimal Polynomial Projections

目录概Motivation代码Gu A., Dao T., Ermon S., Rudra A. and Re C. HiPPO: Recurrent memory with optimal polynomial projections. NIPS, 2021.概 看下最近很火的 Mamba 的前身. 本文其实主要介绍的是一个如何建模历史信息在正交基上的稀疏的变化情况.Motivation对于一个函数…

存储引擎及特点、约束条件、严格模式、基本字段类型(整型、浮点型、字符串、日期时间、枚举和集合)

【一】存储引擎在平常我们处理的文件格式有很多,并且针对不同的文件格式会有对应不同的存储方式和处理机制 针对不同的数据应该有对应不同的处理机制 存储引擎就是不同的处理机制。# 查看所有引擎 show engines;四种主要的存储引擎 (1)Innodb引擎是MySQL5.5版本之后的默认存…

sickos1.1-cms

sickos1.1-cms主机发现和nmap扫描 nmap -sn 192.68.56.0/24靶机ip:192.168.56.105 nmap -sT --min-rate 10000 192.168.56.105PORT STATE SERVICE 22/tcp open ssh 3128/tcp open squid-http 8080/tcp closed http-proxynmap -sT -sV -sC -O -p22,3128,8080 192.16…

自动化类级别前后置和函数级别前后置的区别

一、函数级别的前后置,格式如下: 二、类函数级别的前后置如下: 三、总结: 1、函数级别的用例执行一个用例时,都会执行一遍;类级别的前后置不管用例是多少个,只在执行用例时执行一次。 2、所以根据用例的需要,适当的选择是类级别的前后置还是函数级别的前置后。

Linux容器架构

1.Iaas:基础设施即服务 Infrastructure-as-a-Service Paas:平台即服务 Platform-as-a-Service Saas:软件即服务 Software-as-a-Service Caas:容器即服务 介于IAAS和PAAS IAAS,PAAS,SAAS这些服务,用于帮助人们更快实现目标(搭建环境,使用产品) 从左到右,人们需要管理与维护的地方…

内网穿透教程

内网穿透教程 本文介绍如何使用 FRP(Fast Reverse Proxy)工具实现内网穿透,包括配置 Azure 公网 IP、安装 Docker 和 FRP,以及在内网服务器上配置和运行 FRP 客户端。 一、配置公网 IP 1. 申请 Azure 公网 IP登录到 Azure 门户。 创建一个新的虚拟机实例,建议使用1G内存的…

CFAR检测

目标检测:1幅海上SAR图像和1幅近海光学图像,选择其中一幅检测出图像上的舰船(包括停靠码头)目标。 检测步骤图像裁剪:把原图裁剪成 448 * 640 的 patch,检测每个小 patch 中的舰船目标。读取图像:读取每个图像,并将其转换为灰度图。为了方便处理边缘区域,用补零的方式对…

内网穿透详细教程

内网穿透详细教程 本文介绍如何使用 FRP(Fast Reverse Proxy)工具实现内网穿透,包括配置 Azure 公网 IP、安装 Docker 和 FRP,以及在内网服务器上配置和运行 FRP 客户端。 一、配置公网 IP 1. 申请 Azure 公网 IP登录到 Azure 门户。 创建一个新的虚拟机实例,建议使用1G内…

兴达易控232自由转profinet网关接扫码枪配置及测试案例

232自由口转Profinet网关(XD-PNR100/300)的主要功能就是将具有RS232接口的设备(如扫码枪、打印机、传感器等)接入到Profinet网络中,从而实现了传统设备与现代化工业以太网之间的无缝通信和数据交换。兴达易控232自由口转profinet网关接扫码枪配置及测试案例 232自由口转Pr…

杭州出租车行驶轨迹数据空间时间可视化分析|附代码数据

原文链接:http://tecdat.cn/?p=7324 最近我们被客户要求撰写关于出租车的研究报告,包括一些图形和统计输出 城市化带来的道路拥堵、出行耗时长等交通问题给交管部门带来了巨大的挑战 ▼ 通过安装在出租车上的GPS设备,可以采集到大量的轨迹数据,从而帮助我们分析人们出行信…

【专题】2024客户端游戏市场营销发展报告合集PDF分享(附原数据表)

原文链接:https://tecdat.cn/?p=36402 原文出处:拓端数据部落公众号 报告合集显示,中国客户端游戏市场在2023年创新高,达到662.83亿元,表明精品化和跨端生态趋势对市场的推动作用。报告合集强调客户端游戏的独特优势,如精品内容、视听体验和操作反馈等,促进了市场稳定增…

BD202404 110串

百度之星一场,t4 题目链接: 对于这种连续状态限制的字符串方案数,首先考虑dp, 首先定义好每个状态方便转移,0状态是结尾为0,1状态是结尾1个连续1,2状态是结尾两个连续1,有以下关系if(s[i] == 1) {if(j > 0) dp[i][j][0] = (dp[i][j][0] + dp[i - 1][j - 1][0] + dp[…