作者归档:贺 利华

关于贺 利华

正在学习编程,享受编程 热爱文学,闲来读读《读库》 有思想,没理想 正在学会专注

由于误用 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,可是目前我能想到的这个可能是最简单的方法,当然我们也可以自己写一个工具方法,直接把这段代码拷贝过去,然后换个类名或者方法名都是可以的啦,看大家喜好吧。

UnityEngine.Object.Destory 和 NGUITools.Destroy 方法之间的区别

我们先来看看 NGUITools.Destroy 方法:

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

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

我们可以看到这个方法最终也是调用的 Unity3D 提供的 UnityEngine.Object.Destroy 方法,区别一是根据当前游戏是否在运行,然后选择调用 Destroy 还是 DestroyImmediate 方法,区别二是在调用 Destroy 的逻辑中加入了将销毁对象的 Transform 的 parent 设置为 null 了,这个小细节有什么作用呢?NGUI 这么一个成熟的插件为什么在这个地方会做这样的处理呢?之前我并不知晓其中的缘由,直到今天我碰到了一个跟 Transform.childCount 相关的问题,我才懂了。接下来我们来看一个栗子,这段代码中,我们要做的就是从一个父控件中移除子控件,直到父控件中子控件的个数与目标数量一致。

void DestroyChildenToCount (Transform parentTransform, int destChildCount)
{
    if (parentTransform == null)
        return;
    if (parentTransform.childCount <= destChildCount)
        return;
    while (parentTransform.childCount > destChildCount) {
        UnityEngine.Object.Destroy (parentTransform.GetChild (destChildCount));
    }
}

上面的这段代码,我们有看出什么问题来吗?我是没有看出来,所以我就运行了这段代码,然后我的 Unity3D 就把电脑的 CPU 全给吃掉了,Unity3D 就无法正常响应操作了然后就崩溃了。通常我们这个时候的第一反应就是死循环了对吧,那么问题出在哪儿,这段代码里头就一个 While 循环,那么肯定就是这个循环出问题了,对吧。我们再来看看这段代码可能出问题的地方,我们以为只要持续的每次删掉索引为 destChildCount 的子控件,直到父控件的所有子控件的数目与目标数一致之后退出循环,达到我们预期目的,但是事实上这个 UnityEngine.Object.Destroy 方法的执行并不是及时的,这就导致了 While 循环一直无法退出的问题。我们来看看 Unity3D 官方文档上对 Destroy 方法的描述:

Removes a gameobject, component or asset.

The object obj will be destroyed now or if a time is specified t seconds from now. If obj is a Component it will remove the component from the GameObject and destroy it. If obj is a GameObject it will destroy the GameObject, all its components and all transform children of the GameObject. Actual object destruction is always delayed until after the current Update loop, but will always be done before rendering.

红色字体部分明确说明了 Destroy 的操作会在本次刷新的 Update 循环之后执行,但是会在整体渲染之前完成,这就意味着这个里头还是存在时间差的,例如每帧 0.033333 秒执行时间,有这么多的时间留给 While 循环,它不给你整出事儿来都很难啊,对吧。

所以现在我们就能理解 NGUITools.Destroy 方法中在 Destroy 对象之前将其 Transform 的 parent 指向了 null 的意图了,这样就可以及时地将被销毁对象从其原本的父对象节点上移除了,这样我们的 While 循环就可以及时的退出了。如果你跟我一样有类似的销毁某个对象的指定数目子控件的需求的话,我想这会是一个蛮有意思的知识点的。

「起风了,好好活下去。」

读完渡边淳一的《失乐园》,感觉心里痛快了一些,那些焦虑和矛盾随着久木和凛子的陨灭,一一幻去。

通篇均匀分布的关于性爱过程极致的描写,细腻到位的心理变化的剖析,全书的故事情节起伏不大,从故事的发展上来看甚至显得有些冗长了,但是全篇并无废话,所有的文字都是必须的。

唯有如此细致详尽地书写,方可让久木和凛子两人最终在情爱的顶峰慷慨赴死成为顺理成章的自然事件,前面的众多安排让两人的赴死变成了不是安排,反倒自然了许多,这恐怕也正是作者的意图吧。

久木和凛子这对绝命鸳鸯,在遭遇现实社会伦理的挤压之后,依然选择了追求自由而极乐的情爱,一步步堕入那无底的深渊,深知这深渊下面是绝对的毁灭,而又心甘情愿地堕落,直到最后双双的陨灭,将自己的人生定格在了最幸福的顶峰,最绚烂的时刻。

