作者归档:贺 利华

关于贺 利华

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

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 ();
}
}
}
}

C# Socket如何判断连接已断开

相信很多Unity3D的程序员都会遇到网络编程的情况,虽然目前可能大部分的手游都会优先选择使用非长连接技术来实现,也就是我们常说的直接使用Http来完成跟服务器通信的部分,但是还是难免会有遇到需要使用实时网络连接的时候。那么这时候,我们通常还是需要回到网络编程的根源,那就是直接使用Socket进行编程,.NET Framework中的Socket已经封装得非常的完备了,真的很好使。

但是在使用的过程中难免还是会碰到一丢丢问题的,对吧。今天我要记录的就是在处理连接已断开的这种情况。

在手游的使用场景中,以下情况可能会出现客户端与服务器出现无法连接的情况:

  1. 客户端无法连接到网络,例如:客户端设备当前接入的Wi-Fi热点无法连接到互联网,客户端设备进入无线网络信号很差的地方(例如某些大厦的电梯,某些地铁线路等等),这个时候其实客户端和服务器都没有主动关闭过这个TCP连接,但是网络已经无法正常连接了;
  2. 服务器主动关闭了这条TCP连接,服务器为什么要主动关闭掉我的这条TCP连接呢?设想一下,客户端跟服务器建立TCP连接之后,客户端在10分钟内都没有向服务器发送过任何数据,那么服务器还有必要保留这条连接吗?如果当前在线的玩家较多,服务器可用的连接数已经达到一个容限值,那么是否需要及时将某些已经不活跃(例如10分钟以上没有发送任何消息)的连接主动关闭,回收这些连接资源,以备其他新登录游戏的玩家设备连接使用呢?所以,当服务器主动关闭了TCP连接的时候,10分钟我上完厕所回来了,我重新拿起我的手机,想继续玩我的游戏,那么就需要处理客户端如何重新连接到服务器的问题了;
  3. 服务器网络出现问题,例如服务器所在机房链路出现问题,通过某些网络就是无法访问服务器,例如服务器网线不小心被挖断了或者网线被网管拔了,服务器电源烧了,主板坏了,等等诸如此类的物理设备出现故障的情况下,客户端是无法连接到服务器的,服务器也不可能通过TCP协议给客户端发送什么FIN,ACK,SYN等等状态码啦,这个时候客户端也需要有正常的逻辑来处理这个情况。

其实说了这么多就是想表达一下,我们的客户端可能在任何情况下与服务器失去连接,而这个时候客户端需要合理地来处理这种连接已断开的情况。

我们可以查看一下.NET Framework的文档,看看Socket中的一些相关的属性和方法:

Socket.Connected Property

Gets a value that indicates whether a Socket is connected to a remote host as of the last Send or Receive operation.

The Connected property gets the connection state of the Socket as of the last I/O operation. When it returns false, the Socket was either never connected, or is no longer connected.

The value of the Connected property reflects the state of the connection as of the most recent operation. If you need to determine the current state of the connection, make a nonblocking, zero-byte Send call. If the call returns successfully or throws a WAEWOULDBLOCK error code (10035), then the socket is still connected; otherwise, the socket is no longer connected.

If you call Connect on a User Datagram Protocol (UDP) socket, the Connected property always returns true; however, this action does not change the inherent connectionless nature of UDP.

这个是Connected属性的相关描述,大体的意思就是这个Connected属性会根据当前这个Socket最后一次的I/O操作来进行设定,也就是说这个属性为True还是为False会在Socket最后一次I/O操作完毕之后进行设定,应该也是根据错误返回码SocketError来进行设定的。如果我们使用的是阻塞的Socket的话,那么这个状态会在最后一次I/O操作出错后将这个属性设置为False,如果使用的是非阻塞的Socket就需要我们自己通过发送一个零字节的数据包,然后根据错误返回码来判断Socket连接是否断开。


 

Socket.Receive Method (Byte[], Int32, Int32, SocketFlags, SocketError)

Receives data from a bound Socket into a receive buffer, using the specified SocketFlags.

在我们的项目中我们使用了这个方法来读取Socket中的数据 ,这个方法中的最后一个参数是一个out参数,也就是如果我们在读取数据的时候出现了一些异常,错误码会通过这个参数返回。


Socket.Send Method (Byte[], Int32, Int32, SocketFlags, SocketError)

Sends the specified number of bytes of data to a connected Socket, starting at the specified offset, and using the specified SocketFlags

我们使用这个Send方法来进行数据的发送,同样也是通过最后一个out参数SocketError来进行错误码的返回。

这个SocketError类型中呢有很多很多预定义的错误码,这个需要我们仔细查阅看看有哪些是我们需要使用到的,这个因项目类型和需求不同,这里不一一阐述了。

