月度归档:2015年05月

如何在 Unity3D Editor 脚本中使用 Coroutine 进行异步操作

今天在做编辑器中一个功能时,碰到一个这样的需求,Unity3D Editor 脚本中会通过 System.Diagnostics.Process 这个类来调用外部的系统命令来执行一个操作,其实就是调用系统的 Python 命令执行一个 Python 脚本,来将 Excel 文件转换成文本数据。

但是这样执行完 ImportExcel 菜单项之后,可能会出现我们通过外部的脚本修改了 Unity3D 需要使用到的文本文件,但是 Unity3D 未能及时更新 AssetDatabase,导致 Unity3D 中读取到的文本文件还是执行 ImportConfig 菜单项之前的数据。那么我们能否直接在这个外部程序执行完毕的回调中执行 AssetDatabase.Refresh() 方法呢?不行,因为这些函数都只能在主线程里头调用,而这个回调函数是在启动外部程序的线程中执行的,所以在 ConvertProcessExited() 方法中访问 AssetDatabase 对象会抛出异常。这个时候我们可以通过 EditorApplication 的 update 委托来配合 Coroutine 机制来完成我们想要的功能。

关于 EditorApplication 如何支持 Coroutine,这个是在 GitHub 上找到了一个很棒的参考,地址在 这儿 ,链接页面中是一个完整的独立的 EditorCoroutine 的实现,借助于这个漂亮的工具,我们修改了代码,然后就能达成我们的需求了,在执行完外部转化的 Python 程序之后,及时通过 AssetDatabase 刷新 Unity3D 中需要使用到的文本文件,代码如下:

 

AnimatorStateInfo 中 nameHash 值

AnimatorStateInfo.IsName(sting name) 这个方法是用来判断当前 Animator 播放的否是某个动画,而这个方法不只是比对动画名称的 nameHash 值,通过测试我们会发现传入纯粹的动画状态名称或者传入状态所在层名称和状态名称的组合,都是可以成功匹配的。

AnimatorStateInfo.IsName(string name) 方法不仅会比对这个 FullPath Name,还会比对 Simple Name,例如我们在 Base Layer 中有一个名为 Idle 的状态。

那么通过 AnimatorStateInfo.IsName(“Idle”) 和 AnimatorStateInfo.IsName(“Base Layer.Idle”) 都是 True,但是如果我们要直接比对 nameHash 的值,那就必须使用 Animator.StringToHash(“Base Layer.Idle”) 和 AnimatorStateInfo 的 nameHash 属性值进行比对,而不能使用简称的 nameHash 值 Animator.StringToHash(“Idle”) 来进行比对。

在 Unity4.6 中 AnimatorStateInfo 中得 nameHash 属性值是通过 Animator.StringToHash(“Base Layer.StateName”) 获得的,参数是全路径名,全路径由状态所在层名称和动画状态名称组成,格式为“[LayerName].[StateName]”,请自行使用对应的 LayerName 和 StateName 替换方括号中的内容。


升级到 Unity5 之后,AnimatorStateInfo 中就木有 nameHash 这个属性了,不过新增了两个字面意思更明确的属性,shortNameHash 和 fullPathName,两者的区别就是 fullPathHash 传入的 Name 参数是带上 State 所在的 Layer 的名字的,例如:Base Layer.Idle,而 shortNameHash 就是不带 Layer 名字的,例如:Idle。这样更明确了显然是更好的,看到 Unity3D 的文档一天天往完善了发展,还是深感幸福啊。

用 Python 写一个简单的 Alfred Workflow 用于颜色值的转换

由于自己是程序员,而且比较痴迷于使用各种工具,虽然可能这些工具并没有给我的效率带来太大的提高,但是我就是喜欢折腾这些软件,因为能给我带来快乐和满足感,你说谁不想爽呢。

