分类目录归档:Unity3D

如何搞定 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,接下来就是见证奇迹的时刻了。

好了,就是这么简单。

由于误用 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 循环就可以及时的退出了。如果你跟我一样有类似的销毁某个对象的指定数目子控件的需求的话,我想这会是一个蛮有意思的知识点的。

Unity3D 中如何实现 C# 与 JavaScript 之间的互相访问

在项目研发的过程中,总会依赖众多的插件以及 Unity3D 自带的一些工具脚本等等,而这些插件和工具脚本的作者呢,有些人就偏爱于使用 JavaScript 来实现一些功能。如果在项目中恰好碰到了,而自己项目中使用的脚本语言与使用到的第三方的脚本并不是同一个语言,这个时候不同类型的脚本之间的互访问操作就很有必要了。

我们项目中有一个对 UI 整体进行模糊显示效果的需求,也就是当我们在 UI 界面上再次弹出二次提示确认框时,我们希望不出现多个界面重叠,导致整个画面很乱,视觉焦点不知道在哪儿的问题,我们希望在二次确认框弹出时,能将其后面的整个 UI 面板进行模糊显示,如此一来这个效果就会好不少,我们可以对比一下。

使用了 UI 模糊效果 未使用 UI 模糊效果

 

 

 

 

显然我们还是很有必要做一个模糊效果的,对吧。那么我们就找到了 Unity3D 自带的 Blur 脚本。这货在 Unity3D 的 Standard Assets 包中,我们需要通过 Unity3D Editor 的菜单 Assets -> Import Package -> Image Effects (Pro Only) 导入相应的资源包。然后我们就能在项目工程的 Assets/Standard Assets/Image Effects (Pro Only)/ 下找到 Blur.js 这个文件了,其实同一目录下还有一个 BlurEffect.cs 的文件,不过 Unity3D 官方目前推荐的是 Blur.js 这货,BlurEffect.cs 的这个已经是一个废弃的脚本了,官方文档上的描述如下:

This is now deprecated. Use Blur (Optimized) instead.

所以我们应该果断修改为使用 Blur.js 这货,看看这货的名字就晓得了,Blur (Optimized),名字里头都自带优化了,选它肯定没错了。好,那么我们就开始编码吧,因为这个模糊是需要动态控制其开关的,那么我们通常的做法呢就是动态地获取一下 UI 所使用的 Camera 上是否挂载有这个 Blur 组件,如果没有挂在载呢,就通过 AddComponent 方法将这个组件挂载上去,如果有的话,那么就直接将获得的组件设置的 enable 设置为 true 就好了,关闭的时候就将这个组件的 enable 设置为 false 就行了。

可是泥煤的这个 Blur 组件是个 JavaScript 代码写的呢,我们在 MonoDevelop 中写代码的时候,会发现自动提示不好使了,代码高亮中只要有 Blur 的地方都是个红色的错误代码颜色块在那儿,让人看着有点发怵啊,尼玛到底能不能用啊?我的第一反应就是这货估计不能直接在 C# 中使用,可是为啥这 JavaScript 代码中直接就能继承自 MonoBehavior 呢?反正有点懵,然后就 Google 了一下,也有人碰到这个问题,然后有个哥们给了一个解决方法就是把 Unity3D 提供的这个 JavaScript 语法的组件翻译成了一个 C# 版本的,擦。好吧,那我们就拿来主义了,直接拿进来,貌似可以耶,然后就屁颠屁颠地测试了一下,发现在 Editor 上是好使的,但是在 iOS 设备上不好使了,这是什么鬼?当然因为自己也是拿来主义,并没有仔细去查看人家的代码,想必代码是没啥问题的,真正的问题我会在后面单独描述。

既然怀疑可能是别人翻译的代码有问题(其实这个翻译非常简单,能出错也算是见鬼了,可是谁让程序运行出问题了呢,我只能怀疑人生了),那我就继续 Google 吧,然后发现实际上这个 C# 和 JavaScript 两者之间的交互是完全木有问题的,毕竟都是通过 Unity3D 的脚本引擎来编译的,既然支持两种语言编程,那么必然两者之间的通信是没有问题的,因为实际上我们通过脚本操作的所有对象本质上都是一样的东西才是啊,只是提供了两个不一样的方法而已。最终呢,我找到了这篇文章: Unity3D: JavaScript->C# or C#->JavaScript access ,文中明确地指出这两个脚本语言之间的交互是完全没有问题的但是前提是提供出来给 C# 或者 JavaScript 语言访问的脚本,必须放在 Standard Assets 目录或者 Plugins 目录下,因为这两个目录下的脚本是最先编译的模块,编译完了之后不论是啥语言写的脚本,实际上对于 Unity3D 来说,这就是一个组件对象,你用什么方式都是可以正常访问和操作的。

