Unity tilemap产生缝隙

  • 贴片图要打包成图集
  • 图集的padding要足够大。(我最开始选择2,改成8后缝隙消失)
    • 老外的解释:The tearing comes from oversampling a tile by a tiny bit (i.e. 0.0001), and into an adjacent tile’s content. Padding repeats the border pixels of a tile to ensure the effects of oversampling can’t be seen, even though it’s still happening.

unity ui 性能调优

原文

  • Canvas

Canvas职责: 负责把子元素的几何体合理的批处理,并生成渲染指令,发送到Unity的图形系统。这个过程用c++实现,叫做批处理重建(rebatch)或者批处理构建(batch build)。如果Canvas包含需要rebatch的元素,可以把这个Canvas标记为Dirty。
Rebuild包含: Layout重建和Graphics重建两个步骤,也就是说如果元素的集合体变化(大小、位置)变化、以及图形、材质变化,会分别触发两个两个步骤。

  1. PreLayout
    Update data needed for layout calculations
  2. Layout
    Used by auto-layout system
  3. PostLayout
    Update data dependent on layout
  4. PreRender
    Update vertices and materials
  5. LatePreRender
    Update vertices dependent on generated text character data

Sub-Canvas: 作为Canvas子节点的Canvas称为子画板,子画板和父画板是隔离的:子画板rebatch,要求父画板同时rebatch,反之亦然。不过有部分边界情况会违反这个原则,比如修改Canvas属性。

  • batch原则:

** 两个元素(无论是否重叠),拥有有相同material和texture(同一图集),可以batch。

  • 优化方法
    • 所有UI尽量采用同一Material。
    • 减少Texuture变化,使用图集。
      • 注意:UIText文本时包含在Texture中的,可以被batch(可以观察FrameDebug看到)。(可以猜想,具有图集的TextmeshPro也一定可以batch。)
    • 切换Material的开销远远高于切换图集。切换Material时,内部渲染状态需要重建,而切换图集仅仅是增加一个drawcall,所以尽量使用相同Material。
    • 尽量不要动态修改CanvasRender属性。修改CanvasRender属性,会导致rebuild。
    • 避免修改UI对象的层次关系、兄弟节点的次序:需要重新计算深度关系,导致rebuild。
    • 避免使用PixelPerfect:每次移动Rect时都要计算自己以及子节点,把像素填充到顶点边缘,非常慢。
      • 可以静止时开启PixelPerfect,移动时关闭。
      • PixelPerfect只在WorldSpace模式有用。
    • 拆分Canvas
      Problem: 当Canvas中一个或多个元素变化后,整个Canvas标记为Dirty。需要重新分析整个Canvas:如何优化、如何batch,这个过程非常消耗cpu,很有可能降低帧数。
      Solution:拆分Canvas。每个Canvas就是一个独立的Drawcall,用的过多也会降低FPS,需要自己权衡。
      • 把静态UI和动态UI拆分到不同Canvas,
      • 把关联更新的UI放到一起
      • 也可以考虑把相同图集的元素放到一起,比如UIText:可以把所有HudText放到一个Canvas,把所有血条放到一个Canvas,避免他们因为叠加,而不能batch。
  • fq
    1. UIText重复设置相同Text的时候会触发rebuild吗?
      不会,源码为证,我们要相信unity开发人员,同样,transform设置相同pos的时候也不会。
//setter
                if (m_Text != value)
                {
                    m_Text = value;
                    SetVerticesDirty();
                    SetLayoutDirty();
                }

C#中Exception的性能开销

  • try catch finally 语句本身在编译后,只不过是条件比较、流程跳转,不存在多少额外开销。
  • 在异常发生、或者throw new xxx,只要实际抛出了异常,就会产生不小的开销,因为这时要获取调用堆栈,估计会设计大量的反射API调用,这个开销很高。
  • 所以,如果系统中没有大量发生异常(要注意异常被try catch吃掉、被隐藏的情形,只要出现了异常,开销就逃不掉),就不会对系统运行产生影响。

.net core部署文件服务

点击这里查看原文

  1. 作为普通的http服务,除了处理动态请求以外,静态文件服务基本也是必备的,因为js文件、图片、普通文件下载,都需要用到文件服务。
  2. .net core asp.net 2.0 自带了这个服务,这就是静态文件服务。官方自带了一个教程:点我查看
  3. 基本就是一个简单的配置:app.UseStaticFiles();
    4.同时这个方法也可以接受一个参数,用于配置此服务。比如路径映射等。详情翻阅官方文档吧。