Alfred 是我使用 Mac 之后安装的第二个工具软件(正常工作需要的开发软件和浏览器之类的通用软件不算),第一个是 Dash,第二个是 Alfred。Dash 是在试用了 5 分钟不到,查看 Android 帮助文档如飞的响应速度,让我直接购买了正版授权。Alfred 是在当做 Launch 软件使用了快 1 年之后,看了很多人提到的关于 Workflow 的增强功能之后,在某个非常想购物的日子里头买了一个 PowerPack,从此用上了 Workflow 的功能,当然在这一年里头实际上我也就只用了有道词典这么一个 Workflow,其他的几乎不怎么使用,主要还是习惯问题。

这几天手痒,总觉得自己不像个能折腾的程序员,刚好看到一个用于转化数字的 Workflow 感觉还不错,试用了一下之后,我发现其实我自己也有一个比较强的需求可以通过这个 Workflow 来完成。在做 Unity3D 的开发过程中,我们时常会需要使用到调色板来给界面上的文字进行着色,就是设置颜色值。但是美术一般提供过来的就是效果图,或者是 RGB 值,通常格式是这样的 (234,120,54),但是在实际的开发中我们并不是每次都是针对一个 UILabel(我们的 UI 使用 NGUI 开发)进行字体颜色设置,因为有的时候我们只是想突出显示一段文字中的某一部分,这个时候依赖于 NGUI UILabel 的 BBCode 机制,我们可以通过 [ffbbcc] 需要指定颜色显示的文字内容 [-] 这样的方式来实现同一个 UILabel 下的文字使用不同的颜色进行显示。

那么这个时候有一个能快速转换(234,120,54)为#ea7836 就有一些必要了,参考那个 Number Convertor 我就自己开始造轮子了。

Alfred Workflow设置页面

找到 Number Convertor 这个 Workflow,选中任意一个 Script Filter 右键,弹出菜单之后点击 Configure,或者直接双击也行,然后弹出一个配置页面,配置页面的右下方有一个 Open workflow folder 的按钮,点击之后会打开当前 Workflow 对应的所在目录,然后我们会发现这个目录下面有以下的内容:

Alfred Workflow对应目录

仔细查看一下,我们会发现所有的转化逻辑都是通过 number_convertor.py 文件来完成的,但是这个脚本依赖于一个叫 Workflow 的 Python 库,这个目录里头的 workflow 就是干这个用的。这个 Python 的库可以在 这里 找到。

以下是的脚本内容:

然后我们就可以通过 Workflow -> Add Templates -> Essentials -> Script Filter to Script 创建一个 Workflow,然后通过上面提到的方法进入设置页面打开 Workflow 所在目录,按照 alfred-workflow 的 Python 库的安装方式安装设置好 alfred-workflow,然后将写好的 Python 脚本放到相应的位置,最终目录里头的内容如下:

目录结构

然后再回到 Workflow 设置页面设置一下 keyword 和调用 Python 脚本的方法和参数就 OK 了。

QQ20150511-7@2x

同理可以再设置一个 HexToRGB 的行为:

QQ20150511-8@2x

转换成功到了之后,我们把结果保存到剪贴板中,并且通过鼠标拖拽 Script Filter 节点上的小圆点到 Copy to Clipboard 节点上,这样就能把 Script Filter 中获得的结果保存到系统的剪贴板中了。

Screen Shot 2015-05-11 at 11.35.16 PM

最后来个实际使用的截图吧:

Screen Shot 2015-05-11 at 11.38.12 PM

折腾完毕。

Unity3D 插件之 Behavior Designer Movement Pack For Apex Path 的巨坑

在目前的项目中,我们有使用到一个叫 Apex Path 的插件来做自动寻路的事情,这个插件整体来说还是棒棒哒,完全对得起$65 的价格,我在写这篇文章的时候,刚好这货又在做半价促销了, 链接在此

