作者归档:贺 利华

关于贺 利华

正在学习编程,享受编程 热爱文学,闲来读读《读库》 有思想,没理想 正在学会专注

突然想写点什么

今天是锤子的新手机“坚果”手机的发布会,由于技术故障,发布会延迟了一些时间,有网友戏称“罗永浩重新定义了7:30”。

我并没有完整地看完整个发布会,我也不是锤粉,也不是罗永浩的粉丝,得知罗永浩的一些相关事情大多是从《读库》的老六那儿偶尔听到一些花边新闻啥的,自己貌似看过老罗哪一年在海淀剧院的一个演讲,那会儿他的公司名叫“老罗和他的朋友们”,还是在做英语培训。

有一些感概主要是来自于看到锤子一步步地从零开始到现在这么不错的一个体量,成长为一家健康的企业,创造出来让大家喜欢的产品,很羡慕,当然也有一些唏嘘。

说得好听一点,我跟着之前公司的老板一起进入移动互联网创业的时候,老罗还在张罗着给自己的培训学校找教室呢。用我前老板的话说“我们起了个大早,赶了个晚集”,从2010年5月18日,进入Android开发这个圈子,从一款单机小游戏《黄金矿工——喜讯特别版》,到个人日程管理APP《喜讯天天》,到移动互联网照片分享社区《画说》/《MARK》,一个人扛着整个公司Android开发的大旗横冲直撞,锤炼了自己的手艺,淬炼了自己的筋骨,拓展了自己的视野,长膘了自己的腰围。

随着公司在移动互联网社交领域的失败——前老板如此下的结论,公司集体转型到手游,开始担任项目经理,当然更多的角色还是主力客户端程序和救火队员,产品最终如期发布,事情也做得越来越顺手,但是由于期望过高,让《铁血战神》背负了太多,最终未能撑起让整个公司崛起的大旗,反而成为了我在前任东家的告别项目,今天还有玩家反馈说是《铁血战神》没法登录了,以前的同事在群里还感慨了好一阵子。

如今自己跟几个前同事自己立了一个摊子做手游,但是困难重重,有产品上的,有技术上的,更多还是人的困难,不当家不知柴米油盐贵啊。创业一年有余,方知世事皆艰难,唯有咬牙挺过去,兴许能有机会看到胜利的曙光。

回到感慨这个话题,感慨的是什么呢?感慨的是,“滴滴打车”和“锤子手机”已经从那个创业的死人堆里头爬出来了,而我可能又将再次被埋葬在这一波的寒冬。

记得首次在媒体上看到“滴滴打车”的报道时很不以为然,然后没过多久听到朋友说起自己打车有在使用“滴滴打车”(那是冬天,在北京这货很有用,可以让你不用在寒风中站在马路边等车),再没过几天被一个出租司机——张师傅推荐安装了”滴滴打车“,从此”滴滴打车”成为了我生活中必备的工具,因为当时每天加班都到23:30以后,大部分时间都是凌晨2点左右,这个时候“滴滴打车”每天让我能快速舒服地打到回家的车(那时我在北苑上班,住在天通苑,这个点这个路段几乎百分之九十的出租师傅都会选择拒载,因为路途短,就是个起步价,而且还是往城外方向走,回来肯定放空车),当时跟泰峰聊,移动互联网目前为止让我感觉到改变了我自己生活的有两个APP,一是微信,二是滴滴打车。放个马后炮啊,当时看到每天报道说滴滴的技术不够强什么的,甚至看到过滴滴的招聘文章,如果那个时候真的这么坚定的话,也许可以加入滴滴做个开发人员呢,是不是?

在罗永浩说要做手机之初,看到很多报道,看到他跟很多人打嘴仗,当时只是觉得这些人怎么都觉得自己能做手机,真的有那么简单吗?持一种怀疑的态度,并不看好。当时我的同事张树立已经从前东家离开了,有一次吃饭听他聊起过他还去锤子手机参加过面试呢,不过最终的结果是他并没有加入锤子科技,选择了人人网。当时听他谈到去锤子面试的时候,内心其实是有一些小触动的,这个小触动是心想“如果我去了,被录取了的话,会是神马情况呢”这样的一种情绪。对于罗永浩这号人物,内心是很佩服的,例如在条件允许的情况下,坚决支持正版,坚持做公益捐赠,支持有理想的人去坚持他们的理想,,碰到让自己不爽的东西会触发处女座属性,这些都是我自己很欣赏的特质,我也是这么做的,从人格上来说,简直完美契合,而且他的演讲技巧简直让我拜服,人生的轨迹也足够精彩。能够追随一个这样的人去做些事情,即便最终未能获得普遍意义上的成功,我想也是成功的。

但是,我们终归还是那只留在了井底的蛤蟆,我们有痴心妄想的权利和欲望,但是没有跳出井口的能力和远见。也许未来还有更加美丽的风景在井口出现,我想什么时候要是能老腰一拧,纵身一跃,跳出那井口,站到井沿儿上去看那风景,甚至成为那风景,那个时候再感慨一声“你也有今天啊”,多么美好啊。

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一下就好了。搞定,洗洗睡了。