分类目录归档:Unity3D

Unity3D Mecanim 中 Transition 的 Atomic 属性是个什么鬼

我们游戏中一直有一个让我非常恼火的问题,关键是查了已经无数次了,根本尼玛不屌我啊,像我如此这般坚强的程序猿都要拜倒了。谁知前几天再次出现,因为已经把 Log 打得有点天罗地网的意思了,而且还持久化到磁盘 Log 文件上了,这下也不担心查着查着突然被其他的事情打断之后,然后再次回来想查问题的时候发现 Log 已经不见了,尼玛不是说好的要保护第一现场的咩?

不扯淡了,先说这个问题是啥。我们的游戏中,对于战斗 NPC 的动作都加了 AnimationEvent 回调,游戏中很多逻辑都依赖于 AnimationEvent 的回调,例如战斗 NPC 释放了一个技能,那么在这个技能动画刚刚开始播放和即将结束的某个时间点,会分别调用 OnSkillStart 和 OnSkillEnd 这两个 AnimationEvent,在这两个回调函数中会有相应的处理逻辑,例如在 OnSkillStart 中给 NPC 加上霸体的 Buf,在 OnSkillEnd 中将 NPC 的 Buf 取消。我碰到的诡异现象是这样的,NPC 的技能 A 的动画时间长度为 3 秒,OnSkillStart 方法和 OnSkillEnd 方法分别处于整个动画时间轴的 0.2 秒和 2.8 秒,NPC 在 AI 判断确定释放技能 A,在技能 A 动画播放到 0.1 秒的时候,受到了主角的攻击,此时 NPC 应该立即切换到受击状态,开始播放受击动画,但是事实上却不是这样,我们能看到的日志信息如下:

10:34:05.565 AM ya_zhang_tie_qi_jun(Clone)001 TriggerSkill ya_zhang_tie_qi_jun_attack_3

10:34:05.573 AM #Network# GameClient Emit a Message: [Action, Ping]

