第十二章 宏(魔兽世界Lua插件开发指南)

[复制链接]

该用户从未签到

2380

主题

2433

帖子

9139

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
9139
QQ
发表于 2023-12-23 11:35:45 | 显示全部楼层 |阅读模式

想要查看内容赶紧注册登陆吧!

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
第十二章 宏 (Macros)
可用命令 (Available Command)
安全和普通斜杠命令 (Secure and Normal Slash Commands)
Target选项 (The Target Option)
安全指令列表 (List of Secure Commands)
施法序列 (Cast Sequences)
物品栏ID (Item Slot IDs)
条件 (Conditions)
可用的条件 (Available Conditions)
组合条件 (Combining Conditions)
逻辑与AND (The Logical AND)
逻辑非NOT (The Logical NOT)
逻辑或OR (The Logical OR)简单的控制构造 (Simple Control Structures)
宏的反馈 (Macro Feedback)
突破255个字符的限制 (Circumventing the 255-Character Limit)
在宏中使用Lua (Using Lua in Macros)
发送聊天消息 (Sending Chat Message)
计时宏 (Timers in Macros)
抑制错误消息 (Suppressing Errors)
优化选项 (Tuning Options)
绕过[flyable]bug (Circumventing the [flyable] Bug)
总结 (Summary)
第十二章 宏
(Macros)
对于任何学习Lua以便充分体验WoW的人来说,可以肯定的是,在你们的动作条(action bar)中已经有了一些宏(macros)。例如当你要执行复杂的任务,仅需一个按钮,即可根据条件将不同的法术施放到不同的目标身上,它们会非常有用。不同的条件也可能非常的复杂,例如,你可以检测你当前的目标是敌对的(hostile)还是友方的(friendly),是存活的(alive)还是死亡的(dead)。
  如果你的动作条中有宏,你是自己创建的还是从其他地方复制粘贴的?如果是你自己创建的,你知道你在做什么吗?许多玩家认为宏只是一个由简单的斜杠命令列表组成的,但是它们非常强大。某些斜杠命令可以包含强大的条件,并由多种可以使用的选项。
  宏总是逐行执行的,这意味着你必须在你想使用的斜杠命令之后插入新行。例如,一个宏发送两条消息到聊天框是这样的:
  1. /say Hello...
  2. /say ...World!
复制代码
你可以将游戏中所有可用的斜杠命令添加到宏中,从游戏中提供的斜杠命令如/say,或小的Lua脚本如/script <Lua code>,到插件创建的斜杠命令中复杂的Lua函数。唯一的限制是每个宏的字符数被限制为255。但我们会找到规避这一限制的方法。
  本章是关于创建宏的,因此我们将探索宏是如何工作的,以及如何创建功能及其强大的宏。我们将从宏的可用命令概述开始。
可 用 命 令
(Available Command)

  你可以找到基本的斜杠命令,如“/cast <spell>”、“/use <item>”(内部实际上是相同的命令),或“/castsequence <spell1>, <spell2>, ...”,但是还有更多命令,例如,你知道命令“/petautocasttoggle <pet spell>”吗?

○ 安全和普通斜杠命令(Secure and Normal Slash Commands)
  我们将用于宏的大多数重要斜杠命令都被描述为安全斜杠命令,因为它们通常只是执行单个的受保护的函数。所有这些斜杠命令都存储在一个表中,只能从文件FrameXML\ChatFrame.lua中访问,保存局部变量的表的名称为SecureCmdList。回想一下,普通的斜杠命令(比如由插件创建的斜杠命令)存储在全局可访问的表SlashCmdList中。
  在我们看来,这些普通命令相当乏味,因为它们所能做的一切也可用由Lua脚本完成。你还知道了游戏提供的所有重要的普通斜杠命令,因为你每天都在使用它们。例如,/raid <text>向团队发送一条消息,这是你在聊天框中一直想要输入的内容。还有命令/random去分配战利品。其他常用的普通斜杠命令是表情(emotes),例如,/facepalm <some guy who just wiped the raid>(某人刚刚离开了团队)有时会非常有用。
  然而,本章不涉及这些提供的普通命令。你已经知道了最重要的那些,因为你每天都在使用它们。所有其他不太常用的普通斜杠命令被用于管理公会或团队。而且,这些斜杠命令通常非常简单,它们通常只接收一个参数,比如文本或目标。我们希望编写你可以在boss战或竞技场中使用的强大的宏。
注意:如果你需要更多关于游戏提供的普通斜杠命令的信息,暴雪在其网站上有一个列表:http://www.worldofwarcraft.com/info/basics/slashcommands.html
  现在让我们关注安全命令,乍一看它们要复杂得多。增加复杂性(adding complexity)是指你可以添加到斜杠命令中的条件和选项。在执行命令之前会检查条件,如果条件不满足,斜杠命令将被忽略。稍后你将看到关于条件的更多信息。
  我们将首先研究哪些斜杠命令可以使用,以及它们提供的选项。有一个选项比较特殊,因为它对所有斜杠命令都可用,并且在内部实际上是一个条件——target选项。

○ Target选项(The Target Option)
  让我们使用一个大家都知道的斜杠命令作为例子:/cast。这似乎是一个简单的命令,它对你当前的目标施法。但是你也可用使用/cast [target=focus] <spell>对你的焦点目标施法。在例子中这个选项的值是focus,但是它可以是任何有效的单位ID或附近的玩家或NPC的名字。
  target是一个可用于所有安全命令的选项。这个选项显然不是对所有命令都有意义,但它在每个安全命令的语法上都是有效的,而不是对每个命令都有效。
注意:只有安全命令支持target选项,普通的斜杠命令不需要。但是许多普通的斜杠命令,比如表情(emotes),将动作的目标作为一个参数。
这个选项用来引出我们第一个有用的宏,对目标的目标施放一个法术。例如,治疗可以用它来治疗Boss,于是就会自动治疗它的当前目标:
  1. /cast [target=targettarget] <healing spell>
复制代码
另一个对猎人或术士有用的宏是,将一个法术对准宠物的目标。这可以通过下面的斜杠命令完成:
  1. /cast [target=pettarget] <spell>
复制代码
安全指令列表(List of Secure Commands)
  表12-1列出了所有可用的关于施放法术或使用装备物品的安全命令。稍后将详细解释一些一些更高级的斜杠命令,例如/castsequence
小贴士:所有安全斜杠命令都是用Lua编写的。你可以在文件FrameXML\ChatFrame.Lua中找到他们(在文件中搜索SecureCmdList)。阅读斜杠命令后面的代码可用帮助你理解它。

表12-1 可用的法术和物品的安全斜杠命令
命令

描述

/startattack <target>

你的角色开始攻击<目标>或者你当前的目标(如果它被省略了)。也可以使用target选项替代。

/stopattack

停止自动攻击

/cast <spell or item>

施放<法术>或使用<物品>。别名:/spell

/use <spell or item>

等于/cast

/castrandom <spell1>,<spell2>,...

从被逗号分隔列的列表中随机选择一种法术或物品并使用它。

/castsequence <spell1>,<spell2>,...

当它第一次被执行时将施放<spell1>,第二次点击时将执行<spell2>,以此类推。它的参数也可以是物品。这个命令带有功能更加强大的额外重置选项(reset option),这将在本章后面详细解释。

/stopcasting

取消施法

/cancelaura <buff>

取消buff <buff>

/cancelform

取消你当前的变形形态。

/equip <itemName>

从你的库存中装备物品<itemName>,别名:/eq

/equipslot <slot> <item>

在<slot>中装备<物品>,稍后将解释槽ID(slot ID)。

/changeactionbar <page>

将动作条页面更改为<page>

/swpactionbar <page1> <page2>

动作条<page1>和<page2>之间的切换。如果你当前既不是<page1>,也不是<page2>,则将选择<page1>

这几个主要处理法术和物品的命令是宏中最重要。注意,由于全局冷却时间(global cooldown)限制,每个宏只能施放一个法术。但是有一些法术不会激活全局冷却,比如萨满和德鲁伊的自然之速(Nature’s Swiftness)。如果自然之速准备好了,以下的宏将会立即施放治疗波(Healing Wave)。如果你的自然之速正在冷却,则它将施放普通的治疗波。
  1. /cast Nature’s Swiftness
  2. /cast Healing Wave
复制代码
你也可以对德鲁伊使用这个宏,用德鲁伊的治疗之触(Healing Touch)替代治疗波。其他不能触发全局冷却时间的法术包括牧师(神圣)的心灵专注(Inner Focus)和法师(奥术)的心灵镇定(Presence of Mind)。你可以使用这两种法术构建类似的宏。
  但是除了施法之外,还有更多的斜杠命令。表12-2列出了处理target和focus target的所有安全命令,这些命令也可以派上用场。

