Unity开发心路历程——制作画板

时间:2023-02-20 22:31:56

有人说

编程是份很无聊的工作

因为整个工作时间面对的都是电脑这种机器

因为眼睛盯着的内容都是索然无味的代码

因为总是会有意想不到的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、*。并不断的更换着词条,因为我不知道这个功能应该用哪个词条准确的描述,也不知道解决方法的文章是否用了这个词条。就这样翻箱倒柜式的翻找着。看似毫无目的,但确实是在收集信息。因为很可能一行代码就会带给你灵感,而且这种情况我遇到了不止一次。

果然,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开发心路历程——制作画板的更多相关文章

  1. web开发-心路历程

    从事web开发已经有几年了,感触颇多,在此记录一下. 对于学习: 几年的经历让我认识到了,学习确实是一个持续永恒的过程.目前的社会发展很快,各种新的思想,新的机会不断刷新我的认知,也让我体会到了自己能 ...

  2. 一个C&num;开发编写Java框架的心路历程

    前言 这一篇絮絮叨叨,逻辑不太清晰的编写Java框架的的一个过程,主要描述我作为一个java初学者,在编写Java框架时的一些心得感悟. 因为我是C#的开发者,所以,在编写Java框架时,或多或少会带 ...

  3. VS2012&plus;EF6&plus;Mysql配置心路历程

    为了学习ORM,选择了EntityFramework,经历了三天两夜的煎熬,N多次错误,在群里高手的帮助下,终于成功,现在将我的心路历程记录下来,一是让自己有个记录,另外就是让其它人少走些弯路. 我的 ...

  4. Unity NGUI 网络斗地主 -制作图集 Atlas

    Unity NGUI 网络斗地主 -制作图集 Atlas by @杨海龙 开发环境   Win7+Unity4.2.1f4+NGUI 3.0.4版本 这一节告诉大家如何制作(图集)Atlas! 1.首 ...

  5. G彩娱乐网一个程序员到一个销售高手的心路历程

    0.引言 我大学本科读的是理工科,后来毕业以后,我逐渐走上了程 序员的道路.每天面对电脑一行一行的敲代码,这被我们程序员们戏称为"搬砖头",因为我们所做的事跟民工搬砖头砌墙本质上是 ...

  6. *项目孵化的故事系列——Kylin的心路历程【转】

    现在已经名满天下的 Apache Kylin,是 Hadoop 大数据生态系统不可或缺的一部分,要知道在 Kylin 项目早期,可是以华人为主的开源团队,一路披荆斩棘经过几年的奋斗,才在 Apache ...

  7. Unity开发Android应用优化指南(下)

    http://forum.china.unity3d.com/thread-27044-1-1.html 在Unity开发Android应用优化指南(上)一文中,从游戏性能,脚本等方面进行了分析和总结 ...

  8. Unity开发Android应用优化指南(上)

    http://forum.china.unity3d.com/thread-27037-1-2.html 如今越来越多的开发者使用Unity开发Android及iOS项目,开发过程中难免会遇到一些性能 ...

  9. 一个C&num;开发者重温C&plus;&plus;的心路历程

    不知道为什么,似乎很多人理解跑偏了,在这里我要说明一下. 首先,我并没有对C++语言有偏见,我只是单纯的在学习时,在理解时,对C++语言进行一些吐槽,我相信,很多学习C++的人,也会有类似的吐槽. 其 ...

随机推荐

  1. api接口签名验证&lpar;MD5&rpar;

    不要急,源代码分享在最底部,先问大家一个问题,你在写开放的API接口时是如何保证数据的安全性的?先来看看有哪些安全性问题在开放的api接口中,我们通过http Post或者Get方式请求服务器的时候, ...

  2. JAVASCRIPT和JQUERY判断浏览器信息总汇(备忘)

    <script type="text/javascript">        //jquery判断浏览器信息        $(function(){          ...

  3. &lbrack;置顶&rsqb; 我的Android进阶之旅------&gt&semi;如何将Android源码导入Eclipse中来查看(非常实用)

    Android源码下载完成的目录结构如如所示: step1:将.classpath文件拷贝到源代码的根目录 Android源码支持多种IDE,如果是针对APP层做开发的话,建议大家使用Eclipse开 ...

  4. springMvc&plus;hibernate的web application的构建

    闲来没事,想整理下一些知识. 这篇文章是关于spring的web程序的搭建,有什么不对的地方希望大家批评指正. 首先我们要了解什么是spring,这里可能很多大家也都明白,无非是一个管理对象的一个容器 ...

  5. 再识QT(1)

    2015年的时候开始接触QT,自学了1个月,由于没有项目驱动,也没人指导,最终还是撇下了,水平也仅限于拖拖控件,做一些简单的界面,对QT的内部机制完全是懵逼的.时隔两年,最近由于公司项目需要使用QT, ...

  6. 《Discuz安装时候出现乱码 -- 问题解决方法》

    自我安装discuz时出现安装界面乱码的情况,跟链接所说一样,经过原作的分享,加上我自己的实验,明白了,什么时候修改/usr/local/php/etc/php.ini里面的default_chars ...

  7. 编译原理 &num;02&num; 简易递归下降分析程序(js实现)

    // 实验存档 截图: 代码: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"&g ...

  8. Vue history模式支持ie9

    vue 路由里面的history能让浏览器显示平常一样的链接,可以去掉#这种,但是在ie9下面会强制变成hash,因为history不支持ie9自动降级,可能就会影响美感,解决:可以在路由里面添加fa ...

  9. &lbrack;转&rsqb;&lbrack;LoadRunner&rsqb;LR性能测试结果样例分析

    LR性能测试结果样例分析 测试结果分析 LoadRunner性能测试结果分析是个复杂的过程,通常可以从结果摘要.并发数.平均事务响应时间.每秒点击数.业务成功率.系统资源.网页细分图.Web服务器资源 ...

  10. Sqlite数据库字符串处理函数replace

    Sqlite 字符串处理函数replace官方说明: replace(X,Y,Z) The replace(X,Y,Z) function returns a string formed by sub ...