本文一步一步讲解如何通过Ace3开发框架构建一个WelcomeHome插件。文中大部分内容翻译自gamepedia,原文地址:https://wow.gamepedia.com/WelcomeHome_-_Your_first_Ace3_Addon。
由于英文水平有限,有不对之处还望指正,谢谢!

准备工作

目前魔兽的版本是8.1.5,魔兽插件都在<World of Warcraft_retail_InterfaceAddOns>这个目录下,所以我们先建一个目录:WelcomeHome,当WOW在AddOns目录下发现一个目录时,它会去找这个目录下跟目录同名的TOC文件,这个TOC文件包含了本插件所有文件的清单,WOW会使用这个文件来加载这个插件。下面是我们这个WelcomeHome.TOC文件的基本骨架:

## Interface: 81500## Version: 0.1## Title: Welcome Home## Author: xiaop## Notes: 炉石的时候显示欢迎信息.Core.lua

这时候需要建一个空的Core.lua文件,用来存放插件的代码。现在我们先让它空着,这时候登录WOW在人物选择界面点击插件可以看到WelcomeHome这个插件,虽然它啥也干不了。

引入Ace3库

要想让我们的插件具备具体功能,我们需要引入Ace3相关的库。Ace3使用了一个叫做“嵌入式库”的概念,它允许模块开发者在其他模块加载了相同库的时候不需要再复制一份代码。我们可以在 http://www.wowace.com/addons/ace3/files/这里下载最新的Ace3库,然后解压到插件目录的Libs目录下。本文需要用到以下的库:

  • AceAddon-3.0
  • AceDB-3.0
  • AceConfig-3.0
  • AceConsole-3.0
  • AceEvent-3.0
  • AceGUI-3.0
  • CallbackHandler-1.0
  • LibStub

现在我们有了Ace3相关的库,但是WOW并不知道如何加载他们,我们需要一个embeds.xml文件来告诉WOW需要加载哪些文件,于是我们新建一个embeds.xml文件,内容如下:

<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/..\FrameXML\UI.xsd">    <Script file="Libs\LibStub\LibStub.lua"/>    <Include file="Libs\CallbackHandler-1.0\CallbackHandler-1.0.xml"/>    <Include file="Libs\AceAddon-3.0\AceAddon-3.0.xml"/>    <Include file="Libs\AceEvent-3.0\AceEvent-3.0.xml"/>    <Include file="Libs\AceDB-3.0\AceDB-3.0.xml"/>    <Include file="Libs\AceConsole-3.0\AceConsole-3.0.xml"/>    <Include file="Libs\AceGUI-3.0\AceGUI-3.0.xml"/>    <Include file="Libs\AceConfig-3.0\AceConfig-3.0.xml"/></Ui>

Script标签表示需要引入的lua文件,Include标签表示要引入的xml文件。需要注意的是LibStub必须先加载,因为其他库都依赖它。其他文件也需要注意使用顺序,原则就是被依赖的库需要先加载。
现在我们更新一下TOC文件,在core.lua之前引入embeds.xml文件,以保证在core.lua执行之前所有的库已被加载并可以使用。

## Interface: 81500## Version: 0.1## Title: Welcome Home## Author: xiaop## Notes: 炉石的时候显示欢迎信息.## SavedVariables: WelcomeHomeDB## OptionalDeps: Ace3## X-Embeds: Ace3embeds.xmlCore.lua

Hello World

下面我们编辑一下Core.lua文件,加入Ace3最基本的结构:

WelcomeHome = LibStub("AceAddon-3.0"):NewAddon("WelcomeHome", "AceConsole-3.0")function WelcomeHome:OnInitialize()    -- Called when the addon is loadedendfunction WelcomeHome:OnEnable()    -- Called when the addon is enabledendfunction WelcomeHome:OnDisable()    -- Called when the addon is disabledend

第一行使用NewAddon方法创建一个AceAddon类的实例,因为我们会用到聊天窗口和斜杠命令,我们还混入了AceConsole类。
接下来是三个重写的方法OnInitialize, OnEnable, 和 OnDisable。OnInitialize只会在UI加载的时候执行一次,后面两个分别在插件被启用和禁用的时候执行。
下面我们假设插件正在加载,我们想在聊天窗口打印一句“Hello World”,只需要在OnEnable方法中添加一行代码:

