概述

tModLoader 的网络同步基于一种混合的 权威服务器模型(Steam开服时主机即服务器)确保多人模式下的一致性。

同步内容

内容权威方
Player客机
Projectile(有主)客机
NPC主机
Projectile(无主)主机
世界状态主机

两种基本同步

客机同步: 源客机 => 服务器 => 所有客机(除源客机) 在此同步中,数据权威方为源客机。数据包首先自源客机发送到服务器,而后服务器作为中转站将包转发到所有已连接的客机(除源客机)。

服务器同步: 服务器 => 所有客机 在此同步中,数据权威方为服务器。服务器将数据包发送到所有已连接的客机。

同步的实现情况

层级同步内容同步方式特点
基础层基础实体(Player/Item等)自动同步-
Mod层Mod实体 (ModPlayer/ModItem等)部分自动同步,其他需手动实现仅需关注读写,有现成的方法
自定义层Mod特定游戏逻辑需手动实现需要编写全部内容

前置知识

收发包基础

tModLoader通过网络包在不同端(服务器/客户端)之间传输数据,借助BinaryWriterBinaryReader类来读写信息。

使用BinaryReader和BinaryWriter进行数据读写时,必须严格遵守顺序一致性原则。

  1. 写入顺序必须与读取顺序完全一致 发送端写入数据的顺序必须严格匹配接收端读取数据的顺序。如果写入顺序是:包ID、玩家ID、位置坐标,那么读取顺序也必须是:先读包ID、再读玩家ID、最后读位置坐标。任何顺序错位都会导致数据解析错误。
  2. 字段数量必须严格对应 发送端写入的每个字段都必须在接收端有对应的读取操作。不能多写一个字段,也不能少读一个字段。如果发送端写了3个字段而接收端只读取了2个,或者发送端写了2个字段而接收端试图读取3个,都会导致数据流错位。
  3. 数据类型必须完全匹配 写入的数据类型必须与读取时声明的数据类型严格一致。错误的读取类型会导致解析出无意义数据甚至崩溃,并影响后续的读取。
Test.cs
      // 发送包
BinaryWriter writer;
{
    writer.Write(123); // int
    writer.Write("text"); // string
    writer.WriteVector2(position); // Vector2
}

// 接收包
BinaryReader reader;
{
    int id = reader.ReadInt32();
    string text = reader.ReadString();
    Vector2 pos = reader.ReadVector2();
}

    

⚠️ 重要限制:

  • 数据包最大尺寸:65,535字节
  • 避免高频发送(>20包/秒可能引发延迟)

内置同步

内置同步仅关注读写的部分即可

ModPlayer

      public override void SyncPlayer(int toWho, int fromWho, bool newPlayer);
public override void CopyClientState(ModPlayer targetCopy);
public override void SendClientChanges(ModPlayer clientPlayer);

    

ModSystem

      public override void NetSend(BinaryWriter writer);
public override void NetReceive(BinaryReader reader);

    

ModItem

      public override void NetSend(BinaryWriter writer);
public override void NetRecieve(BinaryReader reader);

    

ModProjectile

      public override void SendExtraAI(BinaryWriter writer);
public override void ReceiveExtraAI(BinaryReader reader);

    

自定义同步

自定义同步是Mod内各机制系统同步的基础,该类同步通过ModPacket来完成 注: 此Packet以提前经过基于Mod的分类处理,不需要担心多Mod冲突。

tModLoader在Mod类中提供了ModPacket统一接收处理的方法,我们需要在此对不同的包进行分离并进行对应的处理。以下是一个基于枚举ID分类的简单实现:

      // 定义消息类型枚举(包ID)
public enum PacketID : byte
{
    PlayerPositionUpdate,  // 0
    // ... 其他消息类型
}

// 发送 - 可以在很多种位置调用
public void SendPositionUpdate(Vector2 newPosition)
{
    ModPacket packet = Mod.GetPacket();
    packet.Write((byte)PacketID.PlayerPositionUpdate); // 写入包ID
    packet.WriteVector2(newPosition); // 写入位置数据
    packet.Send(toClient: -1); // 发送包
}

// 接收 - Mod.HandlePacket(BinaryReader reader, int whoAmI)
public class TestMod : Mod
{
    // ...

    public override void HandlePacket(BinaryReader reader, int whoAmI)
    {
        PacketID id = (PacketID)reader.ReadByte(); // 首先读取包ID
        switch (id)
        {
            case PacketID.PlayerPositionUpdate:
                HandlePositionUpdate(reader, whoAmI);
                break;
            // ... 其他消息处理
        }
    }

    private void HandlePositionUpdate(BinaryReader reader, int sender)
    {
        // 处理位置数据
        Vector2 position = reader.ReadVector2();
        // ...
    }
}

    

本模组内

在上一段落中可以看到,通过枚举ID可以简单的实现对于不同包的分流,但也导致了一个痛点——每个包类型都需要一个枚举键,进而导致枚举类的膨胀,并且难以扩展——这就是Everglow的网络同步系统希望解决的部分。

本模组提供了PacketResolver类用于统一处理包,并使用IPacketIPacketHandler来简化包的操作,使编码者仅需关注读写部分,达到与tModLoader提供的接口类似的效果。

示例

      // 统一同步玩家的鼠标位置
public void SyncMousePosition(bool fromServer)
{
      PacketResolver.Send(new MousePositionSyncPacket(mouseWorld), fromServer, Player);
}

// 包类
public class MousePositionSyncPacket : IPacket
{
      public MousePositionSyncPacket()
      {
      }

      public MousePositionSyncPacket(Vector2 mouseWorld)
      {
          this.mouseWorld = mouseWorld;
      }

      public Vector2 mouseWorld;

    // 在此仅进行数据读取与保存,不使用
      public void Receive(BinaryReader reader, int whoAmI)
      {
          mouseWorld = reader.ReadVector2();
      }

      public void Send(BinaryWriter writer)
      {
          writer.WriteVector2(mouseWorld);
      }
}

// 包处理器类
[HandlePacket(typeof(MousePositionSyncPacket))] // 通过Attribute绑定处理器对应的包类型
public class MousePositionSyncPacketHandler : IPacketHandler
{
      public void Handle(IPacket packet, int whoAmI)
      {
          var player = Main.player[whoAmI];
          var mp = player.GetModPlayer<EverglowPlayer>();
          mp.mouseWorld = ((MousePositionSyncPacket)packet).mouseWorld;
          if (NetUtils.IsServer)
          {
              mp.SyncMousePosition(true); // 将其转发给各个客户端
          }
      }
}