- A+
参考模仿:smilehao/xlua-framework: Unity游戏纯lua客户端完整框架---基于xlua,整合tolua的proto-gen-lua以及各个lua库和工具类 (github.com)
在Lua侧实现了像Unity那样的组件的方式,有两个概念,组件和容器,基础的组件就类似Unity中继承了MonoBehaviour的脚本,基础容器就类似于Unity的GameObject上挂载着一管理自己和子物体组件的组件。所以容器有着组件的所有功能,并额外添加了获取组件的方法,lua侧认为为添加。
一、组件
在Unity中组件可以访问transform、gameobject,修改基础的显示隐藏、改变位置、改变旋转、改变缩放等等,所以基础的组件就可以仿照着实现这些获取和方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 |
--[[ -- 组件基类:所有组件从这里继承 -- 说明: -- 1、采用基于组件的设计方式,容器类负责管理和调度子组件,实现类似于Unity中挂载脚本的功能 -- 2、组件对应Unity原生的各种Component和Script,容器对应Unity原生的GameObject -- 3、写逻辑时完全不需要关注脚本调度,在cs中脚本函数怎么调度的,这里就怎么调度,只是要注意接口变动,lua侧没有Get、Set访问器 -- 注意: -- 1、Lua侧组件的名字并不总是和Unity侧组件名字同步,Lua侧组件名字会作为组件系统中组件的标识 -- 2、Lua侧组件名字会在组件创建时提取Unity侧组件名字,随后二者没有任何关联,Unity侧组件名字可以随便改 -- 3、虽然Unity侧组件名字随后可以随意改,但是不建议(有GC),此外Lua侧组件一旦创建,使用时全部以Lua侧名字为准 -- 5、需要定时刷新的界面,最好启用定时器、协程,界面需要刷新的频率一般较低,倒计时之类的只需要每秒钟更新一次即可 --]] local setmetatable = setmetatable local getmetatable = getmetatable local assert = assert local type = type local rawget = rawget local rawset = rawset local M = Class("Frame.Base.BaseComponent") local COMP_STATE_NONE = 0 local COMP_STATE_ENABLE = 1 local COMP_STATE_DISABLE = 2 -- 构造函数:除非特殊情况,所有子类不要再写这个函数,初始化工作放Awake -- 多种重载方式: -- 1、ComponentTypeClass.New(relative_path) -- 2、ComponentTypeClass.New(child_index) -- 3、ComponentTypeClass.New(unity_gameObject) -- 4、ComponentTypeClass.New(lua_component) function M:ctor(holder, var_arg) -- 持有者 容器 self.holder = holder -- 脚本绑定的transform self.transform = nil -- transform对应的gameObject self.gameObject = nil -- 名字:Unity中获取Transform的名字是有GC的,而Lua侧组件大量使用了名字,所以这里缓存下 self.__name = nil -- 可变类型参数,用于重载 self.__var_arg = var_arg self.enable_state = COMP_STATE_NONE self.active_state = nil --需要的时候再和c#交互,不需要一创建的时候就去获取 local meta = getmetatable(self) or {} setmetatable(self, { __index = function(t, k) local var = meta.__index[k] if var ~= nil then return var end if k == "transform" then local __var_arg = rawget(t, "__var_arg") local __holder = rawget(t, "holder") local value if type(__var_arg) == "string" then value = __holder.transform:Find(__var_arg) elseif type(__var_arg) == "number" then value = __holder.transform:GetChild(__var_arg) elseif type(__var_arg) == "userdata" then value = __var_arg.transform elseif type(__var_arg) == "table" then value = __var_arg.transform assert(value, "__var_arg must LuaObj") end rawset(t, k, value) return value elseif k == "gameObject" then local transform = t.transform if transform ~= nil then local value = t.transform.gameObject if value ~= nil then rawset(t, k, value) return value end end end end }) end -- 创建 function M:Awake() --[[ 创建组件时,如果传入的参数是string, 说明是指定路径,设置组件名为路径,如果不这样设置,两不同路径相同组件创建时将差别是重复 ]] if type(self.__var_arg) == "string" then self.__name = self.__var_arg else self.__name = self.gameObject.name end end --提供给容器调用 容器显示调用 function M:CallOnEnable(...) if self.enable_state == COMP_STATE_ENABLE then return end if not self:GetActive() then return end self.enable_state = COMP_STATE_ENABLE self:OnEnable(...) end -- 打开 function M:OnEnable(...) -- 启用更新函数 end --提供给容器调用 容器隐藏调用 function M:CallOnDisable(...) if self.enable_state == COMP_STATE_DISABLE then return end if not self:GetActive() then return end self.enable_state = COMP_STATE_DISABLE self:OnDisable(...) end -- 关闭 function M:OnDisable(...) -- 禁用更新函数 end function M:IsEnableState() return self.enable_state == COMP_STATE_ENABLE end function M:IsDisableState() return self.enable_state == COMP_STATE_DISABLE end -- 获取名字 function M:GetName() return self.__name end -- 设置名字 function M:SetName(name) if self.holder and self.holder.OnComponentSetName ~= nil then self.holder:OnComponentSetName(self, name) end self.__name = name end -- 激活、反激活 function M:SetActive(active, ...) if self.active_state == active then return end self.active_state = active if active then self.gameObject:SetActive(active) self.enable_param = {...} self.enable_state = COMP_STATE_ENABLE self:OnEnable(...) else self.gameObject:SetActive(active) self.disable_param = {...} self.enable_state = COMP_STATE_DISABLE self:OnDisable(...) end end -- 获取激活状态 function M:GetActive() if self.active_state == nil and self.gameObject then return self.gameObject.activeSelf else return self.active_state end end --设置参考位置 function M:SetPosition( vec3 ) self.transform.localPosition = vec3 end --设置参考位置 function M:GetPosition( ) return self.transform.localPosition end --设置世界坐标 function M:SetWorldPosition( vec3 ) self.transform.position = vec3 end --获取世界坐标 function M:GetWorldPosition( ) return self.transform.position end --设置旋转角度 function M:SetLocalEulerAngle( vec3 ) self.transform.localEulerAngles = vec3 end --设置缩放 function M:SetLocalScale( x, y, z ) self.transform.localScale = Vector3.New(x, y, z or 1) end -- 析构函数 function M:dtor() if self.holder and type(self.holder) ~= "userdata" and self.holder.OnComponentDestroy ~= nil then --通知容器 组件销毁了 self.holder:OnComponentDestroy(self) end self.holder = nil self.transform = nil self.gameObject = nil self.__name = nil self.enable_param = nil self.disable_param = nil end return M |
基础的组件,一般不会直接使用,使用容器进行添加,基础组件的构造函数需要提供持有者就是哪个容器持有这个组件,第二个参数对应的就是容器下的哪个物体,重载参数可以是组件所在的子物体路径、子物体索引、组件的gameobject、lua侧组件实例。
通过元表的形式提供获取transfrom、gameobject,不需要构造的时候就获取,需要时按需获取。还有管理一些基础的激活状态,提供方法给容器调度,基础的修改transfrom方法等。
UI基础组件(UGUI),在基础组件的基础之上,添加了针对RectTransform获取和操作方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
--[[ -- UI组件基类:所有UI组件从这里继承 -- 说明: -- 1、采用基于组件的设计方式,容器类负责管理和调度子组件,实现类似于Unity中挂载脚本的功能 -- 2、组件对应Unity原生的各种Component和Script,容器对应Unity原生的GameObject -- 3、写逻辑时完全不需要关注脚本调度,在cs中脚本函数怎么调度的,这里就怎么调度,只是要注意接口变动,lua侧没有Get、Set访问器 -- 注意: -- 1、Lua侧组件的名字并不总是和Unity侧组件名字同步,Lua侧组件名字会作为组件系统中组件的标识 -- 2、Lua侧组件名字会在组件创建时提取Unity侧组件名字,随后二者没有任何关联,Unity侧组件名字可以随便改 -- 3、虽然Unity侧组件名字随后可以随意改,但是不建议(有GC),此外Lua侧组件一旦创建,使用时全部以Lua侧名字为准 -- 5、需要定时刷新的界面,最好启用定时器、协程,界面需要刷新的频率一般较低,倒计时之类的只需要每秒钟更新一次即可 --]] local setmetatable = setmetatable local getmetatable = getmetatable local rawset = rawset local M = Class("Frame.Base.UIBaseComponent", BaseComponent) -- 构造函数:除非特殊情况,所有子类不要再写这个函数,初始化工作放Awake -- 多种重载方式: -- 1、ComponentTypeClass.New(relative_path) -- 2、ComponentTypeClass.New(child_index) -- 3、ComponentTypeClass.New(unity_gameObject) -- 4、ComponentTypeClass.New(lua_component) function M:ctor(holder, var_arg) -- trasnform对应的RectTransform self.rectTransform = nil --需要的时候再和c#交互,不需要一创建的时候就去获取 local meta = getmetatable(self) or {} setmetatable(self, { __index = function(t, k) local var = meta.__index(t,k) if var ~= nil then return var end if k == "rectTransform" then local transform = t.transform if transform ~= nil then local value = transform:GetComponent(CSTypeRectTransform) if value ~= nil then rawset(t, k, value) return value end end end end }) end function M:SetSizeDelta(size) self.rectTransform.sizeDelta = size end function M:GetSizeDelta() return self.rectTransform.sizeDelta; end function M:GetAnchoredPosition() return self.rectTransform.anchoredPosition end function M:SetAnchoredPosition( x, y ) self.rectTransform.anchoredPosition = Vector2.New(x, y) end function M:GetAnchoredPosX() return self.rectTransform.anchoredPosition.x end function M:SetAnchoredPosX(x) self.rectTransform.anchoredPosition = Vector2.New(x, self.rectTransform.anchoredPosition.y) end function M:GetAnchoredPosY() return self.rectTransform.anchoredPosition.y end function M:SetAnchoredPosY(y) self.rectTransform.anchoredPosition = Vector2.New(self.rectTransform.anchoredPosition.x, y) end function M:GetRect( ) return self.rectTransform.rect end -- 析构函数 function M:dtor() self.rectTransform = nil end return M |
二、容器
因为容器也可以认为是一个基础组件,因为UI组件添加了RectTransform,所以容器也分为了基础容器和UI容器,如果有其他的扩展组件,也需要扩展容器,这里说一下UI容器,继承UIBaseComponent.lua,基础容器是类似的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
--[[ -- UI容器基类:当一个UI组件持有其它UI组件时,它就是一个容器类,它要负责调度其它UI组件的相关函数 -- 注意: -- 1、window.view是窗口最上层的容器类 -- 2、AddComponent用来添加组件,一般在window.view的OnCreate中使用,RemoveComponent相反 -- 3、GetComponent用来获取组件,GetComponents用来获取一个类别的组件 -- 4、很重要:子组件必须保证名字互斥,即一个不同的名字要保证对应于Unity中一个不同的Transform --]] local M = Class("Frame.Base.BaseContainer", UIBaseComponent) local base = UIBaseComponent M:Include(require("Frame.Base.Component.BaseContainerInterface")) function M:Awake() base.Awake(self) self.components = {} -- 二位数组 [component_name][component_class] self.length = 0 end -- 打开 function M:OnEnable() base.OnEnable(self) self:Walk(function(component) component:CallOnEnable(table.unpack(component.enable_param or {})) end) end -- 关闭 function M:OnDisable() base.OnDisable(self) self:Walk(function(component) component:CallOnDisable(table.unpack(component.disable_param or {})) end) end return M |
这里重写了一些生命周期方法,定义存储组件的table,在显示和隐藏的时候调度组件提供的可用不可用调度方法。可以看到脚本前面引入了BaseContainerInterface,其实就是遍历方法复制过来,相当于接口,因为基础容器也是同一套代码,所以定义为一些接口来引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
local M = {} -- 遍历:注意,这里是无序的 function M:Walk(callback, component_class) for _,components in pairs(self.components or {}) do for cmp_class,component in pairs(components) do if component_class == nil then callback(component) elseif cmp_class == component_class then callback(component) end end end end -- 如果必要,创建新的记录,对应Unity下一个Transform下所有挂载脚本的记录表 function M:AddNewRecordIfNeeded(name) if self.components[name] == nil then self.components[name] = {} end end -- 记录Component function M:RecordComponent(name, component_class, component) -- 同一个Transform不能挂两个同类型的组件 assert(self.components[name][component_class] == nil, "Aready exist component_class : ", component_class.__cname) self.components[name][component_class] = component end -- 子组件改名回调 function M:OnComponentSetName(component, new_name) self:AddNewRecordIfNeeded(new_name) -- 该名字对应Unity的Transform下挂载的所有脚本都要改名 local old_name = component:GetName() local components = self.components[old_name] for k,v in pairs(components) do v:SetName(new_name) self:RecordComponent(new_name, k, v) end self.components[old_name] = nil end -- 子组件销毁 function M:OnComponentDestroy(component) self.length = self.length - 1 end -- 添加组件 -- 多种重载方式 -- 1、直接添加Lua侧组件:inst:AddComponent(ComponentTypeClass, luaComponentInst) -- 2、指定Lua侧组件类型和必要参数,新建组件并添加,多种重载方式: -- A)inst:AddComponent(ComponentTypeClass, relative_path) -- B)inst:AddComponent(ComponentTypeClass, child_index) -- C)inst:AddComponent(ComponentTypeClass, unity_gameObject) -- D)inst:AddComponent(ComponentTypeClass, lua_component) function M:AddComponent(component_target, var_arg, ...) local component_inst = nil local component_class = nil if type(var_arg) == "table" then component_inst = var_arg component_class = var_arg._class_type else component_inst = component_target.New(self, var_arg) component_class = component_target component_inst:Awake(...) end local name = component_inst:GetName() --var_arg为string时则将其作为name self:AddNewRecordIfNeeded(name) self:RecordComponent(name, component_class, component_inst) self.length = self.length + 1 return component_inst end function M:GetOrAddComponent(component_target, var_arg, ...) local components = self.components[var_arg] if components and components[component_target] then return components[component_target] end return self:AddComponent(component_target, var_arg, ...) end -- 获取组件 function M:GetComponent(name, component_class) local components = self.components[name] if components == nil then return nil end if component_class == nil then -- 必须只有一个组件才能不指定类型,这一点由外部代码保证 assert(table.count(components) == 1, "Must specify component_class while there are more then one component!") for _,component in pairs(components) do return component end else return components[component_class] end end -- 获取一系列组件:2种重载方式 -- 1、获取一个类别的组件 -- 2、获取某个name(Transform)下的所有组件 function M:GetComponents(component_target) local components = {} if type(component_target) == "table" then self:Walk(function(component) table.insert(components, component) end, component_target) elseif type(component_target) == "string" then components = self.components[component_target] else error("GetComponents params err!") end return components end -- 获取组件个数 function M:GetComponentsCount() return self.length end -- 移除组件 function M:RemoveComponent(name, component_class) local component = self:GetComponent(name, component_class) if component ~= nil then local cmp_class = component._class_type component:Dispose() self.components[name][cmp_class] = nil end end -- 移除一系列组件:2种重载方式 -- 1、移除一个类别的组件 -- 2、移除某个name(Transform)下的所有组件 function M:RemoveComponents(component_target) local components = self:GetComponents(component_target) for _,component in pairs(components) do local cmp_name = component:GetName() local cmp_class = component._class_type component:Dispose() self.components[cmp_name][cmp_class] = nil end return components end -- 销毁 function M:dtor() self:Walk(function(component) -- 说明:现在一个组件可以被多个容器持有,但是holder只有一个,所以由holder去释放 if component.holder == self then component:Dispose() end end) self.components = {} end return M |
容器主要管理组件和记录持有的组件,lua添加组件对应着组件构造的四个重载,持有者就是自己,这样就对应上了。其他的获取组件,移除组件等等就是对记录的修改了和组件的释放了。
三、UI层级构建
说白了就是构建一个列表的有层级差的Canvas提供ui界面挂载渲染,所以需要一个配置表,Canvas的渲染模式使用相机的渲染方式,这样可以做多个相机且多个Canvas的层级关系,多个相机的形式可以针对有些东西需要渲染在ui和ui之间,这样的层级关系就可以处理好这些了。
首先我们需要一个配置,来说明这些层级关系的构建,CameraDepth就是相机的渲染层级,UILayers就是在当前相机下渲染的Canvas,多个相机结构可以在数组中再添加一个类似的配置,注意的是Layer的名字不要重复,以为UI管理是通过名字来识别是哪一个Canvas的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
M.UIBuild = { { CameraDepth = 0, UILayers = { -- 场景UI,如:点击建筑查看建筑信息---一般置于场景之上,界面UI之下 SceneLayer = { Name = "SceneLayer", PlaneDistance = 1000, OrderInLayer = 0, }, -- 主界面、全屏的一些界面 BackgroudLayer = { Name = "BackgroudLayer", PlaneDistance = 1000, OrderInLayer = 1000, }, -- 普通UI,一级、二级、三级等窗口---一般由用户点击打开的多级窗口 NormalLayer = { Name = "NormalLayer", PlaneDistance = 800, OrderInLayer = 2000, }, -- 提示UI,如:错误弹窗,网络连接弹窗等 TipLayer = { Name = "TipLayer", PlaneDistance = 600, OrderInLayer = 3000, }, -- 顶层UI,如:场景加载 TopLayer = { Name = "TopLayer", PlaneDistance = 400, OrderInLayer = 4000, }, } } } |
好了,有了构建的配置表,下面就可以开始构建了,在UIManager单例实例化的时候,就可以遍历配置进行生成了。定义一个变量存储所有的Layer层,SetUIbuildConfig中针对每个相机下的Canvas进行生成。UIRoot是一个只包含一个相机子物体的预制体,在UIBuildGenerator中,获取到相机,并设置它的渲染层级,针对layer进行排序,并新建GameObject在UIRoot下,并实例化一个UILayer组件,在组件中进行相应的Canvas设置,并记录生成的layer,记录的layer是不管是哪个相机显然的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
function M:ctor() self.layers = {} --所有可用的层级 self:__Init() end function M:__Init() self.Resolution = Vector2.New(FrameConst.DesignScreen_Width, FrameConst.DesignScreen_Height) self:SetUIbuildConfig( FrameConfig.UIBuild ) end function M:SetUIbuildConfig( config ) self.layers = {} for i,v in ipairs(config) do self:UIBuildGenerator(v) end end function M:UIBuildGenerator( config ) GameObjectManager:GetInstance():GetAssetInstance("Common/Prefabs/UIRoot.prefab", function(obj) self.ui_root = obj CSGameObject.DontDestroyOnLoad(obj) local camera_trans = obj.transform:Find("UICamera") local camera = camera_trans:GetComponent(typeof( CS.UnityEngine.Camera )) camera.depth = config.CameraDepth or 0 camera.clearFlags = CS.UnityEngine.CameraClearFlags.Nothing local layers = config.UILayers table.walksort(layers, function(lkey, rkey) return layers[lkey].OrderInLayer < layers[rkey].OrderInLayer end, function(index, layer) assert(self.layers[layer.Name] == nil, "Aready exist layer : "..layer.Name) local go = CSGoFind(layer.Name) if not go then go = CSGameObject(layer.Name) local trans = go.transform trans:SetParent(obj.transform, false) end local new_layer = UILayer.New(obj.transform, layer.Name) new_layer:Awake(layer, camera) self.layers[layer.Name] = new_layer end) end) end |
在UILayer组件中,对实例化的GameObject添加相应的组件,让其成为Canvas。操作c#侧添加Canvas、CanvasScaler、GraphicRaycaster组件,并对其进行相应的设置,相应的设置可以看代码,针对需求进行设置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
--[[ -- Lua侧UILayer --]] local M = Class("UILayer", UIBaseComponent) local base = UIBaseComponent -- 创建 function M:Awake(layer, camera) base.Awake(self) -- Unity侧原生组件 self.unity_canvas = nil self.unity_canvas_scaler = nil self.unity_graphic_raycaster = nil -- ui layer self.gameObject.layer = 5 -- canvas self.unity_canvas = self.gameObject:AddComponent(typeof(CS.UnityEngine.Canvas)) -- 说明:很坑爹,这里添加UI组件以后transform会Unity被替换掉,必须重新获取 self.transform = self.unity_canvas.transform self.gameObject = self.unity_canvas.gameObject self.unity_canvas.renderMode = CS.UnityEngine.RenderMode.ScreenSpaceCamera self.unity_canvas.worldCamera = camera self.unity_canvas.planeDistance = layer.PlaneDistance self.unity_canvas.sortingLayerName = FrameConfig.SortingLayerNames.UI self.unity_canvas.sortingOrder = layer.OrderInLayer -- scaler self.unity_canvas_scaler = self.gameObject:AddComponent(typeof(CS.UnityEngine.UI.CanvasScaler)) self.unity_canvas_scaler.uiScaleMode = CS.UnityEngine.UI.CanvasScaler.ScaleMode.ScaleWithScreenSize self.unity_canvas_scaler.screenMatchMode = CS.UnityEngine.UI.CanvasScaler.ScreenMatchMode.MatchWidthOrHeight self.unity_canvas_scaler.referenceResolution = UIManager:GetInstance().Resolution if CS_SCREEN_W/CS_SCREEN_H > FrameConst.DesignScreen_Width/FrameConst.DesignScreen_Height then self.unity_canvas_scaler.matchWidthOrHeight = 1 else self.unity_canvas_scaler.matchWidthOrHeight = 0 end -- raycaster self.unity_graphic_raycaster = self.gameObject:AddComponent(typeof(CS.UnityEngine.UI.GraphicRaycaster)) end --设置canvas的worldCamera function M:SetCanvasWorldCamera(camera) --需要切换layer下面所有layer不对的情况 local old_camera = self.unity_canvas.worldCamera if old_camera ~= camera then self.unity_canvas.worldCamera = camera end end function M:GetCanvasLayer() return self.gameObject.layer end function M:GetCanvasSize() return self.rectTransform.rect.size end -- 销毁 function M:dtor() self.unity_canvas = nil self.unity_canvas_scaler = nil self.unity_graphic_raycaster = nil end return M |
四、UI窗口定义
定义一个记录一个窗口需要的信息类,每一个UI窗口记录,都用这个来保存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
--[[ -- UIWindow数据,用以表示一个窗口 -- 注意: -- 1、窗口名字必须和预设名字一致 --]] local UIWindow = { -- 窗口名字 Name = "Background", -- Layer层级 Layer = 0, -- View实例 View = UIBaseView, -- 是否激活 Active = false, -- ui配置 VIEW_CONFIG = {}, -- 是否正在加载 IsLoading = false, } return Class("UIWindow", UIWindow) |
再定义一个基础的窗口类,所有的UI界面都继承它,而基础的窗口类继承UIBaseContainer,也就是说UI界面就是一个组件同时也是一个容器,可以获取子物体的组件进行逻辑处理了。VIEW_CONFIG配置的是UI界面应该挂载在上面生成好的哪一个Layer下,PrefabPath是一个Addressable的地址,用来加载UI界面的预设。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
local M = Class("UIBaseView", UIBaseContainer) local base = UIBaseContainer M.VIEW_CONFIG = { Layer = "NormalLayer", PrefabPath = nil, } -- 构造函数:必须把基类需要的所有参数列齐---即使在这里不用,提高代码可读性 -- 子类别再写构造函数,初始化工作放OnCreate function M:ctor(holder, var_arg) end -- 创建:资源加载完毕 function M:OnCreate() base.OnCreate(self) end -- 打开:窗口显示 function M:OnEnable(...) base.OnEnable(self) end -- 关闭:窗口隐藏 function M:OnDisable() base.OnDisable(self) end --关闭窗口 function M:CloseSelf() UIManager:GetInstance():CloseWindow(self:GetName()) end function M:DestroySelf() UIManager:GetInstance():DestroyWindow(self:GetName()) end function M:GetRootGameObjectByName( name ) local scene_script = SceneManager:GetInstance():GetCurentSceneScript() if scene_script then return scene_script:GetRootGameObjectByName(name) end return nil end -- 销毁:窗口销毁 function M:dtor() end return M |
五、UI管理器
UI管理器的作用就是打开界面和关闭界面等进行界面管理,打开界面的时候只要传入继承自UIBaseView的lua路径,这里会生成一个UIWindow来记录相关内容,包括实例化lua的UIBaseView的实例,持有者就是指定的layer,参数为界面lua路径,也包括VIEW_CONFIG配置等。打开界面的时候,会通过Addressable加载并实例化UI界面,然后调用View的Awake方法。最后就是激活窗口,把窗口在当前层级置顶,调用SetActive方法,调度窗口及Awake下添加组件的OnEnable方法。同理关闭界面的时候,传入lua路径,同样会调用SetActive方法,调度窗口及Awake下添加组件的OnDisable方法。
完整的UIManager.lua:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 |
--[[ fd: UI管理类,所有UI都应该通过该管理类进行创建 提供UI操作、UI层级、UI消息、UI资源加载、UI调度、UI缓存等管理 ]] local CSGameObject = CS.UnityEngine.GameObject local CSGoFind = CSGameObject.Find local base = require("Frame.Base.Manager.ManagerBase") local M = Class("UIManager", base) local IsNull = IsNull function M:ctor() self.windows = {} --所有存活的窗体 {ui_name:window} self.layers = {} --所有可用的层级 self:__Init() end function M:__Init() self.Resolution = Vector2.New(FrameConst.DesignScreen_Width, FrameConst.DesignScreen_Height) self:SetUIbuildConfig( FrameConfig.UIBuild ) end function M:SetUIbuildConfig( config ) self.layers = {} for i,v in ipairs(config) do self:UIBuildGenerator(v) end end function M:UIBuildGenerator( config ) GameObjectManager:GetInstance():GetAssetInstance("Common/Prefabs/UIRoot.prefab", function(obj) self.ui_root = obj CSGameObject.DontDestroyOnLoad(obj) local camera_trans = obj.transform:Find("UICamera") local camera = camera_trans:GetComponent(typeof( CS.UnityEngine.Camera )) camera.depth = config.CameraDepth or 0 camera.clearFlags = CS.UnityEngine.CameraClearFlags.Nothing local layers = config.UILayers table.walksort(layers, function(lkey, rkey) return layers[lkey].OrderInLayer < layers[rkey].OrderInLayer end, function(index, layer) assert(self.layers[layer.Name] == nil, "Aready exist layer : "..layer.Name) local go = CSGoFind(layer.Name) if not go then go = CSGameObject(layer.Name) local trans = go.transform trans:SetParent(obj.transform, false) end local new_layer = UILayer.New(obj.transform, layer.Name) new_layer:Awake(layer, camera) self.layers[layer.Name] = new_layer end) end) end function M:Broadcast(e_type, ...) CallEvent(e_type, ...) end --@brief 获取UI窗口 function M:GetWindow(ui_name, active, view_active) local target = self.windows[ui_name] if target == nil then return nil end if active ~= nil and target.Active ~= active then return nil end if view_active ~= nil and target.View:GetActive() ~= view_active then return nil end return target end function M:GetView(ui_name) local target = self.windows[ui_name] if target == nil then return nil end return target.View end function M:GetLayer(layer) return self.layers[layer] end --@brief 初始化window function M:__InitWindow(ui_name, window) local luaClass = require(ui_name) local VIEW_CONFIG = luaClass.VIEW_CONFIG assert(VIEW_CONFIG ~= nil, "No VIEW_CONFIG : "..ui_name) local layer = self.layers[VIEW_CONFIG.Layer] assert(layer, "No layer named : "..ui_name..".You should create it first!") window.Name = ui_name window.View = luaClass.New(layer, window.Name) window.Active = false window.Layer = layer window.VIEW_CONFIG = VIEW_CONFIG self:Broadcast(FrameEvent.UIFRAME_ON_WINDOW_CREATE, window) return window end --@brief 激活窗口 function M:__ActivateWindow(target, ...) assert(target) assert(target.IsLoading == false, "You can only activate window after prefab loaded!") target.View.transform:SetAsLastSibling() target.View:SetActive(true, ...) self:Broadcast(FrameEvent.UIFRAME_ON_WINDOW_OPEN, target) end --@brief 反激活窗口 function M:__Deactivate(target) if target.View == nil or target.View.transform == nil then return end target.View:SetActive(false) self:Broadcast(FrameEvent.UIFRAME_ON_WINDOW_CLOSE, target) end --@brief 打开窗口-私有 function M:__InnerOpenWindow(useCroutine, target, ...) assert(target) assert(target.View) assert(target.Active == false, "You should close window before open again!") target.Active = true local has_view = target.View ~= UIBaseView local has_prefab_res = target.VIEW_CONFIG.PrefabPath and #target.VIEW_CONFIG.PrefabPath > 0 local has_loaded = not IsNull(target.View.gameObject) local need_load = has_view and has_prefab_res and not has_loaded if not need_load then self:__ActivateWindow(target, ...) else target.View.__temp_params = SafePack(...) if target.IsLoading then --正在加载时 不要再次加载资源了 return end target.IsLoading = true local function OnLoadGameObjectDone(go) if IsNull(go) then zprint( string.format("UIManager InnerOpenWindow %s faild", target.VIEW_CONFIG.PrefabPath) ) return end local trans = go.transform trans:SetParent(target.Layer.transform, false) trans.name = target.Name target.IsLoading = false target.View:Awake() if target.Active then self:__ActivateWindow(target, SafeUnpack(target.View.__temp_params)) target.View.__temp_params = nil else self:__Deactivate(target) end end if useCroutine then local go = GameObjectManager:GetInstance():CoGetAssetInstance(target.VIEW_CONFIG.PrefabPath) OnLoadGameObjectDone(go) else GameObjectManager:GetInstance():GetAssetInstance(target.VIEW_CONFIG.PrefabPath, OnLoadGameObjectDone) end end end --@brief 关闭窗口-私有 function M:__InnnerCloseWindow(target) assert(target) assert(target.View) if target.Active then self:__Deactivate(target) target.Active = false end end --@brief 判断窗口是否打开 function M:IsActiveWindow(ui_name) local target = self:GetWindow(ui_name) if not target then return false end return target.Active end function M:__OpenWindow(useCroutine, ui_name, ...) local target = self:GetWindow(ui_name) if not target then local window = UIWindow.New() self.windows[ui_name] = window target = self:__InitWindow(ui_name, window) end --先关闭 self:__InnnerCloseWindow(target) self:__InnerOpenWindow(useCroutine, target, ...) return target.View end --@brief 协程方式打开窗口 function M:CoOpenWindow(ui_name, ...) self:__OpenWindow(true, ui_name, ...) end --@brief 异步方式打开窗口 function M:OpenWindow(ui_name, ...) return self:__OpenWindow(false, ui_name, ...) end --@brief 关闭窗口-只是设置为隐藏 function M:CloseWindow(ui_name) local target = self:GetWindow(ui_name, true) if not target then return end self:__InnnerCloseWindow(target) end --@brief 关闭层级所有窗口 function M:CloseWindowByLayer(layer, except_ui_names) except_ui_names = except_ui_names or {} local dict_ui_names = {} for k, ui_name in pairs(except_ui_names) do dict_ui_names[ui_name] = true end for ui_name, v in pairs(self.windows) do if v.Layer:GetName() == layer.Name and not dict_ui_names[ui_name] then self:CloseWindow(ui_name) end end end -- 关闭其它层级窗口 function M:CloseWindowExceptLayer(layer) for _,v in pairs(self.windows) do if v.Layer:GetName() ~= layer.Name then self:__InnnerCloseWindow(v) end end end --@brief 关闭所有窗口 function M:CloseAllWindows() for _,v in pairs(self.windows) do self:__InnnerCloseWindow(v) end end function M:__InnerDestroyWindow(target) self:Broadcast(FrameEvent.UIFRAME_ON_WINDOW_DESTROY, target) GameObjectManager:GetInstance():RecycleObject(target.View.gameObject) target.View:Dispose() self.windows[target.Name] = nil end --@brief 销毁窗口-会将预设回收到内存池中 function M:DestroyWindow(ui_name) local target = self:GetWindow(ui_name) if not target then return end self:CloseWindow(ui_name) self:__InnerDestroyWindow(target) end function M:DestroyWindowExceptNames(except_ui_names) local dict_ui_names = {} for k, ui_name in pairs(except_ui_names) do dict_ui_names[ui_name] = true end for k, v in pairs(self.windows) do if not dict_ui_names[v.Name] then self:DestroyWindow(v.Name) end end end --@brief 销毁层级所有窗口 function M:DestroyWindowByLayer(layer) for k,v in pairs(self.windows) do if v.Layer:GetName() == layer.Name then self:DestroyWindow(v.Name) end end end function M:DestroyWindowExceptLayer(layer) for k,v in pairs(self.windows) do if v.Layer:GetName() ~= layer.Name then self:DestroyWindow(v.Name) end end end function M:DestroyAllWindow() for k,v in pairs(self.windows) do self:DestroyWindow(v.Name) end end function M:IsActiveWindowByLayer(layer) local has = false for ui_name, v in pairs(self.windows) do if v.Layer:GetName() == layer.Name and v.View:GetActive() then has = true break end end return has end --用来调用界面方法 function M:Notify(ui_name, funcname, ...) if not self:IsActiveWindow(ui_name) then return end local view = self:GetView(ui_name) if view and view[funcname] then xpcall(view[funcname](view, ...), function(err) end) end end function M:dtor() self:DestroyAllWindow() if self.ui_root then CSGameObject.Destroy(self.ui_root) end end return M |
六、例子
1 2 3 4 5 6 7 |
local M = { UIWindowNames = { StudyAudioPanel = "Hall.Views.StudyAudioPanel", } } UIManager:GetInstance():OpenWindow( M.UIWindowNames.StudyAudioPanel ) UIManager:GetInstance():CloseWindow( M.UIWindowNames.StudyAudioPanel ) |