模组:制作指南/APIs/Events
← 模组:目录
SMAPI 提供了几个 C#事件,这些事件使模组在某些事情发生时(例如,玩家放置一个对象时)做出响应,或者定期运行代码(例如,每个更新周期一次)
常见问题
什么是事件(events) ?
事件使你可以在发生某些事情时运行代码。可以在引发“事件”(发生的情况)时添加任意数量的“事件处理程序”(调用方法)。 可以将事件和处理程序视为“when...then”语句:
存档已加载 <-- 事件 然后运行我的代码 <-- 事件处理程序
有关详细信息,请参阅C# 编程指南中的事件中的“事件”。
如何使用它们?
通常将事件处理程序添加到 Entry 方法中,可以随时添加和删除它们。例如,在每天开始时打印一条消息。首先,从下面的列表中选择适当的事件 (GameLoop.DayStarted), 然后添加一个事件处理程序,并在方法代码中执行以下操作:
/// <summary>模组的主要入口点。</summary>
public class ModEntry : Mod
{
/**********
** 公共方法
*********/
/// <summary>模组入口点,加载模组后自动调用</summary>
/// <param name="helper">提供用于编写模组的简化API</param>
public override void Entry(IModHelper helper)
{
// 事件 += 方法
helper.Events.GameLoop.DayStarted += this.OnDayStarted;
}
/**********
** 私有方法
*********/
/// <summary>在新的一天开始后调用的方法</summary>
/// <param name="sender">事件对象</param>
/// <param name="e">事件参数</param>
private void OnDayStarted(object sender, DayStartedEventArgs e)
{
this.Monitor.Log("新的一天到来了!");
}
}
提示:不需要记住方法参数。在 Visual Studio 中,输入 helper.Events.GameLoop.SaveLoaded +=
然后按 TAB 来自动生成方法
事件如何呈现到游戏中?
每次游戏计时(游戏更新其状态并呈现到屏幕时)都会引发事件,每秒60次。 一个事件可能会引发多次(例如,如果玩家同时按下两个键),但是大多数事件不会每秒引发60次(例如,玩家不太可能每秒按下60个按钮)
事件处理程序是“同步”运行的:游戏暂停时模组的代码不会运行,因此没有更改冲突的风险。由于代码运行非常迅速,因此除非你的代码异常缓慢,否则玩家不会注意到任何延迟。就是说,当使用诸如 UpdateTicked 或者 Rendered 应该缓存繁重的操作(例如加载资源),而不是在每个刻度中重复执行这些操作,以免影响性能。
如果模组更改了事件的发起?
事件根据游戏状态的快照引发,这通常是“但不一定”是当前游戏状态
例如,考虑这种情况:
- 菜单 GameMenu 打开了
- SMAPI 引发 MenuChanged 事件,并且模组 A 和 B 正在监听
- 模组 A 接收了事件并关闭了菜单
- 模组 B 接收了事件
每个模组仍在处理菜单打开的 MenuChanged 事件,即使第一个模组已将菜单关闭。SMAPI 将在下一个刻度时为关闭的菜单引发一个新的 MenuChanged 事件
这很少会影响模组,但是如果你需要当前状态,则需要牢记 (例如考虑用 Game1.activeClickableMenu 代替 e.NewMenu)
事件
可用的事件记录在下面
内容
this.Helper.Events.Content具有关于从内容管线加载的素材的事件。
事件 | 概述 | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
#AssetRequested | 当一个素材通过内容管线被请求时,触发此事件。此素材无需已加载(例如,游戏会自动检查其存在性)。
可使用如下事件参数以注册欲应用的更改;素材被真正加载后会自动应用这些更改。参见内容API以获取更多信息。 若在同一时刻素材被请求多次(例如,其中一次请求检查存在性,另一次请求用于加载素材),则SMAPI可能仅调用该事件一次,并在余下的次数中使用缓存结果。 事件参数:
| ||||||||||||||||||
#AssetsInvalidated | 模组可以使缓存中某些素材失效,以便在下次请求这些素材时重新加载它们。此事件正是在素材失效后触发。 若素材会被自动重新加载(包括asset propagation[1]),则在其前触发此事件。
待到下次加载素材(包括asset propagation),此AssetRequested事件会被再次触发。 事件参数:
| ||||||||||||||||||
#AssetReady | 在素材文件被内容管线加载后、在应用了任何由AssetRequested给出的模组编辑后触发。
此事件仅在某些东西通过内容管线请求了素材的情况下触发。使内容缓存中的某个素材失效并不意味着会自动重新加载此素材。 事件参数:
| ||||||||||||||||||
#LocaleChanged | 在游戏语言更改后触发。对于非英语玩家,在游戏切换到先前选择的语言时,此事件可能会在启动时触发。
事件参数:
|
- ↑ Asset propagation 是SMAPI的一个功能,用于在模组修改素材数据后自动将修改同步到游戏。
显示
this.Helper.Events.Display具有关于UI和关于在屏幕上绘图的事件。
事件 | 概述 | |||||||||
---|---|---|---|---|---|---|---|---|---|---|
#MenuChanged | 在打开/关闭/替换游戏菜单时触发。
事件参数:
| |||||||||
#Rendering | 游戏在绘制新的一帧时触发此事件。确切地说,只要开启SpriteBatch即会触发此事件。调用此事件后,仍然可能开启和关闭SpriteBatch多次,但每帧只会调用一次此事件。此事件并不用于在屏幕上绘图,因为此事件绘制的所有图形会在随后绘制此帧时被覆盖。
事件参数:
| |||||||||
#Rendered | 游戏每帧绘制各SpriteBatch后触发此事件。确切地说,是在最后一个SpriteBatch即将渲染到屏幕上时触发此事件。由于游戏可能在同一帧内开启/关闭SpriteBatch多次,因此事件参数给出的SpriteBatch可能不会完全包含此帧的图形或已渲染的图形。而所有绘制到参数e.SpriteBatch的图形会置于所有原版内容(包括菜单、UI、光标)顶层。
事件参数:
| |||||||||
#RenderingWorld | 在游戏世界即将绘制到屏幕时触发此事件。此事件并不用于在屏幕上绘图,因为游戏绘图会覆盖它的绘图。
事件参数:
| |||||||||
#RenderedWorld | 在游戏世界被写入SpriteBatch后、但尚未绘制到屏幕时触发此事件。事件参数e.SpriteBatch中的图形将画在世界上层,但会画在菜单、用户界面图标或光标下层。
事件参数:
| |||||||||
#RenderingActiveMenu | 在打开菜单后(即Game1.activeClickableMenu != null)、但尚未将此菜单绘制到屏幕时触发此事件。包括游戏的内部菜单,例如标题界面。事件参数e.SpriteBatch中的图形将被画在菜单下层。
事件参数:
| |||||||||
#RenderedActiveMenu | 若打开一个菜单(即Game1.activeClickableMenu != null),则在此菜单加入SpriteBatch后、但尚未渲染到屏幕时触发此事件。事件参数e.SpriteBatch中的图形会出现在菜单和菜单光标上层。
事件参数:
| |||||||||
#RenderingHud | 在将玩家UI(物品栏、时钟等)元素绘制到屏幕前触发此事件。此时原版游戏的UI可能被隐藏(例如,打开菜单)。事件参数e.SpriteBatch中的图形可能会出现在HUD下层。
事件参数:
| |||||||||
#RenderedHud | 在在将玩家UI(物品栏、时钟等)元素绘制到SpriteBatch后、但尚未渲染到屏幕时触发此事件。此时原版游戏的UI可能被隐藏(例如,打开菜单)。事件参数e.SpriteBatch中的图形会出现在HUD上层。
事件参数:
| |||||||||
#WindowResized | 更改游戏窗口尺寸时触发此事件。
事件参数:
| |||||||||
#RenderingStep | (专用) 在游戏渲染周期中的特定步骤之前触发。这提供了对渲染逻辑的更精细控制,但更容易受到游戏更新中变化的影响。如果可能的话,请考虑使用其他渲染事件之一。
事件参数:
| |||||||||
#RenderedStep | 专用 在游戏渲染周期中的特定步骤之后触发。这提供了对渲染逻辑的更精细控制,但更容易受到游戏更新中变化的影响。如果可能的话,请考虑使用其他渲染事件之一。
事件参数:
|
游戏循环
this.Helper.Events.GameLoop提供了与游戏的更新循环相关的事件。游戏的更新循环约运行60次/秒(即每秒经历60个时刻),以运行状态更改、动作处理等游戏逻辑。这些事件往往很实用,但应当考虑Input等语义事件适用的场景。
事件 | 概述 | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
#GameLaunched | 在游戏启动后触发。即,正好在第一个更新时刻前。此事件对于每个游戏会话仅触发1次(与加载存档无关)。所有的模组都会在该时间节点被加载和初始化,因此这是设置模组整合的好时机。 | ||||||||||||
#UpdateTicking UpdateTicked |
在游戏状态更新前/后触发(1秒约为60个更新时刻)。
事件参数:
| ||||||||||||
#OneSecondUpdateTicking OneSecondUpdateTicked |
在游戏状态更新前/后触发,但每秒仅1次。
事件参数:
| ||||||||||||
#SaveCreating SaveCreated |
在游戏创建存档文件前/后触发(在新存档的介绍部分之后)。在所有模组都处理完此事件之前,存档不会被写入。此事件在某种意义上是专用的,因为在此时间节点世界并没有完全初始化;在大多数情况下,您应当转而使用DayStarted、Saving、Saved事件。 | ||||||||||||
#Saving Saved |
在游戏向存档文件写入数据前/后触发(除了存档初创时)。在所有模组都处理完此事件之前,存档不会被写入。此事件也会对多人游戏中的农场帮手触发。 | ||||||||||||
#SaveLoaded | 在加载存档后(包括创建新存档后的第一天),或连接到多人游戏后触发。此事件正好发生于DayStarted之前;在此时间节点存档文件会被读取,且Context.IsWorldReady为 true。
此事件在保存存档后并不会触发;若您希望在每日开始时做些什么,请另行参阅DayStarted。 | ||||||||||||
#DayStarted | 在游戏中新的一天开始后触发,或在连接到多人游戏后触发。在此时间节点,所有事情都已初始化完毕。(欲在新的一天初始化之前运行代码,请另行参阅DayEnding instead。) | ||||||||||||
#DayEnding | 在游戏结束当天前触发。具体而言,是发生在初始化次日和Saving之前。 | ||||||||||||
#TimeChanged | 在游戏内的时间变化时触发。即,每经过游戏中的10分钟即触发一次。
事件参数:
| ||||||||||||
#ReturnedToTitle | 在游戏返回到标题界面后触发。 |
输入
this.Helper.Events.Input具有在玩家使用手柄、键盘或鼠标时触发的事件。可与Input API搭配使用以获取更多信息或抑制输入。 to access more info or suppress input.
事件 | 概述 | |||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
#ButtonsChanged | 在玩家按下/松开键盘、鼠标或手柄上的任意按钮时触发。这包括鼠标点击。若玩家同时按下/松开多个键,则仅触发一次。
事件参数:
| |||||||||||||||
#ButtonPressed ButtonReleased |
在玩家按下/松开键盘、鼠标或手柄上的任意按钮时触发。这包括鼠标点击。若玩家同时按下/松开多个键,则仅触发一次。
事件参数:
| |||||||||||||||
#CursorMoved | 在玩家移动游戏内的光标时触发。
事件参数:
| |||||||||||||||
#MouseWheelScrolled | 在玩家滚动鼠标滚轮后触发。
事件参数:
|
多人
this.Helper.Events.Multiplayer具有与多人消息和连接相关的事件。
事件 | 概述 | |||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
#PeerContextReceived | 在玩家接收到模组上下文后触发。此事件会对于包括房主和农场帮手在内的所有玩家触发,即使联机玩家未安装SMAPI。这也是消息能够通过SMAPI发送给玩家的最早时间点。
此事件会在游戏允许连接前立即触发,此时玩家尚未存在于游戏中。当连接到房主时,Game1.IsMasterGame或Context.IsMultiplayer等上下文字段可能尚未设置;您可以检查e.Peer.IsHost以获悉当前玩家是否为农场帮手,因为连接时会最先收到此上下文。假设另外的模组不会阻止连接,此连接将在下一个时刻被允许。 事件参数:
| |||||||||||||||
#PeerConnected | 在游戏准许来自另一玩家的连接后触发。此事件会对于包括房主和农场帮手在内的所有玩家触发。此事件发生在PeerContextReceived之后。
在此时间点,玩家已连接到游戏,因此可以使用Game1.server.kick之类的方法。 事件参数:
| |||||||||||||||
#ModMessageReceived | 在某个模组消息 通过网络被接受后触发。
事件参数:
| |||||||||||||||
#PeerDisconnected | 在与玩家的连接断开后触发。
事件参数:
|
玩家
this.Helper.Events.Player具有当玩家数据更改时触发的事件。
目前,这些事件仅对当前玩家触发。在未来的版本中,可能会修改此特性。因此,若仅希望处理当前玩家,请先检查e.IsLocalPlayer。
事件 | 概述 | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
#InventoryChanged | 玩家背包增减物品时触发。
事件参数:
| ||||||||||||||||||
#LevelChanged | 在一个玩家技能等级发生变化后触发. 当玩家技能等级提升的第一时间立马触发 (不是在玩家睡觉后游戏提示时触发).
事件参数:
| ||||||||||||||||||
#Warped | 当玩家移动到新地点的时候触发(农场到车站,玩家所在矿井层数发生变化也算).
事件参数:
|
世界
this.Helper.Events.World具有在游戏世界发生变化时触发的事件。
事件 | 概述 | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
#LocationListChanged | 在添加或移除地点后触发(包括建筑内部)。
事件参数:
| ||||||||||||||||||
#BuildingListChanged | 在任何地点添加/移除建筑后触发。
对于和所在地点一并添加进游戏的建筑,此事件并不触发。若需处理此类建筑,请使用LocationListChanged并检查e.Added.OfType<BuildableGameLocation>() → buildings。 事件参数:
| ||||||||||||||||||
#ChestInventoryChanged | 在将物品加入/拿出箱子时触发。
事件参数:
| ||||||||||||||||||
#DebrisListChanged | 在任何地点添加/移除掉落物后触发(包括掉落或生成的漂浮物)。
对于和所在地点一并添加进游戏的掉落物,此事件并不触发。若需处理此类掉落物,请使用LocationListChanged并检查e.Added → debris。 事件参数:
| ||||||||||||||||||
#LargeTerrainFeatureListChanged | 在任何地点添加/移除大型地形特征(例如灌木)后触发。
对于和所在地点一并添加进游戏的大型地形特征,此事件并不触发。若需处理此类地形特征,请使用LocationListChanged并检查e.Added → largeTerrainFeatures。 事件参数:
| ||||||||||||||||||
#NpcListChanged | 在任何地点添加/移除NPC后触发(包括村民、马、祝尼魔、怪物和宠物)。
对于和所在地点一并添加进游戏的NPC,此事件并不触发。若需处理此类NPC,请使用LocationListChanged并检查e.Added → characters。 事件参数:
| ||||||||||||||||||
#ObjectListChanged | 在任意地点添加/移除物体后触发(包括机器、家具、围栏等)。对于漂浮物,请另见DebrisListChanged。
对于和所在地点一并添加进游戏的物体,此事件并不触发。若需处理此类物体,请使用LocationListChanged并检查e.Added → objects。 事件参数:
| ||||||||||||||||||
#TerrainFeatureListChanged | 在任何地点添加/移除地形特征后触发(包括树、耕地、地板等)。对于灌木,另请参阅LargeTerrainFeatureListChanged。
对于和所在地点一并添加进游戏的地形特征,此事件并不触发。若需处理此类地形特征,请使用LocationListChanged并检查e.Added → terrainFeatures。 事件参数:
|
特殊
this.Helper.Events.Specialised 针对特殊情况的事件. 大多数模组不应使用这些功能
事件 | 概述 | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
#LoadStageChanged | 当游戏加载过程中低级阶段发生变化时,会触发此事件,以供需要在加载过程特定时间点运行代码的模组使用。未来版本中,可用的阶段或它们发生的时间可能会在没有警告的情况下发生变化(例如,由于游戏加载过程的更改),因此使用此事件的模组更容易出错或产生漏洞。大多数模组应使用游戏循环事件代替。
事件参数:
| ||||||||||||
#UnvalidatedUpdateTicking UnvalidatedUpdateTicked |
该事件在游戏状态更新前/后被触发(大约每秒60次),且不受SMAPI正常验证的限制。此事件不是线程安全的,可能会在游戏逻辑异步运行时被调用。在此方法中更改游戏状态可能会导致游戏崩溃或损坏正在进行中的存档。除非您完全了解您的代码将在何种上下文中运行,否则不要使用此事件。使用此事件将在SMAPI控制台中触发警告。
事件参数:
|
进阶
变化监控
您可能希望处理一个没有对应事件的变化(例如,一个游戏内事件结束,一封信被添加到信箱中等)。通常,您可以通过处理一个通用事件来实现这一点(如UpdateTicked),并检测您关注的值何时发生了变化。例如,下面是一个完整的模组,它会在游戏内事件结束时记录一条消息:
/// <summary>The main entry point for the mod.</summary>
public class ModEntry : Mod
{
/*********
** Fields
*********/
/// <summary>The in-game event detected on the last update tick.</summary>
private Event LastEvent;
/*********
** Public methods
*********/
/// <summary>The mod entry point, called after the mod is first loaded.</summary>
/// <param name="helper">Provides simplified APIs for writing mods.</param>
public override void Entry(IModHelper helper)
{
helper.Events.GameLoop.UpdateTicked += this.OnUpdateTicked;
}
/*********
** Private methods
*********/
/// <summary>The method invoked when the game updates its state.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void OnUpdateTicked(object sender, EventArgs e)
{
if (this.LastEvent != null && Game1.CurrentEvent == null)
this.Monitor.Log($"Event {this.LastEvent.id} just ended!");
this.LastEvent = Game1.CurrentEvent;
}
}
自定义优先级
SMAPI 默认按照事件处理器被注册的顺序调用它们,因此每次都会先调用最先注册的事件处理器。然而,这并不总是可预测的,因为它取决于模组的加载顺序以及每个模组注册其处理器的时间。此外,这个顺序也是“实现细节”的一部分,因此并不保证必然如此。
若需控制顺序,可以使用[EventPriority]
属性来指定事件优先级,包括:Low(后于大部分处理器)、Default、High (先于大部分处理器)或自定义值(例如High + 1优先于High)。请仅在必需时使用;不同模组间的处理器顺序是不可靠的(例如,其他模组也可能改变自己的优先级)。
/// <summary>The main entry point for the mod.</summary>
public class ModEntry : Mod
{
/*********
** Public methods
*********/
/// <summary>The mod entry point, called after the mod is first loaded.</summary>
/// <param name="helper">Provides simplified APIs for writing mods.</param>
public override void Entry(IModHelper helper)
{
helper.Events.GameLoop.UpdateTicked += this.OnUpdateTicked;
}
/*********
** Private methods
*********/
/// <summary>The method invoked when the game updates its state.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
[EventPriority(EventPriority.High)]
private void OnUpdateTicked(object sender, EventArgs e)
{
this.Monitor.Log("Update!");
}
}