2015 年 12 月读书笔记

《失乐园》

真心好看的一本小说,毫不避讳如外科解剖般细致的大段落性描写,恰如其分又极端写实的心理活动的呈现,让久木和凛子直接在你的眼前活了。

你可以看到他俩在酒店里,在出租屋里,在度假别墅里做爱,你能感受到久木与妻子在餐桌上的沉默和尴尬,你能感受到凛子和丈夫在家中的冷暴力。

二人在一次次突破世俗伦理的束缚之后,最终成功的走到了一起。但是两人却选择了在这幸福的最高点赴死,不是向那冰冷坚硬的伦常示威,而是向那琐碎的生活和必然衰减的热情抗议。两人深知结合之后,随着时间的推移,家务琐事的牵绊,二人之间的这种热情和默契会日渐磨灭,正如他们与自己的原配之间那般,也无奈找不到破解之道,最终选择在激情燃烧得最为旺盛的刹那走向毁灭,双双赤裸狠狠地抱在一起,满足地离开了这个让他们失望透顶的世界。

《把时间当作朋友》

得知李笑来这个人已经有些年头了,之前只是知晓他是新东方的名师,喜欢写博客,然后就没有了,大概我还一直有订阅他的博客。

再次关注到他,就是因为成功的比特币投资者这个巨大的光环吧。我就是这么俗,哈哈。关注了他的微信公众号之后呢,想着有必要读读这本屡次出现在他文章里头的这本听起来很正能量和主旋律的书(我就是这么装逼,书还没读呢,就先给人定性了),读了之后,有点相见恨晚。

书中反复讲了很多很棒的道理,也非常刻薄地指出了我们绝大多数人的致命弱点,打脸真的好疼,至少我很疼。

疼完了之后呢,我开始尝试去做一些事情了,我继续坚持每天的晨跑(当然北京的这个天气,想必大家也都知道,雾霾天是个很操蛋而且高发的天气,所以会造成一些小间断),然后开始写每天的流水账时间日志。

效果好不好,目前还不能说,我想先坚持个一年再回来看。不论做什么计划,关键在于实践和坚持嘛,书里头就是这么说的,所以,咱们走着瞧。

《甘南纪事》

这是我读的杨显惠先生的第二本书,第一本是《夹边沟纪事》,读完了非常震惊和感动,深为杨先生这种非虚构类文学作品的风格吸引。在叙实之余不落入一种纯文本性的记录和纯对话式的段落,而是适当地通过一些虚拟想象还原每个故事的主人公和出场人物的心理状态和活动变化,又不过分渲染这些私人的揣度,这是一种恰到好处的还原和超越。

此次读《甘南纪事》显然较之前读《夹边沟纪事》要来得轻松许多,心情也没有那么沉重。书中不是穿插一些甘南地区的水美山美,朴实多彩的甘南藏人的风土人情,都让自己有点沉醉其中了,有机会定要去扎尕那探寻一番在书中多次出现的白石头山。

书中多次指明了村民调解委员会在藏人生活中的重要性,特别是在村民公共事务中扮演的重要角色,其中关于僧侣、老人,甚至以前的头人等等在委员会中处理相关事务时所遵循的社会组织原则以及所体现出来的智慧,甚至可以说是迷人的。而这些流传下来的传统和智慧,与现今的社会组织结构难免会产生一些冲突,例如公安局这个角色在藏民们心中和生活中扮演的是一个什么角色,以及藏民们是如何对待各种新型的事物,例如保暖内衣和防风服等等,书中那些淳朴可爱的藏民们会用他们每天的生活给你最真实的答案。

《你的灯亮着吗?》

这本书我第一次看到的是一个在网上流传的 PDF 版本,当时自己拿到打印店去打印了全书,自己也读了一部分,应该是把电梯的那个案例的部分给读完了,也许后面的部分没有继续读下去,也许读完了之后完全忘光光了,因为这次重新再读一回,发现自己隐约能记起来的也就是电梯的案例。

