全站通知:

模组:制作指南/APIs/Multiplayer

来自站务
跳到导航 跳到搜索

← 模组:目录

多人接口(multiplayer API)使模组可以支持多人游戏。虽然现在此接口的功能仍然很少,但在 SMAPI 的后续版本中会陆续添加更多功能。

方法

获取新的ID

某些情况下,游戏需要一个“多人ID”值,后者是一个唯一的 64 位数字。此 ID 主要用于数据同步,例如创建农场动物:

int animalID = this.Helper.Multiplayer.GetNewID();
var animal = new FarmAnimal("Cow", animalID, ownerID);

获取活跃地点

在合作模式下,游戏并不会把所有地点都同步给其他玩家。每个农场助手会接收他们当前地点、农场、农舍、农场建筑的信息。您可以按如下方式获取所有已同步地点的列表:

foreach (GameLocation location in this.Helper.Multiplayer.GetActiveLocations())
{
   // do stuff
}

获取联机玩家信息

您可以利用 GetConnectedPlayer(playerID) 和 GetConnectedPlayers() 方法查看连接到同一个服务器的其他玩家的基本信息。玩家连接到服务器后就可以立即调用这些命令,甚至不需要等游戏批准连接。大多数信息仅适用于也使用 SMAPI 的玩家。您可以访问如下字段:

字段 类型 描述
PlayerID long 玩家的唯一多人游戏ID。您可以将此ID传入游戏方法或需要唯一玩家ID的字段。
IsHost bool 若此玩家为房主则为 true;若为农场帮手则为 false。
IsSplitScreen bool 此玩家是否正在使用分屏模式在同一台电脑上玩多人游戏。
HasSmapi bool 此玩家是否安装了 SMAPI。
Platform GamePlatform (需要 SMAPI)玩家的操作系统,为 GamePlatform 枚举类型(取 Linux、Mac 或 Windows 之一)。
GameVersion ISemanticVersion (需要 SMAPI)玩家安装的游戏版本(形如 1.6.15)。
ApiVersion ISemanticVersion (需要 SMAPI)玩家安装的 SMAPI 版本(例如 2.11)。
Mods IEnumerable<IMultiplayerPeerMod> (需要 SMAPI) 玩家安装的模组。每个模组包括名称、唯一ID和版本。对于那些未安装 SMAPI 或有 SMAPI 但没有安装任何模组的玩家,此字段为空。
ScreenID int? 如果 IsSplitScreen 为 true,则此字段为玩家的屏幕ID。
GetMod(id) 此方法返回 IMultiplayerPeerMod (需要 SMAPI) 使用和 SMAPI 相同的大小写不敏感规则,返回指定模组ID对应的模组(若可用)。例如,此方法可用于检查是否安装了指定模组:if (peer.GetMod("Pathoschild.ContentPatcher") != null)。

下面的例子演示了如何记录当前联机玩家的信息:

foreach (IMultiplayerPeer peer in this.Helper.Multiplayer.GetConnectedPlayers())
{
    if (peer.HasSmapi)
    {
        // prints something like: "Found player -1634555469947451666 running Stardew Valley 1.5.2 and SMAPI 3.9.0 on Windows with 41 mods."
        this.Monitor.Log($"Found player {peer.PlayerID} running Stardew Valley {peer.GameVersion} and SMAPI {peer.ApiVersion} on {peer.Platform} with {peer.Mods.Count()} mods.");
    }
    else
    {
        // most info not available if they don't have SMAPI
        this.Monitor.Log($"Found player {peer.PlayerID} running Stardew Valley without SMAPI.");
    }
}

注意: 尚未被游戏批准连接的玩家也会被返回。尽管您可以通过 SMAPI 的接口发送和接收消息,但游戏本体可能并不知晓这些消息。如果您希望仅获取完全连接的玩家,可以按如下方式操作:

foreach (Farmer farmer in Game1.getOnlineFarmers())
{
   IMultiplayerPeer peer = this.Helper.Multiplayer.GetConnectedPlayer(farmer.UniqueMultiplayerID);
}

发送消息

您可以利用 SendMessage 方法向所有联机计算机(包括本地)的所有模组发送信息。接收方可以很精确(例如,某台计算机上的某个模组),也可以很泛泛(所有计算机上的所有模组)。尽管某台计算机上的某个模组不能给自己发消息,但可以给其他计算机上的同一模组发消息。

