基于反射实现的零GC高效率Unity组件绑定

news/2025/1/20 0:52:21/文章来源:https://www.cnblogs.com/GuyaWeiren/p/18680576

前言

我是狗猥,上一世,我使用传统方式绑定UI上的组件,却因xLua扩展代码太多撑爆丹田沦为废人,失去了争夺主程岗位的资格,最后在测试同学的讥笑声中饮恨西北。

再次睁开眼,我穿越回到了拼UI的那一天。重生归来,这一世我要设计一个船新的绑定方式,夺回本就属于我的一切!

今天要分享的是最近搞的一套组件绑定机制,采用面向数据编程的思维设计。虽然基于反射实现,但运行时没有GC,并且效率非常高。

这是在三星S24Ultra上用执行调用localEulerAngles一伯万次的耗时差别(Get快很多是因为做了值缓存),以及在Update中的GC Alloc。

本项目使用Unity 2021.3.37f1制作,完整工程的git地址和使用方法放在最后面了,嫌我啰嗦的同学可以直接跳转过去。

转载请注明出处:https://www.cnblogs.com/GuyaWeiren/p/18680576.html

设计思路

Transform组件为例,locanPositionlocalEulerAngleslocalScale都是开发中常用到的属性。

使用脚本做热更新时,传统的Wrapper会对每个属性的GetterSetter总共生成6个方法。如下是xLua对Transform生成的扩展:

static int _g_get_localPosition(RealStatePtr L) { ... }
static int _s_set_localPosition(RealStatePtr L) { ... }
static int _g_get_localEulerAngles(RealStatePtr L) { ... }
static int _s_set_localEulerAngles(RealStatePtr L) { ... }
static int _g_get_localScale(RealStatePtr L) { ... }
static int _s_set_localScale(RealStatePtr L) { ... }

显然,属性越多,生成的代码也越多。

于是我们可以对其进行一次抽象,引入中间件模板概念。

这三个属性有没有共同点呢?有的,都是Vector3类型的属性。对于这三个属性,可以定义一个使用Vector3为泛型的中间件,将其与属性关联起来。

class Middleware<T>
{public T Get() { ... }public void Set(T pValue) { ... }
}class MiddlewareVec3 : Middleware<Vector3> { }

不管有多少个属性,只要是Vector3类型的,都可以用这一个中间件来描述,如图所示:

在Unity编辑器中,在GameObject上挂载绑定器脚本,指定要绑定的Component(或GameObject)和属性,在运行时将属性绑定在中间件上,就可以像调用属性一样调用中间件了。

这样不必对每个属性进行Wrap,只需要生成中间件的就行,Wrap代码量会减少很多。

中间件

除了GetSet方法,中间件还需要持有组件对象和属性的Delegate

// “Middleware”名称过于广泛,改用ComponentProperty说明它作用于组件的属性
public abstract class BaseComponentProperty<T>
{private Object m_Target; // Component或GameObjectprivate Delegate m_Getter;private Delegate m_Setter;public T Get() => this.m_Getter?.Invoke();public void Set(T pValue) => this.m_Setter?.Invoke(this.m_Taregt, pValue);
}

这里我对中间件的设计是和属性一一对应。虽然也可以设计成多个Setter以实现同时对多个组件属性赋值,但是后期维护可能会头大,比如改了某个UI布局的时候——“这个中间件到底对应哪几个Component?”

绑定器

每个绑定器可以绑定多个该GameObject上的Component,因此用一个数组存放绑定信息,每个数组项需要三个字段:

public class ComponentPropertyBind : MonoBehaviour
{[SerializeField]private BindInfo[] m_Infos; // 每一项就是一个绑定器[Serializable]public class BindInfo{public string ViewFieldName; // 视图类中的中间件变量名public Component ComponentRef; // 组件引用public string PropertyName; // 属性名称}
}

考虑到在开发中预制件和代码随时会变更,如果使用引用,引用一旦丢失,将无法知晓它曾经是什么。因此采用字符串的形式,即使引用丢失,也能看到内容,便于做调整。

运行时的流程如下:

  1. Awake时向上找到视图类
  2. 反射从视图类中找到名称对应的中间件成员
  3. 反射找到组件的属性
  4. 对属性的GetterSetter生成对应的Delegate
  5. Delegate存入中间件

反射最慢的一步是GetFieldGetProperty,因此实际代码中需要做缓存。

Delegate数据

不采用PropertyInfo.Get/SetValue是因为它的返回值/参数都是object类型,如果我们绑定的是值类型属性,每次赋值或取值都会发生装拆箱的GC,这是不好的。并且这个方法的效率不高。

而使用Delegate.CreateDelegate可以生成MethodInfodelegate。它类似于C++的函数指针,速度非常快。并且它的参数可以使用泛型,直接杜绝了装拆箱:

Delegate.CreateDelegate(methodInfo, typeof(Action<int>)) as Action<int>

由于C#是强类型语言,而绑定的属性是在Unity编辑器里设置的,在编译期无法确定Delegate的泛型参数类型。虽然有dynamic关键字,但访问它会因为额外的类型判定逻辑产生GC,并且IL2Cpp不支持:Unity手册-脚本限制

因此考虑使用泛型类来存放Delegate

internal interface IGetterSetter<T>
{T Get(object pInvoker);void Set(object pInvoker, T pValue);
}// TComponent是组件,TValue是属性的类型
// 如Image的sprite属性,对应GetterSetter<Image, Sprite>
public class GetterSetter<TComponent, TValue> : IGetterSetter<TValue> where TComponent : Object
{private Func<TComponent, TValue> m_GetterDelegate;private Action<TComponent, TValue> m_SetterDelegate;public GetterSetter(PropertyInfo pInfo){this.m_GetterDelegate = (Func<TComponent, TValue>)Delegate.CreateDelegate(typeof(Func<TComponent, TValue>), pInfo.GetGetMethod());this.m_SetterDelegate = (Action<TComponent, TValue>)Delegate.CreateDelegate(typeof(Action<TComponent, TValue>), pInfo.GetSetMethod);}
}// 在绑定时动态生成一个GetterSetter实例:
var t = typeof(GetterSetter<,>)MakeGenericType.(typeof(TComponent), typeof(TValue));
this.m_GetterSetter = Activator.CreateInstance(t, propertyInfo) as IGetterSetter<TValue>;

事件类属性

Button.onClick这类事件,同样可以根据参数类型设计中间件:

public abstract class BaseComponentEvent<T>
{private IAddRemove<T> m_AddRemove = null;public void AddListener(UnityAction<T> pCallback) => this.m_AddRemove?.AddListener(base.m_Target, pCallback);public void RemoveListener(UnityAction<T> pCallback) => this.m_AddRemove?.RemoveListener(base.m_Target, pCallback);
}

AddRemove和上面的GetterSetter类似,是对AddListenerRemoveListener的泛型封装。

需要注意的是无参事件,在C#中System.Void是不允许作为泛型参数的,因此要单独实现:

public class ComponentEventVoid
{private IAddRemove m_AddRemove = null;public void AddListener(UnityAction pCallback) => this.m_AddRemove?.AddListener(base.m_Target, pCallback);public void RemoveListener(UnityAction pCallback) => this.m_AddRemove?.RemoveListener(base.m_Target, pCallback);
}

目前只设计了无参事件和单参事件的绑定。有多参需要请自行修改代码。

AOT代码生成

GetterSetter的泛型实例是运行时动态生成的,因此直接使用IL2Cpp打包后运行会报错。

简单来说就是如果代码中没有GetterSetter<Transform, Vector3>类型,那么编译后的C++代码中也没有。因为Unity编辑器模式是JIT的,这个报错只有打包运行后才会出现:

ExecutionEngineException: Attemping to call method '...' for witch no ahead of time (AOT) code was generated.

于是需要硬编码类型,让AOT编译器能够检测到它们:

[Preserve]
private class GetterSetterWrapTypes
{private GetterSetter<GameObject, Transform> __GS_0__;private GetterSetter<GameObject, int> __GS_1__;...
}

通过反射找到所有的组件,整理出它们的属性的类型,生成对应的GetterSetterAddRemove成员。

直接生成cs文件会导致Unity生成很多未使用成员警告,因此这里我选择用System.Reflection.Emit生成一个dll文件。再使用Preserve标签和link.xml保证它不被代码裁剪忽略。

使用方法

完整项目的GitHub:https://github.com/RenChiyu/ComponentBind

为了提高自己的英语姿势水平,在尝试使用全英文编码,如果出现语法错误请勿大声嘲笑。

工作流程如下:

  1. 创建一个基于BaseView的视图类
  2. 在视图类中添加中间件成员
  3. 在编辑器选中视图根节点,挂载视图类
  4. 选中需要绑定的组件的GameObject,挂载ComponentPropertyBinderComponentEventBinder
  5. 在绑定器中选择中间件成员,组件和属性或引用
  6. 保存

可以参考TestPanel.cs,以及场景中以#开头命名的节点。

如果使用IL2Cpp,需要在打包前调用ComponentBindAOTCodeGenerator.Execute()或点击菜单 -> TooSimpleFramework -> ComponentBind -> Generate AOT Code

这里只实现了比较简陋的功能,没有做一个中间件对应多个绑定器的判定。

实际使用中可以再写一个工具,根据GameObject的名称自动设置要绑定的组件和属性或事件和视图类中的成员代码。每个人有每个人的做法,一键绑定工具就不公开了,具体实现留给读者。

后记