关于这个案例,在我自己身上还有个小事情蛮有趣的。08 年 10 月份前后,腾讯来我们学校校招,然后笔试通过之后,我就去参加面试了,面试的时候,面试官是个年轻帅气的小伙子,然后他先让我尝试着解决一个算法编程问题,我尝试了十分钟左右,没有什么思路,然后面试官考虑到时间有限(当时计划我是下午4点半开始面试,实际上我面试的时间已经6点多了,实际上我已经饿了),换了一个开放式的题目就是这个电梯的案例。我想了大概几秒钟吧,我觉得我还是应该坦白告诉面试官这本书我读过,我要是直接把书里头的内容给搬出来,这个就是抄袭,我也不能确定我能怎么样把我看过这本书的事实掩盖住,又不露痕迹地展现我自己独立思考的能力自己和看问题的视角的独特性。最终,我很快地就告诉我的面试官,我说这本书我看过,希望能换一个题目,面试官笑了笑,然后给我换了另一个开放式的题目。这回的题目是一个智力题,然后我就被我的智商打败了,最终我就这么灰头土脸地结束了我的腾讯校招面试了。

所以这次重新读这本书的过程中,其实我有多次已经读不下去了,但是我还是不断地告诉自己必须读下去,最终读完了。我想说的是,本书的趣味性并不是那么高,有几个还蛮有趣的案例分析但是不多,文中也有不少比较别致的观点,但是说真的读起来确实蛮无趣的。也许以后得某天再读会有新的感受和收获吧。

《21 世纪资本论(精华本)》

原本以为这是全本,后来读了总感觉特别不对,很多的结论和数据扑面而来,完全消化不了,主要阐述的观点貌似是这样的「21 世纪里头,继承遗产的重要性将会更为突出,社会将会再次形成明显的阶级分层,而且财富将会更加集中化」,大概就是这样吧。不建议读这个所谓的精华本,我是在「多看阅读」上面读的,完全不建议,想读的话还是找全本来读吧。

Unity3D 中粒子特效不在摄像机渲染范围内 Play 之后导致特效延时播放的解决方法

从项目启动之初,我们就引进了 PoolManager 插件来做诸多对象的缓存,防止游戏中对象的频繁 Instantiate 和 Destroy,造成不必要的性能问题。PoolManager 是一个非常成熟的插件,也提供了很多非常友好的接口以供开发者使用,其中也有针对 ParticleSystem 对象的 Spawn/Despawn 机制,所以我们就很自然的使用了 PoolManager 插件来管理整个游戏中的所有特效对象。

particle_system_spawn_pool_management

对于特效对象,我们游戏中的管理逻辑大体如下:

也就是说我们的粒子特效对象是重用的,在每次重新被 Spawn 出来的时候都会重新调用一次 Play 方法来重新播放这个粒子特效,这个方案看上去很正常有木有?确实没有啥硬伤对吧。

但是在我们实际的测试过程中,我们很早就发现了一个很奇怪的问题,就是偶尔场景中会出现一些不应该出现的技能特效。例如,在场景中偶遇到一个小怪,小怪在攻击的过程中玩家控制主角快速跑开了(主摄像机一直跟随主角),然后灯小怪的攻击结束之后,主角再次跑回到小怪面前进行战斗,然后此时偶尔就会看到小怪并没有在进行任何攻击,只是在战斗待机或者移动,但是屏幕上会凭空播放一些这个小怪的攻击技能对应的特效(其实找到这个问题重现的方法,也就是这个 Bug 的规律也是费尽心思啊,想想看这货都存在快 TMD 一年了,直到现在我才真正地捉到它)。

既然已经找到了重现的方法,也清楚了 Bug 出现的规律,也就是当小怪释放技能时(技能动作带有技能粒子特效),由于主角的移动导致摄像机跟着主角在场景中移动,最终小怪释放的技能粒子特效并不在摄像机的 Culling 区域中,而此时我们通过了 PoolManager 插件 Spawn 出来了一个新的粒子特效,然后调用了 ParticleSystem 的 Play 方法来播放粒子特效。我们预想的是该粒子特效在我们调用 Play 之后就会播放,然后就交给 PoolManager 自行管理就好了(PoolManager 插件会监听粒子是否已经完全发射/播放结束,如果是,就会将这个粒子对象 Despawn 掉,放回到 PoolManager 中对应的 SpawnPool 中以备后续再次使用)。

问题就出在这里,PoolManager 中监听粒子对象是否完全发射/播放结束的方法是通过轮询 ParticleSystem 的 IsAlive 方法来进行判断的,代码如下:

// ParticleSystem (Shuriken) Version...
private IEnumerator ListenForEmitDespawn(ParticleSystem emitter)
{
    // Wait for the delay time to complete
    // Waiting the extra frame seems to be more stable and means at least one 
    //  frame will always pass
    yield return new WaitForSeconds(emitter.startDelay + 0.25f);

    // Do nothing until all particles die or the safecount hits a max value
    float safetimer = 0;   // Just in case! See Spawn() for more info
    while (emitter.IsAlive(true))
    {
        if (!PoolManagerUtils.activeInHierarchy(emitter.gameObject))
        {
            emitter.Clear(true);
            yield break;  // Do nothing, already despawned. Quit.
        }

        safetimer += Time.deltaTime;
        if (safetimer > this.maxParticleDespawnTime)
            Debug.LogWarning
            (
                string.Format
                (
                    "SpawnPool {0}: " +
                        "Timed out while listening for all particles to die. " +
                        "Waited for {1}sec.",
                    this.poolName,
                    this.maxParticleDespawnTime
                )
            );

        yield return null;
    }

    // Turn off emit before despawning
    //emitter.Clear(true);
    this.Despawn(emitter.transform);
}

这个 IsAlive 判断就是问题的关键,经过测试我们发现了一个 Unity3D 的机制,当我们首次将 PartileSystem 对象 Instantiate 出来时,不论其是否在摄像机的 Culling 区域内,调用 ParticleSystem 的 Play 方法或者将其 playOnAwake 设置为 True 都会播放该 ParticleSystem 对象。而在其首次 Instantiate 之后,再次调用 Play 方法来播放该粒子特效时,如果粒子不在任何一个 Camera 的Culling 区域内(包括 Culling Layer Mask 是否相符以及是否在 Camera 的可视范围内),实际上粒子并不会发射,直到其进入到某个 Camera 的 Culling 区域内才会触发粒子的发射(实际在 Unity Editor 中测试的时候,最终发现粒子进入 Scene Window 的可视区域和 Game Window 的可视区域都会触发粒子的发射,实际上 Scene Window 的 Preview 就是有一个我们不可见的 Camera 在进行渲染)。此处需要说明的是以下两点:

  1. Unity 版本为 4.6.9f1;
  2. 粒子特效的 Renderer Mode 是 Billboard。

至此,我们终于完全弄明白了为什么会出现某些技能的粒子特效无端地在场景里播放了,原因就是因为该特效在 Instantiate 出来调用过一次 Play 方法之后(由于我们所有的特效默认 playOnAwake 属性都是 true,所以我们通过 PoolManager 加载的粒子特效的 Prefab 实际上在首次 Spawn 之前都已经自动调用过一次 Play 方法了),再次调用 Play 方法来播放该粒子特效的时候,该粒子特效所处的坐标位置并不在主摄像机的 Culling 区域内,所以根本就不会触发粒子的发射而是在等候下次进入摄像机的 Culling 区域时再触发。在我们游戏中的最终表现就是在屏幕边缘与小怪发生战斗,当小怪释放技能的时候快速往小怪的反方向移动,等着小怪来追击,我们再折返跑回刚刚与小怪发生战斗的地方,这个时候我们就能看到几个小怪的技能特效凭空在场景中自动播放了,真乃煞是奇妙啊。

好吧,说了这么多,那么我们既然知道是这个原因了,那么怎么解决呢?我们看到了  ListenForEmitDespawn 方法中有一个针对粒子特效在 Spawn 出来之后时间的检测,其检测粒子对象自 Spawn 以来的时长是否超出 SpawnPool 中定义的最大 Despawn 时长,跟实际的粒子对象并无直接关系,而且即便该粒子对象 Spawn 出来的时长已经超出最大的 Despawn 时限,也只是会简单地输出了一个警告的 Log 信息。

