月度归档:2015年07月

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

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

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

接下来我们来看两个图:capusle_collider_1动图1capsule_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 ps = go.GetComponent<ParticleSystem> ();
if (ps != null) {
    if (ps.renderer.sharedMaterial == null) {
        Debug.Log (string.Format ("{0} Render Has no Material", go.name));
    } else {
        var matName = ps.renderer.sharedMaterial.name;
        if (matName.StartsWith ("Default-Particle")) {
            Debug.Log (string.Format ("Prefab: {0}, Mat: {1}", go.name, matName));
            DestroyImmediate (ps, true);
        }
    }
}

执行一下,发现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进行显示的。那么我们就再修改一下我们的代码就好了。

ParticleSystem ps = go.GetComponent<ParticleSystem> ();
if (ps != null) {
    if (ps.renderer.sharedMaterial == null) {
        Debug.Log (string.Format ("{0} Render Has no Material", go.name));
    } else {
        var matName = ps.renderer.sharedMaterial.name;
        if (matName.StartsWith ("Default-Particle")) {
            Debug.Log (string.Format ("Prefab: {0}, Mat: {1}", go.name, matName));
            DestroyImmediate (ps, true);
        }
    }
}

ParticleSystemRenderer psr = go.GetComponent<ParticleSystemRenderer> ();
if (psr != null) {
    if (psr.sharedMaterial == null) {
        Debug.Log (string.Format ("{0} Render Has no Material", go.name));
        DestroyImmediate (psr, true);
    } else {
        var matName = psr.sharedMaterial.name;
        if (matName.StartsWith ("Default-Particle")) {
            Debug.Log (string.Format ("Prefab: {0}, Mat: {1}", go.name, matName));
            DestroyImmediate (psr, true);
        }
    }
}

也就是需要独立地将这个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都用上了,最终还是没有找到为啥。好吧,那就试试把昨天改的这点代码一点点恢复,最终发现就是因为这个鬼导致的。

bool pollError = TcpClientConnection.TcpClientConnection.Poll (-1, SelectMode.SelectError);

就是这个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.

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