为了在 Mac 上玩 Galgame,我折腾了一套远程串流系统
本来只是想在图书馆摸鱼推个 Galgame,没想到最后折腾出了一套 Sunshine + Moonlight + Tailscale 的远程串流方案,中间还顺带复习了一遍计算机网络。记录一下这次折腾的过程,以及其中遇到的一些问题和思考。
Phase 1:邪念产生
我平时会带着 MacBook Air 跑东跑西,宿舍里则放着一台 Windows 游戏本。
问题来了,假如我在图书馆学累了想摸摸鱼,MacBook 只能看看漫画、小说,不能推 Galgame,浑身难受。但我又不想把游戏本也背着到处跑,纯折磨自己,太重了。于是我就想,诶,我为什么不能直接在 MacBook 上玩呢?

Phase 2:方案选型
一开始发现 iOS 上有个软件叫 KrKrCat,貌似可以运行一些 Galgame,便尝试在 Mac 上下载。
结果发现这软件压根没对我这个使用场景做好适配,随便拿个游戏试都运行不了,遂放弃。如果我有台 iPad,可能会考虑用它。随后又研究了 Aether、Tyranor-Mac、KrKr2-Next 等 GitHub 项目,也考虑过 Sikarugir / Wine 和商业软件 CrossOver。
这里其实有两个问题。
首先是兼容性。Galgame 使用的引擎并不统一,吉里吉里、NScripter、TyranoScript 等各有各的实现,不同游戏还可能存在额外依赖、汉化补丁等问题。即使某个工具支持其中一种引擎,也不代表所有游戏都能正常运行。
其次是成本。有些方案需要自己配置运行环境,有些需要付费。而我本来只是想摸个鱼,不想以后每玩一部游戏都先研究半天兼容性。
我没招了,于是又上网搜,发现还有在 Mac 上运行 Windows 虚拟机的办法。听着就麻烦,遂 pass。这些方案并不是不能用。Wine 通过兼容层实现 Windows API,虚拟机则提供另一套运行环境,在合适的场景下都很有价值。但对于我来说,真正的需求并不是研究 Windows 游戏如何跨平台运行,而是尽可能低成本、低折腾地玩到游戏。
Phase 3:逆向思维
于是问题又来了。我为什么一定要在 Mac 上运行游戏?你可能会问,你不是要玩吗,不在 Mac 上运行,难不成在其他电脑上运行?诶,你还真说对了。
原本我想解决的是:
如何让 Windows 游戏在 Mac 上运行? 这意味着我要考虑引擎适配、系统 API、补丁兼容、运行环境等一系列问题。
但如果重新描述我的需求:如何让我只带 MacBook Air 出门,也能玩到宿舍 Windows 上的 Galgame? 这两者其实并不完全一样。前一个问题已经预设了实现方式——必须在 Mac 本地运行游戏。而后一个问题只关心最终结果,并没有限制游戏到底在哪台电脑上运行。既然我宿舍本来就有一台 Windows 游戏本,为什么不直接让它负责运行游戏,Mac 只负责显示画面和发送操作呢?
于是,问题从软件兼容性,转变成了远程计算与实时音视频传输。
我放弃了本地运行所具备的离线能力,换取了更好的游戏兼容性。但代价是 Windows 必须保持运行,而且整个体验开始依赖网络。Trade-Off这块