那么我们自己的处理逻辑应该是怎样的呢?我的处理方法是在粒子对象被 Spawn 出来之后,除了通过其自身的 IsAlive 方法来检测该粒子对象是否还是激活状态,在其 Spawn 出来的同时就启动一个 Coroutine 用来检测其自 Spawn 出来的时长是否超出 ParticleSystem 的 duration 设置值,如果已超出,那么就将其 Clear 并且 Stop,然后再将其 Despawn 掉。依次修改后的 ListenForEmitDespawn 方法如下:

// ParticleSystem (Shuriken) Version...
private IEnumerator ListenForEmitDespawn(ParticleSystem emitter)
{
    // Wait for the delay time to complete
    // Waiting the extra frame seems to be more stable and means at least one 
    //  frame will always pass
    yield return new WaitForSeconds(emitter.startDelay + 0.25f);

    // Do nothing until all particles die or the safecount hits a max value
    float safetimer = 0;   // Just in case! See Spawn() for more info
    while (emitter.IsAlive(true))
    {
        if (!PoolManagerUtils.activeInHierarchy(emitter.gameObject))
        {
            emitter.Clear(true);
            yield break;  // Do nothing, already despawned. Quit.
        }

        safetimer += Time.deltaTime;
        if (safetimer > emitter.duration) {
	        Debug.LogWarning
	        (
	            string.Format
	            (
	                "SpawnPool {0}: " +
	                    "Timed out while listening for all particles to die. " +
	                    "Waited for {1}sec.",
	                this.poolName,
	                emitter.duration
	            )
	        );
			break;
		}
            
        yield return null;
    }

    // Turn off emit before despawning
	emitter.Stop(true);
    emitter.Clear(true);
    this.Despawn(emitter.transform);
}

如何搞定 UnityEngine.UI.dll is in timestamps but is not known in guidmapper… 错误

在使用 Unity 开发的过程中,不知道什么情况下就会碰到这么两个错误(根据平台和使用的 Unity 版本不同,路径可能不一样,我这边列出的也不是完整的路径):

UnityExtensions/Unity/GUISystem/4.6.9/UnityEngine.UI.dll is in timestamps but is not known in guidmapper…

UnityExtensions/Unity/GUISystem/4.6.9/Editor/UnityEditor.UI.dll is in timestamps but is not known in guidmapper…

有的人建议是直接把我们的 Project 目录下的 Library 目录直接删除掉,不过这样一来,重新再次打开 Unity Editor 的时候,整个工程的资源都需要重新导入,而且通常我们还是在移动平台模式下进行开发,也就是等它把资源导入进来之后,我们还需要做一次 Switch Platform 的操作,真的好慢好慢好慢啊(也许可以通过 Asset Cache Server 来提速,但是我没有用过,所以不敢乱说)。

那么我们有什么办法呢,这里介绍一个比较好的方法,总共分三步:

  1. 进入出错的两个 dll 文件所在目录中,将出错的两个 dll 文件重命名为任意文件名,例如:UnityEngine.UI.dll.backup 和 UnityEditor.UI.dll.backup;
  2. 重启 Unity Editor,等整个工程加载结束之后,我们会看到很多红色的错误日志输出,甭理它就好了,关闭 Unity Editor;
  3. 回到那两个 dll 文件所在的目录,将刚刚重命名的两个 dll 文件分别修改回来,UnityEngine.UI.dll 和 UnityEditor.UI.dll,然后再次启动 Unity Editor,接下来就是见证奇迹的时刻了。

好了,就是这么简单。

互联网品牌贩卖的是什么?

先说明我的观点:互联网品牌贩卖的是一种微妙而完整的体验。

1.

很难想象在乔布斯发布 iPhone 之前会有哪家手机厂商给自己的一款手机开一个盛大的发布会(也许有,只是我不知道而已),但是现在如果哪家厂商发布一款新的手机而不搞一个发布会,就显得那么的异类了。那么发布会是不是很有必要呢?有,非常有。

想象一下 3 年前或者更远一些 5 年前自己要去买手机的时候,我们心中的第一个念头是啥?去中关村看看,去苏宁/国美看看。再对比一下现在,我们其实已经在计划自己下一次要换手机的时间和机型了,也许我们手上正在使用的手机也才使用了 8 ~ 10 个月而已,这个就是发布会的力量。