全篇细致入微的性场景描写,会让你自然地产生生理反应,在此之余,久木和凛子面临的社会伦理的挤压,心中的矛盾、负疚和渴望,会让你在焦虑之中渴望事态尽快进入下一个阶段的发展,直至二人在轻井泽的别墅中对饮下那口猩红的玛歌堡葡萄酒。

「起风了,好好活下去。」

2015 年 11 月读书笔记

《皮囊》

忘了什么时候在哪儿听说过《皮囊》这本书的名字,原以为这会是一部小说。但是在多看上试读了之后,方才知晓这是一部非虚构文学作品,看到阿太对于皮囊的态度以及母亲对于房子的执念之后,买了一本纸书回来,每日在上下班地铁路上读。

蔡崇达的文字给我一种可怕的真实,毫无保留地将自己内心剥开来呈现在你面前,那一个个人那一件件事,其实并没有什么波澜,都是平淡又熬人的日子在前面一天天地等着你,你在迈出步子之前已经准备好了。而在这种真实面前,很多的时候我们只能是无能为力,或者欺骗自己「只要我能 XX 样,那么这个事情就能 XX 样」,然后义无反顾地一头扎进这滚烫的红尘中去洗练自己,我们努力,我们付出,我们最终小有成绩,我们偶尔沮丧,我们偶尔欣喜,我们梦醒。

回头一望,这些日子还是在前面等着你,感觉就像是在异乡车站偶遇多年未见的少时伙伴,一句话哽在喉头不知如何说起,然后车来,伙伴上车了,回头看了你一眼。

这样的书读来并不轻松,心情并不会太愉悦,但是读下来有一种出了一口浊气的感觉。


《尘埃落定》

这是一本打开之后就不愿意合上的小说,一周之内就读完了,觉得意犹未尽。整部小说的故事性非常好,读起来非常的畅快,通过傻子二少爷的眼睛看到整个土司群的没落。

文中诸多对于麦其土司官寨这个小社会组织以及土司领地,乃至整个土司族群的组织结构的描述让人仿佛置身其中,领略到这种古老落后而又存在了很长时间的社会组织结构的特别。麦其土司、土司太太、麦其家大少爷、卓玛、塔娜、索郎泽郎、尔依、书记官、管家、茸贡土司和拉雪巴土司等等,读完之后个个鲜活地存在你的脑海,随时都可能记起来,因为你总会在生活中偶尔碰到这样的人。


《一句顶一万句》

我喜欢杨百顺,也许我就是杨百顺。通篇故事没有什么起伏,我更喜欢上部《出延津记》,出场人物繁杂,每个人都是非常非常小的角色,所有人都这么活着,这么过着。杨百顺也想这么活着这么过着,可是偏偏事情总是找上他,找上他的事情呢不大不小,却总是能要了他的饭碗,而他无非就只是图个过活罢了。最终他丢了吴巧玲,出了延津,改名罗长礼,虽然未能做成叫丧的活计,总算过活了下来。


《了不起的盖茨比》

也许是因为有个电影口碑还不错,小李子还因着这个再次被奥斯卡提名了,所以原本就抱着比较高的期望来读的,在读书前我并未看过电影,想着等把书读完了,在看看电影究竟拍得如何。

这是本小书,应该算个大中篇吧,故事并不引人入胜,不过对当时美国社会的现状和生活描绘得应该还是蛮到位的。读完之后,我是没有感受到有多强烈的冲击,只能说是一部还算不错的作品吧,也许是翻译的问题(我读的是多看上李继宏的版本),当然也许原文也就这样吧。

如何在 OSX 10.11 EI Capitan 中给移动硬盘分区并支持 NTFS

趁着这几天京东白条免息,买了一个新的移动硬盘,想着给自己服役快5年的老移动硬盘做个备胎来着,上午的单下午送到公司,拿过来那就先分一下区呗,想着布局应该是这样的,两个分区为 OSX Extended (Journaled) 格式,一个用来备份一些重要数据,一个用来做 Time Machine 备份用,剩下的那个分区就留作共享的 NTFS 分区吧,偶尔还是会用到这个 Windows 的,家人同事偶尔用用的话,这个 NTFS 还是比较有必要的,否则空有个硬盘完全无法用上也是件蛮悲剧的事情呢。

