月度归档:2015年07月

Unity3D 中 CapsuleCollider 碰撞挤压将对象挤到空中的问题

这几天一直在尝试着修改各种战斗中的 Bug,优化一些战斗细节等等,然后发现了在某些怪和主角对抗的时候,有的时候主角会莫名其妙地被挤到空中去,有的时候是怪在攻击主角的过程中把自己给挤到空中去了。你说这能忍吗?显然不行,对伐。

然后就开始使劲浑身解数尝试着在不同的状态下,主动控制怪和主角不要离开地面(通过动态获取目标所在位置点的地表 Y 坐标值,然后设置为目标的 Y 坐标),可是这又谈何容易呢,先不说这么做合不合理,优不优雅了,这一眼看上去就不是啥好招啊,而且实际上并没有找到为神马会出现相互挤压就会把某一方给挤到空中去的原因,那么我们肯定不能善罢甘休的,对伐?

接下来我们来看两个图: capusle_collider_1动图 1 capsule_collider_2动图 2

上方的动图 1 中,我们看到绿色的 Capsule 朝着红色的 Capsule 快速移动,并且在移动的过程中与其发生了碰撞,碰撞之后绿色和红色的 Capsule 错开,然后绿色 Capsule 继续运动了一段时间。从动图 2 中,我们能看到绿色的 Capsule 在与红色的 Capsule 碰撞之后继续运动的过程中,明显有被挤到空中的情况。咦,貌似我们已经发现什么了呢。

那我们再来对比一下这两个 Capsule 的设置值,到底是哪个参数导致的呢?

capsule_collider_inspector_1 capsule_collider_inspector_2

从上面的两张图对比中来看的话,其实只有一个 Capsule 的 Scale 调整了 Y 为 2,同时 Y 坐标也因为 Capsule 变高了所以变高了。如此一来,这个绿色的高个子的 Capsule 在与红色的矮个子 Capsule 发生碰撞和挤压的时候就会因为质心过高而被挤到空中去。如果希望避免这种情况,可以考虑将所有的 Capsule Collider 的中心点值和高度值都统一设置。

Unity3D 中 Mecanim 动画切换与 AnimationEvent 的关系

在我们的游戏中,我们使用了 Mecanim 动画中的 Generic 动画类型,几乎所有的动画切换和融合,我们都直接交给 Animator 自行处理了。省事倒是省事了,随之也带来了一些不可预知(其实是自己能力不足好吧)的问题。

今天我要分享的是一个这样的问题,例如主角正在进行 A 动作,此时受到攻击通过 Trigger 设置,切换到 B 动作,主角的 AnimatorController 中并没有直接从 A 动作过渡到 B 动作的 Transition,A 动作和 B 动作都是可以从 AnyState 直接切换的,也就意味着 AnimatorController 会自动根据当前动画的状态从当前播放的动画过渡到目标动画中去。

在 A 动作还没有播放完的情况下,我们通过设置 Trigger 的方式将 A 动作切换到 B 动作,而 A 动作上又绑有一些 AnimationEvent,这些 AnimationEvent 会在动画执行到对应的时间轴的时候触发回调,用图来表达,最好不过了。 mecanim_transition_animation_event图一中的这种情况,OnSkillEnd 这个 AnimationEvent 是不会被回调的,而在图二中,OnSkillEnd 这个 AnimationEvent 就会被回调。因为每个从 AnyState 出发的 Transition 都是有过渡时间的,如果绑定的 AnimationEvent 所处的时间轴在动画过渡时间之内,那么就会触发这个 Event,否则就不会回调。

关于 Animator.SetTrigger 的一些迷思

近期一直在修改各种战斗中碰到的问题,其中有一个问题,让我困扰了很久,大概场景简单描述一下,有一天我们的策划同学给我反馈了一个问题,问题是这样的:

如果主角在战斗副本中一直按住攻击按钮进行攻击的话,貌似一直都处于霸体状态,小怪命中主角之后,能看到主角在掉血,但是主角几乎不会播放受击后仰的动画,让人觉得主角很 Bug,小怪完全不是对手。

好,这是一个很好的问题,在我听到这个反馈的第一时间里,我的反应是“擦,这不可能啊”,好吧,程序猿总是这般自信和分裂。实际上这肯定是可能的,而且正在发生,可是我们总是想选择不相信事实,在看着我们的策划同学演示了大概 3 分钟,我发现这尼玛就是有这个问题。那么肿么解呢?

先检查代码呗,各种检查,各种推演,最终发现有可能存在一种情况,那就是 Animator 同时调用了多个 SetTrigger 方法,设置了多个不同的 Trigger 的值,可能会导致这个问题,那么接下来就是验证了,因为实际的工程中触发个情况肯定不是那么必然和容易的,那么我们验证的时候就直接非常直接地通过代码的方式来验证就好了。

我们直接就在某一个按钮按下的事件回调中,直接通过 Animator.SetTrigger() 方法设置了两个动作差别非常大的状态对应的 Trigger,最终会发现 Unity3D 会自动融合这两个过程,例如我们在 Idle 状态的时候通过设置 Attack 和 BackOff 两个状态的 Trigger:Attack 和 BackOff,然后我们就会发现,主角会先往后仰几帧,然后继续 Attack 的动作直到其完整播放完毕。 unity3d_mecanim_set_trigger_myth测试结果是,会以当前触发的所有动画中播放时长最大的为主,其余的动画会在整个时间轴中对主角的动画综合地产生影响,所以一定要注意尽量不要让多个不同的 Trigger 在同一时间触发,因为 Unity4.6 版本未提供获取 Animator 参数列表的接口,所以我们得自己维护好 Trigger 列表,如果我们的实际需求中,不存在需要多个 Trigger 同时被触发的情况,那么记得在设置新的 Trigger 之前(当然也可以考虑优先级和权重值)将其他已经设置过但是还没有被 Animator 应用的 Trigger Reset 掉。

