unity3D Shader变灰原理

特色

使用shader置灰一张图或者一个文字的代码很简单,但是之前一直不知道它的原理是啥。

今天通过查找资料发现,其实很简单,置灰的核心代码如下:

 fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                //-----------add-------------
                // gray
                float gray = dot(col.rgb, float3(0.299, 0.587, 0.114));
                col.rgb = float3(gray, gray, gray);
                //---------------------------
                return col;
            }
 float gray = dot(col.rgb, float3(0.299, 0.587, 0.114));
这一句是最主要的一句,其实就是把采样出来的颜色和一个固定的颜色值做一个点乘,但是为什么是这个颜色呢?为什么要和这个颜色值做点乘返回最终的片元着色器颜色?

原理:是因为人眼对绿色的敏感度最高,对红色的敏感度次之,对蓝色的敏感度最低,因此使用不同的权重将得到比较合理的灰度图像。实验和理论推导出来的结果是0.299、0.587、0.114。
所以为图片的RGB分别乘以权重然后加起来得到一个灰色值,并且将这个灰色值作为新的RGB值
返回给最终的颜色值。 

如何优化结构体和类的大小?

类和结构体的区别,大家应该都知道,但是在开发过程中到底是用类还是结构呢?

要想知道这个问题的答案首先应该知道怎样估算对象和结构体的大小。

一、如何估算结构体的大小

结构是值类型,它的结构体的实例是存放在栈中或者堆中。结构体在内存中所占的大小,就是其字段所占的大小,但是,它的大小并不是所有字段大小相加,而是存在一个对齐的规则,在默认的对齐规则中,基本类型字段是按照自身大小对齐的,如byte是按1字节对齐。

struct A
{
byte a1;
}

如上面这个结构体的大小就是1字节,如果是下面这个:

struct A
{
byte a1;
int a2;
}

这个结构体所占内存大小是8字节,因为int是4字节对齐的,所以只能从第四个字节开始。

如果再添加一个字段:

struct A
{
byte a1;
int a2;
byte a3;
}

这个结构体大小是12,由于struct本身也要是对齐的,所以它的对齐规则是按照其中元素最大的对齐规则决定的。也就是说上面这个结构体要按照4字节对齐,不足4字节要补齐,所以是12个字节大小。

如果想要优化它的大小,可以调整顺序如下:

struct A
{
byte a1;
byte a3;
int a2;
}

这个时候这个结构体所占的大小就是8字节了。

二、如何估算类的大小

类是引用类型,它的对象实例存放在堆中,对象实例一定是会占用堆内存的,而在栈中,保存的是实例的引用。对象在堆中分成3个区域,vtable、monitor和字段。其中vtable是类的共有数据,包含静态变量和方法表,这个应该就是类本身所占用的大小和具体的对象无关。monitor是线程同步用的,这2个指针分别占用一个inptr.Size大小,字段是从第9个字节或17个字节开始的,字段的对齐规则和结构体的对齐规则相同,区别是Mono中对象的实例会把引用类型的引用放在最前面。一个对象实例的大小就是 inptr.Size *2+字段的大小。

通过调整字段的顺序,也可以优化对象的大小。

还可以通过StructLayoutAttribute自定义类和结构体的对齐方式。

[StructLayout(LayoutKind, Sequential, Pack = 1)]
public struct A
{
byte a1;
int a2;
byte a3;
}

上面这个结构体强制按照1字节对齐,所以他的大小是6字节。但是这样做可能会降低性能。

所以具体情况还是要具体分析,懂得了如何估算结构体和类的大小,就更容易知道该如何使用它们了。

unity3D优化之内存管理

在U3D游戏中内存管理一直都是让人比较头疼事情,现在手机游戏是越做越大,和端游一样,每次卡顿和每次的内存增长对玩家来说都是一个比较差的体验。听到过一句话,说游戏开发做久了就会变成“GC怪”,因为在游戏开发过程中,需求变化多,功能不停的迭代,内存问题也一直存在,需要不停的去优化它。