我们现在回头数数今年一年里,叫得上号的手机发布会有多少,叫得上号的智能电视发布会有几个,叫得上号的智能手环发布会有几个,这个列表还可以有很长。发布会是一个非常直接的渠道和手段,通过发布会能让诸多的媒体参与到发布会里头来(这年头科技媒体的主营业务应该就是跑发布会了吧),媒体会在发布会前后发布各种相关的资讯,让读者和消费者们充分的了解到这款产品的特点。同时这还会造成一种现象就是「这款产品真的蛮不错的,看起来还挺火的呢,很多媒体上都在报道他们的产品发布会呢」。配合产品发布会呢,还有诸多衍生的产品线,例如:谍照泄漏,工信部网站公示信息、专利申请被披露、公司高层在社交媒体上各种秀和各种暗示,这种在未见其面之前就勾起大家深思的蛛丝马迹;再例如:工艺名词解析、参数大比拼等,这种将原本属于工业制造和实验室领域的各种专有名词的普及运动;还例如:期货抢购、工匠情怀、体验至上等,这种更高层次的营销策略和手段。

这一切造成一个很直接的结果:「这款产品不错,体验很好,参数很高,性能很强,得赶紧预约才能买到」,这让我们对一款新推出的产品有了一个很好的想象。然后我们来到某家手机厂商的在线商城,打开首页就是一个四周留白,页面正中间放着一张硕大的产品图片(图片通常都美得让你觉得不是实物,实际上也确实仅供参考),然后你可以通过滚动鼠标或者左右滑动手指等等方式将这些精美的图片浏览一遍,然后会有一个产品参数页面,总之这个页面是精心设计非常精美的,通常以白色为主色调,兼而可能使用产品主色调作为辅助,产品图片无一不是那么的精致美丽,哪怕只是一节电池,除了当作模特拍上好些漂亮照片之外,可能还要惊喜绘制一些剖面图/截面图/内部构造拆解图之类的。每次当我看到这些美丽的图片的时候,无一例外,我都想立刻能拥有它们,哪怕它们实际上并不是我急需要的东西,而我可能正囊中羞涩。

互联网公司天生离用户的距离比较近,与互联网媒体的亲密度也相对较高,通过发布会以及相关的事件,让你的微博时间线、微信朋友圈和同事午间吃饭闲聊都离不开某款新手机的屏幕尺寸和芯片型号,这让我们快速地与自己当前手上拥有的设备产生了对比,然后我们很自然地就发现了更好的更新的设备,这下我们已经有了对这款产品的渴望了。

2.

接下来我们会从各大测评网站和咨询媒体那儿看到诸多「记一次亲密接触-XX 手机开箱与轻体验」类似标题的文章,各大媒体的小编们在收到快递之后迅速拆开包装把玩一番之后,我们就看到了前面的这篇文章,接下里的几天里,除了媒体小编收到了测试设备,也有一些发烧友或资深玩家会纷纷分享自己的开箱体验,其中会有诸多对新款设备不足槽点的吐槽和埋怨,但是请相信任何一款新设备只要不是极其坑爹,通常大方向上其与老旧设备的对比都是鲜明的,而且通常也确实会比老旧设备更胜一筹的。

然后我们才慢慢发现公交车上、地铁上和办公室里,慢慢地多出来了不少新的设备,看到别人优雅地用拇指解锁屏幕,看到别人轻轻按压手机屏幕便可呼出支付宝的向商家付款界面时,那前几日在发布会上或者在线上观看发布会视频时萌发的渴望愈发强烈了一些。

3.

比别人更先一步体验到流行就是时尚,如今互联网品牌已经成为了某种时尚,第一时间知晓某品牌新产品的各项详细参数以及不同型号之间的区别和售价,会让你成为周边同事和朋友的购物指导人,这也是目前诸多自媒体公众号做买手生意的由头,毕竟很多人都想赶时尚但是就是没有这个能力啊。当我们比别人更先一步拥有这新款设备的时候,这种优越感来得更为直接,很多人也对这新设备感兴趣呢,当他们开口向我们询问这款产品怎么样的时候,我想除非这款产品真的伤害到了我们,通常自己都不会做过分负面的评价的,毕竟我们更倾向于相信自己的选择是明智的,眼光是独到的,品味是高雅的,所以很多的时候我们的评价实际上并不是客观的。

