菜单 学习猿地 - LMONKEY

VIP

开通学习猿地VIP

尊享10项VIP特权 持续新增

知识通关挑战

打卡带练!告别无效练习

接私单赚外块

VIP优先接,累计金额超百万

学习猿地私房课免费学

大厂实战课仅对VIP开放

你的一对一导师

每月可免费咨询大牛30次

领取更多软件工程师实用特权

入驻
153
0

事件

原创
05/13 14:22
阅读数 75656

一、窗体事件

(1)一般事件

我们把游戏界面的一个元素,称为一个窗体,比如一个按钮、一个输入框等。我们可以通过如下代码,简单的创建一个窗体:

local mFrame = CreateFrame("Frame")

窗体一般用于展示信息,同时要与用户交互,界面交互基于窗体事件(比如点击、鼠标划入等,我们统一称为script)。这些操作会以事件的方式传给窗体的事件处理器(如果已经set),去执行我们希望的逻辑。

我们可以通过如下代码为窗体添加一个事件处理器:

mFrame:SetScript(script, func) --设置事件处理器

这里的script是窗体事件,不同类型的窗体可能会有不同的事件,比如Button有“OnClick”,其他窗体则没有;func是事件处理器,实际上就是一个函数,它的参数对应事件script的返回值。

另外几个与窗体事件相关的API:

mFrame:GetScript(script) --获取事件处理器
mFrame:HookScript(script, func) --添加而非覆盖原事件处理器
mFrame:HasScript(script) --判断窗体是否有该事件,切记不是判断是否有事件处理器

(2)一个例子

经常会有人在聊天频道发一些物品链接,我们希望鼠标移到这些链接上时,会在提示框中显示相应的物品信息。接下来我们就来写这么一个插件,其中会用到我们刚学的窗体事件。

首先,我们需要知道几个即将用到全局变量和API:

ChatFrameX --聊天窗口,不止一个,X可以为1,2...
DEFAULT_CHAT_FRAME --默认聊天框
NUM_CHAT_WINDOWS --最大聊天框个数,默认7

getglobal(name) --获取全局变量

OnHyperLinkEnter --ChatFrame提供的超链接鼠标滑入事件
OnHyperLindLeave --ChatFrame提供的超链接鼠标滑出事件

接下来,我们为每一个聊天框添加超链接滑入/滑出事件

local function showTooltip(...)  --打印超链接信息
    print(...)
end
local function hideTooltip(...)  --打印超链接信息
    print(...)
end

local function setOrHookHandler(frame, script, func) --设置或添加事件处理器
    if frame:GetScript(script) then  --如果已经有
        frame:HookScript(script func)  --添加
    else
    frame:SetScript(script, func)  --否则设置
    end
end

for i = 1, NUM_CHAT_WINDOW do  --遍历聊天框
    local frame = getglobal("ChatFrame"..i)
    if frame then  --如果聊天框存在
        setOrHookHandler(frame, "OnHyperLinkEnter", showTooltip)  --将showTooltip设置为超链接滑入事件处理器
        setOrHookHandler(frame, "OnHyperLinkLeave", hideTooltip)  --将hideTooltip设置为超链接滑出事件处理器
    end
end

加载插件后,我们可能获得类似如下数据:

table:168BAC00 item:40449:3832:3487:3472:0:0:0:0:80 [信仰法袍] --其中第一个参数table是窗体;第二个参数item是物品信息,40449是物品id,其他暂时不管

到目前为止,我们已经为聊天框添加了事件处理器,并且能够打印超链接信息。但是仅仅打印信息是不够的,我们希望将物品信息优雅地展示在信息窗口中。

这里直接借助游戏默认的提示框GameTooltip,通过它的SetHyperlink(linkData)方法,将我们上面获取的item作为参数传入即可。

