月度归档:2015年05月

Unity3D中使用位运算判断LayerMask是否匹配

在Unity3D开发的过程中,偶尔会碰到需要对物体的Layer进行匹配运算,例如针对不同Layer的GameObject我们可能有不同的处理逻辑。
这个时候能使用位运算来完成这个匹配的话,肯定比我们写一堆的==显得更有逼格啊。那么具体要怎么做呢?

是否直接通过GameObject.layer获取到Layer的值,然后直接跟LayerMask进行位运算呢?不是。因为我们通过GameObject.layer获取到的Layer的值,是我们通过Unity3D Editor中的Edit -> Project Setting -> Tags and Layers 进行Layer设置时候的Layer的数值,这是从0开始依次递增的连续的值,这个显然不适合我们用来做位运算。

实际上我们在通过GameObject.layer获取到Layer值之后,需要先进行移位操作,然后再进行位运算『与』,示例代码如下:

bool IsInLayerMask(GameObject obj, LayerMask layerMask)
{
    // 根据Layer数值进行移位获得用于运算的Mask值
    int objLayerMask = (1 << obj.layer);     
    return (layerMask.value & objLayerMask) > 0;
}

AnimationUtility.GetAnimationEvents获取的动画片段信息中time和SerializedObject对象中保存的time字段信息的区别

今天的工作中原本是需要做一个小工具来批量给某些动画加上一个事件,也就是AnimationEvent这货。查了一下官方文档,也Google了一番,最终在这里找到一个比较符合我需求的解决方案,方案是由Unity Technologies的童鞋提供的,看上去还是蛮靠谱的,传送地址在这里

我也就果断拿过来用了,当然前提还是我们自己需要做一番修改的,因为我的需求是只需要在原有的AnimationClip已有的AnimationEvent列表的最后面添加一个OnSkillEnd的AnimationEvent,所以我需要先获取到已有的AnimationEvent信息列表,这里我就想着直接用AnimationUtility.GetAnimationEvents方法来搞定了,而且我看到前面提到的Unity Technologies的童鞋也是这么做的,那么我也就想着这么搞应该就OK了。

不过事实证明这样还是有问题的,因为通过AnimationUtility.GetAnimationEvents获得的AnimationEvent信息列表中得所有AnimationEvent实例中的time字段确实是触发时间,也就是动画播放的具体时间,是绝对时间。而Unity实际保存到动画FBX文件对应的meta文件中的time字段是相对动画播放进度,可以认为是相对时间。

当我们使用文本方式打开动画FBX的meta文件时,我们会发现在这个YAML文件中有一个events节点,这个节点下面保存的就都是这个AnimationClip上绑定的所有AnimationEvent信息,我们找到time字段看一下就会发现,所有Event节点中的time字段都小于等于1.0,然后我们再通过AnimationUtility.GetAnimationEvents方法将AnimationClip中的AnimationEvent信息读取出来,我们会发现,这个AnimationEvent中time实际上就是等于meta文件中的time字段(动画播放进度百分比)乘以AnimationClip的完整时长。

我们可以看一段YAML文件:

events:
      - time: .0572792366
        functionName: OnSkillTrigger
        data: a_ke_zhan_attack_1#0
        objectReferenceParameter: {instanceID: 0}
        floatParameter: 0
        intParameter: 0
        messageOptions: 0
      - time: .143198088
        functionName: OnSkillTrigger
        data: a_ke_zhan_attack_1#1
        objectReferenceParameter: {instanceID: 0}
        floatParameter: 0
        intParameter: 0
        messageOptions: 0
      - time: .393794745
        functionName: OnSkillTrigger
        data: a_ke_zhan_attack_1#2
        objectReferenceParameter: {instanceID: 0}
        floatParameter: 0
        intParameter: 0
        messageOptions: 0
      - time: .744630039
        functionName: OnSkillTrigger
        data: a_ke_zhan_attack_1#3
        objectReferenceParameter: {instanceID: 0}
        floatParameter: 0
        intParameter: 0
        messageOptions: 0

所以为了避免出现使用实际时间来作为相对时间保存,我们可以选择自行换算或者直接采用同一个标准进行计算,例如我们读取的时候采用直接读取SerializedObject对象中的SerializedProperty来获取time字段,保存的时候也通过SerializedObject对象中SerializedProperty来设置新的time字段就可以了。

NGUI中Panel切换Camera之后丢失尺寸大小的问题

最近在项目中碰到一个比较诡异的问题,让自己着实头疼了一阵子。

具体问题是这样的:

游戏中使用PoolManager将使用过的所有界面Panel进行了缓存,在需要的时候从Pool中Spawn。