好,名字我都起好了,就分三个盘:

  1. Data-OSX
  2. Backup-OSX
  3. Data-NTFS

感觉棒棒哒,那么开始吧。打开 Disk Utility 开始吧,然后碰到了各种奇怪的问题,这儿我就不多说了,后面一步步来吧,碰到问题的地方,我都会一一吐槽的。

第一步,格式化整个硬盘

QQ20151110-2

从左侧列表中,选择 External 下的 HGST TOUR S Media (这个自己要加小心啊,别选错了,一定要选外接的自己要格式化的硬盘),选中硬盘了之后,点击工具栏上面的那个 Erase 按钮(中文应该是擦除吧),这时会弹出一个提示框,然后我们就可以输入各种参数了。

  • Name,名字随便起吧;
  • Format,看上我们可以选择的有 OS X Extended (Journaled) 系列格式、MS-DOS (FAT) 和 ExFAT 三个格式可以选择,然而实际经验告诉我们如果你想分区的话,实际上最终只有 OS X Extended (Journaled) 格式可以选择,相信我吧,不会错的;
  • Scheme,看上我们又有得选择噢,有 GUID Partition Map、Master Boot Record(俗称 MBR)和 Apple Partition Map 这三个选项,Apple Partition Map 我压根就没有试过,我想 MS 肯定是不会支持这个鬼的(其实我希望我错了),然后我尝试了一个我听得比较耳熟的 MBR,实际证明我又错了,最终的结论就是如果我想让这块硬盘能分区,我能选择的就是 GUI Partition Map。

综上,请按照上图所示的选项进行设定,也就是 Format 必须是OS X Extended (Journaled),Scheme 必须是 GUID Partition Map,然后点击 Erase,稍微等一会儿,整个硬盘格式化就完成了。

第二步,新建分区

Mac OSX 下面显示硬盘是这样的,一个物理硬盘下多个逻辑分区,那么我们分区是针对物理硬盘来进行操作的,所以还是先选中 External 下的 HGST TOUR S Media 物理硬盘,然后点击工具栏上的 Partition(分区)按钮,这时候我们会看到一个这样的弹出面板:

QQ20151110-3

这个弹出面板描述的内容就是,目前这个物理硬盘上就有一个名为 Laputa-HGST 的逻辑分区,分区格式是 OS X Extended (Journaled),容量大小是 999.861 GB。

这个时候,我们点击左侧蓝色圆饼下面的加号按钮,新建一个分区,用鼠标点击圆饼中你想要的那个分区,那个小白点是可以用来拖动调整磁盘大小的(当然通常我们数学都还蛮好的,还是键盘输入更妙一些):

QQ20151110-4

然后给分区命名为 Data-OSX,设定文件格式为 OS X Extended (Journaled),设定其大小为 300 GB。

依次再创建一个名为 Backup-OSX 的分区,文件格式为 OS X Extended (Journaled),设定其大小为 350 GB:

QQ20151110-6

现在一个饼实际上已经分成三块了,那么剩下的那个分区就是我们要的那个用来做 NTFS 格式的分区了,用鼠标选中之后,设置分区名为 Data-NTFS,调整分区格式为 MS-DOS (FAT):

QQ20151110-7

这下三个分区都设定好了之后,点击 Apply(应用)按钮,等一会儿就 OK 了,由于 Mac OS µX 自身并不支持 NTFS 文件的写入(但是支持读),所以也没有将磁盘格式化为 NTFS 的功能。Mac OS X 上有一个付费的 NTFS for Mac OS X 软件很好用,装上之后,读写 NTFS 硬盘文件就全无障碍了,他们家还有 ExtFS for Mac OSX (用在 Mac OS X 上访问 Linux 系统下常用的文件格式 Ext 系列格式的磁盘),还有一个 HFS+ for Windows (用在 Windows 系统下访问 OS X Journaled 系列格式的磁盘)。

那么接下来格式化 NTFS 盘的这一步,我们就得自己把硬盘插到 Windows 电脑上再格式化一下就好了。


这次格式化硬盘碰到的问题就是 Disk Utility 这货跟之前版本的 OS X 中的不一样了,然后碰到了一些隐性的设定,来回折腾了好些工夫,而网络上能找到的都是早些时候版本的 OS X 下的移动硬盘格式化的教程,既然自己遭罪了,我想可能也会有人碰到跟我一样的问题,那么就纪录一下吧。