分类目录归档:Programming

Linode 上 VPS 重启后 MySQL 服务器挂了的恢复方法

做个备忘,省得每次遇见都得重新 Google 了。

这次 Linode 整体更新 XEN 的安全漏洞补丁之后,我的 VPS 重启之后,自己也没有注意,今天打开一下自己的博客站点,发现 WordPress 报了了一个「连接数据库出错」的问题,首页无法正常打开了。

之前也碰到过类似的问题,但是之前每次都是直接丢给我们可爱的耀华同学来搞定的,现在耀华同学不在身边,有的时候用起来就没有那么顺手了,所以捏,咱们还是得学会自己动手,丰衣足食啊。在询问了耀华同学咱们 MySQL 的错误日志在哪里无果之后,本想尝试直接使用耀华同学提供的通过 find 命令暴力查找了(事实证明这样可能可以找到,但是会比较麻烦),刚好看到网上有个默认地址提供了,直接 cd 到那个目录之后,看到一个长得很像的 error log 文件,打开一看,果了个然啊。打开看看错误日志里头的错误是啥,原来是 mysql 依赖的临时文件创建不成功,文件权限有问题,那么修改一下文件权限吧,修改好了文件权限之后,重启 mysql 服务,一切 OK。

那么我们来回顾一下整个流程是怎么样的。

  1. 发现 WordPress 报告 MySQL 数据库连接出错了,那么 ssh 到 VPS 上,执行一下 ps aux | grep mysql 命令,发现并没有 mysql 服务进程,可以肯定 mysql 服务没有成功启动;
  2. 那么接下来我们就要看看 mysql 服务为啥没有在此次 XEN 补丁升级后自动启动成功呢,找 error 日志来看吧,最终我们发现 rpm 默认安装的 mysql 错误日志路径在 /var/lib/mysql 目录下,跟 mysql 的核心数据库文件就在同一个目录,建议不要这么干,还是在 my.cnf 中好好地配置一下 mysql 的日志文件存放路径吧(由于我并非一个合格的服务器开发人员或维护人员,原谅我使用了默认配置);
  3. 找到这个名称为 li402-16.err(文件名格式为:[host-name].err)的错误日志文件,滚到最下面一行,看看错误是啥吧,/usr/libexec/mysqld: Can’t create/write to file ‘/dev/shm/ibinQuSV’ (Errcode: 13);
  4. 这下可以确认是因为不能往 /dev/shm 这个目录里头写入文件了,如果不是磁盘满了那么肯定就是权限不足,所以无法写入文件,但是这个目录其实是一个内存里头的临时文件目录,所以不会出现满了的情况,那么肯定的权限的问题;
  5. 接下来我们就应该给 mysql 用户分配 /dev/shm 目录的写权限就好了,由于我比较懒(实际上是不太会,这个会有安全隐患的),我直接把 /dev/shm 的权限修改为 777 了,建议还是自行严格地将写入权限分配给 mysql 用户吧;
  6. 然后通过 /etc/init.d/mysqld start 启动 mysql 服务;
  7. 再次打开博客站点首页,测试通过,OK 搞定了。

此事的原因很有可能是因为 VPS 重启之后,对于某些敏感文件目录,XEN 容器管理的原则就是将其恢复到默认的文件权限吧,不是很了解,但是直觉会是这样,也就是说以后碰到重启这样的情况再次出现 MySQL 启动错误,都应该先检查一下是否为临时文件写入权限的问题导致的。


喵的,这次 Linode VPS 硬件问题之后,机器再次重启了,泥煤的啊,突然发现,在这里的备忘压根达不到备忘的效果啊,VPS 挂了,博客打不开啊,你上哪儿看去呢?好像是个问题呢。

用 Python 写一个简单的 Alfred Workflow 用于颜色值的转换

由于自己是程序员,而且比较痴迷于使用各种工具,虽然可能这些工具并没有给我的效率带来太大的提高,但是我就是喜欢折腾这些软件,因为能给我带来快乐和满足感,你说谁不想爽呢。