也就是说,我们可以对那些代码自动完成的提示不生效,代码高亮时红色警告的部分开启忽略模式了,这只是因为 MonoDevelop 自身对脚本语言支持的原因导致的,MonoDevelop 只是对 C# 语言有一个很好的支持,而对于 JavaScript 实际上它什么也做不了,我们就径直把代码写好,交给 Unity3D 去编译就好了,只要不报错,运行一下,发现效果正确。

QQ20151029-0@2x

那我们就再放到手机上试试呗,谁知道在 iOS 上依然木有效果,这就奇怪了,那就一步步来呗,既然我们都用的是 Unity3D 官方提供的脚本和资源,而且官方声明了这个是支持所有平台的,那么应该不至于出现这种奇怪的问题的啊。那么首先排除是是否通过 AddComponent 动态挂载 Blur 组件导致的,试了一下貌似还真是这么回事,如果我们通过在场景或者 Prefab 中将 Blur 组件挂载到 Camera 上,那么运行的时候一切正常,如果采用 AddComponent 的方法的话,除了需要在代码里头通过 Shader.Find 方法找到我们需要的 Shader 资源还需要将其赋值给 Blur 组件的 blurShader 变量。

不过即便是我们按照上面的这个代码来做,测试表现只在 Editor 环境下有效,在 iOS 平台上依然没有效果,最终我们只能看 Log 了,不看不知道,一看就晓得了,错误日志如下:

– Completed reload, in 0.131 seconds
Missing shader in Camera (Blur)
UnityEngine.Debug:Internal_Log(Int32, String, Object)
UnityEngine.Debug:Log(Object)
PostEffectsBase:CheckShaderAndCreateMaterial(Shader, Material)
Blur:CheckResources()
PostEffectsBase:Start()

从这个日志中输出的信息来看,就是缺失了 Blur 脚本中使用到的名为 「Hidden/FastBlur」 的 Shader 了,那么为啥会缺失呢?为啥在 Editor 中就不会出现这个问题呢?这么一问就猜到可能是因为打包发布到 iOS 设备的过程中,这个 Shader 没有被一同打包发布到 iOS 设备的安装包里头。既然有了疑问,那就去找证据吧。我第一时间查看了 Shader.Find 方法的文档,文档中有这么一句:

When building a player, a shader will only be included if it is assigned to a material that is used in any scene or if the shader is placed in a “Resources” folder.

这已经非常明确地解释了问题出现的原因了,也就是说如果我们这个 Shader 不是放在一个名为 Resources 的目录(子目录也行)下,那么只有在场景中引用到了这个 Shader,它才会被打包到最终的执行游戏包中。这下问题就明了了,就是因为我们采用了完全动态的方法来添加这个 Blur 组件,而这个 Blur 组件依赖的这个 Shader 呢因为其处于 Standard Assets/Image Effects (Pro Only) 目录下又没有被其它场景中的任何对象引用和使用,所以 Unity3D Editor 在打包发布的时候呢,就没有计算到需要把这个 Shader 资源给包含进来,最终导致了这个问题。所以,我们知道了其实之前提到的改写 JavaScript 为 C# 的那位童鞋其实并没有做错,我们这个问题是因为其它原因导致的。

那么解决这个问题就是简单的在我们可能需要使用到 Blur 组件的某个对象上事先挂载一个 Blur 组件,但是将其 enable 设置为 false,这样一来呢,可以确保名为「Hidden/FastBlur」 的Shader会被打包进来,同时呢又可以满足我们动态开关 UI 界面模糊的功能了。

总结一下:

  1. Unity3D 中的 C# 和 JavaScript 脚本之间是可以互相访问并交互的,但是要求这些被访问和操作的 C# 和 JavaScript 组件必须放在名为 Standard Assets 或者 Plugins 目录下,这样保证被访问和操作的组件是第一时间被编译的,那么在这两个目录之外的其它脚本就可以随意使用它们了,同时呢,我们不要被 MonoDevelop 的一些表现给唬住;
  2. Blur 组件不生效的原因在于 Shader 资源没有被打包到执行程序中,而并不是其它别的原因,所以打蛇要打七寸就是这个道理。

NGUI 中如何通过代码让列表自动滑动到指定位置

