标签归档:Unity3D

【撒利学Shader】写在前面的话

作为一个Unity3D开发者,我时常提醒自己并不是一个3D 游戏开发者,甚至都不是一个游戏开发者,更多的时候只是一个脚本程序员。这是我对自己的一个定位,也是一种现状的描述,我身边有不少做 Unity3D 开发的朋友,他们对于3D 程序开发实际上了解得并不多,但是貌似也能胜任他们正在做的工作。

这是 Unity3D 带来的一种效应,有其积极的一面,同样也有其消极的一面。积极的一面是,更多原本对3D 编程敬而远之却对游戏编程怀有兴趣的人,可以就着 Unity3D 直接开工,上手去做一些有趣的事情了,降低了整个3D 游戏开发的门槛,让游戏编程更加的亲民了;消极的一面呢,这部分半路出家杀到 Unity3D 开发圈子里头来的童鞋们呢,由于 3D 编程基础几乎为 0,所以在涉及到 3D 图形技术领域很可能直接就抓瞎了,整半天完全摸不着头脑的并不是啥奇事。我就是这半天摸不着头脑的人之一,为了让自己能摸着头脑,那么就只能从零开始,一点点琢磨了。

3D 编程中诸多的技术问题,Unity3D 通过它那万能的 Editor 实际上已经解决了很多 3D 编程中需要去面对的问题了,例如摄像机、坐标运算和坐标系转换、光照、投影,渲染等等等等。而在 Unity3D 开发过程中,可能让我这种非正规 3D 游戏开发者最为头疼的可能就是对 Shader 的一知半解了,而在一个 3D 开发者的成长之路上,这个问题早晚得是我们需要直接面对并且勇敢翻过去的一座山,唯有如此我们才能看到更加美丽的风景,如果我们有女朋友的话,还可以带着女朋友一起看。

作为一个 Shader 小白,我们最好的学习方法是啥?当然是看书咯,不过貌似这方面的书真心不多,曾经有一本《GPU 编程精粹》如今依然绝版,今天在网上搜寻了一番,最近貌似新出了两本 Shader 相关的图书,直接立刻下单《Unity 3D ShaderLab 开发实战详解(第 2 版)》和《Unity 着色器和屏幕特效开发秘笈 [Unity Shaders and Effects Cookbook],买回来看看先。

不过作为程序员的我们,在碰到问题之后的第一反应通常都是找文档,第二反应就是找 Google,对吧。不过相信所有 Unity3D 的开发者都会有一个这样的感受,Unity3D 的开发文档跟翔的区别可能都没有半毛钱那么多,所以我们也就能从官方文档中获得有限而又可怜的帮助了。剩下的我们就交给 Google 吧,Google 了一番之后,我倒是发现了两个比较不错的教程。

一个是猫大@onevcat的猫都能学会的 Unity3D Shader 系列文件两篇:

另一个是90后大神@浅墨_毛星云的【浅墨 Unity3D Shader 编程】系列文章了:

感谢两位非常棒的分享,让我能快速地理解了 Shader 的基础用法,用了差不多3天时间消化了一下这几篇文章,现在已经能对着各种 Shader 插件中提供的 Shader 逐句分析其用意和用途了。

不过本着记录和分享,以及锻炼一下自己技术写作能力,敦促自己继续学习的目的,我想把我自己持续学习 Shader 的整个历程记录下来。所以这个系列并不会像浅墨_毛星云或者 onevcat 的文章一般抽丝剥茧地从头细说 Shader 的学习路线,而是会以一个对 Unity3D 开发已经较为熟悉,但是对 Shader 不是那么了然的程序员的视角来记录学习过程中遇到的小问题,以及在解决这些问题的过程中获得的信息以及其相应的延伸内容。

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