在unity2018中集成来正版的.NET4.X和C#7.3,引入了ref return 和ref locals,让值类型的操作更加便捷,在U3D2019中更是加入来增量式GC,减少来GC带来的卡顿问题。比以前的版本灵活来许多。

讲了这么多,可能很多人还不知道什么是GC,GC的全称是Garbage Collection,也就是垃圾回收的意思,是一种自动管理堆内存的机制,管理堆内存上对象的分配和释放。

一、内存管理方式

我们常用的内存管理方式有三种:

1.手动管理,像C/C++一样使用malloc/free或者new/delete来为对象分配释放内存。这张方法的优点是速度快,没有任何额外开销,缺点是要去人工了解每个对象的使用情况,这样很容易发生各种问题,比如内存泄漏,野指针和空悬指针等。

2.使用引用计数,它的思想是对象创建出来以后,维护一个针对该对象的计数,使用该对象的地方对该计数加1,使用完后减1,当计数为0时,销魂该对象。这种方法类似半自动内存管理方式,优点是可以把分配和释放的开销分布在实际使用过程当中,速度比较快,不过会存在一个循环引用的问题。引用计数是一种比较常用的内存管理方法,比如U3D中物理引擎PhysX就是使用引用计数来管理各种对象的。

3.追踪式GC器,unity使用的GC器是一种叫标记/清除的算法,它的思路是当程序需要进行垃圾回收时,从根出发标记所有可以到达的对象,然后回收没有标记的对象,这是一种全自动的内存管理方法,程序员完全不用追踪对象的使用情况,也不存在循环引用无法回收的问题。在unity中使用的是一种叫boehm-Demers-Weiser的GC器,它的特点是:

(1)stop the world ,即当GC发生时,程序的所有线程都必须停止工作,在回收时也要停掉所有线程。

(2)不分代,.NET和java会把托管堆分成多个代,新生代的内存空间非常小,而且一般来说,GC主要集中在新生代上,让每一次GC的速度很快,但是在U3D中GC是完全不分代的,只要发生GC,就会对整个托管堆进行GC。

(3)不压缩,不会对堆内存进行碎片整理,类似我们的磁盘一样,使用久了就会有很多的碎片,造成磁盘上有很多小空隙。同样在U3D中GC会造成托管堆出现很多这样的间隙,这些间隙不会合并,当申请一个新对象时,如果没有任何一个间隙大于这个新对象的大小,堆内存就会增加。

二、影响GC性能的因素

主要因素有2个:

1、可达对象的数量

2、托管堆的大小

可达对象是不会被GC回收的对象,减少这类对象的方法是减少对象的数量,如下:

将会产生10个Actor对象。

如果使用以下方法:

只产生一个对象,方法很简单,但是却很有用,如果在需要每帧去处理的对象上去使用这类优化,会得到意想不到的效果。

而优化托管堆的大小主要通过以下几个方面:

1、减少临时内存分配,因为临时内存会使内存短暂的增长,而且会产生碎片

2、防止内存泄漏,也就是存在互相引用无法回收对象的情况,或者没用到的对象,但是对它还有引用,导致释放不掉。

关于在lua中读取excel表的优化

最近在做项目的优化时发现,我们项目中有个锻造的UI界面打开特别慢而且使用内存分析工具发现每次打开这个界面都会导致内存暴涨,在手动GC后内存被回收,可确定是临时内存暴涨,最后发现这个界面每次打开都会去读取一个很大的表数据,造成临时内存的上涨。

如果在lua中去读取一个很大的表,比如策划配置的excel表,假如这个表有4000多行,而且数据项比较多,那么会导致读取时间变长,而且会使临时内存暴涨

就算这个表已经被加载到lua内存中,当你再次去读取这张表并且把它保存在局部变量时也会使临时内存暴涨 ,因为这个是值数据需要开辟临时内存空间

所以在读取表数据时应该避免去读取整张表的数据,而是根据索引去读取单个数据或者是某些数据。如果表结构不满足条件就应该和策划商量把表拆开来配置,这样才不会导致临时内存暴涨。

