Skip to content

Repository files navigation

UniVue

UniVue 是一个面向 Unity 的运行时通用高性能轻量级的响应式UI框架

UniVue只提供具体的机制,不提供具体的实现。更符合你项目的定制需要你扩展实现内部的接口,这些接口的实现往往也非常简单。


安装

一、方式一

通过PackageManger的add package from git URL添加,填写下面的url

https://github.com/Avalon712/UniVue.git#v3.1.0

二、方式二

下载v3.1.0的版本到你的本地,然后通过PackageManager的add package from disk功能

三、方式三

在你项目的Packages目录下创建一个子目录,名称可取org.avalon712.univue@3.1.0,然后下载3.1.0版本到这个目录中,Unity会智能识别package.json而将包添加到你的项目中。

四、 安装需要注意的一些事项

UniVue3.1.0版本依赖了一些其他包,如果Unity未能正确将依赖包拉取下来,你需要手动去添加这些依赖包。如果你本地已经由了Mono.Cecil依赖,但是版本如果报错,可自行修改UniVue的package.json中有关Mono.Cecil的版本号,UniVue依赖的Mono.Cecil为官方提供的标准依赖com.unity.nuget.mono-cecil

协程依赖来自我的另一个作品,链接在这里:

Avalon712/VCoroutine: C# Coroutine For Unity3D

如果安装失败,你可以手动在本地添加这个包的依赖。

使用

一、UI的工作流

BaseUI是一个集成所有核心功能的基类,按照模块的概念可将UI拆分为组件Component、界面View和小组件Widget的概念。小组件则是承担了一个很小的功能,比如一个按钮、一个文本、一张图片等等,他们功能单一具体;组件则承担了一些更多的逻辑,界面则是完整一个功能的核心体现。

UI工作流是:搭建UI界面预制体 -> 挂载自己的脚本(继承自BaseUI),如果你想使用UI代码生成功能,可以在UniVue菜单栏中打开UI Code Export Window窗口,按照模块的概念组织你的UI模块功能。框架只提供了一个基础的代码生成功能,如果你需要自定义符合你自己的代码逻辑,你可以通过继承UICodeGenerator类来实现,你的类将会被自动检查到被进行调用执行你的导出逻辑。你可以决定已何种方式生成代码,内置的导出器将已继承的方式导出。

UIMgr提供了对根界面的管理方式,按照Layer的概念进行组织和划分这些根界面。如果你有自己的层级管理方式,需要实现IUILayerMgr接口,设计这一概念的原因是,你可以针对性的做UI的动静分离,Canvas分组渲染以及游戏逻辑的上的区分,比如:弹窗的层级往往比其他界面要高。

如果你需要创建挂载有BaseUI的GameObject,你应该走UIMgr的CreateUI方式,以便BaseUI的回调函数能被正确执行,销毁同样如此。

框架不提供资源管理,因此很大程度上你需要自己实现你的资源管理方式,内部只提供了一个IUIObjectLoader接口,由你自己决定怎样去加载一个UI对象,是预制体还是场景中以及存在的,如果是预制体,内部会智能进行实例化。

二、使用响应式API

UniVue内部有两个响应式API,一个是事件、一个是数据。当事件触发以及数据变化时都能被正确响应(后文有详细讲解这个机制)。你绑定的函数被称为一个渲染函数,故名思意,他是用于更新UI显示的。

下面是一些API的使用举例:

    class Data : BaseModel
    {
        public string Name { get; set; }
    }

    Data data = new Data();
    Bind(data, () => NameTxt.text = $"Name: {data.Name}", nameof(data.Name));

当Name属性值变化时,绑定的渲染函数将会被回调。检查属性变化通过IL代码织入功能实现。有关更多的API使用,可以自行阅读BaseUI中有关Bind()类的函数,他们的使用很简单,没有什么难的。(从本质上将数据的变化也是通过事件实现的)。

由于BaseUI本身也继承了UIModel,可以被当作一个数据来源使用,因此你在UI中写的属性同样可以被绑定。(虽然这种设计偏离了严格的MVVM的设计,但是从我自己的工作经验来看,很多时候这样做是比较方便的,有时候我们应该更关注效率而不是设计)

三、红点系统

框架内部提供了一个红点系统,它基于树结构实现。同时提供了红点树编辑器以及用于运行时的调试器,与传统使用字符串作为红点Key不同的是,其内部使用ulong类型实现key。这种实现方式有一定的限制(根节点数量不能超过ushort类型的最大值,非叶节点的孩子数量不能超过127),但是对于99.99%的项目而言都是够用的。相比字符串的方式,减少了大量的字符串内存的占用,同时支持动态添加、删除红点树节点。

可通过UniVue / RedPointTreeEditor打开红点树编辑器,UniVue / RedPointTreeDebugger打开红点树调试器,已在运行时调试你的红点树结构是否符合预期的设计。

RedPoint组件允许多个红点key关联一个红点UI,你可以定义这些关联的红点key怎么影响红点UI的激活,And则表示所有的红点key都处于激活时此红点UI被激活,Or则表示至少有一个key处于激活则红点UI被激活。

四、定时器、事件管理、协程