10:34:05.667 AM Player OnSkillStart: player_female_suplex_back
10:34:05.792 AM Player OnSkillTrigger (player_female_suplex_back#0)
10:34:05.799 AM ya_zhang_tie_qi_jun(Clone)001 OnHookAttackHit by Player with player_female_suplex_back#0
10:34:05.803 AM ya_zhang_tie_qi_jun(Clone)001 Hit to Hooking from None
10:34:05.817 AM ya_zhang_tie_qi_jun(Clone)001 OnSkillStart: ya_zhang_tie_qi_jun_attack_3
10:34:06.297 AM GameClient Received a Message: [Action, Ping]

10:34:06.338 AM Player OnSkillTrigger (player_female_suplex_back#1)
10:34:06.345 AM ya_zhang_tie_qi_jun(Clone)001 OnSuplexAttackHit by Player with player_female_suplex_back#1
10:34:06.352 AM ya_zhang_tie_qi_jun(Clone)001 is SuperArmored, cannot be Suplex
10:34:06.359 AM ya_zhang_tie_qi_jun(Clone)001 ApplySuperArmoredFX
10:34:06.744 AM ya_zhang_tie_qi_jun(Clone)001 CancelSuperArmoredFX
10:34:06.859 AM Player OnSkillEnd: player_female_suplex_back
10:34:08.589 AM #Network# GameClient Emit a Message: [Action, Ping]

从日志里头看到的信息就是这个牙帐铁骑军已经释放了一个技能,但是在其技能动画播放到 OnSkillStart 的时间点时,已经被主角攻击了,此时其并未立即切换到受击状态,而是继续播放其技能动画并且触发了 OnSkillStart 的回调。所以游戏的逻辑就出问题了,NPC 因为触发了 OnSkillStart 回调进入霸体状态了,然后主角后续的所有攻击都无法让 NPC 做出相应的受击动作了,关键是这个 NPC 在回调了 OnSkillStart 函数进入霸体之后呢,还是会进入到对应的受击状态,这下就奇了怪了。你要么就不进入受击状态呗,要么就别触发霸体啊,这不是要疯吗。

最后仔细分析排查,知道了这个问题是因为 OnSkillStart 的触发时间点处于动画过渡的时间区间内,也就是说 NPC 从 Idle 状态进入到技能 Attack_3 的状态时间长为 0.3 秒,并且这个动画的过渡 Atomic 属性为 True。问题就这么来了,NPC 在从 Idle 状态时释放技能 Attack_3,动画从 Idle 切换到 Attack_3,动画切换到 0.1 秒的时候,受到了主角的攻击,NPC 的整个 AnimatorController 状态图中,受击状态都是直接从 Any State 跳转过去的。按理来说就应该在任意状态下都能切换到受击状态才对啊,为啥动画还会继续往后播放呢,直到 OnSkillStart 回调完了之后再切换到受击状态呢?

最终发现了就是这个 Atomic 属性给闹的,这个 Atomic 属性的字面意思已经很清楚了,那就是当它为 True 的时候,这个过渡是原子的,也就是说只要进入了这个过渡,那么这个过渡就一定会播放完成,不论是发生任何情况这个过渡都会播放完成,而刚好我们的这个 OnSkillStart 回调函数所处的时间点就在这个过渡时间段里头,所以就出现了,虽然最终 NPC 还是进入了受击状态,但是在进入受击状态之前因为已经回调了 OnSkillStart 函数,所以导致逻辑上出现了错误。

这个问题呢,有两个解决方法:

  1. 取消所有的动画状态切换之间的过渡时间,让动画之间的切换不再有过渡的过程,都是直接切换,这样就可以避免出现过渡的时候触发 AnimationEvent 了;
  2. 将牙帐铁骑军的 AnimatorController 中从 Idle 到 Attack_3 状态的 Transition 的 Atomic 选项取消,允许该 Transition 被打断,这个解决方法看上去更有技术含量一些,也更符合我们设计意图,当我们不论处于一种什么状态的时候,受击之后就应该立刻切换到某个受击状态下,这个从语义上也很能说得通,对伐?

Unity3D中如何通过位运算来开启和关闭指定层

在 Unity3D 开发的过程中,我们经常会有选取指定层的需求,例如设定某个摄像机只渲染某几个层的对象,UI 摄像机通常就只选取 UI 所在的层进行渲染,而对于其他层的对象进行忽略;场景摄像机通常会对非 UI 层的所有对象进行渲染,那么除了在 Unity3D Editor 中对场景中已有的 GameObject 直接进行设置之外,我们还有更为灵活的方法吗?当然有了,只要通过简单的位运算就可以把这个需求满足了。

我们先用摄像机来举例吧,Camera 的 cullingMask 属性就是用来设置摄像机渲染层用的,例如我们想设置名为 mainCamera 的摄像机的渲染层为 UI 层和 PopUI 层,那么代码应该如下:

mainCamera.cullingMask = LayerMask.GetMask ("UI", "PopUI");

上面的这段代码非常容易理解,如果我们的需求就是针对有限的少数几个层进行设置的话,这种方法是最好用的。但是,我们通常都会有一些针对特定层进行动态设置的需求,例如我们需要动态地将 PopUI 层从 mainCamera 的 cullingMask 中剔除掉,那么我们应该怎么做呢?可以简单地依照上方的代码来做:

mainCamera.cullingMask = LayerMask.GetMask ("UI");

这样确实能满足我们的需求,但是这是在我们已经完全知道 mainCamera 具体需要渲染哪些层的前提下才可行的。例如我们有一个名为 fightCamera 的摄像机,其对非 UI 和 PopUI 层之外的所有层进行渲染,由于游戏中设计的层数较多,其渲染的层总数可能达到20个,那么此时使用上面的这种方式来关闭某个层那么就显得非常的蛋疼了,我们总不能把那20层的名称都写到代码里头吧,咱们暂且不说可能会写错,这看上去就不是个聪明的办法啊。

这个时候,我们就需要使用到位运算了,例如我们想确保名为 FX 的层在 fightCamera 的 cullingMask 中,那么我们只需要使用下面的代码就可以了:

int layerFXMask = LayerMask.GetMask ("UI");
// 这段代码和上面的代码功能是一样滴
// int layerFXMask = 1 << LayerMask.NameToLayer ("UI");
fightCamera.cullingMask |= layerFXMask;

如果想把 FX 所处的层给关闭了呢,那么使用下面的代码就行了:

int layerFXMask = LayerMask.GetMask ("UI");
// int layerFXMask = 1 << LayerMask.NameToLayer ("UI");
fightCamera.cullingMask &= ~layerFXMask;

如果是想开/关,这样来回切换 FX 所在层的开启状态,那么使用下面的代码就行了:

int layerFXMask = LayerMask.GetMask ("UI");
// int layerFXMask = 1 << LayerMask.NameToLayer ("UI");
fightCamera.cullingMask ^= layerFXMask;

以上这三种情况,分别为开启、关闭和开/关这三种状态,对应的是位运算的按位或、按位与(取反)和按位异或操作。

以上三种情况中最终得到的 cullingMask 值实际上就是一个 int 类型的数字,该方式适用于 Unity3D 中所有接受 LayerMask 作为参数的方法,例如 Physics.Raycast 方法:

public static bool Raycast(Vector3 origin, Vector3 direction, float maxDistance = Mathf.Infinity, int layerMask = DefaultRaycastLayers);

这个方法最后的参数就可以传入我们通过位运算获得的 LayerMask 值,用于指定我们需要进行碰撞检测的层。

如何在Unity3D中优雅的重命名脚本中字段名而不丢失其引用

在进行 Unity3D 开发的过程中,我们难免会碰到想要修改脚本中变量名的情况,可是呢,我们在代码编辑器或者 IDE 中对代码文件中使用到的变量进行 Refactor 之后呢,代码倒是好看了,也是不出错能编译通过了,但是到了实际运行的时候就发现出现各种空引用的问题了。

这是神马鬼啊?估计初学 Unity3D 开发的童鞋还会觉得尼玛简直不知道碰到什么鬼了,熟悉了 Unity3D 开发的童鞋呢,就会会心的一笑,尼玛的 Unity3D 简直不能再坑爹了,好吧,我再重新关联一下变量的引用吧。

这个问题的根源是啥呢?就是我们通常在脚本文件中使用了 Public 的变量,这些变量都会被 Serialized (序列化)保存到对应的 Prefab 文件并持久化到磁盘上,然后每次我们运行游戏的时候呢,Unity3D 会讲这些变量对应的资源引用加载到内存中来,这样我们就可以非常方便地在一个 Prefab 中愉快地使用另一个需要动态显示或者隐藏的 Prefab 或者 GameObject 了。这是我初次进行 Unity3D 开发时候,惊异于 Unity3D 的魔法之一。当然我现在每天都在施展这个魔法啦,不过有的时候呢,总会觉得有些不太爽,因为我有轻微的代码洁癖,只要我觉得这个字段命名的表意不准确,当我发现有更为准确的表意的变量名时,我一定会将变量名或者 Prefab/GameObject 的名称修改过来。修改 Prefab 和 GameObject 的名称倒是完全 OK,Unity3D 会自行帮你把事情做对,但是修改变量名就不一样了,例如在进行改名之后,GameControl.cs 文件中有一个名为 player 的 Public 变量,这个 player 变量指向一个场景中名为 Player 的 GameObject,此时我们的需求发生了改变,这个脚本需要控制两个 Player 了,原本名为 Player 的 GameObject 更名为 PlayerFemale 了,按照正常情况,我们的代码通常也是需要进行修改的,这样变量名和 GameObject 的名称就能保持一致了,方便阅读和理解代码,但是player变量一改名为playerFemale之后,再次运行游戏,就会得到一个空引用异常了,Unity3D 会提示你 playerFemale 变量的引用为空。

叨逼叨了这么多,那么怎么解决呢?尼玛我自己肯定解决不了,这个完全就依赖于 Unity3D 官方的解决方案了。原文链接在这里,大家直接前往围观。

我这里自己也备个忘,做法入下。

如果原先的变量名为 player,修改之后的变量名为 playerFemale,那么我们只需要在使用 Refactor 工具将 player 字段名重命名为 playerFemale,确保所有引用到该变量的代码文件中的 player 字段名称都修改过来了之后。在这个新的变量 playerFemale 上方一行,加入 [FormerlySerializedAs(“player”)] 即可。代码如下:

[FormerlySerializedAs("player")]
public GameObject playerFemale;

如果后续我们还要再将这个 playerFemale 字段名再修改为 playerAssassin 呢,那又肿么办?那就依顺序再加一行 [FormerlySerializedAs(“playerFemale”)] 在原有的 [FormerlySerializedAs(“player”)] 下方即可,代码如下:

[FormerlySerializedAs("player")]
[FormerlySerializedAs("playerFemale")]
public GameObject playerAssassin;

好了,奏是这个样子滴,好棒哒,希望 Unity3D 能变得越来越好用,省得被我们耀华一天到晚问候各种亲戚朋友,是吧。

【撒利学 Shader】之一 UV Mapping 的概念

UV Mapping就是UV贴图的意思,说白了就是将3D中的不规则多面体沿着某条线剪开之后,然后摊开到一个平面上,这样就可以将3D物体上的每个点映射到一张2D的平面贴图上了。

当然这个显然木有我说的这么简单,毕竟3D物体那么不规则,要将一张2D的贴图上的每个像素与3D物体上的各个顶点以及顶点之间映射上也不是件容易的事情,不过显然这个不是我能讨论和想讨论的事情了。我们只需要知道制作UV贴图是3D建模软件一个高级的基础功能就好了,Unity3D拿到这些UV贴图之后直接往模型上贴就好了,不过Unity3D又是如何读取2D贴图中的UV信息,并且能将这个贴图很好地贴到这个模型表面上捏?好吧,其实我也不知道,希望在这个学习Shader的过程中,我们最终能解答这个问题。

【撒利学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 不是那么了然的程序员的视角来记录学习过程中遇到的小问题,以及在解决这些问题的过程中获得的信息以及其相应的延伸内容。