今天先不展开聊这个 Apex Path 插件了,今天主要是要吐槽一下 Behavior Designer 插件的增强包 Movement Pack,在初次接触到 Behavior Designer 插件之时,顿时觉得这货能帮忙解决 AI 行为树的大部分问题,立马买回来尝试了几把,发现真心不错,可以解决很多问题。然后在他们网站上发现还有一个额外的用于处理移动的增强包,果断再次入手。拿回来简单地测试了一番,感觉还不错。但是在后续的开发中,总是会发现一些奇怪的问题,大体的表现就是 NPC 完全在没有移动到应该移动到的攻击位置就开始攻击了以及类似的问题,而且这个问题是在有多个使用了 Behavior Designer 控制的 NPC 出现在场景中的时候才会出现。

这个真心奇了怪了,作为一个朝内程序猿,我一直都奔着崇洋媚外的态度来看待国外程序员大大们,一直认为他们好牛逼好牛逼,然后先入为主地认为是自己用得肯定有问题,多次检查了自己的代码,依然没能找到问题。最终我就只能搞了一个空场景,就放了两个 NPC,让 NPC 做最简单的行为,把 Log 加上,尼玛最后知道真相的我,眼泪掉下来啊,有木有!

我们来看看 Behavior Designer Movement Pack For Apex Path 中 Patrol 脚本中关于 Apex 中 UnitNavigationEventMessage 回调的处理。

然后我们对比一下 Apex Path 插件中自带的一个 PatrolBehaviour 脚本是如何处理这个 UnitNavigationEventMessage 的回调的。

细心对比一下,我们就会发现,在后者的处理逻辑中,加入了一个关于这个消息的 entity 对象是否为当前脚本所绑定的 GameObject 对象的判断,如果当前接收到的消息不是当前这个 GameObject 发出的,是不做任何逻辑,直接 return 的。然后再回过头来看看 Behavior Designer Movement Pack For Apex Path 中的处理,你就知道问题出在哪儿了。

因为 Apex Path 的 GameServices 中的 MessageBus 实现机制是任何对象都可以通过 GameServices.messageBus 访问到这个全局静态的 MessageBus 对象,并且可以通过 subscribe 和 unsubscribe 方法来进行消息的订阅和取消订阅。也就因为这个任何 MessageBus 的全局机制,任意注册了的对象都会收到回调,所以我们需要在处理消息回调的时候自行判断这个消息是否是我们需要的,或者说需要判断这个消息是否是发给自己的。MessageBus 的机制就是一个广播,只要有任何一个对象触发了事件,注册了消息的所有对象都会收到广播,所以需要收到广播消息的对象自行甄别这货是否是发给自己的。

Behavior Designer Movement Pack For Apex Path 脚本中对于回调的处理显然是忽略了这个甄别的过程,所以就会出现一些完全不在意料之中的事情,一切都能解释得通了。

Opsive 的这个团队在 Behavior Designer 的工作确实非常惊艳,让人竖大拇哥,但是在这种问题上犯错误只能让我觉得这是实习生做的,或者就是完全没有真正地使用过 Apex Path,然后就直接推出了一个似是而非的 Movement 增强包,我是很不以为然的。今天看到这个 Movement 的插件还升级了,Update 需要花费$10,鉴于这个问题,我想暂时还是不升级了,虽然我很想看看他们升级的版本中是否已经修复了这个低级错误,等我今天闲下来了,我得给他们发个邮件问问。


给 Opsive 团队发过邮件了,邮件回复说是新的版本中已经加入这个判断了,问题已经修复,回复内容如下:

Hello,

Thank you for letting me know. Have you imported the most recent version of the Apex Path integration off of the integrations page? This integration doesn’t include an ApexPathSteeringBase file. It only includes ApexPathMovement as the base class. Within that class there is a check for the GameObject:

        public virtual void Handle(UnitNavigationEventMessage message)

        {

            if (!message.entity.Equals(gameObject)) {

                return;

            }

            switch (message.eventCode) {

                case UnitNavigationEventMessage.Event.DestinationReached:

                    arrived = true;

                    break;

            }

        }