表12-2 你的(焦点)目标可用的安全斜杠命令
命令

描述

/target <name>

目标的<name>。这也会把附近以<name>作为名称的玩家或NPC当作目标。你可以使用/targetexact <name>来避免这种行为。也可以使用target选项替代参数。别名:/tar

/targetexact <name>

以确切的名称作为目标。你可以使用target选项替代参数。

/targetenemy

以最近的敌对单位作为目标。

/targetenemyplayer

以最近的敌对玩家作为目标。

/targetfriend

以最近的友方单位作为目标。

/targetfriendplayer

以最近的友方玩家作为目标。

/targetraid

以最近的副本成员作为目标。

/cleartarget

清除你的目标。

/targetlasttarget

以你的最后一个目标为单位。

/targetlastenemy

以你的最后一个敌对目标作为目标

/targetlastfriend

以你的最后一个友方目标作为目标

/assist <target>

协助<target>,也可以使用target选项替代参数。别名:/a <target>

/focus <target>

把<target>设为焦点,你也可以使用target选项。

/clearfocus

清除焦点。


表12-3列出了对猎人和术士来说特别重要的斜杠命令,因为他们可以命令你的宠物。

表12-3 宠物的可用安全斜杠命令
命令

描述

/petattack <target>

使你的宠物开始攻击<目标>。也可以使用target选项。

/petfollow

设置你的宠物跟随。

/petstay

设置你的宠物停止。

/petpassive

设置你的宠物为被动。

/petdefensive

设置你的宠物防守。

/petautocaston <spell>

启用自动施放宠物的法术<spell>

/petautocastoff <spell>

禁用自动施放宠物的法术<spell>

/petautocasttoggles <spell>

切换自动施放宠物的法术<spell>

表12-4列出了所有处理副本角色的命令,如主坦克和主辅助的分配(assignments)。你可能想知道为什么要保护下面的函数,因为他们似乎不处理可以自动执行游戏的功能。原因是,如果他们没有被保护,你可以写一个插件,总是将你团队中生命值最低的玩家设置为主坦克。一个单位框体插件可以使用一个安全的组页眉(secure group header)来显示你所有的主坦克。这将总是显示最低生命值的玩家,而安全/受污染代码系统(secure/tainted code system)的目的是防止这样的插件。

表12-4 副本角色可用的安全斜杠命令
命令

描述

/clearmaintank

清除你的主坦克分配。别名:/clearmt

/maintank <target>

将目标设定为新的主坦克。也可以使用target选项。别名:/mt

/maintankoff <target>

移除<taget>的主坦克角色。别名/mtoff

/clearmainassist

清除主要协助分配。别名/clearma

/mainssist <target>

设置<target>为主协助。别名/ma

/mainassistoff <target>

移除<target>的主协助状态。别名/maoff


这是四种主要的斜杠命令。但是还有一些其他的命令可以使用。表12-5列出了所有剩余的安全命令。

表12-5 其他可用的安全斜杠命令
命令

描述

/duel <target>

请求与<target>进行决斗。这里不可以使用target选项。

/forfeit

取消决斗。别名:/yield和/concede

/stopmacro

停止宏的执行。

/click <secure frame> <mouseButton>

在全局变量<secure frame>与<mouseButton>中执行存储的安全框体的OnClick处理程序。这是一个非常强大的命令,因为它允许我们点击任意的安全按钮模板,稍后你将看到如何使用它。


在本节的表中有两个命令我们没有完全解释:/castsequence和/equipslot。现在让我们仔细看看。

○ 施法序列(Cast Sequences)
  施法序列命令一个额外选项:reset选项,它定义了何时重置为施法序列中第一个法术的标准。该选项不是必须的,默认情况下,施法序列在到达列表中最后一个法术时重新启动。你可能希望reset选项和target选项一样,以方括号的方式添加到命令参数的开头,但事实并非如此。重置选项必须放在可选条件(即target选项)和法术列表之间。下面的例子会说明这一点:
  1. /castsequence [target=focus] reset=30 <spell1>,<spell2>,...
复制代码
每次宏被执行的时候都会启动一个30秒的计时器,当计时器到期时就会重置为第一个法术。这意味着如果30秒内没有施法序列,它将重置。但reset接收的不仅仅是数字,它也可以是定义重置操作的触发器和字符串。你可以使用以下触发器。
1、当你改变你的目标时,目标重置序列。
2、当你离开战斗,战斗触发。
3、当你在执行宏时,按住键盘修饰键ctrl、alt和shift重置序列。
斜杠命令处理程序后面的代码只是在这个选项上使用string.find,如果它找到匹配的触发器,就会重置序列,因此你可以设置多个触发器,当其中一个触发器发生时重置。例如,reset=ctrltarget30,当你按下Ctrl再点击该宏时、当你更改你的目标时,或当你30秒没有使用宏时,将重置它。

○ 物品栏ID(Item Slot IDs)
  安全命令/equipslot <slot> <item>带有一个物品(item)将要被装备到的物品栏ID(item slot ID)。命令/use <item>和/cast <item>也接收这样的ID代替物品名称。
  有两种类型的清单ID(inventory IDs):字符串(比如你头部的头部装备栏(HeadSlot))和数字(比如你的头部是1)。宏只能使用数字标识符,而插件也可以(而且应该)在API函数中使用字符串标识符:
  1. id, texture = GetInventorySlotInfo(slotName)
复制代码
图12-1 显示了所有装备栏的槽位标识符(slot identifiers)

222222.jpg
 这些数字对我们很重要,因为我们不能字宏命令中使用字符串。我们现在可以编写简单且强大的物品使用宏。想象你装备了两个“在Y秒内产生X额外伤害”的花哨饰品。他们通常共享一个短暂的冷却时间,因此你不能同时激活他们。有一个宏可以激活当前没有冷却时间的饰品是非常有用的。
这里用/use(或/cast)命令会非常简单,我们可以尝试在同一个宏中激活这两个饰品。
  1. /use 13
  2. /use 14
复制代码
如果其中一个处于冷却状态,它将被跳过,并显示通常的“该物品尚未准备好”消息。稍后你将看到如何屏蔽(suppress)此错误消息。
  到目前为止,我们尝试的宏非常简单,你可能已经知道了如何使用大多数命令。让我们创建一些更高级的带有条件的宏。
条 件
(Conditions)

  条件以方括号的形式添加到宏中,就像target选项一样(从语法上讲,它实际上也是一个条件)。一个简单条件的好例子是exists,如果命令的目标存在,则赋值(evaluates)为true,如果目标不存在则赋值为false。如果你有一个目标并且不做其他事情,下面的斜杠命令对你的目标施放<spell>。
  1. /cast [exists] <spell>
复制代码
你还可以使用target选项来修改命令的目标。exists条件也指向被目标选项指向的单位。如果你有目标,以下命令对你的目标施放<spell>。
  1. /cast [target=focus, exists] <spell>
复制代码
 在一个命令中,多个条件总是用逗号分隔。我们现在可以向这个宏添加其他条件,但是首先让我们对现有的条件进行概述。
小贴士:使用红色问好作为宏的图标,因为它会根据条件自动选择适当的图标。
可用的条件(Available Conditions)
  表12-6列出了所有可用的条件,并进行了简短的描述。一些更复杂的条件,如装备,将在本节后面详细解释。

条件

描述

bar:x

如果动作条x当前被选中,则为true。

btn:x

如果宏被鼠标按钮x点击则为true,就像安全模板属性按钮后缀一样。x也可用是任何作为按钮传递给/click命令的字符串。

channeling:<spell>

如果你当前正在引导<spell>则为true。该法术是可选的,如果你当前正在使用任何法术,只要是引导而不是法术,则赋值为true。

combat

如果你在战斗中则为true

dead

如果命令目标死亡则为true

equipped:<itemType>

如果你一个<tiemType>类型的物品被装备,则为true。这个参数稍微复杂一些,稍后会详细解释。别名:worn:<itemType>

exists

如果目标存在则为true

flyable

如果你在外域(Outlands)或诺森德(Northrend)并且可以飞行(mount),则为真。警告:这个条件像在冬拥湖(Wintergrasp)或达拉然(Dalaran)这样的区域中赋值为true(这是一个使这个条件几乎完全没用的bug)

flying

如果你是正在飞行则为true

group:<type>

有效的群组类型(group type)是队伍和团队,如果你在这样的群组中,则赋值为true。

harm

如果你的目标是敌对则为true

