维护提醒

BWIKI 全站将于 9 月 3 日(全天)进行维护,期间无法编辑任何页面或发布新的评论。

全站通知:

模组:制作指南/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 枚举类型(取 LinuxMacWindows 之一)。
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 属性)会同时清除所有玩家的信息,而非设置当前玩家的值。

另请参阅