Alfred 是我使用 Mac 之后安装的第二个工具软件(正常工作需要的开发软件和浏览器之类的通用软件不算),第一个是 Dash,第二个是 Alfred。Dash 是在试用了 5 分钟不到,查看 Android 帮助文档如飞的响应速度,让我直接购买了正版授权。Alfred 是在当做 Launch 软件使用了快 1 年之后,看了很多人提到的关于 Workflow 的增强功能之后,在某个非常想购物的日子里头买了一个 PowerPack,从此用上了 Workflow 的功能,当然在这一年里头实际上我也就只用了有道词典这么一个 Workflow,其他的几乎不怎么使用,主要还是习惯问题。

这几天手痒,总觉得自己不像个能折腾的程序员,刚好看到一个用于转化数字的 Workflow 感觉还不错,试用了一下之后,我发现其实我自己也有一个比较强的需求可以通过这个 Workflow 来完成。在做 Unity3D 的开发过程中,我们时常会需要使用到调色板来给界面上的文字进行着色,就是设置颜色值。但是美术一般提供过来的就是效果图,或者是 RGB 值,通常格式是这样的 (234,120,54),但是在实际的开发中我们并不是每次都是针对一个 UILabel(我们的 UI 使用 NGUI 开发)进行字体颜色设置,因为有的时候我们只是想突出显示一段文字中的某一部分,这个时候依赖于 NGUI UILabel 的 BBCode 机制,我们可以通过 [ffbbcc] 需要指定颜色显示的文字内容 [-] 这样的方式来实现同一个 UILabel 下的文字使用不同的颜色进行显示。

那么这个时候有一个能快速转换(234,120,54)为#ea7836 就有一些必要了,参考那个 Number Convertor 我就自己开始造轮子了。

Alfred Workflow设置页面

找到 Number Convertor 这个 Workflow,选中任意一个 Script Filter 右键,弹出菜单之后点击 Configure,或者直接双击也行,然后弹出一个配置页面,配置页面的右下方有一个 Open workflow folder 的按钮,点击之后会打开当前 Workflow 对应的所在目录,然后我们会发现这个目录下面有以下的内容:

Alfred Workflow对应目录

仔细查看一下,我们会发现所有的转化逻辑都是通过 number_convertor.py 文件来完成的,但是这个脚本依赖于一个叫 Workflow 的 Python 库,这个目录里头的 workflow 就是干这个用的。这个 Python 的库可以在 这里 找到。

以下是的脚本内容:

然后我们就可以通过 Workflow -> Add Templates -> Essentials -> Script Filter to Script 创建一个 Workflow,然后通过上面提到的方法进入设置页面打开 Workflow 所在目录,按照 alfred-workflow 的 Python 库的安装方式安装设置好 alfred-workflow,然后将写好的 Python 脚本放到相应的位置,最终目录里头的内容如下:

目录结构

然后再回到 Workflow 设置页面设置一下 keyword 和调用 Python 脚本的方法和参数就 OK 了。

QQ20150511-7@2x

同理可以再设置一个 HexToRGB 的行为:

QQ20150511-8@2x

转换成功到了之后,我们把结果保存到剪贴板中,并且通过鼠标拖拽 Script Filter 节点上的小圆点到 Copy to Clipboard 节点上,这样就能把 Script Filter 中获得的结果保存到系统的剪贴板中了。

Screen Shot 2015-05-11 at 11.35.16 PM

最后来个实际使用的截图吧:

Screen Shot 2015-05-11 at 11.38.12 PM

折腾完毕。

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 Mecanim 动画系统骨骼动画问题解决方法

这几天开始做游戏中跟动画相关的部分了,此次新项目我们决定一次从新开始,就是能用新的东西就都用新的东西,没有必要总是把自己局限在之前的认知里头,所以此次我们大胆而又现实的采用了 Unity 4.x 版本新增的 Mecanim 动画系统,并且我们果断地又选择了使用 Humanoid 动画类型。好吧,一切从新开始。

一切从新开始就意味着会有很多新的坑需要自己一个个去填,我们一直都拥有一颗坚强受虐的心不是吗。在我们的动画设计师将骨骼动画调好了之后,给了我一组 FBX 文件,导入 Unity 之后,预览一下这个动画吧。

animation-1

