标签归档:Unity3D

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 被打断,这个解决方法看上去更有技术含量一些,也更符合我们设计意图,当我们不论处于一种什么状态的时候,受击之后就应该立刻切换到某个受击状态下,这个从语义上也很能说得通,对伐?

如何在Unity3D中优雅的重命名脚本中字段名而不丢失其引用

在进行 Unity3D 开发的过程中,我们难免会碰到想要修改脚本中变量名的情况,可是呢,我们在代码编辑器或者 IDE 中对代码文件中使用到的变量进行 Refactor 之后呢,代码倒是好看了,也是不出错能编译通过了,但是到了实际运行的时候就发现出现各种空引用的问题了。

这是神马鬼啊?估计初学 Unity3D 开发的童鞋还会觉得尼玛简直不知道碰到什么鬼了,熟悉了 Unity3D 开发的童鞋呢,就会会心的一笑,尼玛的 Unity3D 简直不能再坑爹了,好吧,我再重新关联一下变量的引用吧。

这个问题的根源是啥呢?就是我们通常在脚本文件中使用了 Public 的变量,这些变量都会被 Serialized (序列化)保存到对应的 Prefab 文件并持久化到磁盘上,然后每次我们运行游戏的时候呢,Unity3D 会讲这些变量对应的资源引用加载到内存中来,这样我们就可以非常方便地在一个 Prefab 中愉快地使用另一个需要动态显示或者隐藏的 Prefab 或者 GameObject 了。这是我初次进行 Unity3D 开发时候,惊异于 Unity3D 的魔法之一。当然我现在每天都在施展这个魔法啦,不过有的时候呢,总会觉得有些不太爽,因为我有轻微的代码洁癖,只要我觉得这个字段命名的表意不准确,当我发现有更为准确的表意的变量名时,我一定会将变量名或者 Prefab/GameObject 的名称修改过来。修改 Prefab 和 GameObject 的名称倒是完全 OK,Unity3D 会自行帮你把事情做对,但是修改变量名就不一样了,例如在进行改名之后,GameControl.cs 文件中有一个名为 player 的 Public 变量,这个 player 变量指向一个场景中名为 Player 的 GameObject,此时我们的需求发生了改变,这个脚本需要控制两个 Player 了,原本名为 Player 的 GameObject 更名为 PlayerFemale 了,按照正常情况,我们的代码通常也是需要进行修改的,这样变量名和 GameObject 的名称就能保持一致了,方便阅读和理解代码,但是player变量一改名为playerFemale之后,再次运行游戏,就会得到一个空引用异常了,Unity3D 会提示你 playerFemale 变量的引用为空。

叨逼叨了这么多,那么怎么解决呢?尼玛我自己肯定解决不了,这个完全就依赖于 Unity3D 官方的解决方案了。原文链接在这里,大家直接前往围观。

我这里自己也备个忘,做法入下。

如果原先的变量名为 player,修改之后的变量名为 playerFemale,那么我们只需要在使用 Refactor 工具将 player 字段名重命名为 playerFemale,确保所有引用到该变量的代码文件中的 player 字段名称都修改过来了之后。在这个新的变量 playerFemale 上方一行,加入 [FormerlySerializedAs(“player”)] 即可。代码如下:

[FormerlySerializedAs("player")]
public GameObject playerFemale;

如果后续我们还要再将这个 playerFemale 字段名再修改为 playerAssassin 呢,那又肿么办?那就依顺序再加一行 [FormerlySerializedAs(“playerFemale”)] 在原有的 [FormerlySerializedAs(“player”)] 下方即可,代码如下:

[FormerlySerializedAs("player")]
[FormerlySerializedAs("playerFemale")]
public GameObject playerAssassin;

好了,奏是这个样子滴,好棒哒,希望 Unity3D 能变得越来越好用,省得被我们耀华一天到晚问候各种亲戚朋友,是吧。

【撒利学 Shader】之一 UV Mapping 的概念

UV Mapping就是UV贴图的意思,说白了就是将3D中的不规则多面体沿着某条线剪开之后,然后摊开到一个平面上,这样就可以将3D物体上的每个点映射到一张2D的平面贴图上了。

当然这个显然木有我说的这么简单,毕竟3D物体那么不规则,要将一张2D的贴图上的每个像素与3D物体上的各个顶点以及顶点之间映射上也不是件容易的事情,不过显然这个不是我能讨论和想讨论的事情了。我们只需要知道制作UV贴图是3D建模软件一个高级的基础功能就好了,Unity3D拿到这些UV贴图之后直接往模型上贴就好了,不过Unity3D又是如何读取2D贴图中的UV信息,并且能将这个贴图很好地贴到这个模型表面上捏?好吧,其实我也不知道,希望在这个学习Shader的过程中,我们最终能解答这个问题。