实在不行也不要每次打开界面都去读取这么大的表,应该在首次打开的时候把这个表数据保存起来,下次打开时避免再次读取。

在lua中使用os.time的时区问题

最近做项目发现在Lua中使用os.time把服务器时间转换为时间戳时,如果修改小时的话会导致在U3D中打出的包如安卓、PC上时区和服务器时区对不上。

比如服务器时间是2020年3月22日12时30分,服务器使用的是中国北京时区, serverTime 传到客户端后,假如客户端时区调整为韩国首尔的时区,如果使用lua的 local a =os.date(“!*t”, serverTime) 转换为世界日期,然后再使用a.hour = 5,修改小时后,再使用 os.time(a) 转换为时间戳,此时的时间戳在安卓包和PC包上将不会是 世界时间的时间戳,而是本地时间的时间戳,因为你中间修改过小时数,但是在U3D编辑器中运行却是正常的。如果你中间没有修改过小时数,那么转换是正常的,这是一个很诡异的问题。

使用Lua实现字典树,进行敏感词匹配

我们在玩游戏的时候,聊天窗口经常会看到有“我**谁”等文字,其中的“**”可能是一个骂人的词语或者是政治敏感词汇,游戏审核部门会要求游戏必须屏蔽敏感词汇,否则不允许上线,所以骂人的词语或句子就被替换成*号了。

游戏项目中是如何处理这样的敏感字匹配的呢?KMP?还是Boyer-Moore算法?其实都不是,基本上这类匹配用的都是字典树的方式, 字典树又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种,它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。

它有3个基本性质:
1. 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
2. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
3. 每个节点的所有子节点包含的字符都不相同。

因为现在大多数项目都是使用Lua版本进行热更新,逻辑都写在lua上,所以使用Lua实现字典树是很有必要的。

核心代码如下:

local WordTree ={}

--树节点创建
function WordTree.CreateNode(self, char, flag, childs)
	local node = {}
	node.char = char or nil		--字符
	node.flag = flag or 0		--是否结束标志,0:继续,1:结尾
	node.childs = childs or {}	--保存子节点
	node.isleaf = true --childs数量为0则是叶子节点
	return node
end

//创建根结点
WordTree.m_RootNode = WordTree.CreateNode('root') 

//更新字典树
function WordTree.UpdateNodes(self, words)
	for i, v in pairs(words) do
		local chars = self:GetCharList(string.lower(v))
		if #chars > 0 then
			self:InsertNode(self.m_RootNode, chars, 1)
		end
	end
end

--插入节点
function WordTree.InsertNode(self, parent, chars, index)
	local node = self:FindNode(parent, chars[index])
	if node == nil then
		node = self:CreateNode(chars[index])
		parent.isleaf = false
		table.insert(parent.childs, node)
	end
	local len = #chars
	if index == len then
		node.flag = 1
	else
		index = index + 1
		if index <= len then
			self:InsertNode(node, chars, index)
		end
	end
end

--节点中查找子节点
function WordTree.FindNode(self, node, char)
	local childs = node.childs
	for i, child in ipairs(childs) do
		if child.char == string.lower(char) then 
			return child
		end
	end

end

function WordTree.GetCharList(self, str)
	local list = {}
	while str do
		local utf8 = string.byte(str,1)
		if utf8 == nil then
			break
		end
		--utf8字符1byte,中文3byte
		if utf8 > 127 then
			local tmp = string.sub(str,1,3)
			table.insert(list,tmp)
			str = string.sub(str,4)
		else
			local tmp = string.sub(str,1,1)
			table.insert(list,tmp)
			str = string.sub(str,2)
		end
	end
	return list
end