animation-2

 

 

 

 

 

 

我们可以很明显得看出这两个动画中人物角色的右手和武器之间的位置关系是不对的,跟动画设计师在 3DS Max 中制作和预览的效果也不对,好吧,碰到第一个坑,上吧。

通常我们先做的第一件事情就是先查查文档,看看 Unity 官方是否有一些最佳实践的建议,可是我翻遍了 Unity Manual 中关于 Mecanim Animation System 的所有文档没有看到相关的内容,而游戏引擎通常对于开发者来说就是个黑盒,出了问题除了查查看看别人是否碰到同样的问题也就是直接找 Support 这条路了,或者就是自己各种尝试呗。好吧,那就先 Google 一下吧,找啊找,找啊找,找了将近两天看了很多跟 Mecanim 动画相关的问题,但是就是没有找到这个这么基础的问题相关的帖子啥的,简直没有天理啊。

既然 Humanoid 不行,那我们试试 Generic 动画吧,这下就都 OK 了,那么显然动画资源自身肯定木有问题了,这下就确定肯定是 Unity 按照 Humanoid 动画导入 FBX 资源的时候有某些设置我们没有按照要求来做,所以导致了这个武器完全不跟着右手骨骼运动的情况。自己纠结了又一天,又是各种查啊各种试,完全木有进展。最后,只能求助于其他有经验的人了,通过我们的动画设计师,找到了一个他一个做技术美术的朋友,该大牛对 Unity 非常熟悉,专门解决项目中碰到的技术问题,哪里有问题哪里就有他。直接远程协助一下,看着大神一点点地调整,先是检查了一下 Avatar 设置中骨骼对应的关系是否正确,确认无误之后,再次运行看看效果依然如此,大牛稍微思考了一下,马上打开了 Unity 导入动画的页面,找到了 Animations 标签页,然后展开 Mask 设置中的 Transform 节点,将所有未勾选的人物角色骨骼节点都勾选上,然后 Apply 一下,再次运行游戏,搞定。

animation-3

animation-4

 

 

 

 

 

 

再回顾一下完整的调整过程吧,首先我们要确定我们出问题的骨骼具体是哪根,因为目前看到的现象是右手拿的刀和挂在腰上的刀鞘出现了问题,打开 Avatar 设置页面,查看一下,确定是 Bip001 Prop1 和 Bip001 Prop2 这两根骨骼出了问题。

Bip001 Prop1

 

确定了是这根骨骼出现问题了,然后进入到出现问题的动画文件 jin_wei_jun@run.FBX 的导入设置页面,打开 Animations 标签页 =》展开 Mask 节点 =》展开 Transform 子节点 =》找到左右手对应的刀和刀鞘的骨骼,勾选上,然后点击 Apply 按钮就好了:

import setting

QQ20140816-6@2x

QQ20140816-7@2x

 

 

 

 

 

 

 

 

 

 

虽然大神帮我们解决了这个困扰了我将近 4 天的问题,那么究竟为啥捏?其实在被困扰的 4 天里头,其实也发现了一些蛛丝马迹的,就是每次我们导入动画 jin_wei_jun@run.FBX 的时候,Unity 总会提示以下的警告:

MuscleClip ‘jin_wei_jun@run’ conversion warning: Bone position is different in avatar and animation
‘Bip001 L Thigh’ : position error = 31.473890 mm
‘Bip001 R Thigh’ : position error = 8.405679 mm
‘Bip001 L Clavicle’ : position error = 37.673038 mm
‘Bip001 R Clavicle’ : position error = 76.461647 mm

UnityEditor.DockArea:OnGUI()

和这样的一个警告:

MuscleClip ‘jin_wei_jun@run’ conversion warning: ‘Bip001/Bip001 Pelvis’ is between humanoid transforms and has rotation animation. This might lower retargeting quality.
MuscleClip ‘jin_wei_jun@run’conversion warning: ‘Bip001/Bip001 Pelvis/Bip001 Spine’ has translation animation. It is not supported.
MuscleClip ‘jin_wei_jun@run’ conversion warning: ‘Bip001/Bip001 Pelvis/Bip001 Spine’ is between humanoid transforms and has rotation animation. This might lower retargeting quality.
MuscleClip ‘jin_wei_jun@run’conversion warning: ‘Bip001/Bip001 Pelvis/Bip001 Spine/Bip001 L Thigh’ has translation animation. It is not supported.
MuscleClip ‘jin_wei_jun@run’conversion warning: ‘Bip001/Bip001 Pelvis/Bip001 Spine/Bip001 R Thigh’ has translation animation. It is not supported.
MuscleClip ‘jin_wei_jun@run’conversion warning: ‘Bip001/Bip001 Pelvis/Bip001 Spine/Bip001 Spine1/Bip001 Neck/Bip001 L Clavicle’ has translation animation. It is not supported.
MuscleClip ‘jin_wei_jun@run’conversion warning: ‘Bip001/Bip001 Pelvis/Bip001 Spine/Bip001 Spine1/Bip001 Neck/Bip001 R Clavicle’ has translation animation. It is not supported.

UnityEditor.DockArea:OnGUI()

这两个警告大体的意思是啥呢,刚开始自己没有太注意,后来总是找不到原因就想着会不会还就真是这俩警告给弄的呢,自己仔细看了一下。第一个警告的意思,应该说的是动画中有几个骨骼的位置跟这个动画实际使用的 Avatar 中骨骼的位置不相符,这个原因我大概能理解的原因是这样的。我们在制作的过程中会将模型文件和动画文件分开,我在导入模型文件的时候会创建一个属于这个模型的 Avatar,其他的动画都会直接引用这个 Avatar 文件而不会每个动画都创建自己的 Avatar,而不同的动画中人物可以会有一些不同的动作,这就会让动画中角色的一些骨骼位置和模型(模型默认姿态就是站姿)站立姿态时的骨骼的位置会出现一些不匹配的情况,所以这个可以理解了。那么第二个警告呢,这个警告都是说某根骨骼有位移动画和旋转动画,而这些动画会降低 Retargeting 的质量,同时 Humanoid 动画不支持在这些骨骼上使用位移动画。虽然这个警告中提到的带有位移和旋转动画的骨骼不少,但是涉及的就是角色躯干、胸部、臀部、左右手、左右脚这几个非常关键的骨骼,并没有提到影响到刀和刀鞘的骨骼 Bip001 Prop1 和 Bip001 Prop2 啊。

综合大神提供的解决方案,应该可以初步得出结论,导入 FBX 动画的时候,选择 Humanoid 动画类型进行导入的时候,Unity 会自行进行计算和判断,然后根据 Retargeting 最佳原则,设置 Animations 选项卡中 Mask 节点下 Transform 子节点中的骨骼是否需要在 Humanoid 动画中应用位移动画,而这个时候 Bip001 Prop1 和 Bip001 Prop2 这俩骨骼就被忽略了,所以我们需要手动的去勾选,如果有必要的话,那么就把所有的未勾选的骨骼都勾选上,然后再 Apply 一下吧,如果出现 Apply 一次不生效的话,重新重复设置遍,然后再 Apply 一下吧(我自己碰到了一次把所有的都勾选,然后 Apply 之后无效,重新展开之后发现只有部分被勾选上了,重新再全勾选 Apply 一次才好的情况)。

刚才我们提到了使用 Generic 动画的时候就不会出现这个问题对吧,那么为了印证这个结论是否正确,我们可以检查一下 Generic 动画导入设置页面中 Animations 标签页中 Mask 节点下 Transform 子节点中的骨骼节点是神马情况就好了。如下图,所有的节点默认都是勾选上的。

QQ20140816-10@2x

鉴于此,我们可以得出结论就是,在我们使用 Humanoid 动画类型导入 FBX 文件时,Unity 会以最佳匹配 Retargeting 规则的方式自行计算,看看那些骨骼是需要勾选 Transform 动画选项的,而默认使用 Generic 动画就会将所有的骨骼节点 Transform 动画选项都勾选上,所以效果是正确的。OK,所以问题到这里就彻底明了了。

如何学习编程语言

标题比较大,我自己能写的东西比较少,毕竟自己从学习编程到现在总共也就 6~7 年的时间,写代码的时间也就 4 年时间,谈不上有什么非常好的经验,不过这些年来也有一些想法和感触,自己记录一下。