那么在我们实际的开发过程中呢,我们肯定可以通过判断SocketError错误码来进行一些异常情况的处理,不过有些时候我们其实并不想在出现异常了之后才处理,例如我有一个发送消息的队列,有一条消息发送出错了,我还得把这条消息再放回队列,可是如果直接入队到消息队列的队尾可能会有消息先后顺序的问题等等。所以有些时候我们就需要在异常出现之前就想好解决方法,也就是在我们读取或者发送数据之前就先检查一下连接是否正常消息是否能够成功发送出去,那么这又要肿么搞呢?

先看看Poll方法的描述吧:

Socket.Poll Method

Determines the status of the Socket.

Remarks

The Poll method will check the state of the Socket. Specify SelectMode.SelectRead for the selectMode parameter to determine if the Socket is readable. Specify SelectMode.SelectWrite to determine if the Socket is writable. Use SelectMode.SelectError to detect an error condition. 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. If you want to check the status of multiple sockets, you might prefer to use the Select method.

 看上去这个方法貌似就是我们想要的呢,对吧?如果你这么想那就太Naive了,在实际使用的过程中,我就犯了一个不仔细看文档,导致栽了一个大跟头的错误。我写了一段代码用来判断当前Socket是否已经跟服务器断开连接。

Socket socket;
// 省去初始化和连接服务器代码若干
if (!socket.Poll(1000, SelectMode.SelectRead)) {
// 做错误提示等相关的操作
}

事实证明这样是瞎搞,因为.NET Framework的文档关于SelectMode.SelectRead这样描述的。

true if Listen has been called and a connection is pending;
-or-
true if data is available for reading;
-or-
true if the connection has been closed, reset, or terminated;
otherwise, returns false.

你说这不是坑爹是啥?这个意思就是说,如果我们是一个服务器端的Socket,调用了Listen方法,并且有一个外部连接在等待我们处理的时候,这个Poll方法会返回true,如果当前Socket中有数据可以读取时,这个Poll方法也会返回true,或者当这个Socket连接被关闭、重置、强制终止了也会返回true,否则返回false。

实际测试的时候,发现这个值只在Socket缓冲区中有数据可以读取和连接被服务器关闭了之后才会返回true,好吧,我承认自己是猪吧。那么回到我们的初衷上来,我们还是希望能在读取数据之前就能知道这个连接是否已经被关闭了(是否与服务器断开连接了)。那么可以通过下面这段代码来进行判断。

// 省去初始化和连接服务器代码若干
if (socket.Available == 0 && socket.Poll(1000, SelectMode.SelectRead)) {
// 因为根据文档的描述,如果连接正常的话,这两个条件不可能同时成立
// 只有在连接已断开的时候,才可能会出现这两个条件同时成立的情况
}

写到这里的时候,突然之间很想看看Available这个属性又是怎么一回事,好吧,我们再来看看.NET Framework的文档吧。

Socket.Available Property

Gets the amount of data that has been received from the network and is available to be read.

Remarks

If you are using a non-blocking Socket, Available is a good way to determine whether data is queued for reading, before calling Receive. The available data is the total amount of data queued in the network buffer for reading. If no data is queued in the network buffer, Available returns 0.

If the remote host shuts down or closes the connection, Available can throw a SocketException. If you receive a SocketException, use the SocketException.ErrorCode property to obtain the specific error code. After you have obtained this code, refer to the Windows Sockets version 2 API error code documentation in the MSDN library for a detailed description of the error.

虽然文档中提到如果我们使用的是非阻塞的Socket的话,那么Available属性将会是一个用于检测Socket缓冲区中是否有数据可以Receive的好选择,这样我们如果使用阻塞的Socket难道就不能用了吗?当然也不是,肯定还是可以用的,只是说非阻塞的Socket如果一直循环调用Receive很有可能会做无用功,因为如果当前Socket缓冲区中没有数据可以读取的话,Receive方法会很快速的结束调用并且抛出一个SocketException,而如果每次Receive之前检查一下变量的成本相对于调用一次Receive方法可能开销会更小一些吧。而对于阻塞的Socket来说,每次Receive调用都是阻塞直到有数据返回,或者超时后抛出一个超时异常。所以我们阻塞的Socket也是可以用Available这个属性来进行提前判断的。但是实际测试中,Unity3D中使用的Mono中的Socket实现并非和.NET Framework的文档描述一致。关于如果Remote Host关闭连接之后,再调用这个Available属性会抛出异常的行为就没有。

 关于SelectMode中另外两个SelectWrite和SelectError类型,显然我们也不能放过啊。一一测试之后发现,在服务器主动断开连接之后,通过Socket.Poll(1000, SelectMode.SelectWrite)返回一直都是true。即便我们通过Send方法发出去的字节数为0,返回的SocketError中的错误码为Shutdown,依然不为所动啊,所以对于这个SelectMode.SelectWrite我已经凌乱了,希望哪天可以找到合理的解释。