Phase 4:RustDesk VS Sunshine + Moonlight
我以前就听说过远程串流这个说法,脑子里第一个闪过的就是 RustDesk 这个开源项目。于是按步骤在两台电脑上配置好,期间还遇到了公共服务器拒绝连接的问题,处理完登录和连接配置后,总算成功远程控制了 Windows。但是体验不太理想。画面帧率偏低,操作起来也没有想象中流畅,本来就是 PPT 游戏,更卡了我玩个 6 啊
不过仔细想想,RustDesk 本身主要面向远程控制、远程办公等场景,而我需要的是持续传输游戏画面,同时尽可能降低延迟。虽然都属于远程桌面技术,但侧重的质量属性并不一样。
于是我又找到了专门用于游戏串流的组合:
- Sunshine: 安装在 Windows 上,负责提供游戏串流服务。
- Moonlight: 安装在 Mac 上,负责接收游戏画面,并将键盘、鼠标等输入发送回 Windows。
安装时还发生了个小插曲。我误下载了 ARM64 版本的 Sunshine,Windows 一直提示处理器架构不支持,折腾一圈才发现自己下错了安装包。换成 AMD64,也就是 x86-64 版本后,成功安装。眼瞎这一块。
接着完成 PIN 配对,成功串流。这一次帧率和操作体验明显好了不少,终于有点能玩的样子了。
4.1 远程串流到底是怎么实现的?
这里我一开始也有个疑问。既然游戏实际运行在 Windows 上,那么 Mac 为什么能够实时看到画面,甚至直接用触控板操作 Windows?
其实整个过程可以分成两条数据流:画面从 Windows 传到 Mac,操作从 Mac 传回 Windows。
先来看画面。
1 | Windows 游戏本 |
Windows 首先正常运行游戏,由 CPU、GPU 完成游戏逻辑和画面渲染。Sunshine 采集画面后,通过视频编码器将其压缩成视频流。我的天选4有 NVIDIA 显卡,可以利用 NVENC 进行硬件编码。
为什么要编码?
因为原始画面实在太大了。假设每帧都是未经压缩的1920×1080、24位RGB图像,那么一帧约有6.22MB,30FPS时每秒就需要约187MB,也就是接近1.5Gbps的数据量。显然,直接把原始画面通过网络传输并不现实。所以 Sunshine 需要利用视频编码技术,将这些画面压缩后再发送出去。Mac 上的 Moonlight 接收到视频流后,利用硬件视频解码能力还原画面,最终显示到屏幕上。
那操作又是怎么传回去的呢?
1 | Mac 触控板点击 |
整个过程就是一个不断循环的交互过程。
因此,我们平时说的串流延迟,也不只是单纯的网络延迟,它还包含画面采集、编码、传输、解码、显示,以及输入处理等多个环节。这里还有个挺有意思的现象。我用 MacBook Air 串流了差不多一个小时,机器几乎没什么发热,电量显示变化也很小。
原因其实很简单:游戏根本不在 Mac 上运行。 Mac 没有承担游戏逻辑、场景渲染等主要计算任务,只是在接收和解码视频。Apple Silicon 本身又有专门的视频解码硬件,所以处理1080P、30FPS的视频流并不是什么重活。
说白了,Windows 负责干活,Mac 负责看结果和发指令。
4.2 Tailscale 为什么能实现跨网络连接?
到这里,局域网串流已经实现了,但还没有真正解决我的需求。
毕竟我最终是想在图书馆或者工作室场景用 Mac 连接宿舍的 Windows,而不是让两台电脑一直连接同一个 Wi-Fi。
新的问题出现了:
为什么在同一个局域网里可以访问 Windows,换个网络就不一定行了?
这就涉及计算机网络里的 NAT(网络地址转换)。我们平时连接 Wi-Fi 后,设备可能获得类似 192.168.1.100 的 IP 地址。但这是私有 IP,只在相应的内网环境中使用,互联网上的其他设备不能直接通过这个地址定位到你的电脑。
为什么会这样?
因为 IPv4 地址有限,不可能简单地给每台设备都分配一个独立的公网 IPv4 地址。于是大量设备通过 NAT 共享外部地址。比如宿舍电脑向互联网发送数据时,NAT 设备会把内网地址和端口转换成外部地址和端口,并维护相应的映射关系。这样外部返回的数据才能找到原来的内网设备。但如果图书馆的 Mac 想主动连接宿舍 Windows,就会遇到一个问题:外部连接并不一定有现成的 NAT 映射可用,更不用说校园网里还可能存在防火墙、运营商级 NAT 等限制。常见方案是配置公网访问和端口映射,但我并不想折腾这些。
于是引入了 Tailscale。
Tailscale 基于 WireGuard 构建虚拟私有网络,可以让处于不同网络中的设备通过虚拟 IP 相互访问。两台电脑加入同一个 Tailnet 后,会分别获得类似 100.x.x.x 的 Tailscale IP。这样,Moonlight 就可以使用 Windows 的 Tailscale IP 连接 Sunshine,而不用关心电脑当前连接的是哪个 Wi-Fi。
但这里又有个新问题。
Tailscale 是怎么让两台没有公网 IP 的设备互相通信的?难不成所有数据都要经过 Tailscale 的服务器中转?
并不是。Tailscale 会优先尝试建立 P2P 直连,其中一个重要技术就是 UDP 打洞。
先简单理解一下 NAT 打洞。假设有 A、B 两台设备,分别位于不同的 NAT 后面。
正常情况下,外部设备不能随意向它们发送数据。但当内网设备主动向外发送 UDP 数据包时,NAT 通常会创建相应的地址和端口映射。如果 A 和 B 能够知道对方可尝试访问的地址、端口,并协调好通信过程,就有可能利用这些映射建立直接连接。Tailscale 的协调机制会帮助双方交换连接信息,并尝试寻找可用路径。
整个过程可以简化为:
1 | Windows(宿舍) Mac(图书馆) |
直连成功后,双方的业务数据可以通过 WireGuard 加密隧道直接传输,不需要绕到 Tailscale 的中继服务器再转发。
不过 UDP 打洞不是万能的。如果网络环境限制比较严格,比如阻止相关 UDP 通信,或者 NAT 行为导致无法建立直接路径,Tailscale 就可能使用 Peer Relay 或 DERP 中继。这些方式都可以提供加密通信,只是中继通常意味着更长的网络路径,也可能带来额外延迟和带宽限制。
实际测试
为了看看我的设备有没有成功建立直连,我在 Mac 上使用了:
1 | tailscale ping <Windows 的 Tailscale IP> |
分别在两种网络环境下测试。
| 网络环境 | 延迟 | 连接方式 |
|---|---|---|
| 两台设备连接同一 Wi-Fi | 18ms | UDP 直连 |
| Mac 切换手机热点 | 23ms | UDP 直连 |
两次结果都出现了 via IP:Port,而不是持续通过 DERP 中继,说明测试时两台设备成功建立了直接通信路径。
这也让我明白了一件事:Tailscale 并不是简单地租了个服务器替我转发所有数据,而是在尽可能帮助两台设备找到一条可以直接通信的路径。 直连不代表不会丢包,Ping 延迟低也不代表串流一定流畅。
Tailscale 解决的是网络连通性问题,至于视频数据能不能稳定、及时地传输,还需要继续看下面的性能问题。
Phase 5:调优——为什么降低码率反而更流畅?
本来以为成功串流之后就可以愉快推 Galgame 了,结果又遇到了新问题。
普通对话场景基本流畅,但一遇到人物跑动、屏幕剧烈抖动之类的动态演出,画面就会明显卡顿。明明是个 Galgame,怎么连这点动画都扛不住?我一开始还怀疑是不是远程串流本身就这样,甚至想,那些串流3A游戏的人岂不是更难受?后来逐步调整 Moonlight 的分辨率、帧率和码率,才发现一个现象:
把码率从10Mbps降低到4Mbps之后,卡顿明显改善了。 甚至连 BG3 都能够远程运行。当然,我也不会真的用这套配置去认真玩 BG3,本来就是为了推 Galgame 才折腾的。
不过为什么把码率调低,反而能让视频更流畅?
5.1 分辨率、帧率和码率有什么区别?
首先要分清三个概念。
分辨率决定一帧画面包含多少像素,比如1920×1080。分辨率越高,通常可以呈现越丰富的图像细节。
帧率决定每秒显示多少帧,比如30FPS就是每秒30帧。帧率越高,动态画面通常越连贯。
码率则决定编码视频每秒使用多少数据,常用单位是 Mbps。
比如:
- 10Mbps,平均每秒约1.25MB的视频数据。
- 4Mbps,平均每秒约0.5MB的视频数据。
这里说的是按码率换算的数据量,不包含网络协议等额外开销,实际传输速率也可能存在波动。这意味着,在同样的分辨率和帧率下,码率越高,通常能够保留更多画面细节,但也需要网络承载更多数据。反过来,降低码率可以减少传输负担,却可能使画面变模糊,出现压缩痕迹。所以码率并不是越高越好,而是需要结合实际网络环境进行取舍。
5.2 为什么静态 Galgame 很流畅,一到动态场景就卡?
这里还涉及视频编码中的帧间预测。
假设我们有两帧连续的 Galgame 画面,背景、人物立绘都没有变化,只是文本框多显示了一句话。如果将两帧完整图像都发送出去,会重复传输大量相同的数据。现代视频编码器可以利用不同帧之间的相似性,通过运动补偿、帧间预测等方式减少冗余信息。对于静态对话场景,相邻帧之间变化很少,因此通常比较容易压缩。但如果游戏突然出现全屏摇晃、人物快速移动或者复杂的动画演出,相邻帧之间的差异可能明显增大。这时编码器如果希望维持同样的画质,就可能需要更多数据。在码率受到限制时,编码器则需要提高压缩程度,以牺牲部分画质换取更低的数据量。而且视频流的数据包并不一定均匀地到达网络设备,实际发送过程中也可能出现瞬时突发。如果这些数据超过了网络链路短时间内能够稳定处理的能力,就可能出现排队、抖动甚至丢包。
这也解释了为什么一个静态画面占绝大多数时间的 Galgame,在某些高动态场景下仍然可能出现串流问题。
5.3 为什么10Mbps会卡,4Mbps流畅?
这里先回顾一下实际测试结果。
| 配置 | 体验 |
|---|---|
| 1080P、30FPS、10Mbps | 动态演出时出现明显卡顿 |
| 720P、30FPS、较低码率 | 流畅度改善 |
| 1080P、30FPS、4Mbps | 当前环境下基本能够正常游玩 |
我最初的理解是,10Mbps应该不算很高,为什么连 Galgame 都会卡?
后来意识到,网络带宽和网络稳定性其实是两回事。
即使网络测速能达到几十、几百 Mbps,也不代表它能够持续、稳定、低延迟地传输实时视频。
这里还需要区分几个概念:
- 带宽: 网络单位时间内能够承载多少数据。
- 延迟: 数据从发送端到接收端需要多少时间。
- 抖动(Jitter): 数据包的传输延迟是否稳定。
- 丢包: 数据包是否在传输过程中丢失。
游戏串流不仅要把视频数据传过去,还要尽可能在规定时间内传过去。假设当前视频是30FPS,那么平均每33.3ms就需要显示一帧。如果某一帧的数据因为网络排队、抖动等原因到得太晚,即使最终成功到达,也可能已经错过它应该被显示的时间。而且实时视频通常不会无限等待过时的数据,否则缓冲越积越多,用户操作到画面反馈之间的延迟也会越来越高。因此,对游戏串流来说,稳定、及时地收到数据,往往比单纯追求高码率更加重要。降低码率之后,每秒需要传输的数据减少,可能缓解网络链路的拥塞和瞬时传输压力,从而改善流畅度。
这就能解释我观察到的现象。
目前能够确认的是,降低码率改善了串流体验;至于具体瓶颈在哪里,还需要进一步测量。
5.4 为什么游戏串流会使用 UDP?
折腾 Tailscale 时,我还接触到了 UDP 直连这个概念。这让我想到计算机网络里 TCP 和 UDP 的区别。
TCP 提供可靠、有序的字节流传输,通过确认、重传等机制处理丢失的数据。UDP 则不保证数据一定到达,也不保证按序到达。
那为什么实时游戏串流还会大量使用 UDP?
因为实时音视频有一个很重要的特点:数据具有时效性。 例如视频正在播放第100帧,如果第98帧的某个数据包丢失了,等它重传回来时,可能早就错过了显示时间。对于文件下载,我们通常希望一个字节都不能少。但对于实时视频,我们往往更希望及时显示接下来的画面,而不是一直等待过时的数据。所以实时音视频系统常使用 UDP 作为传输基础,再根据需求在应用层实现缓冲、前向纠错等机制。
UDP 不会强制应用接受 TCP 那一整套可靠、有序字节流的语义,应用可以根据实时性的要求,自己决定如何处理迟到和丢失的数据。
还有一点需要区分:Tailscale 使用 UDP 建立 WireGuard 直连,和 Moonlight 使用网络协议传输音视频,属于不同层次的事情。
Tailscale 主要解决通信路径和加密问题,不能把底层 UDP 变成可靠传输。