大学期间,学校安排的计算机相关课程中有《计算机基础》《C 编程语言》《C++编程语言》《数据结构与算法》《数据库原理与技术》《空间数据库》以及一些计算机编程周边的课程,例如《复变函数》《数值分析》《空间分析》《运筹学》等等。回过头来看看,这些所有课程我的考试成绩貌似都不错,但实际上神马都木有深入学习过,只是为了应付考试,尼玛杯具啊。

在某一次《数据结构与算法》的课上,我们可爱又严厉的王盼成老师(昵称盼盼)在课间跟我们闲聊,语重心长地跟我们讲“做地信的最好能会写程序,这样未来空间会更大一些”,我当时也不知道哪根筋搭错了,然后在下面搭了一句“老师,每个人有每个人的活法,也不是所有人都希望自己成为一个程序猿”,老师听到我说完这句话之后回了一句“我相信每个人有每个人的死法”。在老师说完这句话之后,几秒中之内整个教室都安静了一会儿,然后又该开始上课了,之后的整堂课我都在想这个事情。然后我就开始学习编程了,之前我们学过的《C 编程语言》和《C++编程语言》我已经完全交回给老师了,而宿舍中其他的同学在 C/C++方面已经有了一些成绩了,出于当时的虚荣也罢不服输也好,我选择了跟宿舍其他同学不同的一条路来学习编程。

我开始学习 Java 编程语言,没有同学和老师可以请教,但是我有整个图书馆和网络做老师。从那个时候开始,每天都泡在图书馆的阅览室中读各种 Java 相关的编程书籍,读得很粗糙,但是确实读了不少,每天回到宿舍就开始看一些视频,上 JavaEye(先已更名位 ITEye)去看各种博客和帖子。那时候看得多,写得少,记得当时毕业找工作的简历上,我写的编程经验中有一句话是“有丰富的 Demo 经验”。在那个阶段,我接触了 Java 编程的各个方面,但是每个方面接触得都比较浅显,刚刚踏入编程世界的我,觉得 Java 编程语言中有太多太多的精彩了。这个阶段,应该算作我开始在编程上入门了。

加入超图之后,一直在做 JNI 相关的事情,这应该是 Java 编程世界中的一个非常冷门的方向,当时只有某些做中间件封装的企业会使用该技术来重用之前已有的 C/C++类库。我在超图待了一年的时间,写了一些 JNI 的代码,看了很多 JNI 封装 C/C++ SDK 的代码,对 JNI 也有了一个更全面的认识,但是对于 Java 编程语言本身并没有更深入的理解。

加入喜讯之后,开始做 Android 方面的开发,这在当时可真是个热门的方向,在移动互联网被鼓吹到风口浪尖时,Android 程序猿也算是非常抢手的资源啊。Android 的开发跟普通的 J2SE 开发没有任何不同的地方,在对 Android Framework 有了一个完整的认知之后,所有的代码无非也就是逻辑和 UI 的实现,客户端程序大抵如此。但是在喜讯我已经不再只是一个纯粹写程序实现的了,我需要自己来设计整个程序的结构,如何保证程序能快速响应未来的设计和需求变得更为重要。在喜讯天天、画说和 MARK 这三个项目中,我开始尝试着做不同的设计尝试,并且尝试着形成自己的编码风格,并在团队内推广。

如今开始做游戏,主要是在 Unity3D 环境下完成,Unity3D 下几乎所有的逻辑和功能代码都是通过 C#来完成(当然你也可以通过 JavaScript 和 Boo 来实现),在学习 C#上障碍相对较小,毕竟大部分的工作并不是跟语言的细节纠结而是完成具体的逻辑和功能,天下万般程序逻辑皆相通,所以也还算较为顺利的开始了 Unity3D 游戏开发。术业有专攻,每个人擅长的模块都不太一样,在游戏项目中,我主要负责的是网络通信模块,包括 HTTP 通信和 TCP 通信模块代码的实现。非常感谢已经有众多程序猿在这些方向上作出了很多的努力并且将其劳动成果分享出来。在这些可爱的人无私分享的前提下,我们可以减少很多的代码工作量,只需要阅读这些优美的代码,理解他们思考和实现的逻辑,在此基础上加入自己的思考,融合自己的逻辑实现即可完成我们需要的功能模块。