local function showTooltip(self, linkData)  --显示提示框
    local linkType = string.split(":", linkData)  --提取物品类型,错误类型会导致SetHyperlink报错
    if  linkType == "item"  --物品
    or linkType == "spell" --法术
    or linkType == "enchant"  --附魔
    or linkType == "quest" --任务
    or linkType == "talent" --天赋
    or linkType == "glyph"  --雕纹
    or linkType == "unit"  --?
    or linkType == "achievement" then  --成就
        GameTooltip:SetOwner(self, "ANCHOR_CURSOR") --绑定到窗体
        GameTooltip:SetHyperlink(linkData)  --设置超链接
        GameTooltip:Show()  --显示提示框
    end
end

local function hideTooltip()  --隐藏提示框
    GameTooltip:Hide()
end

 

二、游戏事件

(1)单事件

窗体除了可以监听交互事件,还能监听游戏事件,比如密语、施法等。为了方便,系统将所有游戏事件统一为OnEvent,所以我们只需要为OnEvent注册一个处理器就行了,如下:

frame:SetScript("OnEvent", mEventHandler)

但仅仅这样是收不到任何事件的,还需要为窗体注册我们感兴趣的具体事件,完整的写法如下:

local frame = CreateFrame("Frame")  --创建一个窗体
local function mEventHandler(self, event, msg, sender)  --定义事件处理器
    print(event, sender, msg)
end

frame:RegisterEvent("CHAT_MSG_WHISPER")  --注册密语事件
frame:SetScript("OnEvent", mEventHandler)  --设置密语事件处理器

这里需要注意的是CHAT_MSG_WHISPER事件会返回13个参数,我们处理器只接收了前四个,分别是窗体、事件名、密语内容、发送者。

(2)多事件

我们可以用一个事件处理器处理多个事件,通常用if-else语句区分事件类型

local function onWhisper(msg, sender) --处理whisper
    --处理密语
end

local function onNewZone()  --处理地域改变
    --进入新的地域
end

local function mEventHandler(self, event, ...) --事件处理器
    if event == "CHAT_MSG_WHISPER" then
        onWhisper(...)
    elseif event == "ZONE_CHANGED_NEW_AREA" then
        onNewZone()
    elseif event == "..." then
        --etc
    end
end

因为所有事件的前两个参数都是self和event,所以一般只把第三个参数开始的...可变参数传给对应的事件处理函数。我们可以使用select函数来选择接收可变函数的第几个参数:

local status = select(6, ...)  --获取第6个参数,在CHAT_MSG_WHISPER中它代表发送者的状态

(3)更好的写法

利用table可以更简洁的编程:

local eventHandlers = {}  --table,存储所有事件处理函数
function eventHandlers.CHAT_MSG_WHISPER(msg, sender) --whisper处理函数并存入table
    --...
end

function eventHandlers.ZONE_CHANGED_NEW_AREA() --地域改变处理函数并存入table
    --...
end

local function mEventHandler(self, event, ...)  --事件处理器
    return eventHandlers[event](...)  --直接从table提取对应函数
end

如上,将事件作为key,函数作为value,可以很方便地通过事件获取对应的处理函数,省去了繁琐的if-else结构


 

三、计时器

(1)OnUpdate

OnUpdate是另一个非常重要的事件,表示界面刷新。也就是每次刷新界面,都会执行OnUpdate事件处理器,如果游戏帧数是60帧,那就是每秒执行60次。

OnUpdate会返回处理器两个参数:

  • self 窗体本身
  • elapsed 距离上次OnUpdate处理器被调用时间

利用OnUpdate的特性,我们可以实现在将来某个特定时间点执行某个函数。 其原理就是在OnUpdate回调函数(事件处理器)中不断检测是否已经到了特定时间点,如果到了,就执行指定的函数。

代码如下:

local tasks = {}  --定义任务table
function SimpleTimingLib_Schedule(time, func, ...)  --任务安排
    local t = {...}  --新建一个table,array部分存储任务参数
    t.func = func  --即将执行的任务
    t.time = GetTime() + time  --时间点
    table.insert(tasks, t)  --将任务存入任务列表
end

