魔兽世界的插件系统

简介

「魔兽世界」的插件是用 LuaXML 编写的扩展程序,作用是用于增强、修改和替换「魔兽世界」的用户界面,或者实现一些游戏本身不具备的功能,方式是使用 Lua 调用游戏提供的 API 来实现特定的逻辑。

在游戏领域中,插件地图 是两种常见的 UGC 形式,都是由用户/玩家来开发实现的游戏扩展,但是他们的侧重点有所不同,具体表现为:

  • 插件:通过脚本语言调用游戏接口来实现,更多的是对游戏当前内容进行增强、修改和替换
  • 地图:通过游戏提供的地图编辑器来实现,更多的是用于新增、补充游戏当前所没有的内容

在插件或者地图的开发过程中,体验也有所不同:

  • 插件:插件的开发者更接近于游戏团队中的「程序」职位,为游戏提供当前所没有的功能,工作内容大多数是在撰写代码
  • 地图:地图的开发这更接近于游戏团队中的「策划」职位,利用已有的工具来创造新地图,工作内容大多数是在关卡设计

常见插件

「魔兽世界」常见的插件有如下几类:

  • 界面增强
    • ElvUI:界面重塑插件,可以完全替换官方默认 UI,高度可定制化。
    • Bartender4:动作条替换插件,允许自定义动作条的位置、大小和透明度。
    • WeakAuras:用于创建自定义的图形提示,帮助玩家跟踪技能冷却、增益和减益等效果。
  • 战斗辅助
    • Deadly Boss Mods:提供副本和首领战斗的实时警报和计时提醒,帮助玩家更及时地了解战斗实况。
    • Details! Damage Meter:详细的伤害统计插件,帮助玩家分析战斗表现,了解场上输出和团队整体表现。
    • GTFO:提供声音警报,提醒玩家注意地面效果和危险区域。
  • 经济管理
    • TradeSkillMaster :帮助玩家管理拍卖行、制作和销售物品,优化经济效益。
    • Auctionator:简化拍卖行操作,提供快速的物品搜索和价格比对功能,适合日常交易。
    • Postal:改善邮件管理,提供批量发送和接收邮件的功能,方便玩家处理邮件。

「魔兽世界」的默认界面也是一个插件,可以在控制台输入 exportInterfaceFiles codeexportInterfaceFiles art 来导出默认界面的插件源码和美术素材

技术方案

所有的插件都位于一个名为 AddOns 的目录中,该目录的路径为 %WorldOfWarcraftFolder%\Interface\AddOns,其中 %WorldOfWarcraftFolder% 代表你的电脑中魔兽世界的安装位置,一般情况下,完整路径如下:

  • WindowsC:\Program Files\World of Warcraft\Interface\AddOns
  • MacOS/Applications/World of Warcraft/Interface/AddOns

一个「魔兽世界」的插件是由下面三种文件构成的:

  • TOC:描述文件,用于描述插件的名称、作者、用途和插件逻辑的入口
  • XML:界面布局文件,用于描述插件的 UI 布局
  • Lua:逻辑脚本文件,用于实现插件的具体逻辑

TOC —— 工程描述文件

toc 文件描述了插件工程的基本信息和逻辑入口,主要包含以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
##Interface: 适用的客户端版本号

##Title: 插件标题
##Notes: 插件说明
##Title-zhCN: 插件标题的中文版本
##Notes-zhCN: 插件说明的中文版本
## SavedVariables: 可持久化存储的变量
## SavedVariablesPerCharacter: 按角色存储的可持久化变量
## LoadOnDemand: 是否延迟加载
## LoadWith: 当指定插件加载时才加载,LoadOnDemand 为 1 时才生效
## DefaultState: 默认启用/禁用

# 注释

HelloWorld.lua
HelloWorld.xml
  • ## 开头的是插件元数据,用于描述插件的名称、用途、作者等信息,所有可供使用的元数据如下:
  • 以 # 或 % 开头行的为注释行
  • 在文件最后的 HelloWorld.lua / HelloWorld.xml 则指示了该插件需要加载的 Lua/XML 文件,如果有多个需要加载的文件,则分多行写入,从上到下对应着文件的载入顺序。