--将字符串中敏感字用*替换返回
-- flag == true,替换的 * 的数量 = 敏感词长度;flag == false,默认使用 *** 替换
function WordTree.ReplaceMaskWord(self, str, flag)
	local chars = self:GetCharList(str)
	local index = 1
	local node = self.m_RootNode
	local prenode = nil
	local matchs = {}
	local isReplace = false
	local lastMatchLen = nil
	local totalLen = #chars
	local function replace(chars, list, last, flag)
        local stars = ""
		for i=1, last do
			local v = list[i]
			if flag then
				chars[v] = "*"
				isReplace = true
			else
				if isReplace then
					chars[v] = ""
				else
					chars[v] = "***"
					isReplace = true
				end
			end
		end
	end
	while totalLen >= index do
		prenode = node
		node = self:FindNode(node, chars[index])
		if chars[index] == " " then
			if #matchs then
				table.insert(matchs, index)
				node = prenode
			else
				node = self.m_RootNode
			end
		elseif node == nil then
			index = index - #matchs
			if lastMatchLen then
				replace(chars, matchs, lastMatchLen, flag)
				index = index + (lastMatchLen - 1)
				lastMatchLen = nil
			else
				isReplace = false
			end
			node = self.m_RootNode
			matchs = {}
		elseif node.flag == 1 then
			table.insert(matchs, index)
			if node.isleaf or totalLen == index then
				replace(chars, matchs, #matchs, flag)
				lastMatchLen = nil
				matchs = {}
				node = self.m_RootNode
			else
				lastMatchLen = #matchs
			end
		else
			table.insert(matchs, index)
		end
		index = index + 1
	end
	local str = ''
	for i, v in ipairs(chars) do
		str = str..v
	end
	return str
end

--字符串中是否含有敏感字
function WordTree.IsContainMaskWord(self, str)
	local sCheck = string.gsub(str, " ", "")
	local chars = self:GetCharList(sCheck)
	local index = 1
	local node = self.m_RootNode
	local masks = {}
	while #chars >= index do
		node = self:FindNode(node, chars[index])
		if node == nil then
			index = index - #masks 
			node = self.m_RootNode
			masks = {}
		elseif node.flag == 1 then
			return true
		else
			table.insert(masks,index)
		end
		index = index + 1
	end
	return false
end


return WordTree

假如有一组屏蔽字为:

local data={“二货”,  “傻B”,   “二B”}

首先根据屏蔽字表创建字典树:WordTree.UpdateNodes(data);

然后就可以判断是否存在敏感字,如WordTree .IsContainMaskWord(“你是 二货 “)返回true。

快速排序C#实现

快速排序是7大排序算法中最高效的算法,它在C++中的STL、java sdk 、.Net中都有实现。

它的算法时间复杂度为O(nlogn),它的原理是通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。

具体C#代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Sort
{
    class Program
    {
       //快速排序
        static void quickSort(int[] arr, int low, int high)
        {
	        if (low < high) {
		        // 找寻基准数据的正确索引
		        int index = getIndex(arr, low, high);

		        // 进行迭代对index之前和之后的数组进行相同的操作使整个数组变成有序
		        quickSort(arr, 0, index - 1);
		        quickSort(arr, index + 1, high);
	        }
        }
       //选取枢轴,也是先选取当中的一个关键字,比如3,然后想办法把它放到一个位置,使它左边的值都比它小,右边的值都比它大,将这样的关键字称为枢轴。
       static int getIndex(int[] arr, int low, int high)
       {
	        // 基准数据
	        int tmp = arr[low];
	        while (low < high) {
		        // 当队尾的元素大于等于基准数据时,向前挪动high指针
		        while (low < high && arr[high] >= tmp) {
			        high--;
		        }
		        // 如果队尾元素小于tmp了,需要将其赋值给low
		        arr[low] = arr[high];
		        // 当队首元素小于等于tmp时,向前挪动low指针
		        while (low < high && arr[low] <= tmp) {
			        low++;
		        }
		        // 当队首元素大于tmp时,需要将其赋值给high
		        arr[high] = arr[low];

	        }
	        // 跳出循环时low和high相等,此时的low或high就是tmp的正确索引位置
	        // 由原理部分可以很清楚的知道low位置的值并不是tmp,所以需要将tmp赋值给arr[low]
	        arr[low] = tmp;
	        return low; // 返回tmp的正确位置
        }
        static void Main(string[] args)
        {
            int[] arr = { 1, 3, 2, 4, 0, 5, 6 };
            quickSort(arr,0,arr.Length-1);
            for (int i = 0; i < arr.Length; ++i) {
                Console.WriteLine(arr[i]);
            }
            Console.ReadKey();
        }
    }
}

以上代码在VS2017中运行通过。

XLua中数字签名的使用

数字签名是一种非对称加密算法,首先生成一对密钥,一个公钥,一个私钥。假如A要给B发送数据,那么A首先用自己生成的密钥对里的私钥(这个 私钥只有A自己知道)对数据进行加密(相当于对数据进行签名),然后发送给B,B收到数据后使用A的公钥验证签名的真实性。

使用数字签名主要有以下的作用:

(1)防冒充(伪造),因为A的私钥只有A自己知道,没有A私钥加密的数据用A的公钥验证必然不通过。

(2)可鉴别身份,如果是经过A签名的数据,那么一定可以用A的公钥验证。

(3)防篡改(防破坏信息的完整性),如果数据修改了,用A的公钥验证不通过。

(4)防重放,如果数据有系列号那么可以防止重新发送数据。

(5)防抵赖。可以防止A不承认自己的数据,因为A的公钥验证了A的数据。

(6)机密性(保密性)。 没有A的公钥无法查看数据。

在XLua中用Tools/KeyPairsGen.exe生成公私钥对,key_ras文件保存的是私钥,key_ras.pub保存的是公钥。

用Tools/FilesSignature.exe对源代码进行签名。

签名后使用AddLoader方式加载签名后的lua文件:

  luaenv.AddLoader(new SignatureLoader("公钥", (ref string filepath) =>
            {
                filepath = Application.dataPath + "test.lua";
               
                if (File.Exists(filepath))
                {
                    return File.ReadAllBytes(filepath);
                }
                else
                {
                    return null;
                }
            }));

在SignatureLoader函数中传入生成的公钥和自己定义CustomLoader即可对签名后lua文件进行验证,SignatureLoader 函数中是具体的验证方式。

lua源码文件功能分析

从官网下载到lua5.2的源代码后 ,展开压缩包,源代码文件全部放在 src子目录下。这些文件根据功能的不同,可以分为大模块。

第一部分: 虚拟机运转的核心功能
lapi.c C语言接口
lctype.c C标准库中 ctype 相关实现
ldebug.c Debug 接口
ldo.c 函数调用以及栈管理
lfunc.c 函数原型及闭包管理
lgc.c 垃圾回收
lmem.c 内存管理接口
lobject.c 对象操作的一些函数
lopcodes.c 虚拟机的字节码定义
lstate.c 全局状态机 lstring.c 字符串池
ltable.c 表类型的相关操作
ltm.c 元方法
lvm.c 虚拟机
lzio.c 输入流接口

第二部分:源代码解析以及预编译字节码 lcode.c 代码生成器
ldump.c 序列化预编译的 Lua字节码
llex.c 词法分析器
lparser.c 解析器
lundump.c 还原预编译的字节码

第三部分:内嵌库 lauxlib.c 库编写用到的辅助函数库
lbaselib.c 基础库
lbitlib.c 位操作库
lcorolib.c 协程库
ldblib.c Debug 库
linit.c 内嵌库的初始化
liolib.c IO 库
lmathlib.c 数学库
loadlib.c 动态扩展库管理
loslib.c OS库
lstrlib.c 字符串库
ltablib.c 表处理库

第四部分:可执行的解析器,字节码编译器 lua.c 解释器
luac.c 字节码编译器

关于XLua与C#之间的通信分析

分析了一下XLua与C#之间的通信方式,发现和SLua,Ulua的区别不是很大。

Lua调用C#:

都是需要先生成一个个wrap文件,C#才能被lua调用。

wrap文件相当于一个接口,Lua先调用 wrap文件 然后 wrap 再调用C#,在 wrap 文件里面实际上是把C#的类函数,字段压入到lua虚拟机的虚拟栈上,再由lua虚拟机出栈后给lua调用的。

当索引系统API、dll库或者第三方库时,无法将代码的具体实现进行代码生成,采用C#的反射方式实现交互,缺点是执行效率低。

也就是说Lua调用C#其实就是:lua->wrap->C#

那么在XLua中C#又是如何调用Lua的呢?

看源码很容易知道,其实是使用如下函数:

 LuaEnv luaenv = new LuaEnv();//创建Lua虚拟机
 luaenv.DoString("CS.UnityEngine.Debug.Log('hello world')");执行lua代码

根据XLua的文档,DoString可以直接执行字符串代码,也可以加载lua文件执行,

分析源码发现DoString其实是调用的外部DLL中的xluaL_loadbuffer函数,如下 DoString 定义:

public object[] DoString(byte[] chunk, string chunkName = "chunk", LuaTable env = null)
        {
#if THREAD_SAFE || HOTFIX_ENABLE
            lock (luaEnvLock)
            {
#endif
                var _L = L;
                int oldTop = LuaAPI.lua_gettop(_L);
                int errFunc = LuaAPI.load_error_func(_L, errorFuncRef);
                if (LuaAPI.xluaL_loadbuffer(_L, chunk, chunk.Length, chunkName) == 0)
                {
                    if (env != null)
                    {
                        env.push(_L);
                        LuaAPI.lua_setfenv(_L, -2);
                    }

                    if (LuaAPI.lua_pcall(_L, 0, -1, errFunc) == 0)
                    {
                        LuaAPI.lua_remove(_L, errFunc);
                        return translator.popValues(_L, oldTop);
                    }
                    else
                        ThrowExceptionFromError(oldTop);
                }
                else
                    ThrowExceptionFromError(oldTop);

                return null;
#if THREAD_SAFE || HOTFIX_ENABLE
            }

而 xluaL_loadbuffer 外部引入声明如下:

 [DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)]
 public static extern int xluaL_loadbuffer(IntPtr L, byte[] buff, int size, string name);

也就说xluaL_loadbuffer的函数实现并不在源文件中,而是在外部DLL文件中实现的。

继续查找发现它其实是xlua.dll里面的函数,在 xlua.dll 源码xlua.c文件中发现如下定义:

LUALIB_API int xluaL_loadbuffer (lua_State *L, const char *buff, int size,
                                const char *name) {
	return luaL_loadbuffer(L, buff, size, name);
}

根据文件后缀,其实它就是C代码,而且调用的luaL_loadbuffer其实就是lua源码里面的函数。Xlua只是把它封装了一下而已。

继续查找Lua源码,分析Lua.5.3.3的源码发现xluaL_loadbuffer 其实是调用了 lua_load 函数,而 lua_load 实现如下:

LUA_API int lua_load (lua_State *L, lua_Reader reader, void *data,
                      const char *chunkname, const char *mode) {
  ZIO z;
  int status;
  lua_lock(L);
  if (!chunkname) chunkname = "?";
  luaZ_init(L, &z, reader, data);
  status = luaD_protectedparser(L, &z, chunkname, mode);
  if (status == LUA_OK) {  /* no errors? */
    LClosure *f = clLvalue(L->top - 1);  /* get newly created function */
    if (f->nupvalues >= 1) {  /* does it have an upvalue? */
      /* get global table from registry */
      Table *reg = hvalue(&G(L)->l_registry);
      const TValue *gt = luaH_getint(reg, LUA_RIDX_GLOBALS);
      /* set global table as 1st upvalue of 'f' (may be LUA_ENV) */
      setobj(L, f->upvals[0]->v, gt);
      luaC_upvalbarrier(L, f->upvals[0]);
    }
  }
  lua_unlock(L);
  return status;
}

上述代码中调用了luaD_protectedparser来进行parse过程, 在luaD_protectedparser中又调用了f_parser ,在f_parser中根据一些选择来分别处理不同的情况,这就是lua的词法语法语义分析过程。

从上述分析发现,其实C#调用lua,就是C#先调用C代码,然后C调用lua的过程,因为Lua的源码是C写的,lua的代码需要Lua虚拟机解释执行,也就是需要C代码来解析执行。