c#反射、expression、lambda调用函数效率对比

点击这里查看原文

  1. 输出结果:
  • 执行环境:windows开发机,release模式
  • 时间单位:毫秒,越小,速度越快。
  • rps是每秒调用次数
direct   ret : 10000000,         time : 8,       RPS : 1250000000
lamd     ret : 10000000,         time : 31,      RPS : 322580645
expr     ret : 10000000,         time : 492,     RPS : 20325203
refl     ret : 10000000,         time : 3758,    RPS : 2660989
  1. 输出解释:
  • 直接调用函数,速度非常快。可能编译器做了优化。
  • 用lambda包装,调用函数的速度也很快。
  • 用编译并缓存后的expression,速度会慢很多,是正常情况下的5-10%不到
  • 用反射的方式调用方法,速度最慢,调用瓶颈就是在invoke本身,和创建的那个参数数组基本没关系。
  1. 结论
  • 如果追求极致效率,应该直接调用,或者用生成代码的方式批量生成调用封装代码。
  • 如果整个服务器每秒调用次数在几十万,可以用缓存的 Compiled Expression,浪费的开销基本可以忽略。
  • 反射Invoke去调用函数,效率非常低,根据以上测试看,单线程260万次/秒,如果自己代码每秒使用几万次,也是可以的。如果是做UI开发,可以放心使用,瓶颈一般不会在这里。游戏开发需要慎重考虑,如果整体调用次数很高,可能会导致掉帧。
  • 以上数据是单核情况下,服务器都有多少个核心N,以上数据可以乘以N。
  1. 测试代码:
    对expression代码不熟悉,部分代码抄自网络。
using commcs;
using commcs.wsrpc.msg;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;

namespace TestMain.TestReflectionPerformance
{
    public class ClientMsgController
    {
        public int handle1(int a, int b)
        {
            return a + b;
        }
    }
    public class TestReflectionPerformance
    {
        private const int N = 10000000;
        public static void Test()
        {
            testDirectCall();
            testLambdaCall();
            testDelegate();
            testReflect();
        }

        static void print(string label,int ret,long startTime, long endTime)
        {
            double rps =  N *1.0 / (endTime - startTime) * 1000;
            Console.WriteLine("{0} \t ret : {1},\t time : {2},\t RPS : {3}", label, ret, endTime - startTime, (long)rps);
        }

        private static void testDirectCall()
        {
            var obj = new ClientMsgController();
            var n = 0;
            long startTime = MyTime.Now();
            Func<ClientMsgController, int, int, int> lamd = (t, a, b) => t.handle1(a, b);
            for (int i = 0; i < N; i++)
            {
                n = obj.handle1(n, 1);
            }
            long endTime = MyTime.Now();
            print("direct", n, startTime, endTime);
        }

        private static void testLambdaCall()
        {
            var obj = new ClientMsgController();
            var n = 0;
            long startTime = MyTime.Now();
            Func<ClientMsgController,int,int,int> lamd = (t, a,  b) =>  t.handle1(a, b);
            for (int i = 0; i < N; i++)
            {
                n = lamd(obj,n, 1);
            }
            long endTime = MyTime.Now();
            print("lamd", n, startTime, endTime);
        }

        private static void testDelegate()
        {
            var obj = new ClientMsgController();
            var method = obj.GetType().GetMethod("handle1");
            var n = 0;
            long startTime = MyTime.Now();
            var mcall = CreateDelegate(method);

            for (int i = 0; i < N; i++)
            {
                n = (int)mcall.Invoke(obj, n, 1);
            }
            long endTime = MyTime.Now();
            print("expr", n, startTime, endTime);            
        }

        private static void testReflect()
        {
            var obj = new ClientMsgController();
            var method = obj.GetType().GetMethod("handle1");
            var n = 0;
            long startTime = MyTime.Now();
            var mcall = CreateDelegate(method);

            for (int i = 0; i < N; i++)
            {
                n = (int)method.Invoke(obj, new object[] { n, 1 });
            }
            long endTime = MyTime.Now();
            print("refl", n, startTime, endTime);
        }

        public delegate object MethodInvoker(object instance, params object[] parameters);