function WelcomeHome:OnEnable()    self:Print("Hello World!")end

这样,在我们重新进入WOW的时候就会在聊天窗口底部看到这句“Hello World”。

响应事件

这个插件的目标是在WOWer回到炉石所在区域的时候提示欢迎信息,那么我们要如何知道WOWer已经回到炉石所在区域了呢?很简单,我们只需要响应某一个ZONE_CHANGED事件即可,这个事件会在WOWer进入一个新区域的时候触发。
WoW是事件驱动的,我们插件能做的事情都要依赖于各种事件,否则我们啥也干不了。
为了响应事件我们需要混入AceEvent库,然后通过RegisterEvent方法监听ZONE_CHANGED事件,在事件触发的时候调用ZONE_CHANGED方法打印一句“You have changed zones!”在聊天窗口。Core.lua的代码如下:

WelcomeHome = LibStub("AceAddon-3.0"):NewAddon("WelcomeHome", "AceConsole-3.0", "AceEvent-3.0")function WelcomeHome:OnInitialize()    -- Called when the addon is loadedendfunction WelcomeHome:OnEnable()    self:RegisterEvent("ZONE_CHANGED")endfunction WelcomeHome:ZONE_CHANGED()    self:Print("You have changed zones!")end

无需重启游戏,使用/reload命令即可重新载入修改后的插件。

使用WoW API

现在我们有一个方法可以在WoWer切换区域的时候被调用,那么我们如何知道他现在在哪个区域,他的炉石又绑在哪里呢?这个可以在WoW API中找到答案,

  • GetBindLocation 方法会返回子区域的名字,其中包含了炉石所在地
  • GetSubZoneText 方法返回WoWer当前所在子区域,如果当前没有子区域,会返回空串。

下面修改代码来看看效果,我们在切换区域的时候把上面两个方法的返回值打印到聊天窗口中,代码如下:

function WelcomeHome:ZONE_CHANGED()    self:Print(GetBindLocation())    self:Print("================")    self:Print(GetSubZoneText())end

这样我们可以在切换区域的时候打印出炉石所在区域和当前的区域。效果如下:

聊天命令和配置

我们可以使用AceConfig来注册一个option table,以支持开箱即用的斜杠命令。

WelcomeHome = LibStub("AceAddon-3.0"):NewAddon("WelcomeHome", "AceConsole-3.0", "AceEvent-3.0")local options = {    name = "WelcomeHome",    handler = WelcomeHome,    type = 'group',    args = {    },}function WelcomeHome:OnInitialize()    -- Called when the addon is loaded    LibStub("AceConfig-3.0"):RegisterOptionsTable("WelcomeHome", options, {"welcomehome", "wh"})end

重载UI以后在聊天窗口输入/WelcomeHome或者/wh,可以看到插件的提示信息(插件名字,描述, 可用命令等)。
下面我们添加一个让用户可以改变提示文字的命令,命令名叫msg,可以携带一个文本参数,使用get和set方法来操作底层变量,:

local options = {    name = "WelcomeHome",    handler = WelcomeHome,    type = 'group',    args = {        msg = {                type = "input",                name = "Message",                desc = "The message to be displayed when you get home.",                usage = "<Your message>",                get = "GetMessage",                set = "SetMessage",            },    },}function WelcomeHome:GetMessage(info)    return self.messageendfunction WelcomeHome:SetMessage(info, newValue)    self.message = newValueendfunction WelcomeHome:ZONE_CHANGED()    self:Print("welcome msg is:",self.message)    self:Print("================")    self:Print("your heartstone set is : ",GetBindLocation())    self:Print("================")    self:Print("current subzone is : ",GetSubZoneText())end

执行命令/wh msg helloworld,然后再切换区域的i时候就会弹出以下信息:

GUI和暴雪接口选项

我们应该不仅满足于使用命令来实现插件,暴雪从2.4补丁开始重做了接口选项,可以将插件添加到游戏的“插件”标签。为了做到这一点,我们需要稍微改变以下代码的处理方式:

function WelcomeHome:OnInitialize()    -- Called when the addon is loaded    LibStub("AceConfig-3.0"):RegisterOptionsTable("WelcomeHome", options)    self.optionsFrame = LibStub("AceConfigDialog-3.0"):AddToBlizOptions("WelcomeHome", "WelcomeHome")    self:RegisterChatCommand("welcomehome", "ChatCommand")    self:RegisterChatCommand("wh", "ChatCommand")    WelcomeHome.message = "Welcome Home!"endfunction WelcomeHome:ChatCommand(input)    if not input or input:trim() == "" then        InterfaceOptionsFrame_OpenToCategory(self.optionsFrame)    else        LibStub("AceConfigCmd-3.0"):HandleCommand("wh", "WelcomeHome", input)    endend

AddToBlizOptions方法返回一个frame,我们可以用这个frame来打开暴雪的接口选项。然后我们用ChatCommand方法来控制插件的行为表现,如果输入的内容为空,则会直接打开暴雪接口选项,这样我们就可以通过GUI来配置我们的插件,否则还是按照命令原来的显示逻辑在聊天窗口中显示。
以下是输入/wh的效果:

让提示消息更突出

我们可以使用UIErrorsFrame 来将提示消息显示在屏幕的指定位置,简单来说只需要添加一行代码:

function WelcomeHome:ZONE_CHANGED()    self:Print("welcome msg is:",self.message)    self:Print("================")    self:Print("your heartstone set is : ",GetBindLocation())    self:Print("================")    self:Print("current subzone is : ",GetSubZoneText())    UIErrorsFrame:AddMessage(self.message, 1.0, 1.0, 1.0, 5.0)end

效果如下:

在不同的session之间保存配置

目前我们的欢迎消息在我们退出游戏以后就丢失了,WoW提供了一种在不同的session之间保存配置的方法叫做Saved Variables,而Ace的方法是AceDB,通过AceDB我们将提示消息持久保存。

local defaults = {    profile = {        message = "Welcome Home!"    },}function WelcomeHome:GetMessage(info)    -- return self.message    return self.db.profile.messageendfunction WelcomeHome:SetMessage(info, newValue)    self.db.profile.message = newValueendfunction WelcomeHome:OnInitialize()    -- 命令行的方式    -- LibStub("AceConfig-3.0"):RegisterOptionsTable("WelcomeHome", options, {"welcomehome", "wh"})        -- GUI的方式        self.db = LibStub("AceDB-3.0"):New("WelcomeHomeDB", defaults, true)        LibStub("AceConfig-3.0"):RegisterOptionsTable("WelcomeHome", options)    self.optionsFrame = LibStub("AceConfigDialog-3.0"):AddToBlizOptions("WelcomeHome", "WelcomeHome")    self:RegisterChatCommand("welcomehome", "ChatCommand")    self:RegisterChatCommand("wh", "ChatCommand")    WelcomeHome.message = "Welcome Home!"end

至此,这个插件基本就完成了,虽然功能比较简单,但是是一个很好的入门项目.

完整代码

  • WelcomeHome.TOC
## Interface: 81500## Version: 0.1## Title: Welcome Home## Author: xiaop## Notes: 炉石的时候显示欢迎信息.## SavedVariables: WelcomeHomeDB## OptionalDeps: Ace3## X-Embeds: Ace3embeds.xmlCore.lua
  • embeds.xml
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/..\FrameXML\UI.xsd">    <Script file="Libs\LibStub\LibStub.lua"/>    <Include file="Libs\CallbackHandler-1.0\CallbackHandler-1.0.xml"/>    <Include file="Libs\AceAddon-3.0\AceAddon-3.0.xml"/>    <Include file="Libs\AceEvent-3.0\AceEvent-3.0.xml"/>    <Include file="Libs\AceDB-3.0\AceDB-3.0.xml"/>    <Include file="Libs\AceConsole-3.0\AceConsole-3.0.xml"/>    <Include file="Libs\AceGUI-3.0\AceGUI-3.0.xml"/>    <Include file="Libs\AceConfig-3.0\AceConfig-3.0.xml"/></Ui>
  • Core.lua:
WelcomeHome = LibStub("AceAddon-3.0"):NewAddon("WelcomeHome", "AceConsole-3.0", "AceEvent-3.0")local options = {    name = "WelcomeHome",    handler = WelcomeHome,    type = 'group',    args = {        msg = {                type = "input",                name = "Message",                desc = "The message to be displayed when you get home.",                usage = "<Your message>",                get = "GetMessage",                set = "SetMessage",            },    },}local defaults = {    profile = {        message = "Welcome Home!"    },}-- 保存在db中就不需要这个变量了-- WelcomeHome.message = "Welcome Home!"function WelcomeHome:GetMessage(info)    -- return self.message    return self.db.profile.messageendfunction WelcomeHome:SetMessage(info, newValue)    self.db.profile.message = newValueendfunction WelcomeHome:OnInitialize()    -- 命令行的方式    -- LibStub("AceConfig-3.0"):RegisterOptionsTable("WelcomeHome", options, {"welcomehome", "wh"})        -- GUI的方式        self.db = LibStub("AceDB-3.0"):New("WelcomeHomeDB", defaults, true)        LibStub("AceConfig-3.0"):RegisterOptionsTable("WelcomeHome", options)    self.optionsFrame = LibStub("AceConfigDialog-3.0"):AddToBlizOptions("WelcomeHome", "WelcomeHome")    self:RegisterChatCommand("welcomehome", "ChatCommand")    self:RegisterChatCommand("wh", "ChatCommand")    WelcomeHome.message = "Welcome Home!"endfunction WelcomeHome:ChatCommand(input)    if not input or input:trim() == "" then        InterfaceOptionsFrame_OpenToCategory(self.optionsFrame)    else        LibStub("AceConfigCmd-3.0"):HandleCommand("wh", "WelcomeHome", input)    endendfunction WelcomeHome:OnEnable()    self:RegisterEvent("ZONE_CHANGED")endfunction WelcomeHome:ZONE_CHANGED()    self:Print("welcome msg is:",self.db.profile.message)    self:Print("================")    self:Print("your heartstone set is : ",GetBindLocation())    self:Print("================")    self:Print("current subzone is : ",GetSubZoneText())    UIErrorsFrame:AddMessage(self.db.profile.message, 1.0, 1.0, 1.0, 5.0)end
  • WelcomeHome目录结构:
│  Core.lua│  embeds.xml│  WelcomeHome.TOC│└─Libs    ├─AceAddon-3.0    │      AceAddon-3.0.lua    │      AceAddon-3.0.xml    │    ├─AceConfig-3.0    │  │  AceConfig-3.0.lua    │  │  AceConfig-3.0.xml    │  │    │  ├─AceConfigCmd-3.0    │  │      AceConfigCmd-3.0.lua    │  │      AceConfigCmd-3.0.xml    │  │    │  ├─AceConfigDialog-3.0    │  │      AceConfigDialog-3.0.lua    │  │      AceConfigDialog-3.0.xml    │  │    │  └─AceConfigRegistry-3.0    │          AceConfigRegistry-3.0.lua    │          AceConfigRegistry-3.0.xml    │    ├─AceConsole-3.0    │      AceConsole-3.0.lua    │      AceConsole-3.0.xml    │    ├─AceDB-3.0    │      AceDB-3.0.lua    │      AceDB-3.0.xml    │    ├─AceEvent-3.0    │      AceEvent-3.0.lua    │      AceEvent-3.0.xml    │    ├─AceGUI-3.0    │  │  AceGUI-3.0.lua    │  │  AceGUI-3.0.xml    │  │    │  └─widgets    │          AceGUIContainer-BlizOptionsGroup.lua    │          AceGUIContainer-DropDownGroup.lua    │          AceGUIContainer-Frame.lua    │          AceGUIContainer-InlineGroup.lua    │          AceGUIContainer-ScrollFrame.lua    │          AceGUIContainer-SimpleGroup.lua    │          AceGUIContainer-TabGroup.lua    │          AceGUIContainer-TreeGroup.lua    │          AceGUIContainer-Window.lua    │          AceGUIWidget-Button.lua    │          AceGUIWidget-CheckBox.lua    │          AceGUIWidget-ColorPicker.lua    │          AceGUIWidget-DropDown-Items.lua    │          AceGUIWidget-DropDown.lua    │          AceGUIWidget-EditBox.lua    │          AceGUIWidget-Heading.lua    │          AceGUIWidget-Icon.lua    │          AceGUIWidget-InteractiveLabel.lua    │          AceGUIWidget-Keybinding.lua    │          AceGUIWidget-Label.lua    │          AceGUIWidget-MultiLineEditBox.lua    │          AceGUIWidget-Slider.lua    │    ├─CallbackHandler-1.0    │      CallbackHandler-1.0.lua    │      CallbackHandler-1.0.xml    │    └─LibStub            LibStub.lua