月度归档:2015年10月

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 哒。其实还是蛮好实现的呢,有木有?

Unity3D Mecanim 中 Transition 的 Atomic 属性是个什么鬼

我们游戏中一直有一个让我非常恼火的问题,关键是查了已经无数次了,根本尼玛不屌我啊,像我如此这般坚强的程序猿都要拜倒了。谁知前几天再次出现,因为已经把 Log 打得有点天罗地网的意思了,而且还持久化到磁盘 Log 文件上了,这下也不担心查着查着突然被其他的事情打断之后,然后再次回来想查问题的时候发现 Log 已经不见了,尼玛不是说好的要保护第一现场的咩?

不扯淡了,先说这个问题是啥。我们的游戏中,对于战斗 NPC 的动作都加了 AnimationEvent 回调,游戏中很多逻辑都依赖于 AnimationEvent 的回调,例如战斗 NPC 释放了一个技能,那么在这个技能动画刚刚开始播放和即将结束的某个时间点,会分别调用 OnSkillStart 和 OnSkillEnd 这两个 AnimationEvent,在这两个回调函数中会有相应的处理逻辑,例如在 OnSkillStart 中给 NPC 加上霸体的 Buf,在 OnSkillEnd 中将 NPC 的 Buf 取消。我碰到的诡异现象是这样的,NPC 的技能 A 的动画时间长度为 3 秒,OnSkillStart 方法和 OnSkillEnd 方法分别处于整个动画时间轴的 0.2 秒和 2.8 秒,NPC 在 AI 判断确定释放技能 A,在技能 A 动画播放到 0.1 秒的时候,受到了主角的攻击,此时 NPC 应该立即切换到受击状态,开始播放受击动画,但是事实上却不是这样,我们能看到的日志信息如下:

10:34:05.565 AM ya_zhang_tie_qi_jun(Clone)001 TriggerSkill ya_zhang_tie_qi_jun_attack_3

10:34:05.573 AM #Network# GameClient Emit a Message: [Action, Ping]

