- 游戏开发
- 移动开发
- WebAssembly
【免费下载链接】minigame-unity-webgl-transform
微信小游戏Unity引擎适配器文档。
xLua 中,C# 复杂值类型(struct)默认以引用方式传递到 Lua 侧,每次传递都会产生一次 boxing 与一次 C# GC 分配。本文以 xLua 复杂值类型(struct)gc 优化指南 为骨架,结合仓库内 xLua 的配置、无 GC 示例 与生成器源码 Generator.cs,系统讲解 struct GC 优化方案的工作原理、满足条件、三种配置方式与落地步骤,帮助读者在微信小游戏场景下把 Vector 系列、Quaternion、Color 等值类型在 Lua 与 C# 之间的传递彻底做到无 GC 分配。
复杂值类型为什么会产生 GC
在 xLua 中,复杂值类型(struct)的默认传递方式是引用传递。这个机制的工作过程是:
- 值类型需要先进行boxing(装箱),在托管堆上创建一个新对象;
- 将装箱后的引用传递给 Lua 侧使用;
- Lua 侧使用完毕、释放该对象的引用时,触发一次 C# GC。
由于每次 boxing 都会产生一个新对象,因此只要 struct 在 Lua 与 C# 之间来回传递一次,托管堆上就会多出一个待回收的垃圾对象。在高频调用(如每帧更新位置、逐物体同步坐标)的场景下,这类分配会迅速累积,直接表现为 GC 频率上升、CPU 峰值抖动,在内存更敏感的微信小游戏运行时(WASM)上影响尤为明显。
为此,xLua 实现了一套struct 的 GC 优化方案:只要通过简单配置,满足条件的 struct 在传递到 Lua 侧时即可做到无 GC。
struct 需要满足什么条件
要让一个 struct 享受到 GC 优化,需要同时满足以下三个条件:
1. 字段类型约束
struct 允许嵌套其它 struct,但它以及它嵌套的 struct 只能包含以下基本类型:
byte、sbyte、short、ushort、int、uint、long、ulong、float、double
例如 Unity 定义的大多数值类型(Vector 系列、Quaternion、Color 等)以及用户自定义的满足上述字段约束的 struct 均符合条件。字段不能包含 string、引用类型对象或其它不符合约束的类型。
2. 配置 GCOptimize 属性
该 struct 必须配置GCOptimize属性。对于常用的 UnityEngine 值类型(Vector 系列、Quaternion、Color 等)xLua 已经内置配置好了,用户无需重复配置;用户自定义 struct 则需要自行配置。属性可以通过配置文件或C# Attribute两种方式实现(详见下文)。
3. 加入生成代码列表
使用到该 struct 的地方(struct 自身、相关 delegate、相关数组等)需要添加到生成代码列表,并执行代码生成,让 xLua 为该类型生成专门的 GC 优化包装代码。
如何配置:三种方式详解
xLua 的所有配置都支持三种方式:打标签(Attribute)、静态列表、动态列表。GCOptimize同样遵循这一规则。下面逐一展开。
方式一:C# Attribute 打标签
直接为 struct 类型打上GCOptimizeAttribute(同时建议打上LuaCallCSharp)。仓库示例 05_NoGc/NoGc.cs 中的自定义 struct 就是这样配置的:
using XLua; [GCOptimize] [LuaCallCSharp] public struct Pedding { public byte c; } [GCOptimize] [LuaCallCSharp] public struct MyStruct { public MyStruct(int p1, int p2) { a = p1; b = p2; c = p2; e.c = (byte)p1; } public int a; public int b; public decimal c; public Pedding e; }注意MyStruct嵌套了同样打了GCOptimize标签的Pedding,且所有字段均为值类型,完全符合前述字段类型约束。从属性定义源码 GenAttributes.cs 可以看到GCOptimizeAttribute还带一个可选的OptimizeFlag参数,默认值为OptimizeFlag.Default:
[Flags] public enum OptimizeFlag { Default = 0, PackAsTable = 1 } public class GCOptimizeAttribute : Attribute { OptimizeFlag flag; public OptimizeFlag Flag { get { return flag; } } public GCOptimizeAttribute(OptimizeFlag flag = OptimizeFlag.Default) { this.flag = flag; } }方式二:静态列表配置
有时我们无法直接给一个类型打标签(例如系统 API、没有源码的第三方库中的类型),这时可以在一个静态类里声明一个静态字段,字段类型只要实现了IEnumerable<Type>即可(BlackList和AdditionalProperties例外),再为这个字段打上GCOptimize标签:
using System; using System.Collections.Generic; using XLua; public static class MyGCOptimizeConfig { [GCOptimize] public static List<Type> gc_optimize_list = new List<Type>() { typeof(MyNamespace.MyStruct), typeof(MyNamespace.MyStruct2), }; }字段需要放在静态类中,且建议放在Editor 目录下。这也是 xLua 对几个 UnityEngine 值类型的官方内置配置方式。在 GenAttributes.cs 的SysGenConfig类中可以看到:
public static class SysGenConfig { [GCOptimize] static List<Type> GCOptimize { get { return new List<Type>() { typeof(UnityEngine.Vector2), typeof(UnityEngine.Vector3), typeof(UnityEngine.Vector4), typeof(UnityEngine.Color), typeof(UnityEngine.Quaternion), typeof(UnityEngine.Ray), typeof(UnityEngine.Bounds), typeof(UnityEngine.Ray2D), }; } } ... }因此 Vector2/Vector3/Vector4、Color、Quaternion、Ray、Bounds、Ray2D 这些常用值类型无需任何额外配置即可享受 GC 优化,直接满足原文档中"UnityEngine 的几个 struct 已经配置了该属性"的说明。
方式三:动态列表配置
如果希望按名字空间、按程序集等规则动态收集类型,可以声明一个打了相应标签的静态属性。GCOptimize同样支持这种方式:
using System; using System.Collections.Generic; using System.Linq; using XLua; public static class MyGCOptimizeConfig { [GCOptimize] public static IEnumerable<Type> by_property { get { return from type in Assembly.Load("Assembly-CSharp").GetTypes() where type.Namespace == "MyGame.Data" && type.IsValueType select type; } } }Getter 是代码,可以实现很多效果,比如按名字空间配置、按程序集配置等等。该属性同样需要放到静态类中,建议放到Editor 目录。
配置通用建议(来自 xLua 的配置):列表方式均必须是
static的字段/属性,且必须放到一个static类;建议尽量不用标签方式(il2cpp 下会增加不少代码量),建议列表方式配置放在 Editor 目录。
使用到该 struct 的地方也要加入生成列表
除了 struct 本身,凡是在 Lua 与 C# 之间传递该 struct 的入口,也需要加入生成代码列表,才能让无 GC 路径真正生效。这些入口包括:
- 携带 struct 参数的
delegate(C# 调用 Lua 函数时)——需要打CSharpCallLua; - 携带 struct 参数的 C# 方法——所在的类型需要打
LuaCallCSharp; - struct 数组——数组访问路径同样走 GC 优化代码。
仓库 05_NoGc/NoGc.cs 给出了完整示例,把 struct、枚举、delegate、数组全部串联起来:
[GCOptimize] [LuaCallCSharp] public struct MyStruct { ... } [LuaCallCSharp] public enum MyEnum { E1, E2 } [CSharpCallLua] public delegate int IntParam(int p); [CSharpCallLua] public delegate Vector3 Vector3Param(Vector3 p); [CSharpCallLua] public delegate MyStruct CustomValueTypeParam(MyStruct p); [CSharpCallLua] public delegate MyEnum EnumParam(MyEnum p); [CSharpCallLua] public delegate decimal DecimalParam(decimal p); [CSharpCallLua] public delegate void ArrayAccess(Array arr);配置完成后,需要执行XLua/Generate Code菜单生成代码。生成器源码 Generator.cs 中的逻辑印证了收集流程:GCOptimizeList收集所有被GCOptimizeAttribute标注的类型(包括SysGenConfig静态列表里的内置类型),随后通过GenPackUnpack(types, save_path)为这些类型生成解包/打包代码,最终输出到Assets/XLua/Gen/目录(路径可通过CSObjectWrapEditor.GenPath配置修改)。
无 GC 场景与示例验证
完成配置后,以下场景中的值类型传递均不产生(C#)GC 分配,来自 FAQ 与官方无 GC 示例的验证:
- 所有基本值类型(所有整数、所有浮点数、decimal)——本身无 GC;
- 所有枚举类型——需要把该枚举加入
GCOptimize; - 字段只包含值类型的 struct(可嵌套其它只包含值类型的 struct)——需要把该 struct 加入
GCOptimize。
使用方式包括:
- 用 delegate 调用 Lua 函数、传值类型参数;
- 用
LuaTable、LuaFunction的无 GC 接口(如Func<T1, T2, TResult>)传值类型; - 传递值类型数组(如
Vector3[]、自定义 struct 数组)并允许 Lua 侧直接读写数组元素; - 通过 interface(
CSharpCallLua)让 Lua 实现回调并交换数组内容; - 通过
LuaEnv.Global.Set/Get在 Lua 与 C# 之间传递 struct 值。
NoGc 示例的运行时验证代码展示了几条关键路径(NoGc.cs):
// C# 通过 delegate 调用 Lua,传 Vector3 / 自定义 struct / 枚举,均无 GC f2(new Vector3(1, 2, 3)); MyStruct mystruct1 = new MyStruct(5, 6); f3(mystruct1); f4(MyEnum.E1); // Lua 访问 C# 值类型数组,无 GC(含 primitive / Vector3 / 自定义 struct / enum / decimal 数组) farr(a1); farr(a2); farr(a3); farr(a4); farr(a5); // Lua 侧也可用 table 构造 struct 参数传入 C#,例如 -- local r = monoBehaviour:Vector3ParamMethod({x = 1, y = 2, z = rnd}) -- local r = monoBehaviour:StructParamMethod({a = 1, b = rnd, e = {c = rnd}})Lua 侧同样可以无 GC 地读写 C# 的 struct 数组元素,例如交换Vector3[]中两个元素:
function array_exchange(arr) arr[0], arr[1] = arr[1], arr[0] end进阶:AdditionalProperties 处理私有字段
默认情况下,GCOptimize只对public 的 field打解包。如果某些 struct 把字段做成私有、通过 property 对外访问,就需要用到XLua.AdditionalProperties这一扩展配置。
标签方式比较简单(在对应属性上加标签即可);配置方式要求是Dictionary<Type, List<string>>类型,Key 是要生效的类型,Value 是属性名列表。可参考 xLua 对几个 UnityEngine 值类型的内置配置SysGenConfig类(GenAttributes.cs):
[AdditionalProperties] static Dictionary<Type, List<string>> AdditionalProperties { get { return new Dictionary<Type, List<string>>() { { typeof(UnityEngine.Ray), new List<string>() { "origin", "direction" } }, { typeof(UnityEngine.Ray2D), new List<string>() { "origin", "direction" } }, { typeof(UnityEngine.Bounds), new List<string>() { "center", "extents" } }, }; } }可以看到Ray、Ray2D、Bounds这类 struct 虽然底层字段是私有(如m_Origin、m_Direction、m_Center、m_Extents),但通过AdditionalProperties把origin、direction、center、extents属性纳入 GC 优化的解包范围,从而让这些类型的完整属性访问也做到无 GC。
实操清单与注意事项
在微信小游戏 Unity 工程中落地 struct GC 优化,按以下步骤执行即可:
- 确认字段约束:struct(含嵌套 struct)只包含
byte/sbyte/short/ushort/int/uint/long/ulong/float/double字段; - 配置 GCOptimize:自定义 struct 打
[GCOptimize]标签,或在 Editor 目录的静态类中通过静态列表/动态列表配置;UnityEngine 的 Vector 系列、Quaternion、Color 等已内置配置,无需处理; - 登记使用入口:把携带该 struct 的 delegate/interface 加入
CSharpCallLua,把暴露该 struct 方法的 C# 类型加入LuaCallCSharp; - 执行代码生成:在 Unity 菜单执行XLua/Generate Code,生成代码默认输出到
Assets/XLua/Gen/; - 按无 GC 接口调用:优先使用 delegate 调用、
LuaTable/LuaFunction无 GC 接口与数组访问路径; - 私有字段场景:如 struct 通过 property 暴露私有字段,用
AdditionalProperties补充配置。
需要注意的前提与限制:
- 字段包含 string、引用类型等不符合约束的 struct无法启用 GC 优化,会退回默认的引用传递(boxing + 释放)路径;
- GC 优化依赖生成代码,配置变更后必须重新执行 Generate Code;
- 该方案解决的是C# 侧 GC 分配,Lua 侧自身的 GC 策略(如
LuaEnv.FullGc、GcPause、GcStepmul调优)属于另一个话题,可参考 FAQ 中的相关说明; - 本仓库的 xLua 位于微信小游戏 WebGL 工程 Demo/xLua_WebGL 中,上述配置与生成流程在 Unity WebGL / 微信小游戏导出目标下同样适用。
- 游戏开发
- 移动开发
- WebAssembly
【免费下载链接】minigame-unity-webgl-transform
微信小游戏Unity引擎适配器文档。
相关推荐
xLua 复杂值类型(struct)GC 优化指南:免 GC 传递原理、配置条件与实战示例
xLua 复杂值类型(struct)GC 优化指南:免 GC 传递原理、配置条件与实战示例 xLua 中 C struct(值类型)默认以引用方式跨语言传递,每
游戏开发脚本语言集成一次跑通 yuzu Switch 模拟器:PC 免费玩起 Switch 游戏
一次跑通 yuzu Switch 模拟器:PC 免费玩起 Switch 游戏 yuzu 是一款开源的 Switch 模拟器,能让你的 PC 直接免费运行 Swi
虚拟化桌面应用图形学Unity游戏微信小游戏快速适配完整指南
微信小游戏Unity WebGL适配方案(简称Unity WebGL小游戏适配)是一套专门为Unity游戏开发者设计的完整解决方案。该方案基于WebAssemb
游戏开发移动开发WebAssembly
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考