而为了避免每个Pool中所有的Prefab实例在切换场景时被自动Destory掉,我选择了将这部分通用的弹出式的UI所储存的Pool设置为dontDestroyOnLoad,这样一来这些界面就会一直存在内存里头,省去了来回内存释放又再次请求的过程,只需要每次从Pool里头Spawn就可以了。

当PanelA在Scene_1中显示过一次,那么PanelA就已经缓存到UIPool中了,当我们切换到Scene_2时,UIPool并不会自动Destory掉。那么这个时候如果我们需要显示PanelA,我们只需要调用SpawnPool的Spawn方法就可以直接把PanelA显示出来就好了。问题来了,这个时候显示的PanelA的尺寸完全不对,原本应该与屏幕宽度完全匹配的PanelA变成了宽度只有2个像素了。

我们这么辛苦地搞了一个自己认为可以节省内存重复申请和释放过程的机制,肯定不能因为碰到这个问题就放弃了对吧。那就开始找问题吧,那么为什么这个PanelA的宽度会最终变成了2呢?

首先,我们先来看看这个Panel的GameObject层级图:

UIPanel

|– UISprite(使用Anchors进行背景图尺寸和位置的设置)

|– UISprite

通过Unity3D Editor中,我们发现通过Anchors来进行尺寸和位置设置的背景UISprite的尺寸和位置相对于UIPanel这个根节点确实没有出错,Left Anchor对应UIPanel的Left相对距离为0,Right Anchor对应UIPanel的Right相对距离为0,Top Anchor对应UIPanel的Bottom的相对距离为100,Bottom Anchor对应UIPanel的Bottom相对距离为0。也就是说背景UISprite的高度还是正确的,但是由于整个UIPanel的尺寸出错了,所以导致了背景UISprite的尺寸和相对位置出现了错误。

那么怎么解决这个问题呢?首先我们可以确认UISprite是通过Anchors来设置尺寸和位置,我们可以在NGUI中找到对应的代码,这段代码在UIWidget.cs文件中的OnAnchor()方法中,这个方法中会获取Anchor设置中相对物体的边界信息,在我们这个案例中这个相对物体就是UIPanel了。UIPanel.cs中通过GetSides()方法来获取UIPanel的尺寸和位置信息,而这个最终的计算会依赖于UIPanel所处Layer的Camera。

这下问题就明朗了,因为我们将UIPanel在场景A中创建并显示出来,之后当UIPanel不再显示的时候将其缓存起来了,但是当我们再次从其他场景切换到场景A中时,我们又再次通过Spawn方法将UIPanel显示出来。此时UIPanel依然会调用其继承自UIRect的GetSlides方法来获取边界信息。

public virtual Vector3[] GetSides (Transform relativeTo)
{
if (anchorCamera != null) return mCam.GetSides(cameraRayDistance, relativeTo);

Vector3 pos = cachedTransform.position;
for (int i = 0; i &lt; 4; ++i)
mSides[i] = pos;

if (relativeTo != null)
{
for (int i = 0; i &lt; 4; ++i)
mSides[i] = relativeTo.InverseTransformPoint(mSides[i]);
}
return mSides;
}

而此时anchorCamera已经为空了,因为我们切换场景的时候,UIPanel首次Spawn出来时自动选择的UI层的Camera对象已经被Destroy了, 而现在我们重新将UIPanel显示出来的时候,UIPanel并不会自动重新选择当前场景中UI层的Camera对象,所以这里就需要我们手动来设置一下UIPanel的Camera。

仔细看了一下UIPanel初始化的时候是如何设置Anchors的,然后发现了有ResetAnchors、UpdateAnchors和ResetAndUpdateAnchors和ResetAndUpdateAnchors就是我们想要找的货了。也就是说我们在当场景切换成功之后,应该手动将SpawnPool中缓存的Panels对象调用一遍ResetAndUpdateAnchors方法,这样一来就不会再出现因为丢失Camera对象,而导致UIPanel尺寸不正确的问题了。我们只需要在OnLevelWasLoaded的回调函数中这些处理一下就好了:

void OnLevelWasLoaded (int level)
{
Debug.Log ("OnLevelWasLoaded: " + level);
foreach (KeyValuePair&lt;string, PrefabPool&gt; pair in _uiSpawnPool.prefabPools) {
PrefabPool pool = pair.Value;
foreach (Transform xform in pool.spawned) {
UIPanel panel = xform.GetComponent ();
if (panel != null) {
panel.ResetAndUpdateAnchors ();
}
}
foreach (Transform xform in pool.despawned) {
UIPanel panel = xform.GetComponent ();
if (panel != null) {
panel.ResetAndUpdateAnchors ();
}
}
}
}