UniVue内部的定时器基于小顶堆的数据结构实现,意味着每帧只需要看根节点是否抵达了调用时机,而不需要每帧都遍历所有的定时器已查看是否需要被调用。

事件管理支持带参数匹配回调的机制,以及死循环检测、事件注册点日志输出。带参数匹配回调机制:假如一个事件A,有两个监听回调函数F1(int)、F2(),F1带了一个int参数,F2则没有,当事件A被触发时,派发事件时,会根据是否携带参数以及参数类型进行智能回调。比如某次A触发为无参数触发,那么只有F2会被调用,F1因为参数不匹配而不会被触发;如果A触发带了一个int参数,那么F1、F2都能触发;如果A带了string参数,那么F1不触发、F2触发。(每次事件触发只允许携带一个参数,因此对于多参数,需要使用object[]实现)

参数 F1(int) F2()
无参数 ×
int
string ×

有关协程更多的信息,看这里:

Avalon712/VCoroutine: C# Coroutine For Unity3D

五、 多语言系统

I18n是国际化internationalization的简写,l10n是本地化localization的简写

框架内部的多语言系统支持直接使用默认语言进行开发,以及使用唯一id索引文本的方式。同时你可以实现自己的翻译器ITextTranslator进行语种的翻译(框架内部没有提供具体的实现),可通过UniVue / I18N Editor Window打开多语言相关配置。框架只提供机制,以及一些简单的默认实现,一些更符合你自己项目的定制需要你自行实现内部的相关接口。比如多语言已何种方式导出,需要你扩展实现II10NFileExporter接口,内置的StreamingAssetsI10NFileExporter是二进制方式的导出机制,将每种多语言分部导出一个二进制文件,使用Unity的StreamingAssets目录机制进行加载,对应的实现是StreamingAssetsI10NFileReader

双向索引的原理:基于一种默认语言进行开发时,生成的唯一id都是对默认语言文本使用FNV-1a哈希算法生成一个唯一id,因此你直接使用默认文本时也能正确得到唯一id,再根据这个唯一id去索引当前语言环境下的真实文本。

字体资产往往会占用较高的内存,如果你的游戏支持多语言,你应该妥善管理你的字体,一种好的建议是每种语言都独立使用一个字体,只加载当前语言对应的字体即可。内部IFontProvider接口用来给你管理你自己的字体加载方式。我不建议你使用内部实现的I18NFontConfig,它应该只用于在编辑器模式下的快速开发验证,因为这种直接引用了所有字体资产,一次性全部加载所有语言的字体的方式对游戏而言是灾难性的,通常字体内存+字体纹理占用的内存会很高,你应该优化它,节约内存给那些更重要的功能。

LocalizationText组件是和多语言搭配使用的,我强烈建议你为每个文本组件都挂载上它,这个组件提供了很多快捷的功能,比如在编辑器模式下预览各种语言文本,以及调用翻译接口进行文本翻译。运行时也能智能更加当前的语言环境进行显示文本。

响应式核心原理

UniVue内部响应式本质是数据驱动+事件驱动,数据层主要是通过继承BaseModel,在数据发生变化时通知渲染层,事件层则是UI事件和自定义事件被渲染层监听时触发渲染。

渲染层的结构是一个多索引有向无环图,任意路径的末尾节点都是渲染函数,当数据变化或事件触发时,抵达渲染节点最多不会超过5层,即时间复杂度为O(1)。

带延迟性的响应式渲染。考虑到UI渲染大多数不需要很高的渲染频率,因此内部默认的渲染频率为0.1秒,即在这0.1秒内被触发的所有渲染只会被当作一次渲染调用,这种带延迟性的响应式渲染可以大幅降低重复渲染带来的问题。比如当前帧内会对模型的属性A、B、C修改多次,如果每次都是立即式渲染,渲染函数会被多次重复调用,然而事实上当前帧的所有变化都可以收敛为一次渲染调用。当然也可以修改延迟调用的频率,最低的渲染频率为延迟一帧。

RGraphs.png

M代表数据Model,E代表事件Event,P代表数据Model的属性,R代表渲染函数,G代表RGraph。

一次完整的渲染流程举例:当数据M1的P1属性发生了变化时,从Entry进入索引得到M1绑定了两个RGraph:G1和G4,从路径G1->M1->P1->R3得到渲染函数R3,从路径G4->M1->R5得到渲染函数R5,从路径G4->M1->R6得到渲染函数R6,得到所有要触发渲染函数为R3、R5、R6,这些函数不会立即执行,而是被放到一个HashSet中,等待渲染延迟结束后才被执行。如果在等待期间事件E2触发,从路径G2->E2->R2,G2->E2->R3,G4->32->R6得到渲染函数R2、R3、R6,放入HashSet,去重得到R2、R3、R5、R6,避免了重复渲染。特别是一些渲染函数要加载资源时,这种情况下会极大减少因为UI刷新导致的性能卡顿。

本质绑定就是一个watch函数,数据和事件被监听。

About

High-performance, lightweight reactive UI framework for Unity.

Topics

Resources

Stars

114 stars

Watchers

4 watching

Forks

Releases

Packages

Contributors

Languages