local function onUpdate()  --OnUpdate回调函数
    for i = #tasks, 1, -1 do  --遍历任务
        local val = tasks[i] 
        if val.time <= GetTime() then --如果已到执行时间
            table.remove(tasks, i)  --移除任务
            val.func(unpack(val))  --执行任务,unpack可以提取array部分,这里是任务参数
        end
    end
end

local frame = CreateFrame("Frame")  --新建一个Frame
frame:SetScript("OnUpdate", onUpdate)  --设置OnUpdate回调函数

如果希望将已经添加到任务列表的任务移除,可以通过对比函数名和参数来识别将要移除的任务,代码如下:

function SimpleTimingLib_Unschedule(func, ...)
    for i = #tasks, 1, -1, do  --遍历
        local val = tasks[i]
        if val.func == func then  --函数名相同
            local matches = true  --匹配暂时成功
            for i = 1, select("#", ...) do  --遍历参数,select("#", ...)返回可变参数个数
                if select(i, ...) ~=val[i] then  --参数不相等
                    matches = false  --匹配最终失败
                    break
                end
            end
            if matches then  --如果匹配成功,移除
                table.remove(tasks, i)
            end
        end
    end
end

(2)性能优化

因为OnUpdate的触发频率和界面刷新频率一致,会频繁调用,所以回调函数的执行效率会严重影响游戏性能。我们有必要认真考虑任务执行时间的精确度。如果有0.5s的时间误差也能达到相同的目的,那就应该适当减少执行次数。

我们有两种方法来降低执行次数:最小时间间隔;最小帧间隔。

最小时间间隔,限制每秒最多执行两次:

local function onUpdate(self, elapsed) --self是OnUpdate返回的frame,elapsed是距离上次OnUpdate的时间差
    --update任务代码
end

local frame = CreateFrame("Frame")
local e = 0
frame:SetScript("OnUpdate", function(self, elapsed)  --匿名函数
    e = e + elapsed  --累计时间间隔
    if e >= 0.5 then  --如果时间间隔大于0.5s,则执行任务
        e = 0
        return onUpdate(self, elapsed)
    end
end)

最小帧间隔,限制每5帧执行一次:

local frame = CreateFrame("Frame")
local counter = 0  --计数器
frame:SetScript("OnUpdate", function(...)  --匿名函数
    counter = counter + 1  --计数器加1
    if counter % 5 == 0 then  --如果间隔帧数达到5帧,则执行任务
        return onUpdate(self, elapsed)
    end
end

 

四、一个DKP插件

接下来我们将利用上面学的知识来写一个DKP插件,协助完成工会活动时的物品竞拍。

先说一下工会DKP规则:

  1. 团长或DKP管理员发起一件物品竞拍
  2. 所有团队成员可以通过密语的方式向团长或DKP管理员出分
  3. 出分最高者以次高者+1的分数赢得竞拍
  4. 如出分最高者不止一个,Roll点决定
  5. 如只有一人出分,则以最低分赢得竞拍
  6. 如无人竞拍,则流拍

根据以上规则,我们的插件需要完成如下功能:

  1. 启动竞拍——通过命令行,对一件物品发起竞拍,并在Raid频道开始倒计时30s
  2. 成员出分——在倒计时期间,团队成员可以通过密语你出分
  3. 竞拍结束——倒计时结束后,在Raid频道列出所有出分,并给出竞拍结果
  4. 中止竞拍——在竞拍过程中,可以通过命令行随时中止竞拍
  5. 远程命令——可以为官员添加权限,使其可以通过密语你的方式启动或中止竞拍

该插件需要用到上面的计时器插件SimpleTimingLib,需在toc文件中配置:

##Dependencies: SimpleTimingLib

 lua代码如下:

local currentItem   --the item auctioned on
local bids = {}  --bid list
local prefix = "[SimpleDKP]"  --prefix

SimpleDKP_Channel = "RAID"  --default channel
SimpleDKP_AuctionTime = 30  --auction duration
SimpleDKP_MinBid = 15  --minimum dkp
SimpleDKP_ACL = {}  --remote control list

local startAuction, endAuction, placeBid, cancelAuction, onEvent   --declare function keywords at first


----------------------------------------------------------------------start auction-------------------------------------------------------------------------------
do
local auctionAlreadyRunning = "There is already an auction running! (on %s)"
local startingAuction = prefix.."Starting auction for item %s, please place your bids by whispering me. Remaining time: %d seconds."
local auctionProgress = prefix.."Time remaining for %s: %d seconds."

function startAuction(item, starter)
    if currentItem then  --one auction already running
        local msg = auctionAlreadyRunning:format(currentItem)
        if starter then
            SendChatMessage(msg, "WHISPER", nil, starter)
        else
            print(msg)
        end
    else
        currentItem = item
        SendChatMessage(startingAuction:format(item, SimpleDKP_AuctionTime), SimpleDKP_Channel)  --start auction ,time remain 30s
        if SimpleDKP_AuctionTime > 30 then
            SimpleTimeLib_Schedule(SimpleDKP_AuctionTime - 30, SendChatMessage, auctionProgress:format(item, 30), SimpleDKP_Channel)
        end
        if SimpleDKP_AuctionTime > 15 then  --schedule message (remain time 15)
            SimpleTimingLib_Schedule(SimpleDKP_AuctionTime - 15, SendChatMessage, auctionProgress:format(item, 15), SimpleDKP_Channel)
        end
        if SimpleDKP_AuctionTime > 5 then  --schedule message (remain time 5)
            SimpleTimingLib_Schedule(SimpleDKP_AuctionTime - 5, SendChatMessage, auctionProgress:format(item, 5), SimpleDKP_Channel)
        end  --schedule function endAuction
        SimpleTimingLib_Schedule(SimpleDKP_AuctionTime, endAuction)
    end
end
end


----------------------------------------------------------------------end auction-------------------------------------------------------------------------------
do
local noBids = prefix.."No one wants to have %s :("
local wonItemFor = prefix.."%s won %s for %d DKP."
local pleaseRoll = prefix.."%s bid %d DKP on %s, please roll!"
local highestBidders = prefix.."%d. %s bid %d DKP"

local function sortBids(v1, v2)
    return v1.bid > v2.bid
end

function endAuction()
    table.sort(bids, sortBids)
    if #bids == 0 then --case 1:no bid at all
        SendChatMessage(noBids:format(currentItem), SimpleDKP_Channel)
    elseif #bids == 1 then --case 2:one bid; the bidder pays the minimum bid
        SendChatMessage(wonItemFor:format(bids[1].name, currentItem, SimpleDKP_MinBid), SimpleDKP_Channel)
        SendChatMessage(highestBidders:format(1, bids[1].name, bids[1].bid), SimpleDKP_Channel)
    elseif bids[1].bid ~= bids[2].bid then --case 3:highest amount is unique
        SendChatMessage(wonItemFor:format(bids[1].name, currentItem, bids[2].bid + 1), SimpleDKP_Channel)
        for i = 1, math.min(#bids, 3) do --print the three highest bidders
            SendChatMessage(highestBidders:format(i, bids[i].name, bids[i].bid), SimpleDKP_Channel)
        end
    else -- case4: more then 1 bid and the highest amount is not unique
        local str = "" --this string holds all players who bid the same amount
        for i = 1, #bids do --this loop builds the string
            if bids[i].bid ~= bids[1].bid then --found a player who bid less -->break
                break
            else --append the player's name to the string
                if bids[i + 2] and bids[i + 2].bid == bid then
                    str = str..bids[i].name..", " --use a comma if this is not the last
                else
                    str = str..bids[i].name.." and " --this is the last player
                end
            end
        end
        str = str:sub(0, -6) --cut off the last " and "
        SendChatMessage(pleaseRoll:format(str, bids[1].bid, currentItem), SimpleDKP_Channel)
    end
    currentItem = nil --set currentItem to nil as there is no longer an ongoing auction
    table.wipe(bids) --clear the table that holds the bids
end
end


----------------------------------------------------------------------place bids-------------------------------------------------------------------------------
do
    local oldBidDetected = prefix.."Your old bid was %d DKP, your new bid is %d DKP."
    local bidPlaced = prefix.."Your bid of %d DKP has been placed!"
    local lowBid = prefix.."The minimum bid is %d DKP."
    
    function placeBid(msg, sender)
        if currentItem and tonumber(msg) then
            local bid = tonumber(msg)
            if bid < SimpleDKP_MinBid then  --invalid bid
                SendChatMessage(lowBid:format(SimpleDKP_MinBid), "WHISPER", nil, sender)
                return
            end
            for i, v in ipairs(bids) do --check if that player has already bid
                if sender == v.name then
                    SendChatMessage(oldBidDetected:format(v.bid, bid), "WHISPER", nil, sender)
                    v.bid = bid
                    return
                end
            end
            --he hasn't bid yet, so create a new entry in bids
            table.instert(bids, {bid = bid, name = sender})
            SendChatMessage(bidPlaced:format(bid), "WHISPER", nil, sender)
        end
    end
end


----------------------------------------------------------------------cancel auction-------------------------------------------------------------------------------
do
    local cancelled = "Auction cancelled by %s"
    function cancelAuction(sender)
        currentItem = nil
        table.wipe(bids)
        SimpleTimingLib_Unschedule(SendChatMessage)  --Attention: this will unschedule all SendChatMessage includding scheduled by other addons!
        SimpleTimingLib_Unschedule(endAuction)
        SendChatMessage(cancelled:format(sender or UnitName("player")), SimpleDKP_Channel)  --UnitName("player") returns your character name.
    end
end


----------------------------------------------------------------------remote control-------------------------------------------------------------------------------
do
    local addedToACL = "Added %s player(s) to the ACL"
    local removedFromACL = "Removed %s player(s) from the ACL"
    local function addToACL(...) --add multiple players to the ACL
        for i = 1, select("#", ...) do  --iterate over the arguments
            SimpleDKP_ACL[select(i, ...)] = true  --and add all players
        end
        print(addedToACL:format(select("#", ...)))  --print an info message
    end
    
    local function removeFromACL(...)  --remove player(s) from the ACL
        for i = 1, select("#", ...) do  --iterate over the vararg
            SimpleDKP_ACL[select(i, ...)] = nil  --remove the players from the ACL
        end
        print(removedFromACL:format(select("#", ...)))  --print an info message
    end
end



----------------------------------------------------------------------onEvent-------------------------------------------------------------------------------
do
    --register events
    local frame = CreateFrame("Frame")
    frame:RegisterEvent("CHAT_MSG_WHISPER")
    frame:RegisterEvent("CHAT_MSG_RAID")
    frame:RegisterEvent("CHAT_MSG_RAID_LEADER")
    frame:RegisterEvent("CHAT_MSG_GUILD")
    frame:RegisterEvent("CHAT_MSG_OFFICER")
    frame:SetScript("OnEvent", onEvent)

    --event handler
    function onEvent(self, event, msg, sender)
        if event == "CHAT_MSG_WHISPER" and currentItem and tonumber(msg) then
            placeBid(msg, sender)
        elseif SimpleDKP_ACL(sender) then
            --not a whisper or a whisper that is not a bid and the sender has the permission to send commands
            local cmd, arg = msg:match("^!(%w+)%s*(.*)")    -- "!auction xxx" start auction; "!cancel" cancel auction
            if cmd and cmd:lower() == "auction" and arg then
                startAuction(arg, sender)
            elseif cmd and cmd:lower() == "cancel" then
                cancelAuction(sender)
            end
        end
    end
end


----------------------------------------------------------------------slash commands-------------------------------------------------------------------------------
--/sdkp start <item>                                           starts an auction for <item>
--/sdkp stop                                                        stop the current auction
--/sdkp channel <channel>                                set the chat channel to <channel>
--/sdkp time <time>                                           set the time to <time>
--/sdkp minbid <minbid>                                   set the lowest bid to <minbid>
--/sdkp acl                                                          print the list of players who are allowed to control the addon remotely
--/sdkp acl add <names>                                   add <names> to the ACL list
--/sdkp acl remove <names>                              remove <names> from the ACL list

SLASH_SimpleDKP1 = "/simpledkp"
SLASH_SimpleDKP2 = "/sdkp"

do
    local setChannel = "Channel is now \"%s\""
    local setTime = "Time is now %s"
    local setMinBid = "Lowest bid is now %s"
    local currChannel = "Channel is currently set to \"%s\""
    local currTime = "Time is currently set to %s"
    local currMinBid = "Lowest bid is currently set to %s"
    local ACL = "Access Control List:"
    
    SlashCmdList["SimpleDKP"] = function(msg)
        local cmd, arg = string.split(" ", msg)  --split the string with " "
        cmd = cmd:lower()  --the command should not be case-sensitive
        if cmd == "start" and arg then  --/sdkp start item
            startAuction(msg:match("^start%s+(.+)"))  --extract the time link
        elseif cmd == "stop" then  --/sdkp stop
            cancelAuction()
        elseif cmd == "channel" then  --/sdkp channel arg
            if arg then  --a new channel was provided
                SimpleDKP_Channel = arg:upper()  --set is to arg
                print(setChannel:format(SimpleDKP_Channel))
            else  --no channel was provided
                print(currChannel:format(SimpleDKP_Channel))  --print the current one
            end
        elseif cmd == "time" then  --/sdkp time arg
            if arg and tonumber(arg) then  --arg is provided and it is a number
                SimpleDKP_AuctionTime = tonumber(arg)  --set it
                print(setTime:format(SimpleDKP_AuctionTime))
            else  --arg was not provided or it wasn't a number
                print(currTime:format(SimpleDKP_AuctionTime))  --print error message
            end
        elseif cmd == "minbid" then  --/sdkp minbid arg
            if arg and toumber(arg) then  --arg is set and a number
                SimpleDKP_MinBid = tonumber(arg)  --set the option
                print(setMinBid:format(SimpleDKP_MinBid))
            else  --arg is not set or not a number
                print(currMinBid:format(SimpleDKP_MinBid))  --print error message
            end
        elseif cmd == "acl" then  --/sdkp acl add/remove players 1, player2, ...
            if not arg then  --add/remove not passed
                print(ACL) --output header
                for k, v in pairs(SimpleDKP_ACL) do  --loop over the ACL
                    print(k)  --print all entries
                end
            elseif arg:lower() == "add" then  --/sdkp add player1, player2, ...
                --split the string and pass players to our helper function
                addToACL(select(3, string.split(" ", msg)))
            elseif arg:lower() == "remove" then  --/sdkp remove player1, player2, ...
                removeFromACL(select(3, string.split(" ", msg)))  --split & reomve
            end
        end
    end
end            

代码注解和结构已经非常清晰,其中:

  • startAuction——启动竞拍
  • endAuction——竞拍结束
  • placeBid——出分
  • cancelAuction——中止竞拍
  • addACL——添加远程控制人员
  • removeACL——移除远程控制人员
  • onEvent——事件处理器

最后,添加了一些命令行,可用于配置插件:

  • /sdkp start <item>                          启动竞拍
  • /sdkp stop                                      停止竞拍
  • /sdkp channel <channel>              设置输出频道,默认RAID频道
  • /sdkp time                                      设置竞拍时间,默认30s
  • /sdkp minbid                                  设置最小竞拍分数,默认15
  • /sdkp acl                                        打印远程控制人员列表
  • /sdkp acl add <names>                 添加远程控制人员
  • /sdkp acl remove <names>           移除远程控制人员

远程控制人员可以在团队、工会或官员等频道输入如下命令启动和停止竞拍:

  • "!auction <item>"       启动一个竞拍
  • "!stop"                        停止当前竞拍

至此,一个完整的DKP插件就开发完成了!

 

发表评论

0/200
153 点赞
0 评论
收藏