Unity开发心路历程——制作画板
有人说
编程是份很无聊的工作
因为整个工作时间面对的都是电脑这种机器
因为眼睛盯着的内容都是索然无味的代码
因为总是会有意想不到的bug让你怀疑自己的智商
而我认为
编程是件及其有意思的事情
可观的收入,说起来或许太俗气,当然不止这个
Unity游戏开发 让我从校园里上个世纪的知识,直接过渡到一年以内的技术
半年的实际开发锻炼的逻辑思维,远远强过大学数学专业学习三年所锻炼的思维
当电脑按照自己写出的代码做出了自己预期的事情,带有控制欲的满足感绝对刺激
然而,最让我追随的
确是编程过程中苦苦思索不得结果,却忽然间因为小思路柳暗花明的一刻
一个月之前,公司项目组正式进入了新项目的开发阶段。这也是我人生中第一次开发商业项目,也就是第一次靠自己的技术吃饭。
我分到的任务,看似不多,只有三个小功能,但仔细规划一下,每一个小功能都相当于一个独立的小程序,并且要集成到项目中完成交互,工作量还是相当大的。当然,项目涉及许多我之前从未使用过的技术,例如NGUI、UGUI的Dropdown、WWW上传WWWForm下载、Http通信、Unity协程、Unity与window文件交互等等等等,还有好多未知的后期开发会遇到的技术在后面等着我。
这篇文章,我将讲述一下在Unity中开发画板功能的历程。2017年1月5日,在马上要超出规定的时间之前,终于完成了画板功能的制作,并成功修复了Bug。所以兴奋之余写下这篇文章并分享开发的历程。
开发的开始,我给自己规定了14天的时间,2017年1月6日到期。因为公司之前的项目有画板这个功能,我以为这个任务可以快准狠的结束。可是,开发起来才发现,我以为的并不是我以为的,自己真的是没经验图样。
仔细看了下之前项目的源码,画板功能是用NGUI实现的,而且使用的NGUI版本也追溯到3年甚至之前。游戏开发这种东西,3年的技术早就被人们遗忘的无影无踪,查资料都没地方查。我当初学GUI,了解到NGUI被淘汰后,根本连看都没看一眼,而是直接投入了Unity亲生儿子UGUI的怀抱。现在的项目的确实要求用UGUI开发,可是,既然想借鉴,不知己知彼,怎能开发完成。
能怎么办呢,先往下扒吧,能扒多少扒多少。然而越copy越心凉,NGUI的API零零碎碎的贯穿整个脚本(这里只讲述画笔这个小功能,画板的其他功能不做介绍)。第一反应是能不能理解这些API,然后用UGUI中的API换掉。然而,这个想法很快就被否决了,要是能够这么容易理解,那还用得着封装成API嘛。
没办法,时间再紧,也得去学一下NGUI了。不过欣喜的是,学NGUI并没有碰上什么硬骨头,因为UGUI的研发团队中有NGUI的人,也没有花费很长时间。总体来说,两者很多API都是相似的,而且UGUI的实现会比NGUI简单很多,并且看不见的性能消耗也降低了很多。这就是UGUI亲儿子一出生,NGUI立马没有饭吃的原因。
好啦,接下来继续扒代码。其中有一大片使用了NGUI API的代码,但VS IDE的显示是0个引用,既然0个引用,直接不管它们不就好了。边为自己的小心思庆幸,边泛着嘀咕。果然,脚趾头想想也知道,失败了。
查了一下这两个函数OnPress、OnDrag,确实是NGUI的事件系统。于是找到UGUI的事件系统,有没有与之相对应的呢。想象之中,一致的并没有,初步确认了相似的三个事件接口,OnBeginDrag,OnDrag,OnEndDrag。
//原核心代码
private void OnPress(bool isPressed)
{
if (isPressed)
{
RaycastHit hit = UICamera.lastHit;
if (null == hit.collider || !hit.collider.CompareTag("Blackboard"))
{
Debug.Log("Warning : !!!!!!");
return;
} Vector3 pos = hit.point; Bounds bounds = hit.collider.bounds;
Vector3 min = bounds.min;
Vector3 max = bounds.max;
Vector3 size = bounds.size; Vector2 uv = Vector2.zero;
uv.x = (pos.x - min.x) / size.x;
uv.y = (max.y - pos.y) / size.y; MoveTo(uv);
LineTo(uv);
Refresh(); int touceid = UICamera.currentTouchID;
if (!mTouces.ContainsKey(touceid))
{
mTouces.Add(touceid, uv);
}
else
{
mTouces[touceid] = uv;
}
}
else
{
int touceid = UICamera.currentTouchID;
if (mTouces.ContainsKey(touceid))
{
mTouces.Remove(touceid);
}
}
} private void OnDrag(Vector2 delta)
{
UICamera.currentTouch.clickNotification = UICamera.ClickNotification.None;
int touceid = UICamera.currentTouchID;
if (!mTouces.ContainsKey(touceid))
{
return;
} RaycastHit hit = UICamera.lastHit;
if (null == hit.collider || !hit.collider.CompareTag("Blackboard"))
{
return;
} Bounds bounds = hit.collider.bounds;
Vector3 min = bounds.min;
Vector3 max = bounds.max;
Vector3 size = bounds.size; Vector2 uv = Vector2.zero;
uv.x = (hit.point.x - min.x) / size.x;
uv.y = (max.y - hit.point.y) / size.y; Vector2 from = mTouces[touceid];
mTouces[touceid] = uv; MoveTo(from);
LineTo(uv);
Refresh();
}
//成功完成的代码
public void OnBeginDrag(PointerEventData eventData)
{
if (null == eventData || !gameObject.CompareTag("Blackboard"))
{
return;
} if (!isChanged)
{
isChanged = true;
buttonEffect.NewDraw();
} Vector2 pos = eventData.position; Vector2 uv = Vector2.zero;
uv.x = pos.x / CanvasWidth;
uv.y = 1 - pos.y / CanvasHeight; MoveTo(uv);
} public void OnDrag(PointerEventData eventdata)
{
Vector2 pos = eventdata.position; Vector2 uv = Vector2.zero;
uv.x = pos.x / CanvasWidth;
uv.y = 1 - pos.y / CanvasHeight; LineTo(uv);
MoveTo(uv);
Refresh();
} public void OnEndDrag(PointerEventData eventdata)
{
mCurrentPos = Vector2.zero;
}
放上两端核心代码。要用三个事件接口去代替两个事件接口,当时的表情确实是懵逼的。而且这种懵逼是在不确定是否能用这三个接口替代的情况下。
其实对于有源代码可以参考的功能,要自己读懂源代码,内心是拒绝的,因为想参考源代码,不就是为了省力吗,要去读懂再做修改,那就基本不省力了。可是,毕竟没有其他选择,接下来的十天左右,就是对源代码一点儿都不了解到了解每一行并能做出改动的过程。
首先,源代码用了UICamera.lastHit这个API,查了一下,这个API与下面四行的代码就是碰撞检测,并且用碰到的标签是否是画板来决定画笔功能是否执行,若碰到的不是画板就直接return跳出函数。
于是,我更换了UGUI的射线检测,并用Raycast2D来进行尝试。啊哈,这不应该失败的。我又找到了Unity官方API提供的示例代码,建了一个全新的工程,居然不能用。这是让我很摸不着头脑的一件事。在QQ群问了一下,一位大神说,Raycast2D中的2D并不能天真的认为是UI。既然这样,这个方法只能放弃了。
放弃了这个方法,然而我并没有其他的方法可用。这种情况在开发过程中太正常了,不过,我有我的解决方法。我不断的在网上搜索着,百度谷歌、CSDN、StackOverflow。并不断的更换着词条,因为我不知道这个功能应该用哪个词条准确的描述,也不知道解决方法的文章是否用了这个词条。就这样翻箱倒柜式的翻找着。看似毫无目的,但确实是在收集信息。因为很可能一行代码就会带给你灵感,而且这种情况我遇到了不止一次。
果然,OnDrag接口的传入参数(PointerEventData eventdata)的eventdata后跟的一点就给了我灵感。我尝试加了点,VS中果然出现了提示,好多没有用过的API,不过这些API的天机在命名中就可以猜出了。用eventdata的API成功的做出了标签比较,这五行代码就可以过去了。
下面的代码,仔细读了下,是获取了点击到的画板的大小,并将鼠标位置做了归一化。好的,这段可以照抄。
再下面,是判断鼠标点击的是哪个键,这在UGUI是没有必要的,我果断把下面十几行代码删掉了。希望不要出什么差错吧,虽然后面确实没有出差错,但是确实也怀疑过。后面遇到的BUG猜想过是不是这些代码的原因。
OnPress函数暂时移植完毕,已移植到OnBeginDrag中。接下来要把NGUI的OnDrag中的代码移植到UGUI的OnDrag中。不过代码内容跟OnPress差不多,整个过程没有花费太多时间。
好啦,扒皮大功告成。运行,测试结果!猜对了,不能用。
转念一想,这些代码里,是怎么把东西画在画布上的呢?根本就没有实现嘛。于是,又去找直接拔下来的代码,盯了半个小时左右,外表看上去跟发呆似的,实际上很烧脑。终于有些看懂了,原来是把画笔的颜色像素和画布的颜色像素做了个插值,最终将混合后的颜色显示在屏幕上。真庆幸没有去找插件,这么底层的技术真是难得。
然而,看懂了实现原理并没有卵用啊。上面的代码直接把封装好的函数调用了,这跟现在的问题没有关系。
又多测试了下,仔细看才发现,原来每次点击拖动鼠标的时候只有在点击的地方有一个点,也就是说,OnBeginDrag函数成功了,并且同时说明,采用像素插值来画画的代码也成功了。那么问题就在OnDrag函数上。
可是这是什么原因就一点儿都不直观了。我决定还是先查一下API吧。不查不要紧,一查下一跳。原来NGUI的OnPress函数是一直按压的时候返回true,松开的时候返回false,而UGUI的OnBeginDrag函数是鼠标第一次按下的时候激发事件,这根本不是一个用途嘛。于是更换了一下UGUI事件系统的OnPointerDown,测试了下发现更不靠谱,这个才是鼠标按下激发事件。可,这两个接口又有什么区别呢,真的是一脸懵逼。还是用OnBeginDrag测试一下吧。用Debug.Log(uv);测试结果显示,OnBeginDrag在按压鼠标拖动的时候会输出语句,坐标为刚开始点击时的坐标,OnDrag会在拖拽过程中一直输出,坐标为当前鼠标坐标。这没问题啊,跟自己想象的功能一样,这两个接口没错。
既然不是这两个接口的原因,那只能继续查代码了。然而灵感是可遇不可求的,所以经常会带个各种怀疑,各种可能下班,睡觉。不过工作和生活还是要分开的,可以在下班时间考虑一下,不强求,工作因素影像自己的生活就划不来了。
接着调Bug,这也不算是Bug,毕竟还没有实现。接着上次的代码继续往下查。换了换脑子,终于发现问题了。查Bug这件事确实不是时间就能解决的,经常换换脑子,从不同的视角看问题说不定就有不一样的发现。我发现将鼠标位置坐归一化的代码有问题。
Vector2 uv = Vector2.zero;
uv.x = (hit.point.x - min.x) / size.x;
uv.y = (max.y - hit.point.y) / size.y;
这个size是自己定义的,怎么能适配所有屏幕大小呢?运行了一下在scene视图缩小了看了一下,果然,是这儿的问题,鼠标的点点偏了。怎么解决呢,直接用屏幕大小来做归一化不就好了。可是,屏幕大小怎么获取呢?百度了一下,原来是Screen.width,哈哈,自己以前都用过,居然忘了,而且自己还在尝试image.width,还抱怨人家怎么是只读的。
修改好了归一化的代码,迫不及待的又开始了测试。这次,终于可以画出轨迹了!不过不知道为什么画出的轨迹是一些零散的点,不过这也算是初步完成了,至于为什么没有连成线,可以后面调整Bug的时候在处理这个。
确实,我去做UI界面了,因为这个Bug持续几天一点儿头绪都没有。几天过后,终于决心直面这个Bug。
第一个怀疑的对象,就是OnDrag这个接口的刷新频率。然后就又一次查了事件系统的所有借口,又试了一次OnPointerDown。哈哈,事情多了难免会犯点儿傻,明明之前证明了只有OnDrag这个接口可以用。况且我询问了几个大神,并没有得到控制接口刷新帧数的方法。于是这个疑点就先暂时只能记下了。
第二个怀疑对象,具体的划线代码中设置的步数太小。不过现在的步数已经是0.0003了。抱着死马当活马医的心态换个数字试一试吧。果然不行。不对,画的笔迹为什么一点儿都没有变化呢,这不应该。于是回到具体的实现代码中。找到nSteps变量,Debug.Log(nSteps);果然,结果为0。也就是说,to变量和from变量的距离为零,也就是划线笔迹中上一个点与当前点的距离为0。
private void MoveTo(Vector2 pos)
{
mCurrentPos = pos;
} private void LineTo(Vector2 pos)
{
Vector2 from = mCurrentPos;
Vector2 to = pos; Vector2 dir = (to - from).normalized;
int nSteps = Mathf.CeilToInt(Vector2.Distance(from, to) / LineStep);
Color32 c = mbErase ? new Color32(0, 0, 0, 0) : PenColor;
float size = mbErase ? RubberSize : PenSize;
for (int i = 0; i <= nSteps; i++)
{
Vector2 Cur = from + i * LineStep * dir;
DrawPattern(Cur.x, Cur.y, c, size);
}
}
这就跟代码逻辑有关系了。看了下原项目的代码,跟现在的一模一样。这就又丈二的和尚摸不着头脑了。原来的逻辑正常,放在这儿怎么就不对了呢。又是一头雾水。原本的逻辑实现的是先把之前的点存到另一个全局变量,再把当前点的坐标保存到to变量,这样就连续记录了相邻的两个点。可是越想越不对劲,细思恐极,这逻辑明明就不对,源代码是怎么实现的呢?没办法了,之前的代码是不能用了,只能另起炉灶,改写这部分代码了。
又是好久没有思路。一天晚上,睡觉之前想了想,怎么可以在同一段函数中记录两个点呢?这段代码每移动一次都会刷新一次,就会更新一个点。既然这样,那么Unity协程是不是就可以!记录一个点,挂起一帧后再记录一个点。哈哈,太好了,终于有灵感了。
第二天上班果断试了试。可是,结果还是不尽人意,一点用都没有。再仔细配合代码考虑了一下逻辑,确实不对啊。OnDrag是借口函数,不能有返回值,携程有需要写到一个新函数中,这两个函数的配合是受限制的。
没办法,这个想法又被毙了。听段音乐,再换一换脑子,看看能不能有新想法吧。对了,数据结构又一种结构,只能存储两个点,并可以不断更新。翻了翻书,对就是队列!而且C#中的队列还是泛型队列,哈哈,找到这个方法眼前又有了希望。立刻放进代码调试了一下。这种方法在运行中是可以的,可是,第一个点却没法处理,也就是OnBeginDrag得到的第一个点。删掉代码,恢复原样吧。
然而,就是恢复了一下代码,没有做一点改动,居然奇迹般的画出了线!只不过第一个点还是有点儿问题。不管怎么画,第一个点都是从屏幕左上角,也就是(0,0)点,也是初始化变量mCurrentPos的点。如果把OnBeginDrag中得到的点复制给mCurrentPos,那不就大功告成了?
明明逻辑没有问题的,可是却总会画出奇怪的线,这就有点儿百思不得解了,可是就差这一步了,攻克这一步,画板功能就可以大功告成!趁着这股热劲儿,又仔细罗列了一下代码,猛然间发现,是不是是OnBeginDrag中得到的点没有归一化,所以才会有奇怪的笔触。修改了这部分代码,再一次点运行,都不知道这是多少次了。这次终于成功了!
随着这一次点击运行,终于开启了画笔随心画的功能。心里的激动难以掩饰,于是就写了这篇文章,来记录一下第一次商业开发的新路历程。
虽然整个过程中有很多因问题不得解决的苦闷,但也有解决问题后的骄傲,并总是伴随着灵光一现带来灵感的喜悦。这,就是程序员吧。
Unity开发心路历程——制作画板的更多相关文章
- web开发-心路历程
从事web开发已经有几年了,感触颇多,在此记录一下. 对于学习: 几年的经历让我认识到了,学习确实是一个持续永恒的过程.目前的社会发展很快,各种新的思想,新的机会不断刷新我的认知,也让我体会到了自己能 ...
- 一个C#开发编写Java框架的心路历程
前言 这一篇絮絮叨叨,逻辑不太清晰的编写Java框架的的一个过程,主要描述我作为一个java初学者,在编写Java框架时的一些心得感悟. 因为我是C#的开发者,所以,在编写Java框架时,或多或少会带 ...
- VS2012+EF6+Mysql配置心路历程
为了学习ORM,选择了EntityFramework,经历了三天两夜的煎熬,N多次错误,在群里高手的帮助下,终于成功,现在将我的心路历程记录下来,一是让自己有个记录,另外就是让其它人少走些弯路. 我的 ...
- Unity NGUI 网络斗地主 -制作图集 Atlas
Unity NGUI 网络斗地主 -制作图集 Atlas by @杨海龙 开发环境 Win7+Unity4.2.1f4+NGUI 3.0.4版本 这一节告诉大家如何制作(图集)Atlas! 1.首 ...
- G彩娱乐网一个程序员到一个销售高手的心路历程
0.引言 我大学本科读的是理工科,后来毕业以后,我逐渐走上了程 序员的道路.每天面对电脑一行一行的敲代码,这被我们程序员们戏称为"搬砖头",因为我们所做的事跟民工搬砖头砌墙本质上是 ...
- 顶级项目孵化的故事系列——Kylin的心路历程【转】
现在已经名满天下的 Apache Kylin,是 Hadoop 大数据生态系统不可或缺的一部分,要知道在 Kylin 项目早期,可是以华人为主的开源团队,一路披荆斩棘经过几年的奋斗,才在 Apache ...
- Unity开发Android应用优化指南(下)
http://forum.china.unity3d.com/thread-27044-1-1.html 在Unity开发Android应用优化指南(上)一文中,从游戏性能,脚本等方面进行了分析和总结 ...
- Unity开发Android应用优化指南(上)
http://forum.china.unity3d.com/thread-27037-1-2.html 如今越来越多的开发者使用Unity开发Android及iOS项目,开发过程中难免会遇到一些性能 ...
- 一个C#开发者重温C++的心路历程
不知道为什么,似乎很多人理解跑偏了,在这里我要说明一下. 首先,我并没有对C++语言有偏见,我只是单纯的在学习时,在理解时,对C++语言进行一些吐槽,我相信,很多学习C++的人,也会有类似的吐槽. 其 ...
随机推荐
- 使用Azure Blob存储
可以通过多种方式来对Azure Blob进行操作.在此我们介绍通过VS的客户端及代码两种方式来操作Blob. 一.通过VS来操作Blob. 1.首先下载publish settings 文件:打开“h ...
- Windows MFC 两个OpenGL窗口显示与线程RC问题
问题为:背景界面是一个OpenGL窗口(对话框),在其上弹出一个OpenGL窗口(模态对话框)时, 1.上方的OpenGL窗口能响应鼠标操作等并刷新: 2.当移动或放大缩小上方的OpenGL窗口时,其 ...
- AX7: Quick and easy debugging
This purpose of this blog is to show how you can get quickly get started with debuggingin AX7, speci ...
- JS正则大全
验证数字:^[0-9]*$ 验证n位的数字:^\d{n}$ 验证至少n位数字:^\d{n,}$ 验证m-n位的数字:^\d{m,n}$ 验证零和非零开头的数字:^(0|[1-9][0-9]*)$ 验证 ...
- TabLayout 简单使用。
先上效果图 在使用TabLayout 之前需要导入design包. 我使用的是android studio 只要在build.gradle中加入 compile 'com.android.suppor ...
- T-SQL 比较N个指段取其中最大值
今天遇到一个需求,判断3个日期字段取其中最小的一个值,要Select中实现又不想写一堆的CASE,我是用如下方法实现的! select (select min(c) from( values(d1), ...
- Linux 用户和用户组管理
Linux 用户和用户组管理 Linux系统是一个多用户多任务的分时操作系统,任何一个要使用系统资源的用户,都必须首先向系统管理员申请一个账号,然后以这个账号的身份进入系统. 用户的账号一方面可以帮助 ...
- java编码解码乱码问题
服务器设值(中文)到界面使用了两次编码: String pageJson=URLEncoder.encode(URLEncoder.encode(str,"GBK"), " ...
- C语言typedef的用法(转)
http://www.cnblogs.com/afarmer/archive/2011/05/05/2038201.html 一.基本概念剖析 int* (*a[5])(int, char*); ...
- Neural Network学习(二)Universal approximator :前向神经网络
1. 概述 前面我们已经介绍了最早的神经网络:感知机.感知机一个非常致命的缺点是由于它的线性结构,其只能做线性预测(甚至无法解决回归问题),这也是其在当时广为诟病的一个点. 虽然感知机无法解决非线性问 ...