• 事件


    一、窗体事件

    (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插件就开发完成了!

  • 相关阅读:
    MySQL教程详解之存储引擎介绍及默认引擎设置
    最简单MySQL教程详解(基础篇)之多表联合查询
    Postfix常用命令和邮件队列管理(queue)
    备份数据库
    Docker基本命令
    ASCII码表
    mysql基本了解
    顺序对列,环形队列,反向链式栈
    进制的标识符
    多个线程的时间同步
  • 原文地址:https://www.cnblogs.com/not2/p/12097021.html
Copyright © 2020-2023  润新知