业务框架
业务框架
项目逻辑由 Lua 主导,通过插件 XLua 与 Unity 交互。
XLua 把这层交互透明化,和 C# 交互基本无异
项目的业务框架是典型的 Model-View-Control 结构,在项目里为 Cache-Panel-Controller。如果加上网络和各类配合的脚本,可大致分为
- Cache (model)
- Cache 缓存
- Config 配置表
- Panel (view)
- Panel 面板
- Module 模块
- Alert 弹窗
- ToolTip 弹窗
- Cell 列表格子
- Controller (control)
- Controller 控制
- Proxy 网络代理
- Manager 管理器
关系图如下:
服务器通过消息将数据发送到 controller,经过整理的数据存放于 cache 中供其他系统使用(界面,管理器或其他cache)。
可以看到,只有 controller 能直接控制 cache 的更新,而 panel 仅读取 cache 对界面进行刷新。同时 controller 则控制界面的显示。
除此之外,各类模块需要交互都是通过事件 Event。
因为 panel 生命周期短,当 panel 需要持久化数据可以将存放于 cache 中,这是允许的,做好区分即可。
举例
一个典型的模块会包含 cache,panel,controller,以背包为例:
PackCache.lua、PackModule.lua、PackController.lua、PackProxy.lua
起源
界面由 UI 管理器产生,包括 UIManager,AlertManager,ToolTipMananger,它们分别控制模块界面,分别是 Module,Alert,以及 ToolTip。一切界面都是panel,有 BaseUIPanle 派生而来,为了更好管理,我们会把底部的界面设置 Module。
相较于 BaseUIPanel,BaseUIModule 增加模块特有的行为,显隐动画,模块事件传递,模块遮罩等等。继承层后的使用基本和 BaseUIPanel 一致。
BaseUIPanel
BaseUIPanel 是一切界面的基类。
它功能包含
-
定义界面生命周期
-
资源加载、管理
-
子界面管理
-
事件监听
生命周期
为了基类和各类控制器更好的组织界面,会把界面的操作进行分类,并限定在各类生命周期函数中。在编写功能脚本时,应该严格准守。
init(params)
定义界面使用的所有字段以及对它们初始化,不允许此函数以外新增字段。
一般会设定界面的预制体,gameObject 名字,parent 节点等,如
function M:init(params)
self.parent = LayerManager.uiWindowFullContainer
self.prefab = "UI/Prefab/Public/Base/BaseFullPanel.prefab"
self.name = "PackModule"
self.mask = false
end
赋值时留意 self.myValue = nil 的操作,这会把字段抹去,后续赋值将会报错。
AddUIListener()
添加事件的监听,常用用法为
-- 添加事件监听
function M:AddUIListener()
self:Listen(NM.net.roleLevelChanged, self.OnRoleLevelChanged) -- 等级
self:Listen(NM.ui.trackPointStateChange, self.OnTrackPointStateChange) -- 小红点跟踪
self:Listen(NM.ui.packShowRoleModel, self.ShowModel)
self:Listen(NM.ui.roleWeaponUpdate, self.RefreshModel)
end
OnCreateFinished(params)
界面创建完毕后调用,此时界面已经创建完毕,可以加载子界面,或者对界面上的描述文字设置显示。
function P:OnCreateFinished(params)
self.view.txtAdventureTitle.text = Language.GetStrOnly(27001001)
self.view.txtBaseTitle.text = Language.GetStrOnly(27001002)
local createParams = {
gameObject = self.view.negItem,
name = "TestItem",
data = false,
}
self:AddChildPanel("subPanel", ItemCell, createParams, false, true)
end
OnCreateDelayFinished(params)
触发时,表明OnCreateFinished
等待加载的界面都已完成。常用于添加按钮事件监听。
通常来讲按钮触发的逻辑和界面显示相关,所以为了确保按钮相关的界面已经生成,统一在此注册按钮监听
function P:OnCreateDelayFinished(params)
self.view.btnDetailAttr:SetOnClick(function ()
self.itemCell:Show()
end)
end
OnShowFinished(params, bIsModuleShow)
界面被调用Show
时触发,params
为Show
为的参数
OnUpdateView(params)
当界面show
时触发,或界面隐藏期间,监听事件被触发后显示调用。
界面的显示逻辑应该由数据驱动,所以数据源不变的情况下Show
,不对界面进行实际的显示处操作,只调用 Show。
OnHideBefore(bIsModuleHide)
界面隐藏前调用
OnDestroyBefore
界面销毁前调用,用于清空 panel 的字段,释放场景对象,定时器,动画等。
底层会处理通过 childPanel 添加的子界面。
子界面创建与控制
创建界面有两种方式,
- Create
- AddChildPanel
其中 Create
的形式一般用于 Module 层面,由 Manager 底层调用。更常见的方式是使用 BaseUIPanel
提供的 AddChildPanel
,将界面创建并存放于 childPanels
中管理。
为什么使用 AddChildPanel?
AddChildPanel 底层依然是调用了 Create,但通过 AddChildPanel 显式地指定了界面之间的层级关系,子界面的生命周期函数才能正常工作,同时当主界面销毁时,也会通知子界面的销毁
AddChildPanel 说明
-- 添加子Panel的统一方法()
---@param pIndex any 创建的子节点的索引
---@param panelCls BaseUIPanel 子节点类
---@param createParams table 子节点的创建参数
---@param isCreateDelay boolean 是否在OnCreateFinished里面延迟创建成功回调
---@param isImmShow boolean 是否立即显示
---@param newParams table init参数
function P:AddChildPanel(pIndex, panelCls, createParams, isCreateDelay, isImmShow, newParams)
创建界面最重要的三点怎么创建、哪里创建、怎么显示,这些则通过脚本内定义的 prefab,name,parent 字段。以上的参数选取 newParams 和 createParams 进行说明 :
-
newParams 提供给 init 内字段初始化,包括 prefab,name,parent 等。
-
createParams 主要提供给
Create
表示临时地指定的创建界面形式( prefab,name,parent...),但也包含了界面创建后的显示的数据。createParams 将会贯穿 panel 的OnCreateFinished
、OnCreateDelayFinished
、OnShowFinished
传递。
实际情况是,newParams 和 createParams 经常混用,所以后续来说可以统一成一个参数。
添加子界面的两种方式
- gameObject:当界面上已经存在了界面所需的所有内容,则可以指定一个 gameObject 作为界面逻辑依附的顶节点,等同于跳过了加载 prefab 阶段,这时无需指定 parent。
self:AddChildPanel("baseAttrList", UI.List,
{
gameObject = self.view.rctBaseAttr.gameObject,
-- ...
},
false, true)
todo 图例
- prefab:子界面内容需要动态挂载,则指定 prefab 路径加载,这时需要指定 parent 作为创建 gameObject 的节点。
self:AddChildPanel("roleInfo", RoleInfo,
{
prefab = "UI/Prefab/Pet/RoleInfo.prefab",
parent = self.view.rctInfo,
name = "roleInfo",
-- ...
},
false, true)
todo 图例
控制子界面
有两种方式随时 GetChildPanel 或在 创建界面回调中持有引用。
-- 1
local panel = self:GetChildPanel("panel1")
-- 2
self.panel1 = self:GetChildPanel("panel1")
-- 3
self:AddChildPanel("panel1", panelCls, {
callback = function(obj)
if obj then
self.panel1 = obj
end
end
}, false, true)
-- DoSomeThing
self.panel1:DoSomeThing()
对子界面的控制不应该太深
至此,单个界面的生命周期和子界面的创建流程已经说明。接下来试试完整功能模块的创建流程。
和之前提到的,模块分为三类,Module,Alert,ToolTip。以 Module 为例子,有以下需求背包模块,上面有个子界面
-- MainGame/Module/Pack/PackModule.lua
local M = class(BaseUIModule, "PackModule")
function M:init(params)
self.parent = LayerManager.uiWindowFullContainer
self.prefab = "UI/Prefab/Pack/PackModule.prefab"
self.name = "PackModule"
self.panel = false
end
function M:OnCreateFinished(params)
local panelCls = require "MainGame.Module.Pack.TestPanel"
self:AddChildPanel(
"panel",
panelCls,
{
parent = self.view.negTop.transform,
callback = function(obj)
self.panel = obj
end
},
false, true
)
end
function M:OnShowFinished(params)
self.panel:Show()
-- ...
end
-- MainGame/Module/Pack/TestPanel.lua
local P = class(BaseUIPanel, "TestPanel")
function P:init(params)
self.prefab = "UI/Prefab/Pack/Test.prefab"
self.name = "TestPanel"
end
以上创建了 PackModule.lua 和 TestPanel.lua ,并在 Module 中动态加载了 TestPanel 在自己的 negTop 节点上。如果要显示 PackModule ,就要
local moduleCls = require "MainGame.Module.Pack.PackModule"
local uiModule = moduleCls.new()
uiModule:Show()
养成良好的资源组织习惯,同模块的内容放同一个文件夹,如上例中的模块目录 "MainGame/Module/Pack"
但是按照约定,需要统一使用 UIManager 操作 Module,同时为了从冗长的 require 模块名解脱,模块的路径会通过配置保存在 UIModuleConfig.lua,如图所示
最后显隐 PackModule 的方式就会变成这样
一个典型的模块功能还需要配合服务器,数据缓存,配置表,甚至会有额外的工具库。如:
- PackController,cache 整理,proxy 代理
- PackProxy,和服务器交互接口
- PackCache,数据缓存
- PackConfig,背包配置表
- PackUtil,背包相关公用函数
这几类脚本无需绑定界面,代码结构会简单很多。
Controller
模块 Controller 的配置需要在 ControllerManager.lua 定义(和 Module 类似),Controller 无法直接持有,只能通过事件系统调用。
-- MainGame/Module/Pack/PackController.lua
local C = class(BaseController, "PackController")
function C:init()
-- ...
end
-- 事件监听
function C:AddListenerOnInit()
-- ui相关事件 背包整理
self:Listen(NM.ui.packTidy, self.DoSomething_1, false)
-- 服务器推送事件 脱下装备
self:Listen(EGateCommand.ECmdGateAutoUndressEquipType, self.DoSomething_2, false)
end
function C:DoSomething_1()
end
function C:DoSomething_2()
end
return C
Cache
cache 保存着服务器传下的数据,或界面需要保存的数据,模块 cache 需要在 CacheManager 配置(和 Module 类似)
-- MainGame/Module/Pet/PetCache.lua
local C = class(BaseCache, "PackCache")
function C:init()
self.data1 = false
-- ...
end
function C:DoSomething()
end
return C
cache 的调用通过 CacheManager 以及配置的 key
Config
新增 config 也是需要配置,但是支持两种形式
- 配置(和 Module 类似)
- 放在 MainGame/Resource 目录下
使用方式
-- 方式一 的配置表,pack 为模块名
local result = ConfigManager.pack.DoSomething()
-- 方式二 的配置表,TestConfig 为实际的文件名
local result = ConfigManager.TestConfig
Proxy
服务器接口,当需要主动和服务器进行交互时,服务器在和客户端定制好协议后,会在客户端生成一份协议文件,如 IBag.lua,存于 Message/Game 下。
IBag.lua 定义了服务器的接口以及回调数据的结构类型,如整理背包,穿戴物品
然后进行两次配置
- GateSession.lua 对协议文件 IPlayer 进行配置声明
- ProxyManager 中对 Proxy 文件配置声明
-- MainGame/Module/Pack/PackProxy.lua
local P = class(BaseProxy, "PackProxy")
function P:init()
-- body
end
-- 接口原型 1: tidy( int posType, int tidyType )
-- 整理背包
function P:Tidy(posType, tidyType)
local function onTidySuccess()
-- ...
end
local function exception()
-- ...
end
self:GetRMI().bagProxy:tidy_async(posType, tidyType, onTidySuccess, exception)
end
-- 接口原型 2: useByItemCode(Message::Public::SeqInt values)
-- 使用物品
function P:UseByItemCode(values)
-- 注意此处服务器接受参数为数组,所以需要生成 SeqInt 传递
if not values then
values = Message.Public.SeqInt.new()
end
self:GetRMI().bagProxy:useByItemCode_async(values)
end
-- 接口原型 3: showOpenCopyBox(int itemCode, out Message::Public::SeqReward outShowRewards )
function P:OpenBox(itemCode)
local function onSuccess()
-- 接口原型 out 关键字表明这个函数有返回。可在成功回调中这样获取
local result = Message.Game.AMI_IBag_showOpenCopyBox__response()
end
self:GetRMI().bagProxy:showOpenCopyBox_async(itemCode, onSuccess)
end
return P
协议相关的脚本大量使用了 module("Message.Game", package.seeall),使得很多模块或字段只能通过全局搜索查找
常用 Lua 组件脚本组件
- List - 列表
- LoopList - 循环列表
- Tabbar - 切页栏
- TabbarItem - 切页栏格子
- ItemCell - 物品格子
- UseItemCell - 使用道具格子
- UIEffect - 界面特效
- CountDownText - 倒计时
其他常用 Lua 脚本
- API - C# 端接口
- EventManager & NM(EnumMananger) & UIEventEnum - 事件管理器和枚举
- Logger - 日志
- TimerManager - 定时器
- Language & Description & ColorEnum - 界面文字及颜色
- FocusManager & SelectUtil - 选择管理器
目录说明
目录 | 作用 |
---|---|
Assets/RawResources/UI/Prefab | 预制体根目录 |
Assets/RawResources/UI/Prefab/Public | 公用或基础预制体,如通用弹窗,通用组件 |
Assets/RawResources/UI/Texture | UI 图片资源 |
Assets/RawResources/UI/Texture/ArtLabel | 艺术字 |
Assets/TextAssets/LuaScript | Lua 脚本根目录 |
Assets/TextAssets/LuaScript/MainGame/Module | 脚本模块根目录 |
UI 素材的管理
与常见的 UI 管理不同,项目采用的是 TexturePacker 进行图集管理:事先打好图集再导入 Unity。这种好处,
- 能更快查看图集调整的结果,Unity 自带的需要每次全图集重打
- 资源少,载入流程加快
- 更多图集管理策略
操作演示
Lua 脚本加载 Sprite
-- self 继承自 BaseUIPanel 的对象
-- moduleName 素材的上一级目录名,大小写不敏感
-- spriteName 素材名,大小写不敏感,后缀名不敏感
local sprite = self:LoadSpriteTP(moduleName, spriteName)
预制体制作
预制体制作时需要留意节点的命名
为了方便 Lua 和 Prefab 交互,增加了 UIBinding.cs。UIBinding 作用是将预制体下常用的组件事先保存在统一的数据结构内,Lua 端通过 view 索引节点名称访问。
UIBinding 是作用于挂载对象的子节点,所以要求挂在 Prefab 的顶节点。节点的保存过程是自动的,但要符合 UIBinding 要求的命名前缀。
前缀 | 类型 |
---|---|
neg | GameObject |
rct | Transform |
txt | Text |
btn | UIButton |
img | Image |
rmg | RawImage |
sld | Slider |
... | ... |
完整命名规则:前缀+大写英文开头+小写英文或数字结尾
如一个文本节点应该命名 txtName。如果成功被 UIBinding 识别,节点前会有绿色的@标志,并在 UIBinding 组件下可见。
如果一个节点上有多个特征组件,如挂载了 Image 和 UIButton,前缀只能根据自己需要使用的组件取其一,如 imgTest,或 btnTest。另外的组件则需要通过 GetComponent 获取。
此外 UIBinding 没定义的组件,也可以通过 GetComponent 。
self.view.imgTEst.sprite = xxxx
se
另外,做一些常用组件项目进行重写,默认情况下都要使用这些组件
- 文本-UIText
- 按钮-UIButton
- 艺术字-ArtText
- 循环列表-GridList
下面对常用的进行说明
UIText
为了满足颜色的灵活性,游戏内部的颜色都是通过色号的形式设置,和美术沟通时也是通过色号。
所以 UIText 派生自 Text,增加了便捷选取颜色的功能,色号标签会接管文字的颜色。
当设置颜色时,可以通过下拉框。或者输入美术给定的颜色值,如 #323232,内部会转成颜色标签。
如果是特殊的颜色,则要先选择颜色为 None,此时色号标签不起作用,你需要用传统的颜色盘设置颜色。
绝大多数情况下,不都需要手动控制颜色转盘
UIText 的 Color Tag 和 Color String 同步修改的,修改一个的话,另一个会跟随变化
模板
Hierarchy 下右键,选择 Y08 可以选择一系列定制好的预制体模板
拼图工具