从自己开始真正地学习编程到现在,应该有五年有余,在这段时间中我不能说自己编程能力提高得有多么的快多么的高,我只能说通过自己偶尔的横向和纵向跟他人做一个简单的对比,发现自己的不足和找到自己的优势,整体来讲,我肯定不算低效和低能的程序猿,起码能算是一个合格的程序猿,那么我是如何学习的呢?

阅读,在刚刚开始接触编程的时候,阅读了大量的书籍,关于 Java 编程语言的,关于 Java 社区流行的各种框架的书籍,网络上很多程序猿们分享的博客文章和编程教程。初期大量的阅读,让我的视野在很早就打开了,不会只局限于某个细小的方向,并且通篇完整地阅读过一本关于 Java 编程语言的书籍对于我的影响非常大,我当时读的是《Thinking in Java》,通过这本书接触 Java 也算是一种幸运。通过阅读《Thinking in Java》,我了解了 Java 语言基础的使用方法,养成了还算不错的编码风格,同时接触到了 Java API 设计的原则和方法,明白了面向对象编程的概念。之后继续阅读了很多 Java 社区流行框架的书籍,例如 Struts, Spring, Hibernate, Lucene, Tomcat, Heritrix 等等,虽然我对这些我曾经都尝试过编写不少的 Demo 的框架如今了解很少,也无法使用他们来完成我想做的事情。但是这些阅读让我见识到了编程的美好,让我看到了世界上众多优秀的程序猿他们是如何通过自己的努力改变了众多程序猿的工作方式。与其说我在学习这些框架,不如说我在认识这些框架在 Java 社区中的生存方式,正是因为这种开放的社区驱动让我对 Java 编程热情猛增,直至最后毕业选择了做一个程序猿而不是在我上大学之前就一直向往的管理培训生。

社区,阅读 Java 编程相关的书籍,总会不经意的发现 Java 社区中众多开源的框架和这些开源框架的开发者都在网络上积极地做一些事情,出于简单的崇拜也好,和更深层次地学习也罢。在学习 Java 语言前期,我开始尝试泡在各种不同的 Java 社区中,虽然很多时候我完全不懂别人在聊什么在讨论什么,但是看到别人在讨论某些问题,能让我先有一个认知,然后做很多的扩展阅读,然后时间长了就会发现自己的认识逐渐在提高,慢慢地能看懂别人讨论的问题了,然后偶尔还能发个言,帮助甚大,提升明显。Java 社区中,我在 JavaEye(现已更名为 ITEye),在这个社区回过帖,潜过水,开过博客,写过文章,发过简历。还因为在 JavaEye 发布简历,途牛网的陈福炜给过我一个电话面试的机会,但是被我很傻逼的给浪费和错过了。社区能让你了解到很多方向的东西,能看到其他的人在关注什么在做什么,以及很多事情别人已有的解决方案,还能认识很多可爱的人。

搜索引擎,程序猿可以不会写 Hello World,但是一定要能熟练使用 Google,熟练使用 Google 并不难,但是也不是很简单。简单的输入一段自己的需求文字,奢求 Google 给你找到一个满意的答案,这个可能性比较小。Good Question 才能有 Good Answer。学会抽象出来自己碰到的问题和需求的关键字,尽量去除修饰词语,然后交给 Google 就好了,通常 Google 都能找到你想要的答案。很多时候我们搜索的问题在中文世界中是没有相关内容的,这个时候你就要能将你自己的问题和需求翻译成英文,然后再交给 Google 了,然后自己通过阅读英文内容来找到答案了,程序猿英文是一定要好的,不论你写什么程序,否则都是扯淡。

