最近,我制作了一个名为“Twitch Places Super Mario 64”的流媒体。在这个过程中,我演示并破解了一个我称之为“游戏桥”的工具。Gamebridge是一个工具,它可以让游戏与他们实际上不应该能够互操作的程序进行互操作。
Gamebridge的工作原理是积极地连接到游戏的输入逻辑(通过一个自定义控制器驱动程序),并使用一对Unix FIFO在它和它控制的游戏之间进行通信。总的来说,这两个程序之间的数据流如下所示:
将它们粘合在一起的主要魔力是非阻塞I/O的使用。这意味着桥输入线程将在内核级别被阻塞,以便写入VBLACK信号,游戏也将在内核级别被阻塞,以便桥输入线程写入所需的输入。这有效地使用了Linux内核来传递调度量,就像您在L4微内核中所做的那样。这一设计考虑也意味着Gamebridge必须尽可能快地运行,因为它实际上最多只有几百微秒的时间来响应输入数据,以避免人类注意到任何卡顿。因此,游戏桥是用铁锈写的。
作为实现这一功能的第一步,我查看了Mario64PC端口的源代码(但理论上这也适用于其他仿真器,甚至有足够工作的任天堂64仿真器),并开始寻找任何可能对理解游戏部分工作原理有用的东西。我偶然发现了src/pc/Controller,然后发现了两个非常突出的宝石。我找到了向游戏添加新输入法的界面和一个从工具辅助速跑记录中读取的示例输入法。控制器输入界面本身就很漂亮,我在下面附上了它的副本:
要实现您自己的输入法,您只需要一个init函数和一个read函数。init函数用于设置内容,每帧调用read函数以获取输入。工具辅助的快速运行输入法似乎符合ontasVideos.org中描述的Mupen64演示文件规范,我最终用它来帮助测试和验证想法。
让我印象深刻的是它的格式是多么的简单。输入的每一帧都使用它自己的四字节序列。演示文件规范中的常量也对我有很大帮助,因为我想出了从Rust过渡到游戏的方法。我最终创建了两个位标志结构来帮助处理按钮数据,这几乎是Mupen64演示文件规范的1:1副本:
位标志!{//0x0100数字焊盘右//0x0200数字焊盘左//0x0400数字焊盘向下//0x0800数字焊盘向上//0x1000开始//0x2000 Z//0x4000 B//0x8000 A pub(板条箱)struct HiButton:U8{const NONE=0x00;const dpad_right=0x01;const dpad_Left=0x02;const dpad_。
这就是事情变得有趣的地方。对于马里奥64这样的游戏,通过聊天获得输入的一个更有趣的副作用是,你需要按住按钮,甚至是模拟杆,才能做跳到绘画或壁架上的事情。当您通过聊天获得输入时,您只有一帧的输入。因此,您需要某种随时间衰减的模拟输入(或模拟)。您可以使用的一种方法是线性插值(或LERP)。
我使用struct I call a Lerper实现了对按钮和模拟Stick Lerper的支持(它所在的文件名为au.rs,因为.au。这是“欲望”的Lojban情感助词,这个名字的灵感来自于它似乎伪造了所需的输入内容)。
目标(或线性插值的结束位置,对于此代码库中的大多数情况,目标为0或中性)。
每一帧,游戏中每一次输入的解析器都会被应用到更接近于零的位置。马里奥64使用两个有符号字节来表示控制器输入。最大/最小钳位确保删除结果保持在该范围内。
这是我第一次将异步锈蚀与同步锈蚀结合使用。让我震惊的是,只需启动另一个线程并让该线程处理Tokio运行时,而让主线程专注于输入是如此容易。这是处理异步twitch bot与主线程并行运行的代码块:
那么Twitch集成的其余部分在我们到达命令解析器之前都是样板。它的核心是将每条聊天线路分成几个单词,然后查找关键字:
设chat line=msg.data.to_string();let chat line=chat line.to_ascii_lowercase();设mut data=st.write().unwr();const button_add_amt:i64=64;for chatline.to_string().Split(";";).collect::<;Vec<;&;str>;>;().iter(){Match*cmd{";a";=>;data.a_button.add(BUTTON_ADD_AMT),";b";=>;data.b_button.add(BUTTON_ADD_AMT),";z";=>;data.z_button.add(BUTTON_ADD_AMT),";r";=>;data.r_button.add(BUTTON_ADD_AMT),";Cup";=。data.c_up.add(BUTTON_ADD_AMT),";cdown";=>;data.c_down.add(BUTTON_ADD_AMT),";cleft";=>;data.c_left.add(BUTTON_ADD_AMT),";cright";=>;data.c_right.add(BUTTON_ADD_AMT),";start";=。data.start.add(BUTTON_ADD_AMT),";向上";=>;data.ticky.add(127),";向下";=>;data.ticky.add(-128),";左边";=>;data.tickx.add(-128),";右边";=>;data.tickx.add(127),";停止&。data.ticky.update(0);},_=>;{},}}。
目前,模拟杆输入将停留约270帧,按钮输入将停留约20帧,然后漂移回中性。开始按钮是特殊的,对开始按钮的输入最多只能停留5帧。
调试两个同时运行的程序非常困难。我不得不求助于久经考验的方法,即使用gdb编写主要游戏代码,并在Rust中进行大量的printf调试。Pretty_env_logger机箱(它在内部使用env_logger机箱,其环境变量配置Pretty_env_logger)帮助很大。这个补丁修复了我在开发它时遇到的最大问题之一,我将以内联方式粘贴该补丁:
差异--git a/gamebridge/src/main.rs b/gamebridge/src/main.rsindex 426cd3e..6bc3f59 100644@@-93,7+93,7@@fn main()->;result<;()>;{},};-Sticky=Match Stickx{+Sticky=Match Sticky{0=>;Sticky,127=>;{yy。
不知何故,我一直在试着通过比较棍子的x轴位置来调整棍子的y轴位置。找到并修复这个bug是我编写Lerper类型的原因。
总而言之,这是一个非常有趣的项目。我学到了很多关于3D游戏设计、历史源代码分析和进程间通信的知识。我还学到了很多关于异步Rust的知识,以及它如何与Synchronous Rust协同工作。我还为Twitch制作了一个相当超现实的演示。我希望这能对其他人有用,即使它只是一个例子,说明如何从单一的基本原则将事物集成到奇怪的其他事物中。
你可以在GitHubpage上找到更多关于Gamebridge的信息。它的回购还包括马里奥64PC端口源代码的补丁,包括一个禁用马里奥失去生命能力的补丁。这对Twitch尝试很有用,默认情况下5点生命上限在测试中变得相当有限。
这篇文章发表于2020年5月9日。自发表以来,事实和情况可能发生了变化。如果有什么不对劲或不清楚的地方,请在匆忙下结论之前与我联系。