关于244128133

Unity3D游戏程序员

关于Unity游戏在安卓SDK弹出时从后台返回黑屏问题

特色

今天发行那边反馈过来一个问题,是之前我们都没发现过的,当在安卓SDK登录界面的时候切换到后台或者切换其他到其他应用,再次返回游戏的时候,发现SDK登录界面后面的游戏界面黑屏了。

我的初步判断是,游戏的View应该是从后台返回到前台的时候还没被激活,因为只要点击一下SDK的登录,游戏界面就显示出来了。

查询资料发现确实是这个原因。


一、当Unity所在的Activity之上没有其他Activity时候,生命周期的变化如下:

启动App
D/MainActivity: onCreate
D/MainActivity: onStart
D/MainActivity: onResume
D/MainActivity: onWindowFocusChanged true

点Home进入后台
D/MainActivity: onPause
D/MainActivity: onWindowFocusChanged false
D/MainActivity: onStop

从后台回来
D/MainActivity: onRestart
D/MainActivity: onStart
D/MainActivity: onResume
D/MainActivity: onWindowFocusChanged true

退出应用
D/MainActivity: onWindowFocusChanged false
D/MainActivity: onPause
D/MainActivity: onStop
D/MainActivity: onDestroy

二、当Unity所在的Activity之上有其他Activity时候,生命周期的变化如下

弹窗显示另外一个Activity
D/MainActivity: onPause
D/MainActivity: onWindowFocusChanged false

点Home进入后台
D/MainActivity: onStop

从后台回来
D/MainActivity: onRestart
D/MainActivity: onStart

关闭 弹窗Activity
D/MainActivity: onResume
D/MainActivity: onWindowFocusChanged true

三、对比两次的日志打印,我们可以发现:第二次从后台返回少了两个生命周期的调用

D/MainActivity: onResume
D/MainActivity: onWindowFocusChanged true

综上分析所得,要想做到从后台返回不黑屏,只需要在SDK的主Activity的 onStart里面调用onResume和onWindowFocusChanged方法即可。

public void onStart() {
    super.onStart();
    this.mUnityPlayer.resume();
    onWindowFocusChanged(true);
}

这样当Unity所在的Activity之上有其他Activity时候,即使切换到后台再返回游戏,也不会黑屏。

关于Particle System is trying to spawn on a mesh with zero surface area的警告

今天测试反馈了个刷屏的警告”Particle System is trying to spawn on a mesh with zero surface area“根据翻译判断是使用了体积为0的mesh,但是当我在网上查找资料的时候,网上说的是因为 ParticleSystem 中 ShapeModule 要求引用的 Mesh 必须开启 Read/Write 选项.

经过我的实验和调试发现并不是这么回事,简单的就是因为某个特效的shape类型选择了mesh然而没有指定一个mesh给他,而且选择的Mode为Edge,如下:

满足以上几个箭头所指的条件后就会不断刷如下警告:

要想去掉这些警告,可以改变shape的类型或者指定一个mesh给它,也可以改变Mode的类型为Vertex都可以去掉这些烦人的警告,具体方法需要根据自己的特效情况设置.

如果需要查找项目中有问题的特效可以使用以下编辑器脚本:

using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;

public static class MeshToolForParticle
{
    [MenuItem("Tools/ParticleSystem/FindScenes", false, 20)]
    public static void FindScenes()
    {
        var guids = AssetDatabase.FindAssets("t:Scene");

        foreach (var guid in guids)
        {
            string path = AssetDatabase.GUIDToAssetPath(guid);
            var scene = EditorSceneManager.OpenScene(path, OpenSceneMode.Single);
            var roots = scene.GetRootGameObjects();
            bool found = false;
            foreach (var go in roots)
            {
                found |= Find(go);
            }

            if (found)
            {
                Debug.Log(path);
            }
        }
    }

    [MenuItem("Tools/ParticleSystem/FindPrefabs", false, 20)]
    public static void FindPrefabs()
    {
        var guids = AssetDatabase.FindAssets("t:Prefab");

        foreach (var guid in guids)
        {
            string path = AssetDatabase.GUIDToAssetPath(guid);
            var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
            Find(prefab);
        }
    }

    private static bool Find(GameObject go)
    {
        bool found = false;
        var particles = go.GetComponentsInChildren<ParticleSystem>(true);
        foreach (var particle in particles)
        {
            if (particle.shape.enabled && particle.shape.shapeType == ParticleSystemShapeType.Mesh &&
                particle.shape.mesh == null)
            {
                string prefabPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(particle);
                Debug.LogWarning(string.Format("Path: {0}\nPrefab: {1}", particle.gameObject.GetGameObjectPath(), prefabPath));
                found = true;
            }
        }

        return found;
    }
}

把上面的脚本放到项目的Editor目录下然后点击菜单Tools/ParticleSystem/FindPrefabs,即可打印出有问题的特效名称.

Phong和Blinn-Phone光照模型的区别

Lambert光照模型虽然能模拟出粗糙物体表面的光照效果,但是在真实环境中很多光滑的物体它却不能很好的表现出来。

所以在1975年裴祥风提出了一种局部光照的经验模型——Phong光照模型

公式:Color = Ambient + Diffuse + Specular

其中, Specular 为镜面反射,C(specular) = C(light)*M(specular)saturate(v*r)^M(shininess)

v是视角方向,r是光线的反射方向, M(shininess) 为物体材质的光泽度。

后来在1977年 Jim Blinn对Phong光照模型算法进行了改进,提出了Blinn-Phong光照模型,和Phong光照模型相比 Blinn-Phong 只改进了镜面反射的算法。

镜面反射公式:C(specluar)= C(light)*M(specular)saturate(n*h)^M(shininess)

其中h=normalize(v+l),v是视角方向,l是光照方向,n是法线方向。

虽然这一个小小的改进并没有让表现效果在视觉上有很大的提升,但是在性能方面却有很大的提升,因为h是取决于视角方向以及灯光方向的,两者很远的时候h可以认为是常量,和位置及表面曲率没有关系,所以可以减少计算量。

而Phong要根据表面曲率去逐顶点或逐像素计算反射向量r,所以计算量比较大。

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中运行通过。