help

如果你的目标是友方则为true

indoors

如果不能骑乘则为true

mod:<modifiler>

如果宏被带有修饰符的按键点击,则为ture。有效的<modifier>值为ctrl、alt和shift。

mounted

如果你在一个坐骑上则为true

outdoors

如果可以骑乘坐骑则为true

party

如果你的目标在你的队伍中则为true

pet:<name>

如果你当前的宠物是<name>则为true。该参数是可选的,也可以只使用pet来检查任何宠物。

stance:x

如果你目前以ID为x的姿态(stance)或变形形态,则为真。姿态ID将在稍后详细说明。ID是可选的,你可以只使用stance来检查任何姿态或变形形态是否处于激活状态。别名:form:x

stealth

如果你处于潜行则为true

swimming

如果你当前正在游泳则为true

 这些条件可以用于除了决斗命令/duel和/forfeit之外的所有安全命令。
  这个列表中较为复杂的条件之一是equipped,它接受物品的类型而不是名称。每个物品都有一个类型和一个子类型,例如,可以使用他们对拍卖行重点物品进行分类。在这个宏条件中可以使用他们的标识符是拍卖行所显示的物品类型的本地化字符串。例如你可以使用条件equipped:Clot来检查你当前是否穿着布甲装备。在这里使用装备类型没有太大意义,有趣的选择是使用武器类型。例如,盗贼可以使用以下宏,在一个简单的宏中结合背刺(Backstab)(需要一把匕首)和影袭(Sinister Strike),具体取决于当前装备的武器。
  1. /cast [equipped:Daggers] Backstab
  2. /stopmacro [equipped:Daggers]
  3. /cast Sinister Strike
复制代码
 如果你正在使用匕首,这将施放背刺并取消该宏。否则它施展邪恶打击。在下一节中,你将看到使用一个cast命令编写这样一个宏的方法。
  1. 注意:物品类别总是复数,所以使用[equipped:Dagger]不起作用。
复制代码
 另外两个重要的分类是单手武器(One-Hand)和双手武器(Two-Hand),如果你使用单手武器或双手武器,则它们会被赋值为真。
  表中完全没有解释的另一个条件是stance:x。姿态是战士的不同姿态、还有德鲁伊变形形态、牧师的暗影形态和救赎之魂、盗贼潜行、萨满的幽灵狼和术士的恶魔形态。姿态ID 1指的是你在变形/姿态条中可见的第一个姿态或变形,2指的是第二个,以此类推。例如姿态1:指战士的战斗姿态或德鲁伊的熊姿态。
  姿态ID可以指向不同的形态或姿态,取决于德鲁伊的天赋(talent)和专精(specialization,spec)。对德鲁伊来说,stance:5指的是平衡专精下的枭兽(Moonkin)形态,和恢复专精下的生命树(Tree of Life)形态。牧师稍微复杂一点,如暗影形态可用的话,stance:1表示暗影形态,如果救赎之魂可用但暗影形态不可用,则表示救赎之魂。如果两种天赋都点了,救赎之魂是stance:2。
注意:圣骑士的光环和猎人的守护灵不是姿态,即使它们出现在变形/姿态栏上。这意味着你不能使用姿态条件来检查哪一个光环或守护灵当前处于激活状态。
我们已经讨论了所有可用使用的条件。让我们结合它们来构建复杂的表达式。
组合条件(Combining Conditions)
  检查两个条件最简单的方法实际上并不是结合两个不同的条件。所有条件以condition:argument的形式用斜杠分隔开来,这些参数将用逻辑或OR组合在一起。如果你按下Shift或Alt键,下面的命令可用施放<spell>:
  1. /cast [mod:shift/alt] <spell>
复制代码
但是我们也可用在一个命令中组合两个不同的条件。最简单的组合是连词(用逻辑和AND连接条件)。

  · 逻辑与AND(The Logical AND)
  你已经在上一节中已经看到了一个示例,因为target选项在语法上是一个条件:
  1. /cast [target=focus,exists] <spell>
复制代码
我们可用添加另一个用逗号分隔的条件来检查它们。以下示例在你的目标存在且死亡时对其施展复活:
  1. /cast [exists,dead] Resurrection
复制代码
设置条件以检查目标是否存在或是否死亡也非常的有用。我们需要逻辑非NOT来完成这件事。
逻辑非NOT(The Logical NOT)
  条件的否定也可用轻松完成。只需向条件添加前缀no来否定它(应用逻辑NOT)。因此,没有死亡意味着你的目标是或者的。如果你的目标存在并且还活着,以下的宏可用对他施放<spell>:
  1. /cast [exists,nodead] <spell>
复制代码
逻辑NOT被局限于一个单一的条件,不能再宏条件中添加括号。让我们试着否定上一个例子中的条件。想象你把这个否定的表达式写成Lua表达式,就像这样:
  1. not (exists and not dead)
复制代码
在宏中不可能使用像[no(exists,nodead)]这样的条件。我们需要将这个表达式转换成可用在宏中表示的等价表达式。我们可用通过分析条件的含义在这样一个简单的例子中做到这一点。这意味着我们的目标要么不存在,要么已经死亡了。在Lua语法中可用将其写成(不存在)或死亡。稍后你将看到如何使用逻辑或宏。
  这两种形式之间的转换并不总是那么明显,特别是当你有更复杂的表达式时。但是布尔代数中有一个简单的法则可用用来做这个变换。德摩根定律是这样的:
  1. not (A and B) = (not A) or (not B)
复制代码
如果我们把这个应用到我们的表达式not (exists and not dead),我们会得到(not exists)或(not not dead)。显然,not not dead的意思和dead是一样的,所以我们得到(not exists)或dead。
  本法则也适用于用or替代and。
  1. not (A or B) = (not A) and (not B)
复制代码
但我们仍不能在宏中使用表达式,因为我们还没有讨论逻辑,让我们看看它是如何工作的。

  · 逻辑或OR(The Logical OR)
  若要用逻辑OR组合两个条件,请在方括号中添加另一个表达式块。最后一节中的示例如下所示。
  1. /cast [noexists][dead] <spell>
复制代码
如果你的目标不存在或已经死亡,此技能将会使用<spell>。OR操作符在某种程度上也受到了限制。假如你有一个(A or B) and (C or D)形式的表达式,你可能认为可用把宏条件写成[A][B],[C][D],但这是不对的。你还必须将表达式转换成析取范式(disjunctive normal form),将表达式分解为多个子表达式,这些子表达式可用像正常条件一样简单,并与逻辑OR组合在一起。
  对于这种表达式的变换还有一个有用的定律——分配律
  1. A and (B or C) = (A and B) or (A and C)
  2. A or (B and C) = (A or B) and (A or C)
复制代码
让我们试着将这个定律应用于表达式(A or B)and(C or D),将其转换为适合于宏的形式。
  1. (A or B) and (C or D)
  2. = ((A or B) and C) or ((A or B) and D)
  3. = ((A and C) or (B and C)) or ((A and D) or (B and D))
  4. = (A and C) or (B and C) or (A and D) or (B and D)
复制代码
表达式的这种形式与原始形式具有相同的含义,可用用作如下所示的宏条件:
  1. /cast [target=player][target=focus] <spell>
复制代码
第一个代码块总是为true,因为它不包含条件,所以第二个块永远不会执行,它的target选项没有影响。但我们依然可以向代码块添加条件,以根据这些条件设置target选项。我们可用使用这个技术来扩展本章的第一个宏,这个宏可用对你目标的目标施放治疗法术。
  1. /cast [target=targettarget] <healing spell>
复制代码
你可以用这个宏以一个怪物为目标,并且治疗该当前拉住怪物的坦克玩家,但当我们的目标是一个友方玩家时,它是没有用的。下面的宏会检查我们当前的目标是否是友方,如果是,会对他施放治疗;否则它对目标的目标施放治疗。
  1. /cast [target=target,help] [target=targettarget] <healing spell>
复制代码
注意:第一个target选项实际上不是必须的,因为target默认设置为target。
我们可以进一步扩展宏,首先检查鼠标悬停单位ID。这个单位ID与安全单位模板(secure unit templates)一起工作,它允许你用鼠标悬停在你的副本框体来治疗玩家。接下来的检查可以是目标,然后是目标的目标。这些单位ID上可能会出现help fails的情况,但在这种情况下,我们能然可以对自己施放这个魔法。以下是懒惰治疗者的终极宏,它执行前面提到的所有检查,并选择一个可以治愈的目标:
  1. /cast [target=mouseover,hepl][target=target, help][target=targettarget,help] [target=player] <healing sepll>
