游戏UI右键菜单的实现优化

  RPG类端游里面右键菜单的应用场景是随处可见,比如好友,聊天栏玩家名字连接,队友,帮会成员等,不同的应用场景,右键菜单的功能项可能不同,比如:

在这里插入图片描述
  我参与过的一个项目的右键菜单是一个大杂烩的实现方案,有一张巨大的表,大概是这样的:

local ContextMenuRightClick =
{
	[1]={name = "加为好友", userdata = nil, func = "AddFriend", color = red},
	[2]={name = "申请组队", userdata = nil, func = "TeamRequest", color = yellow},
	[3]={name = "邀请组队", userdata = nil, func = "TeamInvited", color = blue},
	...
	[98]={name = "观察角色", userdata = nil, func = "ViewPlayer", color = red},
	[99]={name = "拉黑", userdata = nil, func = "Addblack", color = yellow},
	[100]={name = "跟随", userdata = nil, func = "TailFollow", color = blue},
	...
	[120]={name = "发送邮件", userdata = nil, func = "SendMail", color = blue},
	[121]={name = "私聊", userdata = nil, func = "ChatWith", color = yellow},
	...
}	

  接着有一个巨大的函数来统一处理右键菜单的逻辑:

-- playerid:目标玩家的id
-- pos:右键菜单在哪里显示
function ShowMenuForRightClick(playerid, pos)
    -- SelectedItems中的key是ContextMenuRightClick表里面的索引,value取01,表示是否显示对应的菜单项
	local SelectedItems = 
	{
		[1] = 0,
		[2] = 0,
		...
		[100] = 0,
		[101] = 0,
		...
		[121] = 0,
	}
	...
	-- 以下几乎是所有相关系统的各种复杂的判断逻辑,以检验各菜单功能项是否显示
	if 可以和他组队 then
		SelectedItems[*] = 1
    else
		SelectedItems[*] = 0
    end
    ...
	if 可以加他好友 then
		SelectedItems[*] = 1
    else
		SelectedItems[*] = 0
    end
    ...
    ShowContextMenu(pos, SelectedItems) -- 显示菜单
end

  然后在各应用场景,调用这个函数ShowMenuForRightClick(playerid, pos)

  该方案有以下弊端:

  ✘ 将所有应用场景中的右键菜单集中在一个庞大的核心函数中实现,各种不相干的业务逻辑混合在一起:队伍判断,帮会判断,师徒判断,好友判断等等,构建菜单的逻辑和应用层的业务逻辑紧密耦合在一起;

  ✘  这个核心函数会继续扩展到成千上万行;

  ✘  无法差异化处理不同应用场景的不同需求,比如有的场景中只需要显示业务相关的功能菜单,而不是无条件显示所有可能用到的功能菜单;

  ✘  参数固定为playerid,太过僵化;

  ✘  支持多级菜单比较困难;

  ✘  为一个新场景显示右键菜单,开发时间较长,先找到这个巨大的函数,然后找到具体判断逻辑的地方,小心翼翼地配置SelectedItems表;

  ✘  维护成本高,冗余,如果某个菜单项废弃不用了,则这个菜单项的key得保留,不能被新的功能项复用。

  我在第三次使用这个右键菜单系统时实在忍无可忍,自己动手撸了一个右键菜单的实现。我想象中的右键菜单是这样的:

在这里插入图片描述
  这是好友头像的右键菜单,很自然地,我在撸代码时,想这么配置好友菜单:

FriendMenu =
{
	{name = "申请组队", color = yellow, func = "TeamRequest", check = "CanTeamTo"},
	{name = "邀请组队", color = yellow, func = "TeamInvited", check = "CanTeamIn"},
	{name = "私   聊", color = blue, func = "ChatWith"},
	{name = "拉   黑", color = red, func = "Addblack"},
	{name = "移   入", color = green,
		func = -- 点击“移入”弹出二级菜单,这个功能由UI的菜单系统支持,所以func可以直接配置为一个表
	    {
	  	    {name = "同学", color = blue, func = "Move2Classmate"},  -- 同学分组
	  	    {name = "同事", color = yellow, func = "Move2Colleague"},  -- 同事分组
	    }
	}
}

  配置表中的func字段是点击菜单的回调函数,如果菜单项有子菜单,则func是一个含有子菜单的表,这样的菜单项点击没有实际意义,仅仅是展开或隐藏子菜单,这个功能一般由UI系统(比如CEGUI)自带的功能支持,子表的结构和自身的结构完全相同,构成递归嵌套的多级菜单。
  check字段是一个过滤函数,返回true和false,用来决定这个功能项是否应该显示出来。比如当右键某个好友时,他已经在别的队伍里了,是无法邀请他重新组队的,则CanTeamIn应该返回false,那么“邀请组队”功能项就可以不显示出来;而我还是可以申请组队进入他的队伍,CanTeamTo应该返回true,那么“申请组队”功能项就应该显示出来。

  首先针对这个配置表,写一个构建菜单的函数(为了支持子菜单,需要递归):

-- menu:菜单的UI控件对象
-- menuItems:菜单的功能项
-- ...:各应用场景提供的额外参数,在回调func或check时,传入
function MakeMenuTree(menu, menuItems, ...)
    for key, value in menuItems do
        local n = key
        local item = value
        if not item.check or _G[item.check](arg, item) then -- 如果该菜单项需要显示
            if item.name ~= nil and item.name ~= "" then
                local menuItem = nil -- 菜单的UI控件对象
                if gGuiWindowMgr:isWindowPresent(menu:GetName()) then
                    menuItem = gGuiWindowMgr:getWindow(menu:GetName())
                else
                    menuItem = gGuiWindowMgr:createWindow("Look/MenuItem", menu:GetName())
                end                
                -- menu:AddItem检测功能项是否有子菜单
                if type(item.func) == 'table' then
                    local subMenu = Menu:Create(nil) -- 子菜单的位置不用指定
                    menuItem:setPopupMenu(subMenu:GetMenu())
                    MakeMenuTree(subMenu, item.func, unpack(arg)) -- 递归子菜单
                else
	                -- 回调函数的头两个参数是UI系统的菜单项对象和鼠标点击事件对象,以支持更精细的控制
                    menuItem:subscribe("Clicked",
                                      function(e) _G[item.func](menuItem, e, arg) end)
                end
                menuItem:setText(item.name) -- 功能名
                menuItem:setProperty("NormalTextColour", item.color) -- 颜色
                menu:AddItem(menuItem)
            end
        end
    end
end

  然后提供一个供各个应用场景调用的接口:

-- pos:右键菜单在哪里显示
-- menuItems:菜单项配置表
-- ...:各应用场景提供自己需要的参数,在回调func或check时,传入
function ShowRightClickMenu(pos, menuItems, ...)    
    if table.getn(menuItems) > 0 then
        local menu = Menu:Create(pos)
        MakeMenuTree(menu, menuItems, unpack(arg))
        menu:Show()
    end
end

各应用场景配置好自己的右键菜单(表),在右键点击时,用位置,配置的菜单表以及额外的参数(比如角色id,帮会id之类的)调用ShowRightClickMenu即可,例如好友右键菜单:ShowRightClickMenu(pos, FriendMenu, playerid)

  就是这么简单!原方案的缺陷统统没有了,比较而言,新方案有了以下几个优点:

  ✔  核心函数极为简单,菜单的构建独立于应用层的业务逻辑,只负责构建右键菜单树;

  ✔  无论有多少应用场景,核心函数不用再修改了,代码就这么几十行;

  ✔  各应用场景独立配置各自的右键菜单,颜色、过滤,都可以差异化定制;

  ✔  配置结构与显示布局基本对应,“所见即所得”;

  ✔  可灵活传参;

  ✔  轻松支持多级菜单;

  ✔  为一个新场景显示右键菜单,几分钟搞定,没有需要耗费脑力去额外理解的东西;

  ✔  维护成本可以忽略不计

  事实上,撸出这个右键菜单机制后,客户端代码中上百处的原右键菜单很快都切换到新方案,旧的方案慢慢被束之高阁,无人问津了。

  本文核心就两个字:解耦!除此以外,也没什么了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值