当您发送消息时,必须指定三件事:

  • 希望发送的数据。可以为一个单纯的数值(例如数字或字符串),或类实例。当您希望发送具有自定义构造函数的类实例时,请确保它同时具有默认构造函数。
  • 消息类型,接收方模组由此知道这是何种消息。因为模组一般会检查发送此消息的模组,所以不同模组可以有重名的消息类型。
  • 消息接收方。可以指定为玩家ID和/或模组ID的组合。默认发送给所有玩家和所有模组。

例如:

// broadcast a message to all mods on all computers
MyMessage message = new MyMessage(...); // create your own class with the data to send
this.Helper.Multiplayer.SendMessage(message, "MyMessageType");
// send a message to a specific mod on all computers
MyMessage message = new MyMessage(...);
this.Helper.Multiplayer.SendMessage(message, "MyMessageType", modIDs: new[] { this.ModManifest.UniqueID });

接收消息

您可以通过监听 helper.Events.Multiplayer.ModMessageReceived 事件 来接收消息。此事件参数会给出发送消息方,并使您能够将消息转换为对应的数据模型。

例如:

public override void Entry(IModHelper helper)
{
   helper.Events.Multiplayer.ModMessageReceived += this.OnModMessageReceived;
}

public void OnModMessageReceived(object sender, ModMessageReceivedEventArgs e)
{
   if (e.FromModID == this.ModManifest.UniqueID && e.Type == "ExampleMessageType")
   {
      MyMessageClass message = e.ReadAs<MyMessageClass>();
      // handle message fields here
   }
}

分屏数据

分屏多人模式中不同玩家共享整个游戏状态,因此同一个模组实例需要服务所有玩家(而不是每个玩家有单独的模组实例)。例如,在有 4 名玩家的情况下,UpdateTicked 事件在每次刷新时会被触发 4 次。游戏会自动管理其状态(例如 Game1.activeClickableMenu),但如果您的模组在其自定义字段中存储信息,您就需要手动处理这些逻辑了。

SMAPI 提供了 PerScreen<T> 以便操作。假设您需要存储某个值(其类型既可以像 int 这样,也可以为类实例),您可以创建对应的 只读 字段:

private readonly PerScreen<int> LastPlayerId = new PerScreen<int>(); // defaults to 0
private readonly PerScreen<SButton> LastButtonPressed = new PerScreen<SButton>(createNewState: () => SButton.None); // defaults to the given value

然后您就可以使用其字段和属性了:

成员 描述
Value 获取/设置(get/set)当前分屏玩家的相应值。如有需要,此字段会自动创建值。
GetActiveValues() 获取分屏玩家ID和相应值的列表(玩家需处于活跃状态且拥有对应值)
GetValueForScreen(screenId)
SetValueForScreen(screenId, value)
获取/设置指定分屏玩家ID的相应值,如有需要,创建相应值。
ResetAllScreens 清除所有分屏玩家的相应值。

示例

下面给出一个示例模组。此模组会做一个简单的游戏:房主按下一个键,农场帮手猜按下了哪个键:

internal class ModEntry : Mod
{
    private readonly PerScreen<SButton> LastButtonPressed = new PerScreen<SButton>();
    private readonly PerScreen<int> Score = new PerScreen<int>();

    public override void Entry(IModHelper helper)
    {
        helper.Events.Input.ButtonPressed += this.OnButtonPressed;
    }

    private void OnButtonPressed(object sender, ButtonPressedEventArgs e)
    {
        // main player changes key
        if (Context.IsMainPlayer)
        {
            this.LastButtonPressed.Value = e.Button;
            this.Monitor.Log("The main player changed the key. Can you guess it?", LogLevel.Info);
        }

        // farmhands try to guess the key
        else
        {
            SButton correctButton = this.LastButtonPressed.GetValueForScreen(0);
            if (correctButton != SButton.None)
            {
                if (e.Button == correctButton)
                {
                    this.Score.Value++;
                    this.LastButtonPressed.SetValueForScreen(0, SButton.None);
                    this.Monitor.Log($"Player #{Context.ScreenId + 1} correctly guessed the key: {e.Button}! Their score is now {this.Score.Value}.", LogLevel.Info);
                }
                else
                    this.Monitor.Log($"Player #{Context.ScreenId + 1} guessed incorrectly: {e.Button}.", LogLevel.Debug);
            }
        }
    }
}

提示: 您几乎总是应该将一个分屏字段标记为 readonly。重写整个字段(而非设置对应的 Value 属性)会同时清除所有玩家的信息,而非设置当前玩家的值。

另请参阅