复制代码
注意,第二个代码块中需要target=target,因为第一个块将目标设置更改为mouseover(鼠标悬停)。仅仅使用[help]作为第二个是没有意义的,因为它检查鼠标悬停目标是否是友方,事实并非如此。我们知道,因为如果它是友方的话,就永远不会到达第二个块。
注意:不需要添加检查目标是否还活着,因为条件帮助的作用不仅仅在于检查目标是否友好。它会检查目标是否是有益法术的有效目标,而死亡的目标不是这类法术的有效目标(复活术除外,复活术不能与help检查同时使用。)
 我们现在可以用条件构建复杂的表达式,但是整个命令的形式仍然非常简单:使用if <expression>,之后do一些操作,最后end构造。有一个elseif或else块将是一个很好的补充。
简单的控制构造(Simple Control Structures)
  宏中唯一可用的控制结构是if-else-elseif块。在宏中没有循环这样的东西。
  宏中else或selseif块以分号开始。然后是要使用的条件和宏参数。我之前展示了一个宏,如果你装备了一把匕首,它就会使用背刺,否则就会使用影袭。
  1. /cast [equipped:Daggers] Backstab
  2. /stopmacro [equipped:Daggers]
  3. /cast Sinister Strike
复制代码
这个宏使用stopmacro命令,正如我所说的,我们将尝试它的另一个版本,它只包含一行代码。我们只需要添加一个分号,后面跟着else块的内容,这里只有使用Sinister Strike(影袭)作为参数施放。
  1. /cast [equipped:Daggers] Backstab; Sinister Strike
复制代码
 elseif-block在分号后面使用额外条件,就像下面的宏,如果你有敌方目标,它会施放伤害法术,如果你的目标是友方目标,它会施放治疗法术:
  1. /cast [harm] <damage spell>;[help] <healing spell>
复制代码
控制结构的另一个有趣的用法是使用btn或mod条件来根据所使用的鼠标按钮或键盘修饰符执行不同的操作。盗贼可用使用下面的宏,先用鼠标左键点击,然后再用鼠标右键点击,对两种武器都施加毒药。
  1. /cast Wound Poison VII
  2. /cast [button:1]; [button:2] 17
复制代码
第一个命令使用毒药,然后要求你选择一个目标。如果你左键点击宏,第二个命令就会在你的主手上使用放置的技能,如果你右击宏,第二个命令就会在右手上使用放置的法术。
  另一个非常强大的宏是为了试图打断一个法术的战士准备的。战士可以在战斗中打断法术并且还拥有带盾牌猛击(Shield Bash)(需要盾牌)的防御姿态。拳击(Pummel)可以用于狂暴姿态,这个法术不需要特定的武器类型。以下宏在你处于狂暴姿态时施放拳击,否则它会检查你是否装备了盾牌,然后施放盾牌猛击。如果这个检查也失败了,也就是说你处于战斗姿态或防御姿态,并且没有装备盾牌,它就会释放狂暴姿态,所以你可以第二次执行这个宏。
  1. /cast [stance:3] Pummel; [equipped:Shields] Shield Bash; Berserker Stance
复制代码
注意不可能一下就施放狂暴姿态和拳击,因为狂暴姿态会触发全局冷却时间。
  你可能已经注意到的一件事是,如果你选择红色问好作为宏图标,它们会自动显示正确的法术。但是我们也可以控制显示哪些法术和鼠标提示。
宏的反馈(Macro Feedback)
  当你在当前环境下执行宏时,它总是使用法术的图标和冷却时间。但这并不足够,并且可以在宏的开始使用命令#show或#showtooltip来控制这种行为。这两个命令还需要宏的问号图标。
  这两个命令都很简单。它们带有一个单独的参数,然后显示在该宏的动作条中。下面的宏在动作条中显示为攻击,其行为就像在动作条中有普通攻击,而不是宏一样。但是当你单击它时,它会施放Flash Heal
  1. #show Attack
  2. /cast Flash Heal
复制代码
命令#showtooltip遵循完全相同的语法。唯一的不同是,将鼠标放在宏上时,还会获得该法术的鼠标提示,它还设置图标。#show命令仅设置图标,鼠标提示宏的名称。
  也可以从安全命令中使用已知的条件,并且分号用于简单的if-then-else结构。下面的宏创建了一个按钮,如果你有匕首,则显示背刺,否则显示影袭。
  1. #showtooltip [equipped:Daggers] Backstab; Sinister Strike
复制代码
但是以前使用相同条件的宏已经具有此功能(鼠标提示除外,该提示仅显示了宏的名称)。宏的图标已经正确,并且你几乎不需要操作按钮的鼠标提示。因此你可能向知道这些命令的真正用途是什么。答案是,你不能始终在宏中使用带有条件的简单斜杠命命令。例如,当实际工作由Lua代码完成时,你将需要此功能,因为/script命令没有被解析确定要显示的图标。在本章的最后,当我们创建一个挂载宏时,你将看到一个这样的例子。
  你还需要使用#showtooltip,如果你的宏有一个stopmacro命令,你想要显示一个不同的图标,如果命令的条件是满足的,因为当确定图标/鼠标提示显示时,游戏不检查停止条件。但是,你很少需要根据stopmacro条件显示不同的图标,因为你通常可以通过使用控制结构来避免使用stopmacro。
  showtooltip的另一个可能用途是当你的宏调用其他宏或执行安全按钮的单击操作时。例如,假设在第一个动作中有两个宏,两个动作槽,ActionButton1和ActionButton2。你现在想创建一个宏,如果你有一个敌对的目标,它会执行宏1,否则会执行宏2。
  1. #showtooltip [harm] <damage spell>; <heal spell>
  2. /click [harm] ActionButton1; ActionButton2
复制代码
如果我们不在这里使用#showtooltip,按钮根本不会显示图标,因为在确定要显示图标时,WoW不会解析/click所引用的按钮。但是,我们为什么要编写这样一个简单调用另一个宏呢?因为其他宏的长度也可以达到255个字符,这意味着我们已经成功地绕过了宏只有255个字符的限制。但还有其他方法可以做到这一点,而不必将宏分割成许多小的部分。让我们看看如何结合/click和安全动作按钮模板来创建非常长的宏。
突 破 255 个 字 符 的 限 制
(Circumventing the 255-Character Limit)
一个宏很少需要超过255个字符,但有一种情况是,你想要完成“杀死所有稀有怪物”的成就。它是一个真正有用的宏,试图以所有稀有怪物为目标,和当你以它为目标时显示一条消息。之后你可以在西游怪物的刷新点到处飞,并一直点击宏。
  我们需要使用Lua代码创建一个安全动作按钮,并将其类型设置为macro,将其属性macrotext设置为如下内容。
  1. /targetexact Loque’nahak
  2. /targetexact High Thane Jorfus
  3. ...etc with all 23 Northrend rare spawns
  4. /stopmacro [noexists]
  5. /script ChatFrame1:AddMessage(“Found a rare spawn: ”..UnitName(“target”))

  6. https://nga.178.com/read.php?&tid=24367379
复制代码
 这显然不是一个普通的宏,所以让我们为它构建一个安全操作按钮。创建在宏中使用的按钮的最佳位置是在单独的插件中。我有一个名为RandomCrap的小插件,它由一个XML和一个Lua文件组成,其中包含我经常使用的一些函数和模板。但如果你只是想测试它,你也可以在一个插件如(TinyPad)中输入以下代码。
  创建这样一个宏最简单的方法是使用23个/targetexact命令编写这样一个字符串。注意,用方括号分隔字符串允许在字符串中使用新行,因此可以在代码中逐字地编写长宏。但是,更聪明的做法是创建一个包含所有boss名称的表,并从这个字符串自动宏文本。它更短,更灵活,如果你想允许部分匹配,你可以很容易地将target命令从targetexact更改为/target:
Code lua:
  1. local rarespawns = {“Loque’nahak”, “Hildana Deathstealer”, “Fumblub Gearwind”, “Perobas the Bloodthirster”, “King Ping”, “Crazed Indu’le Survivor”, “Grocklar”, “Syreian the Bonecarver”, “Griegen”, “Aotona”, “Vyragosa”, “Putridus the Ancient”, “High Thane Jorfus”, “Old Crystalbark”, “Icehorn”, “Vigdis the War Maiden”, “Tukemuth”, “Scarlet Highlord Daion”, “Seething Hate”, “Zul’drak Sentinael”, “Terror Spinner”, “King Krush”, “Dirkee”}
  2. local targets = “”
  3. for i, v in ipairs(rarespawns) do
  4.   targets = targets..”/targetexact ”..v..”\n”
  5. end

  6. local frame = CreateFrame(“Button”, “RareSpawns”, nil, “SecureActionButtonTemplate”)
  7. frame:SetAttribute(“type”, “macro”)
  8. frame:SetAttribute(“macrotext”, target..[[
  9. /stopmacro [noexists]
  10. /script ChatFrame1:AddMessage(“Found a rare spawn: ”..UnitName(“target”))]])