但是Socket.Poll(1000, SelectMode.SelectError)在服务器断开连接之后会在第一时间内从false变成true,貌似这个还是蛮靠谱的,但是看了一下文档之后我又有点凌乱了。

true if processing a Connect that does not block, and the connection has failed;
-or-
true if OutOfBandInline is not set and out-of-band data is available;
otherwise, returns false.

这个意思是说,如果正在建立一个非阻塞的连接,然后这个连接失败了会返回true,或者没有设置OutOfBandInline但是又收到了out-of-band数据的时候会返回true,否则就返回false。

综合上面说的那一大堆东西,最终得出的结论就是Connected字段可以用,但是有滞后性,如果想通过这个字段来检测连接是否已经断开的话,就要明白这个字段是在我们通过Receive或者Send方法成功进行一次I/O操作之后,系统根据最后一次I/O操作的错误返回码来设定Connected字段的值的。

而如果想在进行I/O操作之前就检测到连接出现断开的情况的话,是可以通过Poll来达成目的的,当然这个会比较纠结。首先Poll配合SelectMode.SelectRead和SelectMode.SelectWrite都不能非常直接的反馈连接是否被断开 。但是貌似这个SelectMode.SelectError还是蛮靠谱的,所以呢,从目前测试得到的结果中,可以使用这个来提前判断连接是否已经断开。

不过在实际的应用中,我个人倒是更倾向于在使用SelectMode.SelectError做判断的同时,一定还要对在Receive和Send方法中返回的SocketError进行检查,这个返回码是直接从Native层返回的Socket错误码,非常可靠也很能说明问题。例如”HostDown”、”ShutDown“、”NetworkDown“等等状态码还是很能直观地反应当前Socket的状态的,所以还是需要好好地看看.NET Framework中关于SocketError的描述文档。

Unity3D中的碰撞检测

在游戏制作过程中,碰撞检测肯定是经常需要使用到的,例如子弹飞行之后与可攻击目标之间的碰撞,子弹飞行之后与环境中物体的碰撞等等,都需要用到碰撞检测。那么在Unity3D中Collider肿么使呢,又有啥需要注意的捏?

Unity3D中用于碰撞检测的组件叫Collider,由这个货派生出来的又有Box Collider,Sphere Collider,Capsule Collider,Mesh Collider和Wheel Collider,这里我们主要说一下我自己用过的几个货啊,我目前就使用过Box Collider,Capsule Collider和Mesh Collider,其实Sphere Collider应该也跟Box Collider、Capsule Collider没啥实质性的区别,只是适用于不同的物体罢了。不废话了,进正题。

Unity3D中使用碰撞需要注意些什么呢?

  1. 确保物体所处的Layer之间存在碰撞。在Unity3D中,如果需要两个物体之间发生碰撞,首先要确认这两个物体所在的层(Layer)之间是会有物理碰撞的,这个可以通过Edit->Project Settings->Physics打开PhysicsManager进行查看和编辑,编辑面板最下方有一个跟9×9乘法表一样的Mask(掩码)开关面板,如果想让A层和B层之间的物体有物理碰撞,那就找到A和B交叉的那个点,确认勾选上就可以了,如果想去除两个层之间的物理碰撞检测,那么就把勾选去掉就OK了,两个处于同一层的物体碰撞设置也同理哦。
  2. 确保用于碰撞检测的两个物体至少有一个物体上有Rigidbody组件,Unity3D的物理引擎实际上使用的是Nvidia的PhysX系统,这个引擎的所有计算都是通过Unity3D中的Rigidbody来进行表现的,所以这个Rigidbody是碰撞检测的必要条件。
  3. OnTriggerEnter和OnCollisionEnter的区别,理解这两个概念之前需要先了解Collider和Trigger的区别。在Unity3D中我们添加的组件都是Collider但是每种Collider组件都有一个属性『Is Trigger』,只要将这个属性设置为True,那么这个Collider就成为了Trigger,这两者的区别在于:
    • Collider之间(两个都没有设置Is Trigger属性为True)碰撞(接触)之后会回调Collider所在GameObject上绑定的脚本中的OnCollisionEnter方法,碰撞之后物理引擎会根据碰撞物体的质量,运动速度等等信息进行计算,计算碰撞之后物体的运动参数,会产生真实的物体碰撞效果。
    • Collider和Trigger之间(任意一个Collider设置了Is Trigger属性为True)碰撞(接触)之后回回调Trigger所在GameObject上绑定的脚本中的OnTriggerEnter方法,碰撞之后的物体依然会按照原有的运动状态继续在世界中运动,两者之间不会相互影响对方的状态。
    • Trigger之间(两个Collider都设置了Is Trigger属性为True)碰撞的情况同Collider和Trigger之间的碰撞。
    • OnTriggerEnter和OnCollisionEnter不会同时回调,永远都只会调用这两个方法中的其中一个。