使用脚本在 Unity3D Editor 中完全删除 Prefab 中的 ParticleSystem 组件

今天碰到一个小问题需要通过脚本来批量将我们工程中的所有例子特效 Prefab 根节点的粒子发射器给干掉,那么好吧,开始动手吧。

执行一下,发现 ParticleSystem 组件确实木有了,但是留了一个小尾巴,就是我们在 Unity3D Editor 中查看被处理的 Prefab 时,还能在 Inspector 中看到一个 Shader 的小尾巴,如图:

default_particle_shader_tail

这个让我情何以堪啊,你知道的,我有轻微强迫症的,虽然貌似并不会影响程序的功能,但是我要是不知道原因,我岂不是很不爽,好吧,那么就对比一下手动将 ParticleSystem 组件 Remove 和通过我们这个脚本 Destroy 之后的 Prefab 文件之间究竟有甚么区别呢,这个时候就需要借助一下 Unity3D 提供的 binary2text 命令行工具了,Mac OSX 上这个命令还在这个位置 [Unity3D 安装目录]/Unity.app/Contents/Tools/binary2text,通常就是 /Applications/Unity/Unity.app/Contents/Tools/binary2text。使用这个工具将两个 Prefab 文件转成 Text 文件之后我们就可以使用文件对比工具来进行对比了(对比二进制神马的,真的让人很蛋疼啊,幸好 Unity3D 还提供了这么个鬼啊),对比就会发现,这个 Shader 是因为啥而存在的了。

compare_1 compare_2

左侧有红色文本显示的是通过脚本 Destroy ParticleSystem 组件之后生成的 Prefab 的 Text 文件,右侧的是手动通过右键 Remove Component 删除 ParticleSystem 组件之后生成的 Prefab 的 Text 文件,对比之下我们能发现左侧文件中说明了一些问题:

  • 第一处红色文本表示多了一个内部资源的引用,看起来很像是 Shader;
  • 第二处红色文本更是直接说明了问题,左侧表示有两个组件,组件的 Id 是 19935810,刚好与第三处红色文本描述对应;
  • 第三处红色文本描述的是一个 ParticleSystemRender 组件的信息。

这么一看就知道原来还有一个隐藏的 ParticleSystemRender 组件没有被移除掉,因为这个 Render 是依附于 ParticleSystem 才存在的,所以不会在 Inspector 中单独显示,而是作为 ParticleSystem 的子组件 Render 进行显示的。那么我们就再修改一下我们的代码就好了。

也就是需要独立地将这个 ParticleSystemRender 组件也给 DestroyImmediate 一下就好了。搞定,洗洗睡了。

Unity3D 中使用 Socket.Poll 方法遇到的小坑

昨天下班之前提交了一个小小的改动,就是把 Socket.Poll(int microSeconds, SelectMode mode) 方法中的 microSeconds 参数改成了-1,原因是我看到了.NET Framework 文档中的这么一句话。

Poll will block execution until the specified time period, measured in microseconds, elapses. Set the microSeconds parameter to a negative integer if you would like to wait indefinitely for a response.

好吧,我承认我不矜持了,我看到这段描述的时候顿时一尿啊,赶紧把我之前设置的 1000 微秒改成了-1,我想这个 1000 微秒还是很有可能会出现没有得到实际的结果的情况啊,所以统统都搞成-1 吧,我就死等你返回结果还不行吗?好吧,改完了,赶紧测试一下,在 Editor 和自己的 iPhone 5S 上分别测试了一下,貌似木有问题,然后就提交了。

晚上回到家,同事就发来消息说是新版本无法进入游戏,卸载重新装了一个中午的包就 OK 了,当时心里头咯噔一下,难道真的是这个改出来的问题。今天早上一到公司,策划同学打开 Editor(Windows 下)正要进游戏发现一直就卡在那儿。跟服务器的同学折腾了一番,WireShark 和 tcpdump 都用上了,最终还是没有找到为啥。好吧,那就试试把昨天改的这点代码一点点恢复,最终发现就是因为这个鬼导致的。

就是这个 SelectMode.SelectError 配合-1 导致的,SelectMode.SelectRead 和 SelectMode.SelectWrite 配合-1 并不会出现这个问题,所以好吧,那就果断先把 SelectError 这货的时间参数修改回来吧。再仔细一看,原来这个时间参数是用微秒作单位的,所以 1000 是不是有点太短了呢?至少也传个 500 毫秒吧,在目前的这种移动网络环境下,这个时间是不是会更合适一些呢?测试一下呗,这回在 Android 和 Windows Editor 上都 OK 了。

记录一下这个坑,使用 Socket.Poll(int microSeconds, SelectMode mode) 这个方法的时候,在 Android 平台和 Windows Editor 平台下会出现因为将 microSeconds 设置为-1 导致程序会 Hang 住在 Poll 方法这儿,这个线程就算是杯具了,这也是我的应用场景下出现所有网络请求都超时,无法收到服务器的返回(实际上服务器每次都成功返回了,而且每次超时之后重试发送的消息也都能发送出去),就是因为我在读取服务器返回的线程中调用了 Poll 方法查询当前这条 Socket 连接是否出错了导致的。


修改完了之后,我又再看了一遍.NET Framework 中关于 Poll 方法的文档,然后看到了这么一段话:

This method cannot detect certain kinds of connection problems, such as a broken network cable, or that the remote host was shut down ungracefully. You must attempt to send or receive data to detect these kinds of errors.

尼玛啊,好吧,那还得想办法来针对这两种非正常断开与服务器连接的情况进行处理,好吧,等着我啊。