复制代码
该代码首先生成宏文本的/targetexact部分,然后创建一个安全操作按钮,并将其类型设置为macro。然后将属性macrotext设置为前面生成的字符串,然后是停止条件和显示消息的代码。
  这项技术适用于所有需要255个字符以上的宏,但它也有一个小缺点:你必须使用#show(或#showtooltip)命令来在动作条中获得图标和工具提示。我们现在可以构建一个宏,它可以单击带有长宏属性的不可见的安全操作按钮。这个宏非常简单,因为/click命令需要一个包含按钮的全局变量。
  1. /click RareSpawns
复制代码
这与单击我们创建的安全按钮具有相同的效果。
在 宏 中 使 用 Lua
(Using Lua in Macros)

  还有最后一个主题,它实际上与传统的宏没有什么关系——在宏中使用Lua。这非常简单,只需要使用斜杠命令/script。但是在宏中由上面Lua可用呢?请记住,不能从Lua中施展法术或使用物品,因为这些函数受保护,因此只能从安全代码中调用。来自宏的代码永远不安全。

○ 发送聊天消息(Sending Chat Message)
  在上一节中,你在宏中看到了一个简单的Lua聊天消息例子。我们调用ChatFrame1:Addmessage(msg)来在聊天框中显示一个通知。另一个类似的用途是发送聊天消息,但这不一定需要Lua代码。简单的聊天消息可用发送与常见的斜杠命令,如/say和/raid。甚至可用通过在消息中使用%t将目标包含在聊天消息中,它将被替换成为当前目标的名称。这不是一个特定于宏的函数,你可用在你的聊天信息中使用%t来指向你在《魔兽世界》中的目标。以下的宏对德鲁伊很有用,它施放复生,并发送复活玩家的名字到团队聊天中。
  1. /cast Rebirth
  2. /raid Battle res on %t!
复制代码
这个宏根本不使用任何Lua。但是这个宏的另一个有用功能是向复活玩家发送一个私聊,这样它就可用准备回到战斗中。你可能会认为这里有可能使用/w %t Battle res incoming。但它只适用于聊天信息,而不是私聊的目标。这意味着我们需要Lua代码啊向当前目标发送一个私聊。
  1. /cast Rebirth
  2. /raid Battle res on %t!
  3. /script SendChatMessage(“Battle res incoming!”, “WHISPER”, nil, UnitName(“target”))
复制代码
在宏中使用Lua的一个明显缺陷是很快就会达到255个字符的限制。解决方案是将实际的Lua代码外包到一个小插件中,然后提供一个简单的斜杠命令或从宏调用Lua函数。下面的代码创建了斜杠命令/wtarget <msg>,它将发送消息<msg>到你的当前目标:
Code lua:
  1. SLASH_WHISPERTARGET1 = “/wtarget”
  2. SLASH_WHISPERTARGET2 = “/wtar” -- alias /wtar <msg>
  3. SlashCmdList[“WHISPERTARGET”] = function(mag)
  4.   if UnitName(“target”) then
  5.     SendChatMessage(msg, “WHISPER”, nil, UnitName(“target”))
  6.   end
  7. end
复制代码
你现在只需在宏中使用以下命令:
  1. /wtar Battle res incoming!
复制代码
除了更简短和更容易使用之外,当你没有目标时,它不会产生错误消息。
  另外一个有用的功能是安排在特定时间之后发送消息。着允许你创建倒计时宏,这是非常有用的,所以让我们看看如何完成它。

○ 计时宏(Timers in Macros)
  实现这个最简单的方法是从你的宏中使用SimpleTimingLib。但是创建一个以秒为单位的斜杠命令比在某段时间之后执行的斜杠命令更容易。下面的代码创建了一个斜杠命令,该命令在给定时间后执行另一个斜杠命令。它需要安装SimpleTimingLib库,要么作为独立运行的版本,要么嵌入到插件中。
  1. local function runCmd(cmd)
  2.   local old = ChatFrameEditBox:GetText()
  3.   ChatFrameEditBox:SetText(cmd)
  4.   ChatEdit_SendText(ChatFrameEditBox)
  5.   ChatFrameEditBox:SetText(old)
  6. end

  7. SLASH IN1 = “/in”
  8. SlashCmdList[“IN”] = function(msg)
  9.   local time, cmd = msg:match(“(%d+)(.+)”)
  10.   if cmd:sub(1,1) ~= “/” then
  11.     cmd = “/”..cmd
  12.   end
  13.   SimpleTimingLib_Schedule(time, runCmd, cmd)
  14. end

  15. https://nga.178.com/read.php?&tid=24367379
复制代码
函数runCmd通过使用一个技巧来执行实际的命令——它修改默认聊天框中包含的文本,并调用函数ChatEdit_SendText(editBox),该函数在文件FrameXML\ChatFrame.lua中定义。它还可以在这里解析命令并找到与之关联的斜杠命令处理程序或表情,但这将是一项相当大的工作(需要大约30行代码)。如果你想找到斜杠命令在默认UI中式如何处理的,可以在文件frameXML\ChatFrame.lua中读取函数ChatEdit_ParseText(editBox, send)。
  我们现在可以在宏中使用/in命令,例如这样:
  1. /raid Pull in 3 seconds!
  2. /in 1 /raid Pull in 2 seconds!
  3. /in 2 /raid Pull in 1 second!
  4. /in 3 /raid Pull now!
复制代码
还可以省略要调用的命令中的前斜杠,因为/in处理程序会在命令不是以斜杠开头的情况下添加一个斜杠。语法/in 3 raid Pull now!也会执行。
  1. 注意:/in命令无法执行任何安全命令,因为执行路径被提供斜杠命令的插件污染了。
复制代码
Deadly Boss Mods(DBM)附带的另一个计时器斜杠命令式所谓的披萨计时器(pizza timer),可以通过斜杠命令/dbm timer xx:yy <text>来开始。它显示了一个带有名字<text>的DBT计时器。你可以在一个宏中使用它来跟踪某个技能或法术的冷却时间。但是还有另一个更强大的斜杠命令:/dbm broadcast timer xx:yy <text>,它广播一个dbm计时器给你的队伍或团队群组。此命令需要团队助手(raid assistant)或团长(leader)状态。
  团长可以使用如下的宏来为团队宣布一个短暂的休息时间。
  1. /rw Have a short break! (5 min)
  2. /dbm broadcast timer 5:00 Break
复制代码
抑制错误消息(Suppressing Errors)
  为什么会有人想要禁止错误消息呢?答案是有一些宏总是生成错误消息,即使它们被成功执行。这方面的一个例子是我们前面使用的饰品宏。
  1. /use 13
  2. /use 14
复制代码
这个宏试图同时使用两个饰品,这总是会生成一个错误消息。如果第一个/use命令成功,第二个/use命令会因为全局冷却而失败。去掉红色的“物品还没有准备好”消息的一个简单方法是清除显示该消息的框体。这个框体存储在全局变量UIErrorsFrame中,它的类型是MessageFrame。这种消息框体的工作原理类似于聊天框体(滚动消息框体),但没有滚动功能。你可以使用方法frame:AddMessage(msg)向这样的框架添加消息,最重要的是使用methodframe: clear()消除消息。此框体类型(以及所有其他框体类型)的完整参考可在附录A中获得。
  下面的宏在尝试执行两个/use命令后清除UIErrorsFrame。
  1. /use 13
  2. /use 14
  3. /script UIErrorsFrame:Clear()
复制代码
当然,也可以将此代码放在一个插件中。但是这里的Lua代码相对较短,因此不值得这样做。

○ 优化选项(Tuning Options)
  一些常用的宏不同于其他宏,因为它们基本上只是执行一次Lua脚本。这些“宏”简单地设置了一个所谓的cvar(控制台变量(console variable),一个存储游戏设置的变量,比如你的视频选项),这也可以通过编辑文件World of Warcraft\WTF\config.wtf来完成。这样的选项可以通过调用SetCVar(“option”, “value”)来设置,并使用GetCVar(“option”)来获取。
  下面是一个常见的宏示例,它允许你通过设置选项进一步缩小camerDistanceMaxFactor到2.5。在界面选项菜单的距离滑块(The distance slider)最多到2.0。
  1. /script SetCVar(“camerDistanceMaxFactor”, ”2.5”)
复制代码
注意这个选项的指保存在config.wtf中,这意味着执行了这个“宏”一次,并且你可能根本不会将这个简单的Lua脚本存储在一个宏中。
  还有许多其他可用选项,可以将他们设置为比选项菜单所允许的更高的值。控制“地面场景(Ground Clutter)”密度和据伦理的视频选项就是一个很好的例子。下面的宏将这两个选项设置为视频效果菜单中滑块之外的值。
  1. /script SetCvar(“groundEffectDensity”, “256”)
  2. /script SetCVar(“groundEffectDist”, “140”)
复制代码
这增加了杂草和其他植被的数量和可见范围。然而,如果你没有一个高端的配置,这可能会大大降低你的FPS。请注意,配置菜单中的选项被限制为较低的值是有原因的。

○ 绕过[flyable]bug(Circumventing the [flyable] Bug)
  另一个非常有用的宏是在允许飞行坐骑或普通坐骑的区域使用飞行坐骑。如果条件flyable正常工作,这看起来是一个非常简单的宏。你可以简单地使用以下宏来做到这一点:
  1. /cast [flyabel] <flying mount>
  2. /cast [noflyable] <normal mount>
复制代码
但是飞行不能正常工作。如果你在外域或诺森德允许飞行的区域,则此条件的值为true。在《巫妖王之怒》之前,在外域任何地方,普通坐骑允许的地方飞行坐骑也是被允许的。这在巫妖王之怒中不在正确,因为你不能在达拉然(除了克拉瑟斯的着陆点)或冬拥湖,但是那里仍然是true。这意味着宏在这些区域中失效。
  但是使用坐骑不是一个受保护的动作,即使它可以通过安全命令/cast完成。在3.0补丁中引入的函数CallCompanion(type, id)可以用来召唤非战斗宠物或坐骑。类型可以是“CRITTER”来召唤非战斗宠物,也可以是“MOUNT”来使用普通坐骑。坐骑的ID可以从默认的宠物和坐骑菜单获得,第一个坐骑(或宠物)的ID为1,第二个为2,以此类推。
  下面的代码创建一个斜杠命令/mount,它使用基于当前所在区域的可用坐骑列表中随机飞行坐骑或普通坐骑。