Lua

一个最简单的插件 Lua 脚本如下:

1
2
3
4
5
6
local frame = CreateFrame('Frame')

frame.RegisterEvent('PLAYER_LOGIN')
frame.SetScript('OnEvent', function(self, event, ...)
print('Hello World')
end)
  • 该脚本创建了一个 Frame 对象,并通过 Frame 来监听 玩家登录 事件,在玩家登录事件触发后,打印一条 Hello World 的消息。
  • 其中 Frame 是逻辑的入口点,所有的视觉界面元素都是一个 Frame,比如窗口、按钮、文本框,一个 Frame 里面也可以包含其他 Frame,比如窗口里可以包含文字、按钮,利用 Frame 来构成任意复杂的界面。
  • Frame 也可以调用 RegisterEventSetScript 函数用于监听事件并注册响应函数,从而让游戏客户端调用插件实现的自定义逻辑。
  • 在该插件中,不需要可视化图形界面,只需要 打印一条消息 这样的逻辑功能,所以默认创建了一个没有设置任何样式、不可见的 Frame

对于 Lua 中的功能函数,想要让游戏客户端进行调用主要有三种方式:

  • 监听事件:监听游戏事件,事件发生时则触发自定义的逻辑
  • 定时 / 周期任务:设置定时/周期任务,在合适的时间点触发自定义的逻辑
  • 斜杠命令:注册斜杠命令,玩家在控制台输入对应命令时触发自定义的逻辑

监听事件

Lua 中使用 Frame 来监听游戏事件:

1
2
3
4
frame.RegisterEvent('PLAYER_LOGIN')
frame.SetScript('OnEvent', function(self, event, ...)
print('Hello World')
end)

目前所有的事件分类有 159 种,总共 1499 条,可以查阅 Events - Wowpedia - Your wiki guide to the World of Warcraft 文档来获取需要使用的事件

定时任务和周期任务

「魔兽世界」中的定时任务或周期任务有三种方式来实现:

  • C_Timer.After:用于创建一个定时器,在指定的延迟后执行一次函数:
1
2
3
C_Timer.After(5, function()
print('5 秒之后被调用')
end)
  • C_Timer.NewTicker:用于创建一个定时器,在指定的时间间隔内重复执行某个函数:
1
2
3
C_Timer.NewTicker(2, function()
print('每 2 秒被调用一次,总共调用 5 次')
end, 5) -- 如果不设置第 3 个参数,则定时器会一直执行,直到被显式取消
  • Frame.OnUpdate:设置一个函数,在游戏的每一个渲染帧都调用该函数:
1
2
3
4
local frame = CreateFrame('Frame')
frame.SetScript('OnUpdate', function(self, elapsed)
print('上一帧耗时为' .. elapsed)
end)

C_TimerOnUpdate 各自使用的场景有所不同:

  • C_Timer
    • 性能:按指定的时间进行触发,与帧率无关,可以减少对性能的影响
    • 精确:允许开发者基于实际时间而非帧数来调度任务的执行,时间控制更加精确直观
  • Frame.OnUpdate
    • 高频:在游戏的每一渲染帧都会执行,频率更高,适合需要快速相应的任务
    • 灵活:可以在 OnUpdate 中实现定时器的功能,使用更加灵活,对于需要根据时间变化连续处理数据的场景更为直接有效

斜杆命令

对于一些需要玩家主动调用的插件函数,可以对该函数注册一个斜杠命令,在控制台内通过 /命令名 的方式来触发该函数的执行,注册斜杠命令的脚本如下:

1
2
3
4
5
6
7
8
9
10
local function ToggleVisible(message, editbox)
if frame:IsShown() then
frame:Hide()
else
frame:Show()
end
end

SLASH_TOGGLE = '/hello'
SlashCmdList['HELLO'] = ToggleVisible

该脚本注册了一条斜杠命令,在客户端控制台内输出 /hello,即可实现 frame 的显示和隐藏

在插件开发过程中,插件需要调用接口来获取游戏内的状态并做出处理,「魔兽世界」提供了大量 API 来供插件使用,包括拍卖、竞技场、聊天、成就等类别,所有的 API 函数可以参阅 World_of_Warcraft_API 来获取

