跳转至

业务框架

业务框架

项目逻辑由 Lua 主导,通过插件 XLua 与 Unity 交互。

XLua 把这层交互透明化,和 C# 交互基本无异

// C#
Button btnConfirm;
btnConfirm.AddUIListener(()=>{ Debug.Log("hello.") })
-- Lua
btnConfirm:AddUIListener(function() print("hello") end)

项目的业务框架是典型的 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

起源

managers

界面由 UI 管理器产生,包括 UIManager,AlertManager,ToolTipMananger,它们分别控制模块界面,分别是 Module,Alert,以及 ToolTip。一切界面都是panel,有 BaseUIPanle 派生而来,为了更好管理,我们会把底部的界面设置 Module。

层级划分

相较于 BaseUIPanel,BaseUIModule 增加模块特有的行为,显隐动画,模块事件传递,模块遮罩等等。继承层后的使用基本和 BaseUIPanel 一致。

BaseUIPanel

BaseUIPanel 是一切界面的基类。

UIDerived

它功能包含

  • 定义界面生命周期

  • 资源加载、管理

  • 子界面管理

  • 事件监听

生命周期

为了基类和各类控制器更好的组织界面,会把界面的操作进行分类,并限定在各类生命周期函数中。在编写功能脚本时,应该严格准守。

BaseUIPanelLiveTime1

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时触发,paramsShow为的参数

OnUpdateView(params)

当界面show时触发,或界面隐藏期间,监听事件被触发后显示调用。

界面的显示逻辑应该由数据驱动,所以数据源不变的情况下Show,不对界面进行实际的显示处操作,只调用 Show。

OnHideBefore(bIsModuleHide)

界面隐藏前调用

OnDestroyBefore

界面销毁前调用,用于清空 panel 的字段,释放场景对象,定时器,动画等。

底层会处理通过 childPanel 添加的子界面。

子界面创建与控制

创建界面有两种方式,

  1. Create
  2. 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 的 OnCreateFinishedOnCreateDelayFinishedOnShowFinished 传递。

UICreateParams2

实际情况是,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,如图所示

image-20220519114032486

最后显隐 PackModule 的方式就会变成这样

UIManager.Show(NM.md.pack, { tabType = "MeltEquip" })
UIManager.Hide(NM.md.pack)

一个典型的模块功能还需要配合服务器,数据缓存,配置表,甚至会有额外的工具库。如:

  1. PackController,cache 整理,proxy 代理
  2. PackProxy,和服务器交互接口
  3. PackCache,数据缓存
  4. PackConfig,背包配置表
  5. 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

CacheManager.pack:DoSomething()

Config

新增 config 也是需要配置,但是支持两种形式

  1. 配置(和 Module 类似)
  2. 放在 MainGame/Resource 目录下

使用方式

-- 方式一 的配置表,pack 为模块名
local result = ConfigManager.pack.DoSomething()
-- 方式二 的配置表,TestConfig 为实际的文件名
local result = ConfigManager.TestConfig

Proxy

服务器接口,当需要主动和服务器进行交互时,服务器在和客户端定制好协议后,会在客户端生成一份协议文件,如 IBag.lua,存于 Message/Game 下。

IBag.lua 定义了服务器的接口以及回调数据的结构类型,如整理背包,穿戴物品

然后进行两次配置

  1. GateSession.lua 对协议文件 IPlayer 进行配置声明
  2. 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。这种好处,

  1. 能更快查看图集调整的结果,Unity 自带的需要每次全图集重打
  2. 资源少,载入流程加快
  3. 更多图集管理策略

操作演示

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-20220520102425941

image-20220520102548753

如果一个节点上有多个特征组件,如挂载了 Image 和 UIButton,前缀只能根据自己需要使用的组件取其一,如 imgTest,或 btnTest。另外的组件则需要通过 GetComponent 获取。

此外 UIBinding 没定义的组件,也可以通过 GetComponent 。

self.view.imgTEst.sprite = xxxx

se

另外,做一些常用组件项目进行重写,默认情况下都要使用这些组件

  • 文本-UIText
  • 按钮-UIButton
  • 艺术字-ArtText
  • 循环列表-GridList

下面对常用的进行说明

UIText

为了满足颜色的灵活性,游戏内部的颜色都是通过色号的形式设置,和美术沟通时也是通过色号。

image-20220520145146694

所以 UIText 派生自 Text,增加了便捷选取颜色的功能,色号标签会接管文字的颜色。

当设置颜色时,可以通过下拉框。或者输入美术给定的颜色值,如 #323232,内部会转成颜色标签。

如果是特殊的颜色,则要先选择颜色为 None,此时色号标签不起作用,你需要用传统的颜色盘设置颜色。

image-20220520110105698

绝大多数情况下,不都需要手动控制颜色转盘

UIText 的 Color Tag 和 Color String 同步修改的,修改一个的话,另一个会跟随变化

模板

Hierarchy 下右键,选择 Y08 可以选择一系列定制好的预制体模板

image-20220520111654337

拼图工具