虽然 Unity3D 自 4.6.x 开始就已经推出了全新的 uGUI 系统,不过目前为止我们项目中还木有使用,鉴于之前一直都在使用 NGUI,所以我们依然选择了继续使用我们熟悉的 NGUI 来进行这个项目的开发,也许后续的新项目会迁移到 uGUI 上,不过那是后话了。

在做 UI 开发的过程中,我们都有碰到类似聊天界面中对话列表自动滑到列表最底端,以便于对话列表中显示的永远都是最新的聊天消息的需求,那么这个怎么做呢?依着我们程序猿的直觉,那当然是通过列表控件的 ScrollToPosition 类似的方法来搞定了对吧,说得木有错。

首先,我们要先明确一点,在 NGUI 中的 UITable 控件只是一个容器,其自身并不提供滑动的功能,而是依托于其父对象上的 UIScrollView 来完成所有与滑动相关的操作的。那么我们就来看看 UIScrollView 中有哪些方法是可以用来做 Scroll 相关操作的。

	/// <summary>
	/// Move the scroll view by the specified local space amount.
	/// </summary>

	public virtual void MoveRelative (Vector3 relative)
	{
		mTrans.localPosition += relative;
		Vector2 co = mPanel.clipOffset;
		co.x -= relative.x;
		co.y -= relative.y;
		mPanel.clipOffset = co;

		// Update the scroll bars
		UpdateScrollbars(false);
	}

	/// <summary>
	/// Move the scroll view by the specified world space amount.
	/// </summary>

	public void MoveAbsolute (Vector3 absolute)
	{
		Vector3 a = mTrans.InverseTransformPoint(absolute);
		Vector3 b = mTrans.InverseTransformPoint(Vector3.zero);
		MoveRelative(a - b);
	}

这两个方法看起来非常符合我们的需求,在我们通过计算得出了列表需要滑动的向量之后,使用这两个方法会是非常方便的,那么问题现在已经转化为我们如何计算得出需要滑动的向量值了。这里我就不考虑 MoveAbsolute 显然这货看起来都不是我们想要的,因为其计算的相对位置直接采用了世界坐标的原点来进行计算,最终滑到哪里去,谁泥煤晓得捏。那么我们现在锁定了 MoveRelative 方法了,那么我们就只需要想办法计算出列表滑动到页面最底端时与当前时刻,列表中最后一个条目之间的位置差就好了。看看这个图,我们就能找出列表滑动到最底端的时候,其特征是啥了,然后根据这个特征来进行计算就好了。

列表滑动到最底端时的示意图

这下我们只需要得到 UIScrollview 所在的 Panel 的底端坐标和当前时刻列表最底端条目的底端坐标就可以计算这两者之间的差值了,下面的这个计算方法来源于 NGUI 中的 UICenterOnChild 脚本。

    public void MoveToLastVertical ()
    {
        Vector3[] corners = scrollView.panel.worldCorners;
        Vector3 panelBottom = (corners [0] + corners [3]) * 0.5f;

        Transform panelTrans = scrollView.panel.transform;

        Vector3 cp = panelTrans.InverseTransformPoint (lastItem.position);
        // UITable 中的条目根对象为 UISprite,居中对齐高度为 150,所以得到条目中心点在 Panel 中的坐标之后再加上其高度的一半,获得底端条目的底端坐标
        cp = new Vector3 (cp.x, cp.y + 75, cp.z); 
        Vector3 cc = panelTrans.InverseTransformPoint (panelBottom);
        Vector3 localOffset = cp - cc;

        // Offset shouldn't occur if blocked
        if (!scrollView.canMoveHorizontally) {
            localOffset.x = 0f;
        }
        if (!scrollView.canMoveVertically) {
            localOffset.y = 0f;
        }
        localOffset.z = 0f;

        //        SpringPanel.Begin (scrollView.panel.cachedGameObject,
        //            panelTrans.localPosition - localOffset, 13);
        scrollView.DisableSpring ();
        scrollView.MoveRelative (new Vector3 (localOffset.x, -localOffset.y, localOffset.z));
    }

应用这个原理,针对垂直方向上的列表,我们可以实现滑动到列表的最顶端和最底端,针对水平方向上的列表,我们可以实现滑动到列表的最左侧和最右侧功能。另外如果觉得直接使用 MoveRelative 方法不是很优雅没有动画效果过于生硬的话,那么就只需要将上面那段代码中注视掉的使用 SpringPanel 来移动列表的代码放出来,把下面的两行使用 MoveRelative 方法的代码给关起来就 OK 哒。其实还是蛮好实现的呢,有木有?