所以我们看到那么多的微信公众号在我们的心里种草,有的种草手账,有的种草钢笔,有的种草家居装饰,有的种草皮具等等等等。一旦你成为读者和消费者,你已经被引领了,你会认可这种时尚你会追随这种时尚,成为「米粉」成为「煤油」等等各种奇怪的粉丝,然后等着被满足,等着被薅羊毛。

4.

我们看到越来越多的内容创业者最终落地到了卖货上,貌似「一条」视频团队也要通过美食系列节目开始卖「视频里头的炊具」,数字尾巴做了贩卖数码周边的尾巴良品业务,我想他们家卖得最多最好的恐怕就是移动电源了。数字尾巴上有着非常优质的产品测评分享,以数码科技产品为主,周边为辅,各种高大上和 DIY 一应俱全,里头的高端分享玩家也确实够土豪,分享的产品也确实逼格满满。那么为什么我觉得移动电源卖得最好呢?因为他家卖的移动电源单品最多,各种品牌的各种类型款式,而且测评的文章也不好还非常的专业。在这么一个高质量的数码产品测评分享社区里头,这些移动电源一样显得很不错,有高端的感觉,而且这些东西都不贵,很多人都可以轻松获得。在获得实物的同时,我们也获得了之前通过社区里头的文章构建出来的美好想象和拥有此物的渴望被满足的快感。

这也可以从某个角度说明为何有那么多的低收入群体的人会渴望拥有一台售价不菲的 iPhone 手机,这是一个可以通过相对低廉的价格获得一次超高性价比的体验的机会。这个体验绝不只是限于使用手机的时候获得的便利,因为使用其他款式其他品牌的手机,他们也一样可以出色的满足发朋友圈、聊 QQ 、看视频和听音乐这几大核心需求。但是 iPhone 能提供的体验绝不止于此,它在长达一年甚至更长的时间里(苹果发布新产品的事件周期通常为一年,库克也许正在打破这个节奏的路上),能给我们带来一种与时尚比肩的优越感,试想一下「沙特阿拉伯的王子如果不用 VERTU 手机的话,他估计也就用的 iPhone 吧」,当然我们更可以扩大一下「贝克汉姆」「Justin · Bieber」也都用的是 iPhone 噢,是不是有一种顿时跟偶像零距离的感觉呢?时尚从未离我们如此之近。

由于误用 NGUITools.Destroy 导致所有 UI 控件的层都被重置的问题

最近在项目中碰到一个很奇怪的 Bug,表现是在背包中消耗掉某些道具之后,会将所有 UIRoot 下的子对象的层全部都修改的 UI 层,这个会造成很多奇怪的问题。由于我们游戏中使用了多个用于 UI 显示的 Camera,针对不同层的 GameObject 使用了不同的 Camera,这么一整搞得完全乱了套了。

当然确定是因为消耗了道具之后才造成 Bug 出现的这一步也是花费了很长的时间才搞明白的,在此之前我已经把我能想到的所有情况都给试了一遍(当然这个少不了我们士海和杨威童鞋两个人在旁边的指点和敲击),最终发现只能给要被 Destroy 掉的控件添加一个 UIPanel 组件就可以彻底避免这个问题,当然这个还是还是有点懵,直到追查到了 NGUITools 中的 CreateUI 方法中,看到了 SetChildLayer 方法,才知道最终是因为调用了这个方法,那么接下来就是一步步定位了。

通过各种日志输出和调试,最终确认了就是因为调用 NGUITools.Destroy 销毁掉的控件在被彻底的销毁之前调用了自身的 Update 方法,然后调用到了 OnUpdate 这个方法:

protected override void OnUpdate ()
{
    if (panel == null) CreatePanel();
#if UNITY_EDITOR
    else if (!mPlayMode) ParentHasChanged();
#endif
}