文档,不论我们是学习一门新的编程语言还是尝试使用一个新的框架,官方的文档总是最重要的工具和参考。学习 Java 的时候就需要熟练掌握如何从 JDK Help Documention 中找到自己需要的东西,学习 Android 就应该能从 Android Developers 官方站点上找到想要的知识点内容。这些文档中,不但明确地告诉我们如何去使用 API,更会让我们对整体语言和框架形成一个完整的认知,在后续的学习和编码过程中,我们会形成一个非常好的习惯,碰到不确定的问题直接找官方文档,看个究竟之后就能继续了。另外,熟悉 API 文档的同时也会让我们对整个 API 的结构有一个非常全面的认知,并且会形成使用 API 的习惯,当你需要使用一个之前未曾使用过的方法,我们会习惯性的认为某个类有某个方法,直接一试或者直接一查,往往屡试不爽,这能很大程度上节约时间提高效率。关键是熟练掌握 API 内容能带来的好处在前期做设计和技术预言的时候将会更加显著,很多时候我们在设计一个新功能,该功能需要某些接口来帮助我们完成一些事情,如果这时我们清楚地知道某些 API 是可用的,某些 API 是框架或者语言不提供,需要我们自行实现的,马上能辅助我们作出一个合适的判断,包括我们在做整体系统架构和技术选型时,这些都是非常有帮助的。

开源代码,站在巨人的肩膀上,你能看到更多更美的风景。开源的代码,通常都不会难看,通常都是非常优雅的实现,而且有着某个团队或者某人在持续改进的,阅读这些优雅的代码,能让我们学习别人的编码方式,看看别人的思考角度和实现风格。开源代码已经改变了整个世界,Linux 系统和基于 Linux 的 Android 系统已经是每时每刻在互联网和移动互联网上扮演着台前幕后的重要角色,我们每天都在用的各种应用和网络服务,几乎都不可能离开开源软件和代码的支持。阅读 Java API 实现的代码,阅读 Android Framework 和 Android Packages 的源码,真的学习到了很多的东西,包括形成良好的编程风格。

关注变化,编程技术每天都在进步,每一年都会有一些新鲜的技术和框架出来,保持对新鲜技术的关注和好奇,尝试着去使用他们,如果能接触到源码那就更好,尝试去阅读和改进。不一定要追随所有新的技术潮流,不是说每个新的技术都需要放到项目中来,也不是每个新的技术或框架都能解决你现有的问题。关注新的技术,其实就是关注它究竟解决了什么问题,这个问题是否就是你目前碰到的,如果是的话,那么就可以很好地解决你的问题。关注新的技术,也是关注它究竟是如何解决了这个问题,和我们自己的解决方法相对比一下,两个解决方法的优势和劣势分别在哪儿,尝试着将别人的优点借鉴一下,将自己的解决方案完善了,如果能将我们自己的优势反馈到社区中帮助到更多的人,显然是一件美妙的事情。

分享,与别人分享你的技术成果,总是能让我们收到更大的收获。开源社区就是分享精神和开放精神的最佳形式,每个人都可以将自己好的东西分享出来,不断地来改进已有的东西,然后每个能接触到的人都可以再从中获取到更好的东西。分享能将个人的能量放大,如果社区或团体足够大,那么分享出来的能量将被放大的倍数也会更大,分享的人收到的反馈也会更大。程序猿都是好面子滴,谁尼玛不希望那个被称为牛人的人是自己呢?尝试着跟身边的人分享自己的技术成果吧,尝试着跟更多的人分享自己的技术成果吧,GitHub 无疑是一个非常合适的去处,大胆地分享自己的代码吧,不要怕被人嘲笑,每个人提交的 patch 都是在给我们自己能力打补丁呢,拥抱那些人吧,那些人在你成为牛人的道路上扮演的可是推进式发动机的角色啊。

关注那些先进的生产力和代表先进生产力的人,例如 Markdown 代表着文档的先进生产力,使用 Git 管理代码的人代表了先进生产力,使用终端和脚本代替重复性鼠标操作的人代表了先进生产力,那些通过技术改变人们生活的人,他们代表了先进生产力。尝试着去关注这些事物和这些人,看看他们是如何做到的,他们做到这个程度付出了多少,以及他们是如何努力的。争取哪天我们也成为先进生产力的代表,那样岂非很爽,先进生产力(尼玛高中政治就学到了这个词)永远都不会被淘汰的。