        public static MethodInvoker CreateDelegate(MethodInfo method)
        {
            //ExceptionHelper.CheckArgumentNull(method, "method");
            //if (method.IsGenericMethodDefinition)
            //{
            //    // 不对开放的泛型方法执行绑定。
            //    throw ExceptionHelper.BindTargetMethod("method");
            //}
            // 要执行方法的实例。
            ParameterExpression instanceParam = Expression.Parameter(typeof(object));
            // 方法的参数。
            ParameterExpression parametersParam = Expression.Parameter(typeof(object[]));
            // 构造参数列表。
            ParameterInfo[] methodParams = method.GetParameters();
            Expression[] paramExps = new Expression[methodParams.Length];
            for (int i = 0; i < methodParams.Length; i++)
            {
                paramExps[i] = Expression.Convert(
                    Expression.ArrayIndex(parametersParam, Expression.Constant(i)),
                    methodParams[i].ParameterType);
            }
            // 静态方法不需要实例,实例方法需要 (TInstance)instance
            Expression instanceCast = method.IsStatic ? null :
                Expression.Convert(instanceParam, method.DeclaringType);
            // 调用方法。
            Expression methodCall = Expression.Call(instanceCast, method, paramExps);
            // 添加参数数量检测。
            //methodCall = Expression.Block(GetCheckParameterExp(parametersParam, methodParams.Length), methodCall);
            // return Expression.Lambda<MethodInvoker>(GetReturn(methodCall, typeof(object)),
            //    instanceParam, parametersParam).Compile();
            return Expression.Lambda<MethodInvoker>(Expression.Convert(methodCall, typeof(object)), instanceParam, parametersParam).Compile();
        }
    }

}

用photoshop批量缩放图片

点击这里查看原文

  • 用ps批处理的好处:
    • ps图像处理是非常优秀的,非普通处理程序能比。比如减小图片尺寸,在相同文件大小时,ps出来的图像比ImageMagick处理的更清晰。
    • 节约时间。很多时候,都是几百张图片要经过统一处理,输出为统一的尺寸或者格式。如果全部手工操作很容易出错,而且很浪费时间。
  • 步骤:
    • 安装ps
    • 安装 Adobe ExtentScript Tookit
    • 编写脚本
    • 点击运行,就会根据脚本内容,自动打开ps,处理图像,并保存,关闭。
  • 这里的脚本是对目录内的所有文件夹中的图像进行缩放,保存到目标文件夹,同时保留子文件夹结构。
//for photoshop 2018,只能在ps2018运行,注意Tookit编辑器里选择脚本版本。

//前几行是配置
var image_folder = Folder("E:/illustration_12");
var dest_folder = Folder ("E:/tmp/illustration_12");
var max_with = 320
var min_width=320 //如果设置为0,表示不做限制
var max_height = 0;
var min_height = 0;

function processFile(f) {
	if (f instanceof Folder) {
		var files = f.getFiles();
		for (var i = 0; i < files.length; i++) {
			processFile(files[i])
		}
	} else if (f instanceof File) {
		if (isPic(f.name)) {
			var dest = f.path.replace(image_folder.fullName, dest_folder.fullName)
            //$.writeln(dest)
			new Folder(dest).create()
			dest = dest + "/" + f.name
			if (!new File(dest).exists) {
				resizePic(f, dest, max_with)
			}
		}
	}
}

