UE/C++编程方略(14)外部信号驱动

UE/C++编程方略(14)外部信号驱动

相关代码仓库:gitee.com/xarray/unreal

在本节当中,我们将尝试通过外部设备的信号来控制场景角色的运动。很显然,来自鼠标或者键盘的信号已经被Unreal引擎本身所截获和管理了,不需要我们自己代劳,而通常的游戏手柄和触摸屏事件也基本都包含在Unreal引擎的实现代码中,同样不用再专门写C++代码去处理。因此,笔者决定考虑一个Unreal引擎中通常可能顾及不到的设备,也就是如图所示的MIDI键盘设备:

笔者所用的是KORG的nanoPAD2,相关设备的介绍以及驱动程序的下载地址请参考:

korg.com/us/support/dow

京东和淘宝等网站均可以购买到该设备,当然读者也可以选择自己心仪的其它有趣的外设并尝试与Unreal对接,当然如果它依然属于MIDI输入设备的范畴的话,那么依然可以考虑本文所述的方法。

我们需要找到一个第三方库来支持MIDI设备的查找,载入,以及输入数据读取。很幸运的是,在GITHUB等地方有足够多的先辈已经做了相关的工作。我们选择rtmidi这个较为知名和稳定的MIDI设备输入输出库作为本文功能实现的基本依赖库:

github.com/thestk/rtmid

有关如何在Unreal工程中加入自己的依赖库的问题,我们已经先后在第8节(使用tinyobj读取外部模型),第9-10节(使用OpenCV库实现摄像头画面显示和屏幕录制)中涉及到。而本文中,为了能够正确使用rtmidi库的内容,我们需要遵循下面的步骤:

(1)将RtMidi.h和RtMidi.cpp文件放置到工程源码目录下,并将它们拖放到工程管理器界面中。

(2)RtMidi在Windows下使用需要设置__WINDOWS_MM__预编译宏,我们需要在Build.cs文件中将这个宏提前添加到工程预编译步骤中。

(3)RtMidi用到了一些宏判断的方法,如果这些宏没有被定义的话,它们在Unreal工程的编译过程中可能被视为错误导致编译失败,因此需要在Build.cs文件中提前设置bEnableUndefinedIdentifierWarnings参数来关闭相关的警告判断。

由此得到class14.Build.cs文件中需要增加的内容如下所示:

下一步我们需要在Unreal编辑器中,通过内容侧滑菜单中右键点击来添加一个新的C++类,这个类继承自Pawn,名为MidiPawn。一些相关的前置内容已经在前文第11节中介绍过,这里不再赘述。

在头文件声明中,我们需要包含RtMidi.h头文件,然后将RtMidiIn对象_midiIn作为AMidiPawn类的成员变量使用。除此之外,我们还声明了一些必要的方法,让我们至少可以用鼠标来控制自己的观察角度(相关代码在第11节中已经出现过):

下一步,我们需要首先获取可用的Midi设备,即我们提前准备好的这个nanoPAD2键盘。声明一个新的SelectDevice()函数,然后在其中找到所有可用的设备,并选择第一个找到的设备,顺便在Log中显示它的名称:

而BeginPlay()函数中,我们将创建_midiIn对象并搜索和打开可用的设备,同时设置一个回调函数MidiCallback,它负责接收所有的Midi键盘输入信息,并且允许用户做进一步的响应处理;在EndPlay()函数中,只要删除这个_midiIn对象即可:

而回调函数MidiCallback的声明如下:

static void MidiCallback(double deltaT, std::vector<unsigned char>* msg, void* userData);

对应的实现代码中,暂时选择直接打印msg列表中的数据内容:

除此之外,我们还需要在GameMode中提前将它设置为默认的Pawn,这一步可以参考第11节中DefaultPawnClass的设置方法,本文不再赘述。此时如果直接运行编辑器和启动关卡,可以从Log中获取到Midi键盘是否正确加载的信息;随便在键盘上按一按,还可以看到Midi键盘输出的信息:

可以看到,输出的Midi数据大概由三位数字组成,按下不同的键,对应的数字也不相同。有关Midi数据格式的说明,可以参看下面的地址:

midi.org/specifications

而nanoPAD2也有自己的参数设置,如下图所示。最左侧的触摸板对应的指令,第一位为176(Control类型),第二位是1/2/16(对应水平和竖直两个方向的动作,以及停止动作),第三位是滑动距离,从0-127变化。而旁边的两排按钮,按下时第一位均为144(Note on),松开后为128(Note off);第二位指令为具体的按钮ID,从下至上,从左至右依次为36,37,……,50,51;第三位为按下的强度,从0-127变化(松开时恒定为64).如果右上角的功能键被按下,那么这些指令键值还会有所区别,不过本文只考虑默认情况(所有右上角的功能键没有按下,Scene键对应为1)。

知道了这个Midi键盘能够做的事情之后,我们在这个例子中将实现一个相对简单的控制功能,即:通过左侧的触摸板来控制场景相机向前或者向侧面运动,通过左侧第一列按键(对应键码36,37)来控制相机向上运动或者向下运动一段距离(取决于按下的强度)。其它的键暂时没有什么作用,后面的教程中如果还能想到一些用途,我们再回过头来使用它们不迟。

在头文件中,我们将添加一些必要的变量和函数声明如下:

在构造函数中,我们定义这些变量的初始值,令_xStart和_yStart均为-1,而_xDiff,_yDiff和_zDiff均为0。

而Midi的回调函数中,需要根据指令码的第一位来决定应该进入哪个对应的成员函数,我们设置触摸板对应指令码176,进入ControlMotion()函数;而第一列按键对应指令码144(按下)和128(松开),进入NoteMotion()函数。

在这两个函数中,我们不能直接执行和Unreal场景操作有关的功能。这是因为MidiCallback()函数是RtMidi内部在收到键盘指令时触发的,这个触发时机并不是系统的主进称,而是独立运行的线程中。Unreal不允许在另一个不受控的线程中执行有关场景对象和相机的操作,因此,我们将保存必要的数据到MidiPawn的成员变量中,然后在每帧执行的Tick()函数中去执行对应的场景对象操作功能。

执行的结果如下所示:

https://www.zhihu.com/video/1544104062739804160

发布于 2022-08-19 23:32