马上要过年了,大家2024过得好吗?在这里提前祝大家春节遇快,阖家欢洛,万似如意!

2024年我的简历上多了从研发到上线的项目经历,某四字战棋手游,具体名字就不说啦。还是小有成就感的。

但是去年最大的成就感是通过控制饮食将体重从118公斤降到了93公斤,继续加油昂。大家在工作的时候也要注意劳逸结合,身体才是革命的本钱。

很惭愧,就做了一点微小的工作,谢谢大家。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/871943.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

XML+propties

txt文件,properties(属性)文件,XMl文件txt与properties与XML的区别当这些文件存储单个关系数据时, 普通文件 无法存储 关系数据,而properties属性文件以键值对形式存储就很方便,XML文件也可以 见图1 但储存多个用户就不行了,XML更适合, 见图2properties集合properties …

一条SQL更新语句是如何执行的?

与查询流程不同的是,更新流程中会涉及两个重要的模块: (i)redo_log模块(InnoDB中的日志模块):在 MySQL 里也有这么个问题,如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程 IO 成本、查找成本都很高。为了解决这个问题,MyS…

大模型分布式训练并行技术(五)-序列并行

p { font-size: 12pt; line-height: 2 !important } 参考资料 大模型分布式训练并行技术(五)-序列并行 详解MegatronLM序列模型并行训练(Sequence Parallel)一、序列并行(Colossal-AI)背景 Colossal-AI 序列并行诞生的背景是 self-attention 的内存需求是输入长度(sequenc…

1.匿名内部类

使用场景不用多创建类,来使用其方法定义 匿名内部类的语法比较奇特,匿名内部类既是一个类的定义,同时他本身也是一个对象, 所以子类继承抽象类, 实现类实现接口,需要节省内存不创建类,从而创建匿名内部类 例子使用当你的才华配不上你的野心,努力的时候到了!

Arrays 排序

正常来说 Arrays可以用于数组排序, 但如果数组里面是引用类型地址就会报错,这时候, 就需要加个功能(实现接口/继承接口) Comparable接口来定引用类型对象的排序规则(以..属性值进行排序)正常Arrays.sort( 数组对象)进行排序时 , 会在排序的时候将数组对象进行调用comparato方…

Spring Boot 自动配置原理详解

引言 Spring Boot 的一大亮点是它能够自动配置(Auto-Configuration)Spring应用程序,极大地简化了Spring应用的创建过程。开发者只需添加所需的依赖,Spring Boot就会根据这些依赖和一些预设条件自动装配相应的组件,从而减少了大量样板代码的编写。 第三方组件的集成方式 对…

传奇

毋庸置疑,很多人的心里,都有一个传奇、传奇3,80后、90后,甚至70后尤甚。当然也包括我。主要当然因我曾经是盛大游戏传奇工作室研发团队的一员,且是盛大传奇3项目部的第一个程序技术人员,内心对传奇、传奇3的感情非同一般。因工作等原因,我早已不再从事传奇类游戏的开发研…

常用工具

类似gdb的bash调试工具bashdb: https://sourceforge.net/projects/bashdb/files/bashdb/ 非常好用,结合vscode bashdb(bash debug) shift + command + d,配置 .vscode/launch.json

《CPython Internals》阅读笔记:p232-p249

《CPython Internals》学习第 13天,p232-p249 总结,总计 18 页。 一、技术总结 无。 二、英语总结(生词:1) 1.overhead (1)overhead: over-("above") + head(“top part, uppermost section”) overhead的字面意思是:above the head,后来演变成"represent …

2025春秋杯冬季赛MISC部分题目复现

简单算术 异或直接得出flagfind_me 进游戏用fill指令把命令方块填充掉然后切创造或者观察找将文件解压出来后看结构是MC的存档文件,于是用MC跑,提示要找雪屋,在附近雪屋的箱子里有给压缩包密码 解压后得到:unai?535.0a20[189.[4049[ax30[e.j60xaj91x8+随波逐流一把梭音频的秘密…

CogAgent: A Visual Language Model for GUI Agents

CogAgent: 利用VLM操作GUI。主要内容 提出了一个18B的VLM模型CogAgent(CogVLM的新版本),旨在提高对于GUI的理解、导航和交互能力。利用高分辨率和低分辨率编码器适应不同分辨率的输入,在9个VQA benchmarks上取得了sota。同时,CogAgent利用截屏输入,在PC和安卓GUI导航任务…

【原创】MAC OS 本地搭建部署 dify

一、什么是 dify?Dify 是一个开源的大语言模型(LLM)应用开发平台,融合了后端即服务(Backend as Service, BaaS)和 LLMOps 理念,旨在简化和加速生成式AI应用的创建和部署。它支持多种大型语言模型(如OpenAI的GPT系列、Claude3等),并提供强大的数据集管理功能、可视化的…