//保持比例缩放
function resizePic(file, destpath, maxw) {
	try {
		openlayer = open(file);
	}
	catch (err) {
		$.writeln(err);
		return;
	}
	// get a reference to the current (active) document and store it in a variable named "doc"
	doc = app.activeDocument;
	doc.trim()
	// $.writeln (doc.width.as ("px"))
	var px_w = doc.width.as("px") 
	var px_h = doc.height.as("px");
	var scale = limitWidthHeight(px_w,px_h);
	w = px_w * scale;
	h = px_h * scale;
	//h = (w / doc.width.as("px")) * doc.height.as("px")
	$.writeln(w)
	$.writeln(h)
	// change the color mode to RGB.  Important for resizing GIFs with indexed colors, to get better results
	doc.changeMode(ChangeMode.RGB);
	// do the resizing.  if height > width (portrait-mode) resize based on height.  otherwise, resize based on width
	if (doc.height > doc.width) {
		doc.resizeImage(null, UnitValue(h, "px"), null, ResampleMethod.BICUBIC);
	}
	else {
		doc.resizeImage(UnitValue(w, "px"), null, null, ResampleMethod.BICUBIC);
	}
	// Makes the default background white
	//var white = new SolidColor();
	// white.rgb.hexValue = "FFFFFF";
	// app.backgroundColor = white;

	// Convert the canvas size as informed above for the END RESULT
	app.activeDocument.resizeCanvas(UnitValue(w, "px"), UnitValue(h, "px"));

	// our web export options
	var options = new ExportOptionsSaveForWeb();
	//options.quality = 19 //基本清楚,最低标准,给移动网站使用
	//options.format = SaveDocumentType.JPEG;
	options.quality = 60	//PS默认值 
	options.format = SaveDocumentType.PNG;
	options.optimized = true;

	//var newName = 'web-'+doc.name+'.jpg';

	doc.exportDocument(File(destpath), ExportType.SAVEFORWEB, options);
	openlayer.close(SaveOptions.DONOTSAVECHANGES)
}


function getFileExt(filePath) {
	//获取最后一个.的位置
	var index = filePath.lastIndexOf(".");
	//获取后缀
	var ext = filePath.substr(index + 1);
	return ext
}

function isPic(fileName) {
	var ext = getFileExt(fileName)
	if ("jpg".toUpperCase() == ext.toUpperCase() ||
		"jpeg".toUpperCase() == ext.toUpperCase() ||
		"png".toUpperCase() == ext.toUpperCase()) {
		return true
	}
	return false
}

//返回限制以后的长、宽值
function limitInRange(px_w,min,max) {
	var ret = px_w;
	if(max>0) {
		if(ret>max) {
			ret = max
		}
	}
	if(min>0) {
		if(ret<min) {
			ret = min;
		}
	}
	return ret;
}

//返回缩放比例
function limitWidthHeight(px_w,px_h) {
	var w = limitInRange(px_w,min_width,max_with);
	var h = limitInRange(px_h,min_height,max_height);

	var scale_w = w/px_w;
	var scale_h = h/px_h;
	if(scale_w==1) {
		return scale_h; 
	}
	if(scale_h==1) {
		return scale_w;
	}

	return Math.min(scale_w,scale_h);
}

//这里是正式调用处理函数
processFile(image_folder)
$.writeln("处理结束");

unity中如何为精灵动画(序列帧动画)设置偏移

  • 为何要设置偏移?
    如果,所使用的帧图片都是统一大小,坐标都统一,是没必要设置偏移的。
    而在我的开发中,遇到了不同动作的序列帧大小不一致,坐标不统一的情况。起始,我才用的正常的开发方式,建立了动画,但是在预览动画的时候发现了问题:角色在做idle动作时,坐标与arvatar图片统一,但是在播放攻击动作时,角色突然向下移动了0.5个单位,这给人的感觉很突兀。我比对了一下两个动作的图片,发现他们每帧的尺寸不一致,从中心点看,攻击动作的角色整体向下偏移。如何让这两个动作的坐标统一起来,让他们看起来更协调??
  • 我的尝试
    首先,在SpriteEditor中,我把每帧图的pivot从中心挪到了底部中心(bottom),这样呢,当前的问题解决了,可是在做后面的动画时,又遇到了新的问题,坐标又不统一了。
  • 终极方案
    我首先在google上搜索了半天,没有相关现成的解决办法。可能是做2d的人比较少,而且一般美术出图的时候,都让每帧的坐标统一了,别人没遇到这样的问题。于是我就开始了摸索与思考,经过一天的时间,我选择了手动调整pivot方式,彻底解决了这个问题:把每帧的pivot放在每帧角色的“落脚点”上,一般来说,一个动作的序列帧的坐标还是一致的,这样,只要测量出来一个pivot,然后用slice填充过去就行了。

流行序列化方式效率对比

这里找到了一个开源项目,他包含了较新的序列化方式,应该是最近更新的。

eishay/jvm-serializers

Benchmark comparing serialization libraries on the JVM – eishay/jvm-serializers
可以从这里查找一个适合自己需求的。

  • colfer的性能十分抢眼
  • kryo,protostuff和dsl-json的性能和易用性都不错。
  • ZeroFormatter 这个也不错,不过这个只适用于c#,没有其他语言的实现。