XML

插件中的 XML 是布局描述文件,用于描述插件的 UI 布局,作用类似于 Web 开发中的 HTML 文件,利用 XML 来描述界面布局,Lua 来实现具体逻辑,可以很好地将设计与逻辑分离开,实现系统的解耦,一个 XML 文件一般包含以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- UI 注释行 -->
<Ui xmlns="http://www.w3.org/1999/xhtml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.w3.org/1999/xhtml http://www.w3.org/2001/XMLSchema-instance">
<Script file="MyFrame.lua" />
<Frame name="MyFrame" parent="UIParent">
<Scripts>
<OnLoad>
MyFrameOnLoad();
</OnLoad>
<OnEvent>
MyFrameOnEvent(event);
</OnEvent>
</Scripts>
</Frame>
</Ui>

Ui 标签

<Ui> 是最上级标签,其中的 schemaLocation 用来做语法检查或代码提示,如果编辑器不支持的话,也可以简化成:

1
2
3
4
<Ui xmlns="http://www.w3.org/1999/xhtml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
...
</Ui>

Script 标签

<Script> 用于载入 Lua 文件,导入顺序和文件上下顺序关联,除了在 XML 中导入 Lua 脚本文件以外,我们也可以使用 TOC 内声明的方式来导入 Lua 文件

Frame 标签

<Frame> 是最重要的标签,用来声明插件的界面布局和样式,也提供了注册事件、响应输入等接口,所有的「魔兽世界」插件都需要使用 Frame 来做为逻辑的入口点,其他的控件一般都是 Frame 的子类,比如 <Button><Slider><StatusBar> 等等,可以通过 属性 标签来设置 Frame 对象的属性值,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<Frame name="名称" inherits="继承 该控件未标明的属性全都继承自某控件" parent="UIParent 父标签" id="编号" movable="true 可移动">
<Size x="" y="" /> <!-- 大小 -->
<Anchors>
<Anchor Point="本控件的参考点" relativeTo="参考控件" relativePoint="参考控件的参考点">
<Offset x="" y="" />
</Anchor>
</Anchors>
<Frames>
<!-- 嵌入其他 Frame -->
</Frames>
<Scripts>
<OnLoad>处理载入事件</OnLoad>
<OnEvent>处理 RegisterEvent 注册过的事件</OnEvent>
<OnUpdate>处理帧渲染事件</OnUpdate>
...
</Scripts>
</Frame>

比较特殊的是 <Scripts> 标签,在其中可以直接写入 Lua 代码来执行逻辑,效果和把代码写到 Lua 文件中并加载是一样的
「魔兽世界」中的 UI 对象继承关系如下:

XML 和 Lua 的关系与 HTML 和 JavaScript 的关系十分类似,都是为了把逻辑和表现分离,实现解耦,在一些样式比较简单或者逻辑比较简单的情况下:

  • 可以通过 CreateFrame,SetSize,SetPoint 等接口在 Lua 中创建所有的界面和设置样式,无需单独的 XML 文件
  • 也可以通过 Scripts 标签把所有的逻辑代码都写在 XML 文件中,无需单独的 Lua 文件

总结

对于游戏玩家而言:
「魔兽世界」的插件系统允许玩家通过第三方插件来定制和增强游戏体验,在界面定制、信息增强、任务辅助上都可以很好地满足玩家的需求、减轻游玩负担,但是插件的安装和开发都具备一定的门槛,对新手玩家不太友好,且很有可能会产生依赖性,导致没有插件的情况下难以适应游戏。

对于开发人员而言:
通过 exportInterfaceFiles codeexportInterfaceFiles art 可以发现,「魔兽世界」的默认界面本身就是一个插件,某些情况下,游戏的开发者和插件的开发者是在使用同一套框架进行开发,同时插件使用 XML 来定制界面、Lua 调用接口来实现逻辑的技术方案,一定程度上,可以迫使游戏开发者在设计 UI 框架、暴露游戏接口时进行更多的思考,倒逼软件设计的正交性、解耦性等。

参考