正常情况下,CreatePanel 这个方法是不会调用的,但是在我们这个游戏中,道具使用的时候会跳转到另一个 UI 界面下,而背包界面的整个 UIPanel 在玩家操作道具使用时是隐藏的,这样一来背包中所有条目的 UIWidget 中的 panel 属性就都为空了(这个 panel 属性设置为空是在整个背包面板被隐藏的时候 UIWidget 的 RemoveFromPanel 方法被自动调用了),所以在我们消耗掉某个指定的道具回到背包界面的时候,我们会调用 NGUITools.Destroy 方法来将被消耗掉的这个道具的 Item 给销毁掉,这样背包中剩余的所有条目就是当前实际背包中所有的道具条目了。但是我们忽略了 NGUITools.Destroy 方法中对目标销毁条目做了什么操作,这个可以参考我的另一篇文章,最终导致调用 CreatePanel 方法的时候错误的将整个 UIRoot 下的所有 UI 控件的 layer 都给设置成了目标销毁对象所在的层。

public UIPanel CreatePanel ()
{
    if (mStarted && panel == null && enabled && NGUITools.GetActive(gameObject))
    {
        panel = UIPanel.Find(cachedTransform, true, cachedGameObject.layer);

        if (panel != null)
        {
            mParentFound = false;
            panel.AddWidget(this);
            CheckLayer();
            Invalidate(true);
        }
    }
    return panel;
}

就是在 CreatePanel 方法中调用到了 UIPanel.Find 方法:

static public UIPanel Find (Transform trans, bool createIfMissing, int layer)
{
	UIPanel panel = NGUITools.FindInParents<UIPanel>(trans);
	if (panel != null) return panel;
	return createIfMissing ? NGUITools.CreateUI(trans, false, layer) : null;
}

继而又调用了 NGUITools.CreateUI 方法:

static public UIPanel CreateUI (Transform trans, bool advanced3D, int layer)
{
    // Find the existing UI Root
    UIRoot root = (trans != null) ? NGUITools.FindInParents<UIRoot>(trans.gameObject) : null;

    if (root == null && UIRoot.list.Count > 0)
    {
        foreach (UIRoot r in UIRoot.list)
        {
            if (r.gameObject.layer == layer)
            {
                root = r;
                break;
            }
        }
    }

    // If we are working with a different UI type, we need to treat it as a brand-new one instead
    if (root != null)
    {
        UICamera cam = root.GetComponentInChildren<UICamera>();

#if UNITY_4_3 || UNITY_4_5 || UNITY_4_6
        if (cam != null && cam.camera.isOrthoGraphic == advanced3D)
#else
        if (cam != null && cam.GetComponent<Camera>().orthographic == advanced3D)
#endif
        {
            trans = null;
            root = null;
        }
    }

    // If no root found, create one
    if (root == null)
    {
        GameObject go = NGUITools.AddChild(null, false);
        root = go.AddComponent<UIRoot>();

        // Automatically find the layers if none were specified
        if (layer == -1) layer = LayerMask.NameToLayer("UI");
        if (layer == -1) layer = LayerMask.NameToLayer("2D UI");
        go.layer = layer;

        if (advanced3D)
        {
            go.name = "UI Root (3D)";
            root.scalingStyle = UIRoot.Scaling.Constrained;
        }
        else
        {
            go.name = "UI Root";
            root.scalingStyle = UIRoot.Scaling.Flexible;
        }
    }

    // Find the first panel
    UIPanel panel = root.GetComponentInChildren<UIPanel>();

    if (panel == null)
    {
        // Find other active cameras in the scene
        Camera[] cameras = NGUITools.FindActive<Camera>();

        float depth = -1f;
        bool colorCleared = false;
        int mask = (1 << root.gameObject.layer);

        for (int i = 0; i < cameras.Length; ++i)
        {
            Camera c = cameras[i];

            // If the color is being cleared, we won't need to
            if (c.clearFlags == CameraClearFlags.Color ||
                c.clearFlags == CameraClearFlags.Skybox)
                colorCleared = true;

            // Choose the maximum depth
            depth = Mathf.Max(depth, c.depth);

            // Make sure this camera can't see the UI
            c.cullingMask = (c.cullingMask & (~mask));
        }

        // Create a camera that will draw the UI
        Camera cam = NGUITools.AddChild<Camera>(root.gameObject, false);
        cam.gameObject.AddComponent<UICamera>();
        cam.clearFlags = colorCleared ? CameraClearFlags.Depth : CameraClearFlags.Color;
        cam.backgroundColor = Color.grey;
        cam.cullingMask = mask;
        cam.depth = depth + 1f;

        if (advanced3D)
        {
            cam.nearClipPlane = 0.1f;
            cam.farClipPlane = 4f;
            cam.transform.localPosition = new Vector3(0f, 0f, -700f);
        }
        else
        {
            cam.orthographic = true;
            cam.orthographicSize = 1;
            cam.nearClipPlane = -10;
            cam.farClipPlane = 10;
        }

        // Make sure there is an audio listener present
        AudioListener[] listeners = NGUITools.FindActive<AudioListener>();
        if (listeners == null || listeners.Length == 0)
            cam.gameObject.AddComponent<AudioListener>();

        // Add a panel to the root
        panel = root.gameObject.AddComponent<UIPanel>();
#if UNITY_EDITOR
        UnityEditor.Selection.activeGameObject = panel.gameObject;
#endif
    }

    if (trans != null)
    {
        // Find the root object
        while (trans.parent != null) trans = trans.parent;

        if (NGUITools.IsChild(trans, panel.transform))
        {
            // Odd hierarchy -- can't reparent
            panel = trans.gameObject.AddComponent<UIPanel>();
        }
        else
        {
            // Reparent this root object to be a child of the panel
            trans.parent = panel.transform;
            trans.localScale = Vector3.one;
            trans.localPosition = Vector3.zero;
            SetChildLayer(panel.cachedTransform, panel.cachedGameObject.layer);
        }
    }
    return panel;
}