I am not setting message.isHandled to true because all I am doing when destination is reached is setting a flag so a new destination hasn’t been set yet.

Thank you,

Justin

Unity3D Mecanim 动画 AnimatorTransitionInfo 和 AnimatorStateInfo 在角色移动和待机平滑切换中的应用

在之前的开发过程中一直仅仅使用到了 AnimatorStateInfo 这货,平时在做一些判断的时候还特意加入一个判断!Animator.IsInTransition(0) 来确定当前这个 Animator 没有在进行动画过渡,可是这几天同事们总是反应游戏中主角移动起步很慢和停止移动的时候会出现滑步的情况,这个不能忍,听得我很是汗颜啊。

好吧,汗颜的事情就先不表了,我们来看看这个可能是神马问题吧。通常我们游戏当中,角色都会有一个待机的动作,跑步和行走都会有相应地动作,而不同的动作之间的切换,Unity3D 会自行做动画融合,这样主角从待机动作切换到跑步的动作时,就不会出现一帧直接切换导致看起来非常机械和卡顿的问题,看起来整个过程会是非常平滑的,这个就是我们要谈到的重点了。

在之前的实现中,我在主角的移动控制脚本 PlayerMovement 中使用了一段这样的代码来判断当前女主角已经成功的从 Idle 状态切换到 Locomotion 状态了

然后在控制角色位置移动的代码段中,我们根据需要进行计算获得女主角在不同坐标上的移动位置,然后将位移作用到角色上。

但是这样做会有什么问题呢?

  • 首先,当角色在从 Idle 切换到 Locomotion 动画的过渡中时,上面的这段代码会直接忽略动画过渡的这段时间,所以在女主角从静止起步到跑步的过程中,Animator 实际上一直都会保持为 Idle 的状态,直到整个 Transition 完成了,Animator 的 State 才会切换到 Locomotion,这样的话女主角实际上是在原地播放了这个过渡的动画,而这段时间的动画中,女主角的脚步会从静止切换到小碎步,再到大步跑,而这个时候女主角的位置不会发生变化(没有在女主角跑动动画对应的方向上移动),最终的结果就是主角看起来反应非常慢,起步的时候会出现卡顿,要在原地跑一段时间之后才会移动,这显然是不能接受的。
  • 其次,当角色从 Locomotion 切换到 Idle 动画的过渡中时,依然会出现逻辑被忽略的情况,也就是角色实际上已经在从 Locomotion 切换到 Idle 了,角色的脚步动作越来越小了,但是因为 Animator 的设计是在切换过程中,State 的名字不会改变,会保持为状态切换之前的状态名,也就是在动画完全切换到 Idle 之前,State 的名字一直都是 Locomotion,所以主角在这个时间里头,逻辑会让主角继续运动(因为它满足上面的逻辑,所以还会计算位移,并应用到角色对象上),最终的结果就是看起来主角的跑步动画已经停止但是身体还在动画方向上做位移,出现滑步了,尼玛啊。

既然已经找到问题了,那么我们就肯定有办法来解决它。既然我们知道了动画在切换的过程中可以通过 AnimatorTransitionInfo 来获取过渡的信息,那么就有办法了,首先我们能确定 Idle 到 Locomotion 和 Locomotion 到 Idle 的 nameHash 值,通过比对这两个值就能明确知道当前 Animator 是从哪个状态切换到哪个状态了,然后根据 AnimatorTransitionInfo.normalizedTime 可以获取到过渡的进度信息,这样一来我们就能准确的计算出来动画过渡的过程中,角色应有的运动速度了,例如从 Idle 切换到 Locomotion 是从速度 0 到速度 4m/s,那么在 Idle 切换到 Locomotion 的过程中通过 Mathf.Lerp (04normalizedTime) 就可以获取实时速度了。最终代码如下: