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

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

准备工作

目前魔兽的版本是8.1.5,魔兽插件都在<\World of Warcraft_retail_\Interface\AddOns>这个目录下,所以我们先建一个目录: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目录下。本文需要用到以下的库:

现在我们有了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

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


image.png

聊天命令和配置

我们可以使用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时候就会弹出以下信息:


image.png

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的效果:


image.png

让提示消息更突出

我们可以使用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

效果如下:


image.png

在不同的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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,874评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,102评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,676评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,911评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,937评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,935评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,860评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,660评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,113评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,363评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,506评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,238评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,861评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,486评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,674评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,513评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,426评论 2 352

推荐阅读更多精彩内容