我们在设置整个背包界面可见之前已经调用了 NGUITools.Destroy 方法将我们消耗掉的道具的 Item 销毁了,而在道具被消耗掉的同时我们除了调用了 NGUITools.Destroy 方法将其销毁掉,还同时关闭了道具使用的界面回到了背包界面,这样一来这个在道具使用界面被销毁掉的道具 Item 实际上在背包界面再次显示的时候还是会调用 Update 方法,直到其真正被销毁掉 Update 才不会再次被调用,而这一次 Update 的调用就导致了前面一连串的反应,最终导致了错误地将整个 UIRoot 下的所有子控件的 layer 都设置为了被销毁道具 Item 所在的层。

那么我们应该怎么来避免这个问题呢?是不是就应该不是用 NGUITools.Destroy 方法,当然这是一个不错的想法,也是很直接的想法,但是如果你参考了我的另一篇文章之后,也许你就不会这么想了,原来 NGUITools.Destroy 方法还是蛮有门道的,如果盲目的替换成 UnityEngine.Object.Destroy 方法,也许带来的只会是麻烦。那么我是怎么解决的呢?好吧,代码来了:

static public void Destroy (UnityEngine.Object obj)
{
    if (obj != null)
    {
        if (Application.isPlaying)
        {
            if (obj is GameObject)
            {
                GameObject go = obj as GameObject;
                SetActive (go, false);
                go.transform.parent = null;
            }

            UnityEngine.Object.Destroy(obj);
        }
        else UnityEngine.Object.DestroyImmediate(obj);
    }
}

我在 go.transform.parent = null; 之前加入了一行代码 SetActive (go, false); 这样就避免了已经要被销毁了的对象还会再次调用 Update 的问题,所有后续引发的问题就都一一化解了。不过于我而言,实际上我并不太喜欢这种直接修改 nGUI 代码的修改方式,这样的修改总是会在你更新 nGUI 插件之后丢失掉(Unity3D Editor 在更新插件的时候都是直接覆盖的,并不存在智能合并这种东西的亲),所以呢还是有点小遗憾和 Dirty,可是目前我能想到的这个可能是最简单的方法,当然我们也可以自己写一个工具方法,直接把这段代码拷贝过去,然后换个类名或者方法名都是可以的啦,看大家喜好吧。