Code lua:
  1. local groundMounts = {1, 16, 26}
  2. local flyingMounts = {5, 10, 22}

  3. SLASH_MOUNT1 = “/mount”
  4. SlashCmdList[“MOUNT”] = function(msg)
  5.   local zone = GetRealZoneText()
  6.   local subzone = GetSubZoneText()
  7.   if IsMounted() then
  8.     Dismount()
  9.   elseif IsFlyableArea() and zone ~= “WWintergrasp”
  10.   and (zone ~= “Dalaran” or subzon == ”Krasus’ Landing”) then
  11.     CallCompanion(“MOUNT”, flyingMounts[math.random(#flyingMounts)])
  12.   else
  13.     CallCompanion(“MOUNT”, groundMounts[math.random(#flyingMounts)])
  14.   end
  15. end

  16. https://nga.178.com/read.php?&tid=24367379
复制代码
注意,这里有必要检查我们是否以及上了坐骑了,如果上了,就解除,因为CallCompanion (type, id)不像/cast命令那样,它不会解除坐骑。之后,斜杠命令使用一个良好的条件来检查是否真的允许在该区域飞行,如果是这种情况,则使用来自flyingmounth表的随机坐骑。否则,它使用表groundMounts中的随机坐骑。如果你想使用基于此脚本的宏,就必须用你喜爱的坐骑的ID来填充这些表。
  使用提供这个斜杠命令的插件的一个宏可能是这样的:
  1. #showtooltip [flyable] <flying mount>; <normal mount>
  2. /mount
复制代码
#showtooltip只是为了得到宏的图标和鼠标提示,这个图标在达拉然和冬拥湖中是错误的并不重要
● 总  结
(Summary)

  本章节演示了一种与《魔兽世界》API交互的不同方式:通过斜杠命令,乍一看似乎相当简单。但是这些安全的斜杠命令可以与复杂的条件结合使用,这使得它们非常强大。我们查看了所有可用的安全斜杠命令,它们可以执行通常只能从安全代码(即从默认UI)或使用安全模板执行的操作。
  我们讨论了如何使用逻辑运算符组合多个条件来构建复杂的表达式。然后我们了解了如何在斜杠命令中的简单 if-then-else-end 块中使用这些表达式。
  本章的下一个主题是在宏中使用Lua,我们尝试了许多可用在宏中使用的有用的Lua脚本。你还了解了如何将宏与安全模板结合使用以规避255个字符的限制,如果你希望使用长宏,这一解决方案可能非常重要。
  安全斜杠命令和安全模板是执行某些动作的唯一方式,比如施法。在本章中,你看到了如何结合这两种技术来创建及其强大的宏,以帮助你进行副本或PVP对抗。
  如果你正在寻找宏,请一定去WoWWiki页面http://ww.wowiki.com/Useful_macros逛逛。这个页面有一个相当强大的宏编译,可以用于各种目的,你说不定会在那里找到你要找的东西。你可能已经找到了这个宏列表并从那里复制了一些宏。但是你现在知道了这些宏是如何工作的,并且可用根据自己的需要定制它们。理解一段代码并能够对其进行定制总是比只是复制和粘贴要好。








回复

使用道具 举报

该用户从未签到

2380

主题

2433

帖子

9139

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
9139
QQ
 楼主| 发表于 2023-12-23 12:30:42 | 显示全部楼层
优化SimpleTimingLib
(Optimizing SimpleTimingLib)
当前实现的SimpleTimingLib并不是很好。OnUpdate处理程序执行任务的时间为O(n),如果你想在调度大量任务的插件中使用这个库,这是无法接受的。

○ 使用另一种数据结构(Using Another Data Structure)
  熟悉这个主题的人可能会建议使用所谓的优先队列(priority queue)作为数据结构。你可能已经想到了二叉搜索树例如AVL树,或者堆例如二叉堆,或者斐波那契堆;从理论上讲,这些都是解决我们问题的非常有效的方法。然而,这样的数据结构非常复杂,初学者很难理解。所以我们不会在这里看到它。我确实考虑过解释一个简单的优先队列(一个二进制堆);它相对较短,只有大约50行代码。然而,解释它肯定会很长很复杂。因此,我决定在这里跳过这一点,因为它会超出本书的范围。如果你对这个话题感兴趣,你可能会想读一本关于算法和数据结构的书。但是,我们仍然可以在这里改进SimpleTimingLib;在大多数用例中,我在这介绍的解决方案与合适的优先队列之间的性能差异是最小的。
  我们将看到一个非常简单但有效的解决方案:一个存储所有条目的链表。这似乎与我们当前使用数组的解决方案非常相似。但我们将使用一个简单的技巧来加快代码速度:在插入新值时保持数组的排序。如果它是排序的,OnUpdate处理程序只需要查看这个列表中的第一个条目。因此,它的运行时间为O(1),而新的插入操作运行时间为O(n)。
  我已经测试了DBM(Deadly Boss Mods)中调度程序所使用的所有数据结构。一个合适的优先队列,对于调度(insert)操作和extract min操作(删除最小时间的计时器)都是O(log n),与链表相比,具有明显的优势。但是我在这个测试中使用了数百万个同步计时器(simultaneous timers),对于DBM或SimpleTimingLib来说,这都不是一个真实的场景。即使DBM充分利用了它的调度库,但在一场复杂的Boss战斗中,通常只有4到8个计时器在特定时间运行。这样做的好处很小,因为很多时间不是浪费在O(n)插入操作上,而是浪费在构建包含函数参数的表这样的任务上。
  但小的优势仍然是优势,因此DBM使用二进制堆作为其调度的优先队列。你可以在DBM-Core文件中阅读带有良好注释的代码。如果你对这种数据结构是如何工作的感到好奇,可以阅读文件DBM-Core.lua(搜索“Scheduler”以找到它)中注释良好的代码。但是要注意:这里不讨论它,因为它超出了初学者指南的范围,所以代码可能看起来非常复杂。
  现在有足够的理论;让我们开始在SimpleMingLib中实现链表。

○ 构建SimpleTimingLib-1.1(Building SimpleTimingLib-1.1)
  基本上,我们将繁重的工作从OnUpdate处理程序转移到插入条目的函数,这是一个非常聪明的解决方案。OnUpdate调用非常频繁,而insert函数仅由另一个插件时不时地调用。
  以下所有代码块都是嵌入式库SimpleTimeingLib-1.0的更新,这意味着你必须替换相应的函数。将该库的主要版本增加到SimpleMingLib-1.1。
  我们更改的第一个东西是当前存储所有条目的数组;我们将用链表替换它,因此使用nil而不是空表初始化变量任务。替换此项:
Code lua:
  1. SimpleTimingLib.tasks = SimpleTimingLib.tasks or {}
复制代码
 使用以下代码:
Code lua:
  1. SimpleTimingLib.tasks = SimpleTimingLib.tasks or nil
复制代码
 schedule函数现在创建所有被调度(scheduled)任务的升序排序链表:
Code lua:
  1. local function schedule(time, func, obj, ...)
  2.   local t = {...}
  3.   t.func = func
  4.   t.time = GetTime() + time
  5.   t.obj = obj
  6.   if not tasks then
  7.     -- list is empty or the new is due before the first one
  8.     -- insert the new element at the very beginning
  9.     t.next = tasks
  10.     tasks = t
  11.   else -- list is not empty, find last entry which is < time
  12.     local node = tasks
  13.     while node.next and node.next.time < time do
  14.       node = node.next
  15.     end
  16.     t.next = node
  17.   end
  18. end
复制代码
从代码中可以很容易地看到,这个操作现在是O(n),因为它可能会迭代整个链表,以找到插入它的正确位置。以下代码显示了新的OnUpdate处理程序:
Code lua:
  1. local function onUpdate()
  2.   local node = tasks
  3.   while node do
  4.     if node.time <= GetTime() then
  5.       tasks = node.next
  6.       node.func(unpack(node))
  7.     else
  8.       break
  9.     end
  10.     node = node.next
  11.   end
  12. end
复制代码
它看起来像一个典型的O(n)操作,因为它似乎在整个数组上循环。当所有被调度任务都需要在当前帧(frame)中结束(due)时,它可能在O(n)模式下运行。但是,我们必须查看整体性能,即使在最坏的情况下,所有任务都需要在此帧中执行,在下一次调用OnUpdate时也会有一个空的链表。在几乎所有的调用中,函数将只查看第一个元素,因为一旦遇到尚未结束的任务,它就会取消循环。
  非调度(unschedule)函数也需要调整,但我们无法真正优化它。因为我们必须查看所有任务,以检查它们是否与提供的参数匹配:
Code lua:
  1. local function unscheduled(func, obj, ...)
  2.   local node = tasks
  3.   local prev -- previous node required for the removal operation
  4.   while node do
  5.     if node.obj == obj and (not func or node.func == func) then
  6.       local matches = true
  7.       for i = 1, select(“#”, ...) do
  8.         if select(i, ...) ~= node [i] then
  9.           matches = false
  10.           break
  11.         end
  12.       end
  13.       if matches then
  14.         if not prev then --trying to remove first node
  15.           tasks = node.next
  16.         else
  17.           prev.next = node.next
  18.         end
  19.       else -- set prev to the current node if it was not removed
  20.         prev = node
  21.       end
  22.       node = node.next
  23.     end
  24.   end
  25. end
复制代码
 SimpleTimeingLib现在看起来稍微复杂一些,但如果被许多插件使用,它会更快。库的用途是提供给多个插件使用,你必须假设一个库被另一个插件广泛使用。你不会想在这里浪费性能。
  另一个易于应用的优化是再利用表(recycling tables)。

○ 再利用表?(recycling tables?)
  在Lua中,再利用表通常是不值得的。垃圾回收器(The garbage collector)的工作效率非常高,回收表(collecting tables)通常比清空表并填充它们要快。但是《魔兽世界》提供了函数table.wipe(t),它可以快速删除给定表中的所有条目。
  我们可以这样做:创建一个堆栈(stack),并在擦除后将所有不再需要的表推入这个堆栈。然后,调度函数可以尝试从这个堆栈中弹出一个表,或者在不可能的情况下创建一个新表。然而,有几个问题:
●我们必须使用循环将所有参数插入到再利用表中。我们目前使用简单的{...},之前已经看到这比循环要快。
  ●假设在短时间内需要许多计时器。然后在回收堆栈上就会有很多死的计时器对象,而且它永远不会收缩(shrink back)。可能的解决方案是使用弱表(weak table)来存储所有回收的计时器;之后,垃圾回收器将回收所有未被回收的表。但这也带来了另一个缺点,下面将讨论。
  ●使用堆栈作为数据结构是不可能的,因为垃圾回收器会通过在中间回收元素来销毁它。唯一的解决方案是使用哈希表,之后用next从中获取第一个元素。但是请记住,在我们的哈希表实现中,next需要查找表中第一个不是空闲的位置,这可能需要一些时间。这尤其有问题,因为如果启动了很多计时器,哈希表可能会变得相对较大;回想一下,只有向哈希表中添加更多的nil元素,它才能收缩。你稍后将在几乎空的大表上看到更多关于next性能的信息。
  ●很难跟踪当前可回收的表的数量,因为垃圾收集器将以不可预测的顺序和时间点删除它们。但是,仍然可以使用finalizer元方法跟踪数字,该方法是在收集对象时调用的函数。但这会产生更大的开销,因为带有终结器(finalizers)的对象需要特殊处理。在下一节讨论userdata值时,我将告诉你关于终结器(finalizers)的更多信息。
 尽管有缺点,弱哈希表解决方案似乎仍然很有吸引力,因为你可能没想到next会成为这样一个性能大户(performance hog)。让我们看看下一步会有多糟糕。下面的代码创建了一个包含50000个条目的简单示例哈希表:
Code lua:
  1. local t = {}
  2. for i = 1, 50000 do
  3.   t[-i] = i -- negative entries are always in the hash part
  4. end
复制代码
让我们测试一个从表中删除所有项的简单循环:
Code lua:
  1. for k, v in pairs(t) do
  2.   t[k] = nil
  3. end
复制代码
它的速度和预期的一样快,在我的笔记本电脑上大约0.03秒。现在让我们尝试使用以下代码。它也是删除表中的所有条目,但使用while循环和next(t, nil)检索表的第一个条目,直到表完全为空:
Code lua:
  1. local k, v = next(t, nil)
  2. while k and v do
  3.   t[k] = nil
  4.   k, v = next(t, nil)
  5. end
复制代码
这个循环在我的笔记本电脑上需要5秒的CPU时间,因为下一步必须遍历整个表去找一个条目直到最后。这在一个只有几个条目的大表上需要花一些时间,如果一个短的周期内有很多计时器(哈希表增长),而之后的一个周期只有几个计时器(垃圾回收器删除了大部分计时器),那么存储再利用表的哈希表将会变成这样的表。
  在这种情况下,再利用表是不值得做的。你可以增加CPU使用率以节省几千字节的内存。但是在玩魔兽世界的时候,你的CPU通常会超负荷工作,而几千字节的内存不会有任何区别。
  我们现在已经优化了很多表,并且在前面使用了字符串。但是还有两种更有趣的数据类型我们还没有讨论过:userdata值(userdata values)和线程(threads)。我们先看看如何使用userdata。
利用Userdata
(Utilizing Userdata)

  我在第二章中告诉过你,你不能从Lua创建userdata值,但这并不意味着我们不能使用它们。现在,我们可以用userdata对象做什么?此对象最常见的用途是表示Lua主机(Lua host)提供的对象,如《魔兽世界》框体(frame)。所有框体都由一个包含userdata对象的表表示。但我们可以对这个对象做的唯一一件事是将它传递给API提供的函数,这些函数知道这个userdata对象实际上代表什么。这意味着我们目前无法对userdata对象执行任何操作,只能将其用作标识符。
  但是userdata值可以有元表,它们甚至定义了两个额外的元方法:__len在对userdata对象使用长度运算符#时被调用,__gc是一个终结器(finalizer),在垃圾回收器收集userdata对象时执行。但仍然存在两个问题:如何创建userdata对象,以及如何对其应用元表?setmetatable对userdata值无效。根据官方文档,无法创建userdata值并对其应用元表。
  但是,Lua中有一个没有正式说明的(undocumented)函数,它创建一个新的userdata值(没有附加任何数据)和一个附加到它的元表。此函数的名称为newproxy(mt),这已经表明了它的用途:将其用作委托(proxy)。

○ 使用Userdata作为委托(Using Userdata as a Proxy)
  用一个委托对象包装(wraps)表,可以跟踪、修改或阻止对表的访问。在第6章,你看到过一个委托表,在这里我向你展示一些在你的文件环境中使用委托进行调试的有用技巧。该委托跟踪并传递对全局环境的所有访问;即所有全局变量。
  newproxy(mt) 的第一个参数mt可以为true,为新的userdata值创建一个新的空元表,也可以为false,在没有元表的情况下创建它。mt的第三个可能值是另一个已经有元表的userdata对象,该元表将用于新的userdata对象。
  下面的例子展示了用于对另一个表跟踪访问的userdata委托:
Code lua:
  1. local myTable = {}
  2. local p = newproxy(true)
  3. getmetatable(p).__index = function(self, k)
  4.   print(“Accessing ”..tostring(k))
  5.   return mtTable[k]
  6. end

  7. p.test = “123”
  8. p.foo = 1
  9. p.foo = p.foo + 1
  10. print(myTable.test)
复制代码
 你可能想知道使用普通表的优势在哪里。唯一的区别是内存的使用。如果表的唯一目的是通过元表转发访问,则表会分配32字节的内存,这是浪费的。userdata对象不分配任何不必要的内存,但其优势仍然非常小,通常不值得付出努力。
  但是,默认UI在许多与安全模板和受限框体相关的代码中以类似的方式使用newproxy。它在那里被用作包装框体(wraps frames)的委托,以保护框体不受污染。文件中的注释表明,它们使用userdata对象而不是表的唯一原因是内存使用。
Userdata元方法(Userdata Metamethods)
  如前所述,还有两种额外的元方法可用。首先,当对userdata值使用长度运算符#时,将调用__len。我们可以使用它为哈希表对象创建一个包装器(wrapper),用于跟踪哈希表的大小。我们将使用userdata对象的元表作为哈希表内容的存储。元表中的__size字段大小将存储哈希表的大小。代码如下:
Code lua:
  1. local function index(self, k)
  2.   return getmetatable(self)[k]
  3. end

  4. local function newindex(self, k, v)
  5.   local mt = getmetatable(self)
  6.   local old = mt[k]
  7.   mt[k] = v
  8.   if old and v == nil then -- deleting an existing entry
  9.     mt.__size = mt.__size -1
  10.   elseif not old and v ~=nil then
  11.     mt.__size = mt.__size + 1 -- adding a new entry
  12.   end
  13. end

  14. local function len(self)
  15.   return getmetatable(self).__size
  16. end

  17. function NewHashTable()
  18.   local obj = newproxy(true)
  19.   getmetatable(obj).__index = index
  20.   getmetatable(obj).__newindex = newindex
  21.   getmetatable(obj).__len = len
  22.   getmetatable(obj).__size = 0
  23. end
复制代码
我们可以通过添加以下代码来测试哈希表对象:
Code lua:
  1. local t = NewHashTable()
  2. print(#t) --> 0
  3. t.foo = “bar”
  4. print(#t) --> 1
  5. t.x = 1
  6. t.y = 2
  7. t.z = 3
  8. print(#t) --> 4
  9. t.foo = nil
  10. print(#t) --> 3
  11. t[1] = 4 -- also counts entries in the array part
  12. print(#t) --> 4
复制代码
使用这样的表的一个缺点是,访问和设置其中的值会比常规表慢。这是因为每次访问或更改值时都有一个额外的函数调用。
  第二个元方法是终结器(finalizer)__gc,当垃圾回收器删除userdata值时调用它。这种元方法存在的原因是userdata值通常存储Lua无法访问的数据,垃圾回收器无法清理常规的userdata值。终结器通常不是Lua函数,而是由底层API提供的清理userdata值的函数。
  这个元方法的使用对我们来说有些限制,因为我们只能在这里添加Lua函数。下面的示例只是打印一条消息,因为我想不出这个元方法有什么有用的应用程序:
Code lua:
  1. local p = newproxy(true)
  2. getmetatable(p).__gc = function(self)
  3.   print(“Collected ”..tostring(self))
  4. end
复制代码
 不需要通过调用collectgarbage(“collect”) 来触发垃圾回收器,因为当脚本结束时,垃圾回收器还会使用终结器收集所有对象。这样做的原因是userdata对象可能已经打开系统资源(如文件),而终结器会关闭它们。因此,Lua必须确保调用所有终结器,即使脚本以错误终止。
  我们还没有讨论Lua的另一个特性,因为在《魔兽世界》中你很少需要它:协程库(The Coroutine Library)。


● 协程库
(The Coroutine Library)

  这个库是Lua标准库之一,我在书的开头提到过它。该库允许你创建协程对象(coroutine objects),这些对象基本上是函数,但有一个区别:它们可以让步(yield),也可以恢复(resumed)。协程中的让步类似于函数中的返回,但协程将在恢复时从让步的位置继续执行。函数不能存储返回的位置并从那里继续。

○ 协程基础(Coroutine Basics)
  在给定的时间内只能运行一个协程,因为协程不能实现多线程。因此,不能从外部阻止协程;它必须让步(yield),然后才能从外部恢复。
  让我们概述一下在协程中的可用功能:
  1.  ●coroutine.create(func):从给定函数func中创建一个新的协程对象。此函数在第一次协程被恢复(resumed)(即启动时)时执行。
  2.   ●coroutine.resume(co, ...):恢复协程并将参数“...”传递给它。如果协程运行时没有错误,则此函数返回true,后面跟着传递给让步函数(yield function)或返回语句(return statement)的值;否则返回false,后面跟着错误消息。
  3.   ●coroutine.yield(...):让步并且激活被当前活动协程调用的协程。所有被传递到此函数的参数都将作为coroutine.resume调用恢复(resumed)当前活动协程的额外的返回值返回。当协程在下次被恢复时,yield返回传递给resume函数额外的值。
  4.   ●coroutine.running():返回当前正在运行的协程,如果没有任何协程正在运行,则返回nil。
  5.   ●coroutine.status(co):返回协程co的状态,它有以下可能的值:running表示它当前正在运行;suspended意味着它已经让步或尚未启动;normal意味着当前协程已经停止了,因为另一个协程被恢复了;dead表示从中创建协程的函数已返回或发生错误。
  6.   ●coroutine.wrap(func):从func创建一个协程,并返回一个包装函数(wrapper function),该函数在每次调用包装函数时对创建的协程调用coroutine.resume。此包装函数去除第一个返回值(指示发生的错误),并在该值为false时生成错误消息。
复制代码
 它看起来很复杂,现在让我们看一个简单的例子。

○ 协程示例(Coroutine Basics)
  下面的示例创建了一个协程,该协程封装了一个大体上由无限循环组成的函数。此函数在循环体中生成,并将计数器传递给调用函数:
Code lua:
  1. local function foo()
  2.   for i = 1, math.huge do
  3.     coroutine.yield(i)
  4.   end
  5. end

  6. local co = coroutine.wrap(foo)
  7. print(co()) --> 1
  8. print(co()) --> 2
  9. print(co()) --> 3
复制代码
  1. 这个例子非常简单,但你可以想象得到,协程可能很容易变得非常复杂,特别是当你有多个相互恢复并传递参数的协程时。
  2.   然而,在《魔兽世界》的插件中,你不太可能需要如此复杂的协程结构。但是,如果你需要执行更长的任务,并且希望将其分布在多个框体上以避免延迟上升,则可以使用本例中的简单循环。你可以简单地定义一个OnUpdate处理程序,该处理程序检查给定的协程是否仍处于活动状态,并在本例中执行它。然后,协程执行任务的几个步骤并产生让步(yields)。
复制代码
总结
(Summary)

  这一章是本书中最难的一章,因为它非常深入地阐述了细节。如果你不了解每一个细节,不要担心。在你自己编写了第一个插件之后,可能想再次阅读本章。之后,你可以应用我在这里介绍的一些技巧和窍门来改进你的插件。
  你了解了我们如何通过许多小技巧来衡量和改进插件的性能,还了解了许多有关Lua内部工作方式的细节。我们详细讨论了字符串和表是如何工作的,并给出了优化它们的示例。
  之后,我们通过应用所讨论的优化技术改进了我们的库SimpleTimingLib。插入(insertion)操作现在是一个O(n)操作,OnUpdate处理程序是O(1);旧版本则相反。这是更好的,因为OnUpdate处理程序在每一帧(frame)都被调用,而插入只是不时被调用。
  然后我向你展示了Lua API的两个部分,我们之前没有讨论过。其中一个是函数newproxy(),这是一个完全没有文档记录(undocumented)的特性,非常有用,因为它允许你创建userdata对象。我在这里向你展示的最后一个技巧是如何使用协程标准库,这在《魔兽世界》插件中很少使用。这个库相当有用,功能强大,但它非常难以掌握。
























回复 支持 反对

使用道具 举报

快速回复高级模式
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表