Skip to content

联机同步 ​

本文档帮助老用户理清星火编辑器2.0中服务端→客户端的数据同步有哪几条路、各自适合什么,并重点介绍连续运动联机游戏的推荐做法:位置走 Node、数据走 PropertyHostComponent 的混合形态。

目录 ​


先理清:服务端权威与三条同步路 ​

2.0 的联机是服务端权威的:游戏状态在服务端计算,同步给客户端,客户端只读、只负责表现。这点和 1.0 的理念一致。

「把状态同步给客户端」在 2.0 有三条路,按实体有没有连续运动的位置、要不要属性级隐私来选:

  1. PropertyObject——无场景、无位置的数据对象。适合卡牌、棋盘、回合制、放置、背包这类离散状态。位置(如果有)靠 OrderIndex / GroupId 或自定义属性表达。
  2. Node + PropertyHostComponent(本文重点)——位置走 replicated Node(客户端原生平滑),数据走挂在 node 上的同步属性组件。适合连续运动的实体(小鸟、子弹、载具、物理对象),且能按「谁看得到这个实体」叠加属性级隐私。
  3. 裸 Node + SyncVar——实体有位置、但只需要少量「看得到就全看得到」的小状态,不要属性级范围。直接 Node.SetSyncVar(...)。

> 单机游戏不需要这些——直接在客户端管理数据即可,无同步开销。


选型一张表 ​

你的实体选为什么
离散状态、无连续位置(卡牌/棋子/背包/放置)PropertyObject不需要场景与平滑,最轻
连续运动 + 需要属性级隐私(背包只给自己)Node + PropertyHostComponentnode 拿原生平滑;数据按「看得到实体 ∩ 属性范围」分级
连续运动 + 只要「看得到就全看得到」裸 Node + SyncVar不需要属性级范围,最省样板

✅ 推荐路线:Canvas 绘制的联机游戏,只要有移动 / 持续变化的视觉元素(FlappyBird、跑酷、弹球这类),优先用 Node + PropertyHostComponent。 这套架构正是为此而生——PropertyObject 没有内置插值,用它同步连续位置就得在客户端手搓 smoothing / 外插(易错难调);位置走 Node 后,平滑交给引擎,玩法代码一行外插都不用写。

⚠️ 反模式:用 PropertyObject 同步连续运动的坐标。客户端只能拿到一跳一跳的位置,逼你手写线性外插补帧——连续位置请走 Node。纯离散状态(无连续运动)才继续用 PropertyObject。


混合同步:位置走 Node、数据走属性组件 ​

核心思想:让最合适的通道各管一摊。

  • 位置 / 朝向 → replicated Node 的 transform。客户端 SmoothedTransform 自动从当前显示值向最新网络目标收敛,渲染端一行外插都不用写。
  • 游戏数据 → PropertyHostComponent 同步属性。用 [PropertyHostFacet] 代码生成,逐属性可设可见性范围。

三步配方 ​

① 声明数据 facet(枚举驱动,类似 [PropertyObjectWrapper]):

csharp
using EngineInterface.BaseType;
using GameCore.Extension;

[PropertyHostFacet]
[EnumExtension(Extendable = true)]
public enum EPropertyBird
{
    [PropertyType(typeof(int))]  Score,
    [PropertyType(typeof(bool))] IsAlive,
}
// → 自动生成 partial class Bird : PropertyHostComponent

② 服务端:位置写 node,数据挂 facet:

csharp
#if SERVER
var node = graph.RootNode?.CreateChild("Bird", NodeReplication.Replicated);
var bird = node.AddPropertyHost<Bird>(player, SyncType.All); // 一行挂上
node.Position = new Vector3(x, y, 0);  // 位置 → transform(客户端平滑)
bird.Score = 0;                        // 数据 → facet
graph.Publish(player);                 // 发布给能看到这个实体的玩家
#endif

③ 客户端:发现 node,读平滑位置 + facet 数据:

csharp
#if CLIENT
var observer = SceneGraph.ObserveReplicatedNodes(node =>
{
    if ((string)node.Name == "Bird")
        node.OnPropertyHostReady<Bird>(b => Track(node, b)); // 组件可能晚到,须容忍
});

// 渲染循环里(如 CanvasAnimated.OnAnimatedRender):
if (node.TryGetPropertyHost<Bird>(out var bird))
    Draw(node.Position, bird.Score, bird.IsAlive);  // node.Position 已是平滑值
#endif

这里的 ObserveReplicatedNodes 只观察服务端通过 Publish(player) 显式发布的额外脚本图,不包含当前 SCE 场景。访问当前 SCE 场景内的 replicated node 时,从 Player.LocalPlayer.Scene.GetSceneGraph() 获取图,直接订阅该实例的 node 事件,并遍历 GetReplicatedNodes() 回灌已有节点。

按 node.Name 分类是因为节点名随 node 创建快照一同到达(属于复制属性),不必等独立到达的属性组件,分类不会错判。


反作弊:per-property 可见性 ​

给 facet 字段标 [PropertySyncType(SyncType.Self)],它就只发给 owner,敌方读到默认值——即便敌方看得到实体本体与公开字段。

csharp
[PropertyType(typeof(int))]
[PropertySyncType(SyncType.Self)]   // 私有:仅 owner 收到字节
SecretLoadout,

可见性是两层求交:「该玩家订阅了实体所在的图(看得到实体)」且「该玩家在该字段的 SyncType 范围内」。于是能表达「看得到对面在动、却看不到对面背包」——图发布给双方,私有字段设 Self。


常见坑 ​

  • 快速运动看着抖:不是 fps,是 transform 平滑的 snap 阈值默认(5)对快速运动太低。客户端调 graph.ConfigureTransformSmoothing(50, 120)(阈值抬到正常每帧位移之上、瞬移之下)。
  • 组件读到默认值:要么不在你的可见范围(正常,反作弊),要么组件还没到达——用 OnPropertyHostReady / TryGetPropertyHost 容忍,别假设「node 在 ⇒ 数据在」。
  • 服务端给玩家建实体:只给真人(Player.Controller == ControllerType.User)且在线(Player.IsOnline)建;电脑(AI)槽 IsOnline 恒为 true,不过滤会建出无人控制的实体。

深入与示例 ​