[c#]值类型内存分布

前言

最近公司对于gc考虑的很严格,希望堆上内存只在系统初始化的时候才分配,以减少因为gc导致的cpu卡顿。在这个基础上仔细看了一下值类型的内存分布,了解一下值类型在内存上究竟怎么分配的。

值类型

在定义值类型的变量的时候会直接算好占用多少内存。

public struct DataUnion
{
    public long _i64;
    public ulong _u64;
    public double _f64;
}

在定义的时候就已经开辟了24字节的内存保存,就算不存值内存也已经开辟了。
但是他有一个好处就是不会增加gc计算。

public struct DataUnion{
    public data _data
}

在struct中保存的是引用类型对象的引用,是固定大小的,根据操作系统的位数,32位的占4字节,64位的占8字节。每次传递的时候值类型里的数据都会被复制一份,所以引用也会被拷贝一份。

自定义内存布局方式

通过使用[StructLayout(LayoutKind.Explicit)]和[FieldOffset]特性,这个结构体可以在相同的内存位置上保存多种不同类型的数据,通过这个特性,我们能使用同一个结构保存不同类型的值。

[StructLayout(LayoutKind.Explicit)]
public struct SlateDataBase
{
    [FieldOffset(0)]
    public long _i64;
    [FieldOffset(0)]
    public bool _bool;
    [FieldOffset(0)]
    public ulong _u64;
    [fieldOffset(8)]
    public string _str;
    [FieldOffset(0)]
    public IntPtr  _next;
}

SlateDataBase _dataBase = new SlateDataBase(){_bool = false};
private SlateDataBase _dataBase1 = new SlateDataBase();

void main(){
    IntPtr ptr1 = Marshal.AllocHGlobal(Marshal.SizeOf<SlateDataBase>());
    Marshal.StructureToPtr(_dataBase,ptr1,false);
    _dataBase1._next = ptr1;
    SlateDataBase data = Marshal.PtrToStructure<SlateDataBase>(_dataBase1._next);
    data._bool;
}

上段代码使用了IntPtr来保存对象指针,这样避免了结构布局中的循环依赖问题。同时通过了StructLayout来控制内存布局。但是我知道有一个问题:

  1. 如果我分配的内存不是从0开始的,那保存的值会有问题

注意点

  1. 在使用StructLayout做内存布局的时候是在创建的时候就分配内存了,值类型可相互覆盖,所以都可以从统一位置做偏移,但是引用类型保存的是指针大小,它的大小和对齐方式跟值类型不一样,所以它不能覆盖值类型的内存,得重新取一块内存保存。
  2. 需要自己手动做InPtr所占内存的回收,而且使用的是指针很容易出内存问题。
  3. 内存大小是通过最大偏移加类型所占内存。上面所占内存为8字节+string所占(8字节)=16字节

值类型分配地方

一般来说,值类型所分配的地方有两个地方,一个是堆上,一个是栈上。那什么时候分配在堆上,什么时候分配在栈上呢?


public struct SData{
    public int _int;
}

public class Data{
    public int _int;
    public ulong _ulong;
}

public class Data1{
    public Data _data;
    public SData _sdata;
}

public Data1 _data1 = new Data1();

public void Main(){
    SData sdata = new SData();
    sdata._int = 10;
    _data1._sdata = sdata;
    sdata._int = 20;
    Data data = new Data();
    data._int = 20;
    _data1._data = data;
}

上面的示例代码值类型的struct分别在堆和栈上都分配了。
栈上分配:
在函数内创建的sdata是分配在栈上,它的什么周期是跟着函数体的结束而一起结束,内存也就被回收掉了。
堆上分配:
当我实例化了_data1这个引用类型对象的时候,它就在堆上分配了内存,分别是_data的指针大小+_sdata这个struct所占用的内存大小。所以这个时候_sdata是分配在堆上,当我把函数内的
sdata赋值给_sdata的时候,是把sdata的内容给复制过去。所以当我赋值完了之后把sdata的int改成20,实际上_sdata中的_int还是10。

引用类型

在定义引用类型的时候在没有实例化引用类型对象的时候是不会申请内存的。

public class Data{
    public long _i64; 
}

void main(){
    public Data data;
}

在使用_i64的时候需要创建一个Data的引用类型对象,相当于对_i64做了一次装箱操作。
当内部保存的类型是引用类型的时候,因为不会出现拷贝引用类型数据,所以引用不会被拷贝。

实例化

当我实例化的时候,就会在堆上分配引用类型中值类型所占用的内存,在堆上分配一个8字节的空间,然后把指针地址返回给data。data就通过这个地址找到这片空间找到里面的_i64所写入的内容。

gc回收

之前说过,不讲了。

泛型类型

根据上面的特性,在构造值类型的泛型时,使用struct来保存

public struct ValueVariable<T> where T : struct{
    private T _value;
}

public ValueVAriable<int> _int_value = 10;
public ValueVAriable<bool> _bool_value = false;

这样我在保存一个值类型的时候也是通过值类型保存,所以不会产生gc问题。
引用类型就可以直接使用object来保存。

不安全代码

暂无

[工具]配表工具

前言

配表在一个游戏里是一个很重要的部分,它能让策划通过数值来控制游戏很大一部分体验。所以配表在一个游戏里是必不可少的一个环节。最近我就在做一个配表工具,现在就写了一篇文章来介绍一下我这个打表工具的思路

正文

配表的关注点

  1. 方便
  2. 内存
  3. 健壮

初级版本

现在在我们客户端开发游戏,一般都需要在两个地方读取配置,unity中的c#使用,热更新lua里使用的。所以一个简单的配表工具,就是直接生成一份lua的table配表

table = {
    key = {
        value =
    }
}
return table

用来在lua中读取。还有一份资源专门用来给c#读取的配置,这个格式很多比如json,xml,或者unity中的asset的资源。这样的可视化的好处是方便策划用来检查配置是否正确,也方便程序直接修改配表来更改配置的可以不用打表就可以测试功能,使用也比较方便,都有第三方的插件用来生成对应结构供运行时使用。但是这种的坏处就是内存占用比较高,一套配置在运行中中需要分别生成c#和lua两套结构,也容易被破解然后直接被获取应用配置。

升级版本

碰到问题,那我们就去解决这个问题,首先我们先解决这种可视化,能被用户破解修改的这个问题。我决定选择二进制文本来解决这个问题,因为二进制文件必须知道我们序列化结构,才能把二进制文本解析成我们代码所需要的结构。所以我们直接把配表生成二进制文件,然后在运行时,在反序列化成对应的类型(int,string,bool等),这样之后,我们的内存压力也变小了,因为二进制文件比json这种可视结构要小很多。而且我们还能按需序列化了,能占用更少的运行时内存。但是使用二进制去解析有个问题,就是如果生成二进制所使用的结构和解析二进制使用的结构不匹配,会出现解析失败。

三代版本

现在先解决上一个版本新出现的一个优化点,按需加载配置。先说一下这个概念吧,就是按游戏的进度,每次所需要使用的配置内容不会特别多,很多配置很有可能后面都不需要使用了,所以我们可以每次使用的时候就加载那一行的数据,这样我们就可以少序列化很多数据,减少运行时内存压力。在配表中,我们会有一个唯一key,用来读取某一行数据,那根据这个功能,我就生产了一张key对应当前行的二进制索引位置,从这个位置开始序列化这一行的数据。

最后解决序列化的问题

在解决序列化问题的时候,我决定使用protobuf的一部分特性,然后把那些需要定制化的结构,都在运行时候自己再解析,只支持一些基础类型,而且还能结构公用

message ConfigValue{
    int32 int_value = 1;
    string string_val = 2;
    float float_val = 3;
    bool bool_val = 4;
}

message ConfigRow{
     repeated ConfigValue config_val = 1;           //行数据
}

message ConfigIndex{
    ConfigValue key = 1;
    int32 star_index = 2;
    int32 end_index = 3;
}

message ConfigTable{
    repeated string config_title = 1;   //表头
    repeated string config_type = 2;    //字段类型
    string key = 3;                     //key的字段名字
    repeated ConfigIndex config_index = 4;  //key对应数据中的位置

}
message ConfigData{
    repeated ConfigRow config_row = 1;  //数据内容
}

结构如上,定义了一个configValue这个结构,它是用来保存基础类型,在我的规划里,我只需要知道int,string,float,bool这四种基础类型,List这样的数组通过定义一个特殊的切割符号,通过string导出使用。configRow这个结构就是用来导出一行的数据;configData就是结构就是这张配置的数据内容。
Configindex是代表每一行的唯一key和当前行在configData二进制中的位置。
Config_title代表的是表头,就是这个字段列表
Config_type代表的就是这个表头所代表的类型

示例

{ "configRow": [ { "configVal": [ { "intValue": 1 }, { "stringVal": "测试普通+伴随" }, { "intValue": 2001 }, { "stringVal": "111,0,47.5" }, { "floatVal": 10 }, { "stringVal": "1|2|3" } ] }, { "configVal": [ { "intValue": 2 }, { "stringVal": "测试普通+嘿嘿" }, { "intValue": 2004 }, { "stringVal": "888,255,47" }, { "floatVal": 33 }, { "stringVal": "4|5|6" } ] } ] }
{ "configTitle": [ "id", "des", "key", "coordinate", "testFloat", "TestListInt" ], "configType": [ "int", "string", "int", "vector3", "float", "listInt" ], "configIndex": [ { "key": { "intValue": 2001 }, "starIndex": 2, "endIndex": 64 }, { "key": { "intValue": 2004 }, "starIndex": 66, "endIndex": 128 } ] }

打出的内容如上所示
我们就可以通过获再configTitle中获取key知道key是第三个元素,并且它的类型是int,所以我们就能从configIndex里的intvalue取值,2001这个key的元素的在二进制里的索引和大小,然后序列化configRow得到相对应的值。因为我们的字段和类型都是一一对应的,所以当我们删除了字段coordinate的时候

{ "configRow": [ { "configVal": [ { "intValue": 1 }, { "stringVal": "测试普通+伴随" }, { "intValue": 2001 }, { "floatVal": 10 }, { "stringVal": "1|2|3" } ] }, { "configVal": [ { "intValue": 2 }, { "stringVal": "测试普通+嘿嘿" }, { "intValue": 2004 }, { "floatVal": 33 }, { "stringVal": "4|5|6" } ] } ] }
{ "configTitle": [ "id", "des", "key", "testFloat", "TestListInt" ], "configType": [ "int", "string", "int", "float", "listInt" ], "configIndex": [ { "key": { "intValue": 2001 }, "starIndex": 2, "endIndex": 50 }, { "key": { "intValue": 2004 }, "starIndex": 52, "endIndex": 100 } ] }

对应的索引下标就会删除,我们的二进制位置也就做了一次偏移。这样我们只需要在处理配表的时候做一次容错,找不到configtitle的时候,返回一个空的对象,那么在没有使用这个字段的时候不会因为字段的删除导致解析出错,影响到程序运行。
这样策划在打表的时候就不需要关心结构是否匹配,只需要无脑的把表打出来,然后程序在用的时候再对字段做出处理。

最后再说点

每家公司,每个策划组都有自己配表的方式,有的人因为一张表过大,喜欢分多个excel然后打表的时候整合再一起;有的人想要简化运行时做结构生成,想要在打表的时候就把结构也生成出来;所以需求千变万化,都是使用者的习惯来做定制化的,我只能表达我遇到的问题,所解决的问题。

【URP】Android上URP失效

前言

这篇主要是讲一下最近遇到在android上urp不生效的问题。

正文

在ab中资源不一致

问题原因

在做功能的时候,因为贪图方便,所以在pass里处理过的rt直接渲染保存到resource中的rt上,然后在ui上就直接使用这张resource上的rt。当把这个ui做成ab之后,就会发现ui上使用的rt是黑的。通过打印这两个rt的GetInstanceID之后发现,这两个id是不一样的。其实这个很好理解的,因为在打ab的时候,是会把当前这个ab所依赖的资源在不打包成ab的时候,会生成一份放在这个ab里,所以在这步的时候,这个ui所依赖的rt就已经和resource的rt不一样啦。为啥会出现resource里的资源不能被ab资源所依赖,而会产生冗余的资源呢?我的理解就是因为ab是属于一个单独的资源系统,他其实是可以脱离untiy单独使用的,它的使用场景可能和打ab的unity工程不是同一个,所以不能保证ab的使用场景一定能有resource的资源。

解决办法

  1. 在pass里保存的rt直接通过代码新建一个,然后在ui里直接调用这个rt去显示。
  2. 直接把urpasset的资源和所依赖的资源都打成ab包。

renderTexture 格式问题

问题原因

在通过新建RenderTexture去解决上面那个显示资源部一致的问题时,发现这张rt在有的时候会出现纹理不能写入。我通过把rt保存到手机里发现这张rt是一张全透的图片。

解决办法

  1. 修改创建RenderTexture的Depth为0可解决。

UI刷新问题

问题原因

因为在截屏处理rt的时机是我们在打开UI界面的时候,所以当Features处理完设置好rt的时候,我们的UI是不会做刷新的。

解决办法

  1. 设置一个回调函数,当Features处理好RT已经保存结束,回调到ui上调用SetMaterialDirty来强制刷新

结束

在android上显示urp失效找了我一个礼拜的时间,其实核心的问题都很简单,但是因为是多个原因混合在一起导致的问题,所以在找线索的时候花了很长时间。在查找问题的时候,我们首先需要明确问题出现的点,在这个问题,因为是多个问题混合在一起出现的,首先我先确认一点,就是相机的纹理是否能获取到;这里我做的事情就和写代码做一些日志输出一样,我把相机问题保存在手机里。然后很幸运,这个纹理是有的;这就能解决我很大的一个顾虑,不是urp底层问题。后面就开始查为啥显示不出来,就一个节点一个节点的输出保存。ui上使用的rt是不是有纹理,获取之后纹理为啥没有更新。这样一点一点查,最后就解决掉。遇到问题不能害怕这个问题,而是要做一次拆分,去了解功能的每个关键节点;去看看这些节点是不是有问题。

[UGUI]ugui中同一batch的遮挡顺序

前言

最近被人问到同一batch中ugui的前后顺序是怎么表现出来的,之前我只考虑过一帧是怎么前后顺序的渲染,所以这次我讲说一下一个batch中的图片渲染顺序

知识点

  1. 使用xcode的FPS截帧
  2. 渲染的点的坐标
  3. 渲染三角形的顺序
  4. 渲染三角形的uv顺序

正文

渲染图片的本质

在一般情况下,我们常说的渲染一张图片其实是记录下一张图片的四个顶点位置,通过mvp变换,从模型空间转换到屏幕空间,然后渲染到显示设备上。在渲染的时候,我们需要定义三角形的顺序,根据对应渲染设备所使用的坐标系,我们通过左手或者右手定则,来确定渲染的朝向。定义的uv坐标来确定这三个点所需要渲染的颜色在uv图上的什么位置。通过上面的概念我们就应该能知道渲染一个图所需要简单的数据结构了。

我们只考虑最上面的那张图我们来简单定义一下数据结构

vector3[]index   --点坐标
int[2*3] vertex     --三角形的渲染顺序
int[2*3] UV             --uv坐标id

通过上面的结构体,我们就能正常的渲染出一张图片出来的。

ugui合批本质

我这里所说的合批是ugui的动态合批。一般来说,你可以认为它就是问了把多个mesh合并成一个mesh传到gpu去渲染,那什么是mesh呢?其实就是我们上面所说的那个结构体。既然知道了它是什么,我们就好来理解mesh的合并了。首先在ugui我们会对渲染的物体进行一次排序(这个内容我已经拖更很久了,大家可以去看官方的一节课)然后通过这个排序结果,我们就可以开始记录合并的mesh内容了。如果是同一个材质,同一个贴图,并且是相邻的,那就记录一次合并,如果不可以,那就把上一次合并的结果callback到gpu做一次渲染,剩下的再进行合并记录。

同一批次的遮挡顺序

通过上面我们就可以知道,同一个批次里面,如果图片前后有遮挡关系;首先我们通过排序,记录下所有点的位置信息,然后通过顺序我们记录下来三角形的渲染顺序

在这张图片,我们就能看出来,其实在这个批次的渲染里,我们是记录下来所有点,和所有三角形的信息给gpu去渲染的,在这次合批中我们是不会进行三角形的剔除操作。就算是所有做了一次完全的遮挡,其实我们上传的数据也是做了一次全量的记录的。所以同一batch的遮挡就是在合批时候的排序顺序的关系。

总结

渲染的本质是通过三角形的排布来做纹理的展示,所有的优化方式都是围绕这三角形怎么减少呀,三角形的颜色该怎么计算出来,这样的方式来做优化的。了解本质,你才能更轻松的理解方案。

[XLua]lua和c#交互

前言

在移动游戏发展到今天,热更技术已经有很多种渠道了,一种是使用lua,一种是c#更新dll,还有最新出来的TypeScript的热更新。这次我就简单的聊些使用率最多的lua热更新中的交互,也希望能说出一些交互上可以优化的东西。

知识大纲

  1. 函数
  2. 值类型
  3. 引用类型
  4. struct类型
  5. string类型
  6. lua和c的交互调用栈
  7. IntPtr类型

初始化

获取全局表

在lua初始化的时候会初始化一个luaEnv对象,他会去c里拿到一个LUA_REGISTRYINDEX这是一个registry的表的索引,这个表是c可以自由使用,但是lua代码不能访问他。

注册lua状态机

使用luaL_newstate创建了一个新的lua状态机(可以理解为lua虚拟机),主要用来为每一个lua线程创建独立的函数栈和线程栈,以及lua所需要的内存管理,字符串管理,gc管理。创建两个主要的数据结构lua_state和global_State。

  • lua_State:主线程栈结构
  • global_State:全局状态机,维护全局字符串,内存管理函数,gc等信息

加载lua标准库

使用luaL_openlibs用来加载lua的标准库到指定的lua状态机。

注册一个用来lua和c#交互的对象

通过创建一个ObjectTranslator对象来做lua和c#的对象交互,传入一个luaEnv对象,和一个lua状态机的指针。创建了一个弱value的一个table放在lua中。注册lua对象回收中__gc的回调方法。使用lua_pushlightuserdata创建一个自己管理的xlua标志位。注册所有被此应用所用的所有程序集到assemblies中

注册xlua全局表

translator.OpenLib(rawL);
import_type这个方法为lua中获取全局c#方法的回调函数

注册一个lua异常捕获

LuaAPI.lua_atpanic(rawL, StaticLuaCallbacks.Panic);使用这个方法注册一个在c#中去处置lua程序崩溃的方法

注册一个lua的print方法

LuaAPI.lua_pushstdcallcfunction(rawL, StaticLuaCallbacks.Print);LuaAPI.xlua_setglobal(rawL, "print")
注册一个print的方法注册到global全局表中的print

初始化Init脚本

注册一个全局的CS表,设置CS的元方法。设置xlua的元方法。

获取_G全局表

LuaAPI.xlua_getglobal(rawL, "_G");translator.Get(rawL, -1, out _G);获取_G

注册类

通过Wrap中的__Register的方法用来注册当前这个类的实例化函数,静态函数,属性,静态属性,实例化类的方法

注册函数

通过BeginObjectRegister在栈顶注册一个class.FullName名字的metatable表。给他塞入一个xlua的标志,覆盖两个关于“gc”和“tostring”的元方法。创建三个table,用来存放类的函数,设置属性方法,获取属性方法。调用RegisterFunc把所需要的函数方法注册进去。最后在结束的时候替换掉“index”和“newIndex”这两个元方法

注册静态函数

通过BeginClassRegister中把当前类的静态函数注册到table中,然后通过SetCSTable把当前类名的table放在cstable下,然后替换元方法__call为c#的函数,最后创建两个table用来放置静态属性;还是通过RegisterFunc把静态属性的获取和设置添加进去。

加载lua脚本

DoString

通过xluaL_loadbuffer编译lua脚本,通过lua_setfenv设置当前lua编译脚本为传进来的table的upvalue值,然后通过Lua_pcall执行编译之后的脚本,放回栈顶的对象。

Lua和c#交互

c#调用lua方法

在DoString之后把DoString所执行的脚本设置到一个luatable对象里面,然后通过这个luatable对象去lua栈里拿到luatable所在的table。在tryGetGetFuncByType发现我们要转换的类型不是我们定义的那些基础类型,所以通过GetObject去获取到一个转换委托ObjectCast这个委托函数里,然后通过需要转换到类型调用不同的转换函数,函数就是通过调用CreateDelegateBridge来创建一个action委托的,委托里包含了lua方法的索引。当函数调用的时候就是调用委托里的PCall来调用到lua中的方法。因为在tryGetGetFuncByType没有注册,所以使用了反射去创建当前类型的对象。

c#获取lua值类型

也是在tryGetGetFuncByType里把int ,bool,float这的值类型通过从lua栈里push出number,boolean,integer的值通过类型转换变换出来。

c#获取lua的table类型

当把table定义成一个list对象的时候,它也和调用函数一样回去判断有没有在tryGetGetFuncByType里定义了这个list类型的ObjectCast,如果没有也是通过反射在genCaster里去创建这个对象,把lua栈内的值取出来。

c#获取lua的string

当需要在lua中使用到c#的string时,把string转成byte[]传到lua中。
当把lua中的string传到c#的时候,也是把string转成byte[]接收过来,转成string

lua调用函数

在init的时候生成的时候会设置import_type为CS的index方法中的查找到c#里的ImportType去根据className去translator.FindType找到当前这个className到类型,然后通过GetTypeId去找放回类型的唯一id,如果这个类型没有缓存,那就会去生成一个当前类型名字的metatable,然后通过DelayWrapLoader中注册的loader去加载生成的wrap类的Register生成的当前所需要的方法的metatable,给lua调用,然后把类型的id传到交互栈中。当调用这个方法的时候就可以通过在RegisterFunc的时候push进去的lua_pushstdcallcfunction来掉到这个函数了。

lua调用值类型

当lua需要设置值类型的时候,同样会定义一个属性赋值方法,通过函数调用的方法调用到这个方法,然后通过lua_tointeger去栈里拿到这个方法一起传过来的number类型的值,然后赋值过去。
当lua需要设置值的时候,还是定义一个属性获取的方法,通过函数调用先通过Get方法从userdata中获取到这个需要赋值的对象,然后通过UnPack来把这个需要赋值的属性取出来,然后修改掉对应的属性;最后通过push把属性塞到lua栈中

lua调用struct类型

当lua需要获取一个struct类型的值时,因为lua中没有struct这样的结构类型所以会把struct包成一个object传到lua栈中当userdata存下来。
当lua需要设置一个struct类型的值时,获取到栈内的userdata然后存成ObjectCast然后转成struct,取lua栈内第二个参数来设置struct的值

但是在xlua中vector和color这两种struct会做一次处理,会把里面的struct的结构内容给拆分出来,通过基础的值类型进行传递。

lua调用引用对象

通过addObject把创建出来的object存在objecttranslator中,返回一个存放的index,然后把这个index传到xlua_pushcsobj里去创建一个userdata并且把这个对象的方法,属性都传进去做metatable属性。然后就可以获取到这个userdata赋值给lua。

lua和c#相互引用对象的生命周期

通过上面知道在c#中需要被lua所调用到的引用类型对象都需要在objecttranslator中存起来,然后把所生成的index传到lua的userdata中去,然后给这个userdata设置了一个元表,修改了gc这个元方法,所以当lua的userdata不在被引用的时候就会触发lua的gc,然后调用到gc这个元方法,调用到ObjectTranslator中的collectObject然后保存列表中清理掉。
在c#获取到lua的对象需要创建一个object的对象,然后把lua中的reference通过luaL_ref获取到保存下来,然后当这个对象不在被引用的时候,调用到c#的dispose函数,然后在里面去LuaAPI.lua_unref释放掉这个lua对象的引用。

总结

通过上面的数据交互,我们可以知道,在lua和c#的交互,我们需要注意到引用类型的交互,因为他们会分别在lua端和c#端创建一个objcet和table(userdata)对象,而这两种对象都会产生gc压力,还有一个就是struct对象也是需要注意的,虽然在xlua中vextor会拆成基础值类型传递,但是大部分我们自定义的struct还是会被包装成一个object传递的,这里还产生了一个装箱操作,所以消耗会更大。

[lua]upvalue和闭包

概念

当一个函数内部A嵌套另一个函数B定义时,内部的函数体可以访问外部的函数的局部变量,这种特征我们称为词法定界,B中被A访问到的变量我们称为upvalue。而闭包是指一个函数加上它可以正确访问到的upvalue。技术上来讲,运行时,我们执行的是闭包,而不是函数,函数仅仅是闭包的一个原型声明;闭包在完全不同的上下文中也是很有用途的。因为函数被存储在普通的变量内我们可以很方便的重定义或预定义函数。通常当你需要原始函数有一个新的实现时可以重定义函数。upvalue你可以理解成是一块特殊的内存,独立管理内部数据的,所以可以在多个闭包之间提供一种数据共享的机制。

示例解释

upvalue

function f1(n)
    local function f2()
        print(n)
    end
    return f2
end
local g1 = f1(11)
g1()                    --打印11
local g2 = f2(22)
g2()                    --打印22
g1()                    --打印11

在上面的例子里,我们可以看出来当g1 = f1(11)这个被执行完的时候,局部变量n的生命周期应该是结束了的。但是因为它成为了f2函数的upvalue,所以它的生命周期跟着g1的生命周期一起了,被延长了。下面的g2是重新执行了一遍f2所以它重新包了一个n做upvalue,这个时候upvalue的数据没有共享。

闭包

function f1(n)
    local function f2()
        print(n)
    end
    n = n+10
    return f2
end
g1 = f1(10)
g1()                    --打印20

在上面的这个例子里,我们发现n在包在f2内部的时候,成为f2的upvalue,但f1还没执行完成n还是f1的局部变量,所以执行了n = n+10的这个的时候,n的变化也在f2执行的时候被表现出来的。

upvalue的数据共享

function f1(n)
    local function f2()
        print(n)
    end
    local function f3()
        n = n +10
    end
    n = n+1
    return f2,f3
end
local g2,g3 = f1(1)
g2()                        --打印2
g3()
g2()                        --打印12

从上面的例子我们可以发现,当f1执行结束之后,lua会把内部所引用的n做成一个upvalue来作为f2和f3的upvalue,所以当我们g3调用了n = n+10的时候,n的改变也就改了那共同upvalue内的n,所以可以被g2函数所打印出来变化。

function f1(n)
    local function f4()
        local function f2()
            print(n)
        end
        local function f3()
            n = n +10
        end
        return f2,f3
    end
    n = n+1
    return f4
end
local g1 = f1(1)
local g2,g3 = g1()
g2()                    --2
g3()
g2()                    --12

所以我们就很容易理解上面这个例子为啥还是一样的打印结果,因为upvalue在一个函数内就只会有一份相同的。

总结

闭包这个概念在很多语法中都有的,之前有一点点了解,最近在做一些lua的内存泄漏优化的时候,发现因为一些全局表的函数引用,导致了一些传入进去的值被引用到,没办法释放,引起了一些内存上的增长,所以我们在给函数传值的时候,需要注意到这个函数什么时候被释放的,是不是不会被释放掉,如果它在一个全局table中,那么它所引用到的值我们就需要手动释放。关于upvalue的lua中是怎么实现的,之后有机会讲解一下源码。

[资源管理]代码赋值资源管理

前言

在前几天做了一次友好的学术交流,在这次交流中,对方提出了一种关于资源管理的新的思路,就是自己管理asset直接释放ab。我在事后想了一下,感觉这个能省一部分的内存,但是其中的坑还是有点的,所以现在就能写一篇文章来介绍这个方案。

正文

方案

在这套方案里,我们在加载好asset的之后,我们就可以调用unload(false)把ab给卸载了,然后通过resource.unload(asset)把这个资源卸载掉,那这样的好处在哪里呢?这样的好处就在我们可以在内存中减少一部分的ab的内存。

注意点

  1. 在我上一期关于资源管理的文章上说过,我的最后一套方案对于ab的卸载还是要基于ab的依赖关系进行的。那是因为如果不做ab的依赖管理卸载,那如果ab还没卸载,但是它所依赖的ab被卸载了,那下一次再加载依赖的时候,它就没办法被自动依赖赋值上prefab上的。所以我们在使用这套方案的时候,ab是不会被ab包所依赖的
  2. 如果这个资源被ab包所依赖,那还会产生一个问题,当我们调用false卸载资源之后,下一次加载ab包的asset的时候,它所依赖的资源也被加载出来了,那这个依赖的asset就会被重新创建一个。出现内存的冗余资源。

结论

所以我们在使用这个方案的时候,最好就是为了一些头像icon这类的texture资源,然后通过我们手动维护这个asset的生命周期,这样能少一部分内存。而且icon这类资源基本上都是使用我们代码赋值的。使用这个方案的资源一定不能被ab所依赖。

[资源管理器]之前资源加载的纠错

前言

这篇文章是为了之前在做资源依赖的时候关于Asset->Assetbundle依赖加载之中错误的一个纠正

正文

在之前的文章我说了一种依赖加载的方案,就是记录用Asset资源对于它所依赖的assetbundle的依赖关系,然后通过这种的依赖关系来进行资源加载,这种方案的好处就是,当我Asset不在依赖Assetbundle的时候就可以卸载这个Assetbundle的,而不需要因为我Asset所在的assetbundle没有卸载就不去卸载依赖的Assetbundle。但是今天在和大佬在聊天的时候,他和我说了一种情况,我们选确定一个依赖关系AB1依赖AB2,然后我们先加载AB2在加载AB1然后卸载AB2再加载AB2然后去实例化AB1中的资源。是不是看起来很绕,其实很简单,就是去确定一点,在Unity中的AB包之间的依赖关系,会不会因为多次加载他们的唯一标识会修改,导致AB1中所记录到的AB2的标识没有更新,导致找不到AB2。通过测试,我发现如果第一次走这样的情况,其实AB1的资源是不会丢失的。loadAB1->instanceAB1(丢失资源)->loadAB2(资源被自动赋值上去,正常现实)。但是第二次加载了就会出现问题loadAB1->instanceAB1(丢失资源)->loadAB2(自动赋值)->DestroyAB1->unloadAB2->loadAB2->instaceAB1(丢失资源)。在这种情况下资源就不会赋值上去了。

结束

因为我在之前的测试不研究,也因为对AB的加载过程没有那么熟悉,导致得到了一个错误的结论。现在特出一篇文章作为纠错。之后我会很严谨的测试不同的情况。

[技巧]表情输入的特殊处理

前言

在和前同事聊天的时候,他因为在接入翻译,所以问了我一个问题,关于在聊天中加入了表情的翻译处理。这个问题的点在哪里呢?是因为我们一般做聊天表情包的时候,绝大多数的处理都是加入一个特殊字符表示表情的起始关键字符,然后通过一个特殊的规则来匹配表情。但是在翻译的时候,很有可能会把这个表情给翻译成一个单词。通过他的讲解,我发现了其实通过一个小处理就能完美的解决掉它。

正文

转义字符

在接触字符的时候,我们肯定知道字符里有个特殊的字符,它叫转义字符。在ASCII码都可以用“\”加数字来标识的,那这个特殊字符就是“\”。它的含义就是用它来做一个关键字,然后它和后接的数字来表示一个特殊的含义。通过“\”这个字符把它后接的字符的含义给改变了。我们就可以通过定义一个特殊的字符来表示我们的表情。可能会有人问了,这个不就是一个简单的关键字标识嘛,有啥好讲的。

特定环境出现的问题

那后面我就讲一个特殊的环境。如果我们定义\1代表的是一个笑脸。我们在输入的时候,肯定是玩家选择了一个笑脸然后在发给服务器的时候,我们把笑脸替换成\1来发送的,这个是一个简单的表情发送,但是如果玩家输入了一个\1怎么办呢?是不是我们还把它翻译成笑脸呢?微信里就是这样处理的,你发送一个特殊格式的字符它就给你转换成特定的表情了。那如果我们在游戏里表情是到达一个等级或者一个条件开放的。那玩家是不是就这样卡了一个bug。那我们就思考下怎么处理吧。

一种简单的处理

其实处理起来很简单的。我们在玩家输入的"\"之前在加入一个"\"。那玩家在输入的时候就变成了“\1”那我们在做处理的时候,通过"\"后接"\"就翻译成反斜杠的特殊含义,我们就把这个当一个正常的"\"输出,最后在另外一端看到的玩家输出就是"\1"了。这样的处理就正常表示了玩家的输入。

测试代码

static void zhuanyi(string str,List<string> strs)
        {
            if (string.IsNullOrEmpty(str))
            {
                return;
            }
            int index = str.IndexOf('a');
            if (index < 0)
            {
                strs.Add(str);
                return;
            }
            strs.Add(str.Substring(0,index+2));
            zhuanyi(str.Substring(index+2), strs);

        }
        static string zhuanyi1(List<string> strs)
        {
            string str = "";
            for(int i = 0; i < strs.Count; i++)
            {
                int index = strs[i].IndexOf('a');
                if (index > 0)
                {
                    str += strs[i].Substring(0, index);
                    string temp = strs[i].Substring(index + 1);
                    if(temp == "1")
                    {
                        str += "哈哈";
                    }
                    else
                    {
                        str += temp;
                    }
                }
                else
                {
                    string temp = strs[i].Substring(index + 1);
                    if (temp == "1")
                    {
                        str += "哈哈";
                    }
                    else
                    {
                        str += temp;
                    }
                }
            }
            return str;
        }

这就是一个简单的实现,没有优化过

结束

其实这个的处理是一个很简单的转义字符的特殊应用,我们可以在聊天呀,邮件呀,或者一些需要在文字里插入一些特殊含义内容的字符串里的应用。很多时候我们一些知道的知识点在应用的时候想不到。其实很大的一部分是我们只是把概念当成了概念,而没有转换成我们的智慧。

【基础】VO算法

前言

最近遇到了怪物在ai寻路的目标点为同一个的时候,会出现相互挤压的情况。所以开始研究群体避障算法。这片文章主要讲一下最基础的VO(Velocity Obstacle)算法。

正文

其实vo算法不是很难的,就是你得实时算它前面是不是会碰到物体,因为是简单的,所以单个物体只管自己的。我先晒一张经典的图

这张图在所以讲vo算法的文章里都会出现的,它直观的展示了vo是怎么算物品之间的碰撞。现在我就基于这个图来详细讲解下相关的知识点。

闵可夫斯基和

在这张图里我们会看到B里有一个实体圈,和一个虚线圈。然后你会发现生成的B⨁−A这个三角形实际上是从A的中心点PA为起点画的,所以可以理解那个虚拟的B圈实际上是把A圈给叠加上了。这里涉及到了一个集合知识点就是闵可夫斯基和(闵氏和)。
概念:两个图形A,B的闵氏和C={a+b|a∈A,b∈B}
其实就是从原点向着图形A内部的每一个点做向量,将图形B沿每个向量移动,最终位置的并集就是闵氏和。
我们来看一个图片吧

在这张图里a(A,B,C,D)b(A,C,E)。现在我们用它做一个闵氏和,先把b往DA向量方向移动,得到了一个(F,B,Q)这个图型,其他的向量也按这样的移动,我们最后就会得到一个并集成(F,G,I,M,L,J)这个图型。这个图形就是我们要算的a和b的闵氏和。
结合这个知识点,我们现在来看VO的那张图我们就很容易理解了,把B和A放在同一个圆心上,然后沿着圆的四周往外扩散A的半径向量。得到虚框B。

相对位移

当我们A沿着VA移动,B沿着VB移动的时候,我们要确定两个图形是否会碰撞到,因为我们是从a画的一个扇形,所以我们需要计算下A的相对于B的移动向量,这个就用向量相减就好了。算出一个VA-VB的向量,然后我们PA质点沿着VA-VB的方向画一个和B虚框相切的一个图形B⨁−A。因为实际计算不是求的相对速度,是VA的实际速度,所以我们需要加上VB。这样我们就得到了一个深色的三角形。

结论

当我们的速度不属于这个深色三角形内的时候,他们就不会发生碰撞。

补充

因为VO是把对象都当成一个独立的个体,所以它在计算的时候是实时计算的,当两个物体独立计算的时候,所以他们会单独计算自己的避障速度方向,当A,B沿着避障方向移动的时候,他们在下一次计算会发现自己不在对方的碰撞区域内,所以会修改方向沿着之前会碰撞的方向移动,再下一次计算的时候发现自己又在碰撞区域内了,又开始偏移移动,这样往复以后,就会出现抖动。后面我研究研究VO的优化算法,再讲解下怎么解决这个问题。