多人游戏
🌐 Multiplayer
基于节点的用户界面通常用于创建视觉化和探索性强的应用。对于协作功能,这通常意味着没有令人满意的“折中”解决方案,比如仅稀疏同步用户之间的状态,因为这会破坏视觉一致性或使协作探索更加复杂。通常,需要建立一个完整的实时多人系统。
🌐 Node-based UIs are often used to create applications that are visual and explorative in their nature. For collaborative features, this usually means that there is no satisfying “middle-ground” solution like sparsely synchronising the state between users, because it would break visual consistency or make collaborative exploration more complicated. Often, a full live multiplayer system needs to be put in place.
在我们的Collaborative Flow Pro 示例中,我们展示了一个使用 React Flow 和Yjs 的实时多人协作流程。在本指南中,我们将探讨在你的 React Flow 应用中集成多人协作的一些最佳实践。
🌐 In our Collaborative Flow Pro Example, we show a realtime multiplayer collaboration flow using React Flow and Yjs . In this guide, we will explore some best practices for integrating multiplayer collaboration in your React Flow application.
多人应用是什么?
🌐 What are multiplayer applications?
虽然在这里“实时协作”是正确的说法,但我们称其为“多人模式”,这一说法很可能是在这一背景下由 Figma 推广开来的。除了在市场推广上更直观之外,它还更好地传达了致力于创造真正实时体验的承诺,因为它将其放在了与竞技视频游戏相同的类别中。
🌐 Although realtime collaboration would be the correct term here, we refer to it as multiplayer, which was most likely popularized in this context by Figma. Besides being more intuitive for marketing purposes, it also does a better job of communicating the commitment to creating something truly realtime by putting it in the same category as competitive video games.
要弄清楚你的应用支持什么程度的协作,你只需要看看它与多人游戏的相似程度。这些功能可能包括:
🌐 To figure out what degree of collaboration your application supports, you just need to look at how closely it resembles a multiplayer game. These features likely include:
- 无需手动保存;所有用户共享一个世界,所有操作都会被保留。
- 其他用户所做的任何更改你都能立即看到。
- 不仅已完成的动作,而且及物状态也会同步(例如拖动一个节点,而不仅仅是在节点被放置时)。
- 你可以看到其他用户(例如,他们的光标或视口位置)
一句警告
🌐 A word of caution
制作多人应用很困难!当用户协作编辑共享对象时,冲突、延迟、连接中断和并发操作都是预期中的情况。多人应用中最困难的挑战是冲突解决:你始终需要某种同步引擎,能够将来自服务器(或其他客户端)的任何更改合并到(通常是乐观的)客户端状态中。你需要优雅地处理冲突和断线情况。你需要构建一个可靠的网络层。
🌐 Making multiplayer applications is hard! When users are collaboratively editing shared objects, conflicts, delays, connection drops and concurrent operations are expected. The hardest challenge in multiplayer apps is conflict-resolution: you always need some kind of sync engine that is able to merge any changes coming from the server (or other clients) into the (often optimistic) client state. You need to gracefully handle conflicts and disconnects. You need to build a reliable network layer.
最简单的多人同步引擎将是“先到先得”的方法,同时忽略失败的请求和发送的消息。如果你尝试手动处理这个问题,你会看到出现大量不同的边缘情况。网络很慢,客户端会断线,而且操作的顺序很重要。
🌐 The simplest sync engine for multiplayer would be a “first-come-first-served” approach, and failed requests and messages sent being ignored. If you try to manually handle this, you will see a quite vast variety of edge cases emerge. Networks are slow, clients disconnect, and the order of actions matters.
本地优先和 CRDT
🌐 Local-first and CRDTs
根据你想在多大程度上处理冲突和断开连接,你很快就会进入本地优先的字段。当你的同步引擎允许用户无限期断开连接时,你就有了一个本地优先的应用。如果你想对这个主题有一个好的入门介绍,我们推荐Ink & Switch的文章 ,这篇文章创造了这个术语。
🌐 Depending on how far you want to take handling conflicts and disconnects, you very quickly land in local-first territory. When your sync engine allows a user to be disconnected for an indefinite amount of time, you have a local-first application. If you would like a good primer on this topic we recommend Ink & Switch’s essay that coined the term.
CRDT(无冲突复制数据类型)是一种数据结构,允许多个用户在不同设备上同时编辑相同的数据,然后自动合并所有更改而不会产生冲突——无需中央服务器。CRDT不仅仅存储数据的当前值,还在每个连接的客户端上保留操作历史,因此任何副本都可以将更改合并到相同的最终状态,无论更新到达的顺序如何。因此,它们通过作为多副本同步数据结构来解决冲突和断开问题,这些数据结构可以完全离线工作,并在重新连接时自动上传、获取和协调其状态。
🌐 A CRDT (Conflict-free Replicated Data Type) is a data structure that lets multiple users edit the same data simultaneously on different devices, then automatically merges all changes without conflicts—no central server needed. Instead of just storing the current value of the data, CRDTs keep operation history on every connected client, so any replica can merge changes into the same final state, regardless of the order updates arrive. They are thus solving the conflicts and disconnects by being multi-replica synced data structures that can work completely offline, and automatically upload, fetch, and reconcile their state on reconnection.
一些流行的解决方案是 Yjs 、Automerge 和 Loro 。
🌐 A couple of popular solutions are Yjs , Automerge and Loro .
我应该同步哪个 React Flow 状态?
🌐 What React Flow state should I sync?
你首先需要考虑的是你希望如何同步应用本地状态的哪些部分。
🌐 Among your first considerations should be how you want to sync what parts of the local state of your app.
短暂的 vs 持久的
🌐 Ephemeral vs Durable
虽然有许多方法可以在客户端之间同步状态,但我们可以将它们分为两类:短暂的和持久的(又称原子)更改。短暂的更改不会被持久化,其一致性或正确性对应用的功能也不是非常重要。这些包括光标或视口位置,或者任何临时的交互状态。丢失其中的一些更新不会破坏任何功能,如果发生连接中断,客户端只需监听最新的更改并恢复任何丢失的功能。
🌐 While there are many ways to sync state between clients, we can categorize them into two groups: ephemeral and durable (aka atomic) changes. Ephemeral changes are not persisted and neither its consistency nor its correctness is very important for the functionality of the application. These are things like cursor or viewport positions or any transient interaction state. Missing some of these updates will not break anything and in case of a connection loss, a client can just listen for the newest changes and restore any lost functionality.
另一方面,对节点和边的更新应该是持久且一致的! 试想一下,如果爱丽丝删除了一个节点,而鲍勃遗漏了这个更新。现在如果鲍勃随后移动该节点,我们就会有不一致的应用状态。对于这种类型的数据,我们必须使用一种能够更积极地处理断开连接的解决方案,并且能够丢弃不可能的操作,比如移动一个已删除的节点。
🌐 On the other hand, updates to the nodes & edges should be persistent and consistent! Imagine if Alice deletes a node and Bob misses this update. Now if Bob subsequently moves the node, we would have an inconsistent application state. For this kind of data we have to use a solution that handles disconnects more aggressively and is able to discard impossible actions like moving a deleted node.
在多人 React Flow 应用的核心是同步节点和边。然而,并非状态的所有部分都应该同步。例如,哪些节点和边被选中,每个客户端应该不同。此外,还有一些字段与某些库函数相关,例如 dragging、resizing 或 measured。
🌐 At the core of a multiplayer React Flow application is syncing the nodes and edges. However
not all parts of the state should be synced. For instance, what nodes and edges are selected
should be different for each client. Also there are some fields that are relevant for the
certain library functions like dragging, resizing or measured.
这是关于哪些部分建议同步以及哪些不建议同步的概述。
🌐 This is an overview of what parts are recommended to sync and what not.
节点
🌐 Nodes
| 字段 | 持久 | 短暂 | 说明 |
|---|---|---|---|
id | ✅ | ❌ | 重要,总是需要同步 |
type | ✅ | ❌ | 重要,总是需要同步 |
data | ✅ | ❌ | 重要,总是需要同步 |
position | ✅ | ✳️ | 重要,包括短暂状态 |
width,height | ✅ | ✳️ | 重要,包括短暂状态 |
dragging | ❌ | ✅ | 短暂交互状态 |
resizing | ❌ | ✅ | 短暂交互状态 |
selected | ❌ | ❌ | 每个用户的界面状态 |
measured | ❌ | ❌ | 从 DOM 计算得出 |
边缘
🌐 Edges
| 字段 | 持久的 | 短暂的 | 说明 |
|---|---|---|---|
id | ✅ | ❌ | 重要,始终需要保持同步 |
type | ✅ | ❌ | 重要,始终需要保持同步 |
data | ✅ | ❌ | 重要,始终需要保持同步 |
source | ✅ | ❌ | 重要,始终需要保持同步 |
target | ✅ | ❌ | 重要,始终需要保持同步 |
sourceHandle | ✅ | ❌ | 重要,始终需要保持同步 |
targetHandle | ✅ | ❌ | 重要,始终需要保持同步 |
selected | ❌ | ❌ | 每用户的界面状态 |
连接与游标
🌐 Connections and Cursors
短暂数据的好例子是connections(正在创建的边的瞬态部分)和光标。
🌐 Good examples of ephemeral data are
connections (the transient
part of edges being created) and cursors.
共享每个用户的连接状态可以提高客户端之间的视觉一致性,但在断开连接时丢失此状态并不影响共享数据的正确性:它纯粹是呈现上的。
🌐 Sharing each user’s connections status improves visual consistency across clients, but losing this state on disconnect doesn’t affect the correctness of the shared data: it’s purely presentational.
光标也是如此。它们有助于创建共享工作区的沉浸感,但对于应用的完整性并非必需。只要节点和边保持同步,光标状态就可以安全地丢弃。
🌐 The same applies to cursors. They help create the immersion of a shared workspace, but they’re not essential to the application’s integrity. As long as the nodes and edges remain in sync, cursor state can be safely discarded.
两者都可以作为纯粹的暂态状态共享。如果你想减少实时更新的频率,可以对更新进行防抖处理,并平滑其他用户光标的移动。你可以使用像 perfect-cursors 这样的库来平滑其他用户光标的移动。
🌐 Both can be shared as purely ephemeral state. If you want to reduce the frequency of live updates, you can debounce the updates and smooth the movements of other users’ cursors. You can use a library like perfect-cursors to smooth the movements of other users’ cursors.
第三方库和服务
🌐 Third Party Libraries and Services
我们尝试了不同的多人后端解决方案,以了解它们在 React Flow 应用中的最佳使用场景。我们发现,没有一种解决方案适合所有情况。在基于 CRDT 的本地优先库(如 Yjs 和 Jazz )与服务器权威方式(如 Supabase 和 Convex )之间的选择,将改变你构建多人 React Flow 应用的方法:
🌐 We experimented with different multiplayer backend solutions to understand their best use cases for React Flow apps. What we found out, is that there’s no one-size-fits-all solution. The choice between CRDT-based local-first libraries (like Yjs and Jazz ) and server-authoritative approaches (like Supabase and Convex ) will change the way you approach building multiplayer React Flow applications:
- CRDT 库 将使你在离线优先的多人支持方面的生活更加轻松,为你免费提供自动冲突解决,但如果你的应用也围绕数据库构建,你将需要在 CRDT 库和数据库之间实现自己的适配器。
- 服务器权威的解决方案 在与数据库和传统应用逻辑一起使用时更容易,但会使你在离线优先的多人支持、冲突解决和开发复杂性方面的工作变得复杂。
| Name | Architecture | Offline Support | Conflict Resolution | Notes |
|---|---|---|---|---|
| Yjs | CRDT (Local-first) | ✅ Full offline support | Automatic (CRDT) | Collaborative editing, offline-first CRDT for local-first apps, with a React Flow Pro Example |
| Jazz | CRDT (Local-first) | ✅ Full offline support | Automatic (CRDT) | A full, offline-first database solution that handles multiplayer natively. |
| Supabase | Server-authoritative | ❌ Requires connection | Manual/RLS policies | Classic SQL-based database solution, with realtime support, but no CRDT (conflict resolution must be implemented manually). |
| Convex | Server-authoritative | ❌ Requires connection | Optimistic | TypeScript-first, database solution with real-time queries and a sync-engine with some conflict resolution capabilities. Can be paired with Automerge |
| Liveblocks | Server-authoritative, CRDT-like | 🟡 Partial offline support | Uses CRDTs under the hood | Real-time collaboration platform with hosted proprietary backend. React Flow Example . |
| Velt | CRDT with managed backend | ✅ Full offline support | Uses CRDTs under the hood | Collaborative editing platform with managed backend, with a dedicated React Flow Library |
| Automerge | CRDT (Local-first) | ✅ Full offline support | Automatic (CRDT) | The canonical CRDT library. Can be used together with Convex and other databases. |
| Loro | CRDT (Local-first) | ✅ Full offline support | Automatic (CRDT) | A fast CRDT library in Rust and WebAssembly with history tracking and version control. |