Ace3魔兽世界插件开发之旅一-WelcomeHome

33次阅读

共计 10300 个字符,预计需要花费 26 分钟才能阅读完成。

本文一步一步讲解如何通过 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: Ace3

embeds.xml
Core.lua

Hello World

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

WelcomeHome = LibStub("AceAddon-3.0"):NewAddon("WelcomeHome", "AceConsole-3.0")

function WelcomeHome:OnInitialize()
    -- Called when the addon is loaded
end

function WelcomeHome:OnEnable()
    -- Called when the addon is enabled
end

function WelcomeHome:OnDisable()
    -- Called when the addon is disabled
end

第一行使用 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 loaded
end

function WelcomeHome:OnEnable()
    self:RegisterEvent("ZONE_CHANGED")
end

function 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.message
end

function WelcomeHome:SetMessage(info, newValue)
    self.message = newValue
end

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())
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!"
end
function WelcomeHome:ChatCommand(input)
    if not input or input:trim() == "" then
        InterfaceOptionsFrame_OpenToCategory(self.optionsFrame)
    else
        LibStub("AceConfigCmd-3.0"):HandleCommand("wh", "WelcomeHome", input)
    end
end

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.message
end

function WelcomeHome:SetMessage(info, newValue)
    self.db.profile.message = newValue
end

function 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: Ace3

embeds.xml
Core.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.message
end

function WelcomeHome:SetMessage(info, newValue)
    self.db.profile.message = newValue
end

function 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

function WelcomeHome:ChatCommand(input)
    if not input or input:trim() == "" then
        InterfaceOptionsFrame_OpenToCategory(self.optionsFrame)
    else
        LibStub("AceConfigCmd-3.0"):HandleCommand("wh", "WelcomeHome", input)
    end
end

function WelcomeHome:OnEnable()
    self:RegisterEvent("ZONE_CHANGED")
end

function 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

正文完
 0