Phase 6:总结
到这里,这次折腾算是暂时告一段落了。
从最初的 KrKrCat、Wine、CrossOver,到 RustDesk,再到 Sunshine + Moonlight + Tailscale,最终实现了1080P、30FPS、4Mbps的可用串流配置。
目前在测试环境下,Galgame 已经能够比较流畅地游玩。至于图书馆 Wi-Fi 下的长期稳定性,还需要在实际使用场景中进一步验证。
回头看,这次折腾让我对几个原本比较抽象的计算机知识有了更直观的理解。
比如 NAT 穿透,以前学计算机网络的时候,只知道 NAT 用来转换地址,UDP 不保证可靠传输。但当我真的需要让两台处于不同网络的电脑通信时,才开始理解为什么需要打洞,为什么有些连接能够直连,有些却需要中继。再比如带宽、码率、延迟和丢包,以前这些只是几个独立的概念,现在却能直接影响我到底能不能流畅地玩游戏。不过这次最大的收获,可能还不是这些技术知识,而是重新理解了需求和技术方案之间的关系。一开始我满脑子想的都是,怎么让 Windows 游戏在 Mac 上运行。于是一直围绕模拟器、兼容层、虚拟机这些方案折腾。
但回过头来,我真正的需求只是:在图书馆休息的时候,能拿 MacBook Air 推一会儿 Galgame。我并不在乎游戏究竟运行在哪台电脑上。当我不再把「Mac 本地运行游戏」当成前提,远程串流这条路线才变得顺理成章。
而这套方案也不是没有代价。
我用对网络和宿舍电脑的依赖,换取了跨平台兼容性和出门时的轻便。对于主要游玩 Galgame、对操作延迟不那么敏感的我来说,这个取舍是可以接受的。说起来,这也算是印证了我最近学习架构设计时的一点感受:做技术选型之前,先想清楚自己真正要解决的是什么问题,而不是一开始就把某种技术方案当成需求本身。
本来只是为了摸个鱼,折腾这么一出
只能说,邪念果然是第一生产力。
参考资料
- Sunshine 官方文档:Sunshine 的架构、配置与硬件编码。
- Moonlight 官方 FAQ:串流性能统计、网络丢帧及 Jitter。
- Tailscale — How NAT traversal works:NAT 穿透与 UDP 打洞的实现原理。
- Tailscale — Connection types:直连、Peer Relay 与 DERP 中继。
- Tailscale — Ping types:如何检查 Tailscale 连接路径。


