cocos2dx-lua UI编辑器的设计思路
在目前的cocos2dx项目开发中,基本只有2个编辑器可选。一个是现在官方推荐的CocosCreator, 但它并不支持我们常用的lua脚本。另一个是CocosStudio, 官方已经不再对其维护,且使用也并不太方便。那么有没有什么方法能让编辑器更好的接入lua脚本,又能方便地自定义控件呢?
想要编辑器对lua脚本更好地支持,那么我们的编辑器可以用lua来开发,这样编辑器可以直接分析lua代码。这里推荐使用imgui来开发编辑器, 比直接用cocos-x更快,关于imgui请点击:cocos2dx上适合做工具的UI库ImGui。为了让实现编辑器中结点名与lua变量的自动绑定,我想到了让所有界面继承于BaseLayer,在基类中自动处理lua变量和事件的绑定。一个典型的游戏界面如下,由编辑器自动生成:
local XxxLayer = class("XxxLayer", require("common.BaseLayer"), function() return display.newLayer() end) -- UI结构定义 XxxLayer.uiTree = {} -- 自定义内容 -- 构造函数 --[[ params: --]] function XxxLayer:ctor(params) -- 初始化Layer, 可传入参数详情参见BaseLayer self.super.ctor(self) -- 初始化UI self:initUI() end -- 初始化UI function XxxLayer:initUI() self:createUITree() -- 进行UI的额外调整 end return XxxLayer
其中createUITree就是由BaseLayer提供的方法,主要创建uiTree中定义的控件。我们约定"-- 自定义内容"之前的交给编辑器,用户不能编辑(编辑器保存时会被重置)。uiTree中保存的是结点的树形结构,里面定义了结点的类型及各种创建参数,比如加入一个按钮后,会变成:
XxxLayer.uiTree = {[1]={["cType"]="Button",["children"]={},["name"]="closeBtn",["params"]={["image"]="c_13.png",["pos"]={["x"]=320,["y"]=568,},["scale"]=1,},},}
为了更好的支持控件的自定义,我们可以定义一种简单的控件格式,如:
UIWrap = { --[[ return { hint = "创建sprite", params = { {name = "image", type="file", hint = "图片名", required = "c_11.png"}, {name = "pos", type="ccp", hint = "位置", required = cc.p(100, 100)}, {name = "anchor", type="ccp", hint = "锚点"}, {name = "scale", type="number", hint = "缩放"}, }, } --]] ["Sprite"] = function (params) local retSprite = display.newSprite(params.image, params.pos and params.pos.x, params.pos and params.pos.y) if params.anchor then retSprite:setAnchorPoint(params.anchor) end if params.scale then retSprite:setScale(params.scale) end return retSprite end, }
所有控件都通过此格式来定义,编辑器打开时就可以解析这个lua文件,获得所有的控件类型。其中hint表示控件或参数提示,params中是此控件的所有参数,type主要用于参数设定的个性化(如ccp类型, number类型, file类型甚至可以直接给出文件选择),required表示此参数必须设置并给出了缺省值。可以看到此控件格式最终返回的是一个function, 那么编辑器中创建Sprite结点时,实际上就是调用了一次此function, 而实际游戏代码中仍然也是调用此function来创建Sprite结点, 那么就实现了编辑中的所见及所得。
BaseLayer:createUITree方法里面主要是循环创建uiTree里面定义的结点,并将结点名和事件与layer进行绑定。大致流程如下:
function BaseLayer:createUITree() -- 创建一个cocos2dx结点列表并添加到指定的父结点上 local function createChildrenNode(children, parentNode) for _,item in ipairs(children) do -- 使用编辑器中的参数创建结点 local newNode = UIWrap[item.cType](item.params) parentNode:addChild(newNode) --[[ 默认的结点名以"untitled"开头,表示我们并不关心此结点 将需要访问的结点,直接绑定到self上 如结点名为"closeBtn", 代码中可以用self.closeBtn直接访问 --]] if not string.find(item.name, "untitled") then self[item.name] = newNode end -- 创建子结点 if next(item.children) then createChildrenNode(item.children, newNode) end end end -- 从uiTree开始创建 createChildrenNode(self.uiTree, self) end
按钮事件绑定: 有些特殊控件(如button等)需要设定点击回调,在lua代码中对应的是function类型。为了实现编辑器中参数与lua代码中function的绑定,我们在编辑器中记录下绑定的lua函数名(通过分析Layer的function类型可获得所有函数名)。然后在创建结点之前,将回调参数与Layer中的函数进行绑定:
-- 结点创建之前,点击事件绑定 for key, value in pairs(item.params) do -- 我们规定点击事件必须以"on"开头来过滤Layer中普通的函数名 if type(value) == "string" and string.find(value, "on") == 1 and self[value] then item.params[key] = handler(self, self[value]) end end
模版控件处理: 对于像ListView这样的控件,需要特殊处理。我们可以定义一个特殊控件”Layout“,它并不随Layer的创建而创建(在createChildrenNode中发现控件类型为Layout时,则仅记录其结点树数据,不创建其结点),只能代码调用BaseLayer:createLayoutNode来创建。createLayoutNode函数与createUITree类似,只是创建内容变成了Layout和它的子结点。填充ListView时,可以这样:
-- 填充ListView数据 for i=1,10 do local childLayout = self:createLayoutNode("ListLayout") self.goodsListView:pushBackCustomItem(childLayout) -- 下面对各childLayout设置表现差异 end
我们甚至可以在编辑器中创建多个不同的Layout,来丰富ListView上的显示效果(如ListView中最后显示一个"更多")。
在编辑器实现基本的布局、增删、修改功能后,控件类型的增加就已经和编辑器无关了。我们甚至可以创建一些复杂的控件类型,如英雄头像,英雄星级之类。比如和之间仅仅就是创建的starNum参数值不一样而已。