10:34:05.667 AM Player OnSkillStart: player_female_suplex_back
10:34:05.792 AM Player OnSkillTrigger (player_female_suplex_back#0)
10:34:05.799 AM ya_zhang_tie_qi_jun(Clone)001 OnHookAttackHit by Player with player_female_suplex_back#0
10:34:05.803 AM ya_zhang_tie_qi_jun(Clone)001 Hit to Hooking from None
10:34:05.817 AM ya_zhang_tie_qi_jun(Clone)001 OnSkillStart: ya_zhang_tie_qi_jun_attack_3
10:34:06.297 AM GameClient Received a Message: [Action, Ping]

10:34:06.338 AM Player OnSkillTrigger (player_female_suplex_back#1)
10:34:06.345 AM ya_zhang_tie_qi_jun(Clone)001 OnSuplexAttackHit by Player with player_female_suplex_back#1
10:34:06.352 AM ya_zhang_tie_qi_jun(Clone)001 is SuperArmored, cannot be Suplex
10:34:06.359 AM ya_zhang_tie_qi_jun(Clone)001 ApplySuperArmoredFX
10:34:06.744 AM ya_zhang_tie_qi_jun(Clone)001 CancelSuperArmoredFX
10:34:06.859 AM Player OnSkillEnd: player_female_suplex_back
10:34:08.589 AM #Network# GameClient Emit a Message: [Action, Ping]

从日志里头看到的信息就是这个牙帐铁骑军已经释放了一个技能,但是在其技能动画播放到 OnSkillStart 的时间点时,已经被主角攻击了,此时其并未立即切换到受击状态,而是继续播放其技能动画并且触发了 OnSkillStart 的回调。所以游戏的逻辑就出问题了,NPC 因为触发了 OnSkillStart 回调进入霸体状态了,然后主角后续的所有攻击都无法让 NPC 做出相应的受击动作了,关键是这个 NPC 在回调了 OnSkillStart 函数进入霸体之后呢,还是会进入到对应的受击状态,这下就奇了怪了。你要么就不进入受击状态呗,要么就别触发霸体啊,这不是要疯吗。

最后仔细分析排查,知道了这个问题是因为 OnSkillStart 的触发时间点处于动画过渡的时间区间内,也就是说 NPC 从 Idle 状态进入到技能 Attack_3 的状态时间长为 0.3 秒,并且这个动画的过渡 Atomic 属性为 True。问题就这么来了,NPC 在从 Idle 状态时释放技能 Attack_3,动画从 Idle 切换到 Attack_3,动画切换到 0.1 秒的时候,受到了主角的攻击,NPC 的整个 AnimatorController 状态图中,受击状态都是直接从 Any State 跳转过去的。按理来说就应该在任意状态下都能切换到受击状态才对啊,为啥动画还会继续往后播放呢,直到 OnSkillStart 回调完了之后再切换到受击状态呢?

最终发现了就是这个 Atomic 属性给闹的,这个 Atomic 属性的字面意思已经很清楚了,那就是当它为 True 的时候,这个过渡是原子的,也就是说只要进入了这个过渡,那么这个过渡就一定会播放完成,不论是发生任何情况这个过渡都会播放完成,而刚好我们的这个 OnSkillStart 回调函数所处的时间点就在这个过渡时间段里头,所以就出现了,虽然最终 NPC 还是进入了受击状态,但是在进入受击状态之前因为已经回调了 OnSkillStart 函数,所以导致逻辑上出现了错误。

这个问题呢,有两个解决方法:

  1. 取消所有的动画状态切换之间的过渡时间,让动画之间的切换不再有过渡的过程,都是直接切换,这样就可以避免出现过渡的时候触发 AnimationEvent 了;
  2. 将牙帐铁骑军的 AnimatorController 中从 Idle 到 Attack_3 状态的 Transition 的 Atomic 选项取消,允许该 Transition 被打断,这个解决方法看上去更有技术含量一些,也更符合我们设计意图,当我们不论处于一种什么状态的时候,受击之后就应该立刻切换到某个受击状态下,这个从语义上也很能说得通,对伐?

霍光这厮创造了一个奇迹

霍光,汉朝名将霍去病的弟弟。霍去病死的时候,他以奉车都尉的身份和金日磾(dī)、上官桀一直侍奉在汉武帝的跟前,是为『内朝』,区别于丞相、御史大夫这种政府部门被称为『外朝』的官员。

汉武帝死前托孤的就是这三位时常侍奉其左右的臣子。年幼的汉昭帝(刘弗陵)即位后(当时年仅八岁),霍光是大司马、大将军,金日磾是车骑将军,上官桀是左将军,形成了三驾马车式的制衡结构。

金日磾的去世,让这个原本还有制衡存在的权利结构崩溃了,而皇帝尚年幼,这就造成了霍光与上官桀两虎相争的局面,最终霍光胜出成为了权倾朝野的独裁者。

在霍光与上官桀争斗之前,两家还有联姻呢,霍光的女儿嫁给了上官桀的儿子上官安,生了一个闺女。上官安还曾私下里跟他老丈人提议让自己的闺女做昭帝的皇后,但是被霍光以『七岁尚早』为由拒绝了他女婿的这个请求。

不过上官家既然也是豪族,不可能完全没有办法的嘛,他们找到了抚养昭帝的蓋长公主,最终还是成功地让上官家的孙女成为了昭帝的皇后。

在后续的斗争中,霍光胜出了。霍光以上官家勾结曾经叛乱的燕王(由于他的同胞姐姐蓋长公主在昭帝面前求情没有被处死)再次发动叛乱为由,把上官家族全给灭了,除了他那年幼的贵为皇后的小外孙女(难道连他自己的姑娘,也就是皇后的母亲也给干掉了?)。

昭帝又是个死得早的皇帝,昭帝过世时,皇后十七岁,他俩还没有子嗣,这样就把昌邑王刘贺(汉武帝时期昌邑王刘髆的儿子,在其父死后继承了王位)给找来继位,认了这小皇后为母亲,最终又以谋反的罪名被废,连王位都给剥夺了,随行入京的随从除三人之外全被杀了(这三人是什么鬼?难道有阴谋?)。

这下又要找谁来当皇上呢?霍光竟然从民间找到了汉武帝时期的太子刘据的孙子——刘病已,即位后为宣帝(这一年他十八岁,比他名义上的母亲太后还大一岁呢)。宣帝在民间的时候已经娶了许氏成家了,在宣帝即位后,许氏莫名其妙地就死了,然后霍光就让宣帝娶了他的小女儿,并册封其为皇后。

这下奇迹出现了,霍光让他的外甥女成功的当上了他女儿的婆婆。也就是说原本太后要管皇后叫小姨,现在成了小姨得叫外甥女母亲了,这难道不是一个奇迹吗?

当然创造奇迹的霍光最终给他的家族带来的也是毁灭,宣帝熬到霍光死了以后,通过各种手段一步步把霍光多年经营的权利集团给瓦解了,并且最牛逼的是立了他在民间娶的许皇后的儿子刘奭(shì)为太子,让霍氏一族的如意算盘彻底落空了。

最终霍氏一族被逼得走投无路了,发动政变被宣帝平定,霍氏一族被灭,霍皇后也被废了,霍光的老婆被弃市。

退避三舍这个成语的出处

晋文公是春秋五霸之中继齐桓公之后的霸主,晋是西周成王(就是那个年幼的成王,经历了三监之乱的幼主)弟弟的封地,晋也是陆续吞并了晋周边的几个小国之后,到文公的时候成为一霸。

文公名叫重耳,因家族内乱,少时出走各国,过了十九年的流浪生活,最终修成大招,回到晋国之后掌握霸权,尼玛还臭不要脸地召唤周襄王到他的领地河阳参加所谓的诸侯会盟,承认其实盟主,简直碉堡了,有木有?

文公牛逼的事迹是啥捏?就是作为南蛮子的楚国(湖南和湖北)牛逼了之后,北上想干掉处在其旁边的宋(河南省),文公这货联合了齐和秦前去救援同属中原北国正统的宋,在城濮(山东省)把楚军给干了。

当然这只能算是军事实力和才能牛逼,远不足以流传千古啦。牛逼的是,这货竟然做了一件牛逼的事情,就是在与楚交战之际,退避三舍了。

原来文公还叫重耳的时候,流浪到楚地时得到了楚成王极大的关照,当时楚成王问他,如果你回国之后当了君主,你丫怎么报答我捏?文公答道:

——晋楚治兵,遇于中原,其辟君三舍。

『舍』是一天行军的距离,约合 12 公里。『退避三舍』就意味着后退 36 公里地。然后在城濮之战中,文公还真就信守诺言退了 36 公里,咱们先不说实际的战争中这个退 36 公里究竟对于战局有何影响,最起码这个重诺的德行是值得称赞和肯定的吧。