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取0或1,表示是否显示对应的菜单项
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)
就是这么简单!原方案的缺陷统统没有了,比较而言,新方案有了以下几个优点:
✔ 核心函数极为简单,菜单的构建独立于应用层的业务逻辑,只负责构建右键菜单树;
✔ 无论有多少应用场景,核心函数不用再修改了,代码就这么几十行;
✔ 各应用场景独立配置各自的右键菜单,颜色、过滤,都可以差异化定制;
✔ 配置结构与显示布局基本对应,“所见即所得”;
✔ 可灵活传参;
✔ 轻松支持多级菜单;
✔ 为一个新场景显示右键菜单,几分钟搞定,没有需要耗费脑力去额外理解的东西;
✔ 维护成本可以忽略不计
事实上,撸出这个右键菜单机制后,客户端代码中上百处的原右键菜单很快都切换到新方案,旧的方案慢慢被束之高阁,无人问津了。
本文核心就两个字:解耦!除此以外,也没什么了。