作者归档:贺 利华

关于贺 利华

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

用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的库可以在这里找到。

以下是的脚本内容:

# -*- coding: utf-8 -*-
import sys
from workflow import Workflow

def main(wf):
    cmd = wf.args[0]
    input_str = wf.args[1]

    values_str = ""
    if cmd == "2rgb":
        values_str = conver_hex_to_rgb(input_str)
        desc = u'将%s转化为RGB格式' % (input_str)
    elif cmd == "2hex":
        values_str = convert_rgb_to_hex(input_str)
        desc = u'将%s转化为HEX格式' % (input_str)
    wf.add_item(title=values_str, subtitle=desc, arg=values_str, valid=True)
    wf.send_feedback()

def ignore_exception(IgnoreException=Exception,DefaultVal=None):
    """ Decorator for ignoring exception from a function
    e.g.   @ignore_exception(DivideByZero)
    e.g.2. ignore_exception(DivideByZero)(Divide)(2/0)
    """
    def dec(function):
        def _dec(*args, **kwargs):
            try:
                return function(*args, **kwargs)
            except IgnoreException:
                return DefaultVal
        return _dec
    return dec

sint = ignore_exception(ValueError,0)(int)


def convert_decimal_to_hex(numstr):
    decimal_var = sint(numstr)
    return "{0:#0{1}x}".format(decimal_var,4)[2:]

def convert_rgb_to_hex(rgb_str):
    if rgb_str.startswith("(") and rgb_str.endswith(")"):
        splits = rgb_str.strip("()").split(",")
        if (len(splits) == 3):
            red_decimal_str = splits[0]
            red_hex = convert_decimal_to_hex(red_decimal_str)
            green_decimal_str = splits[1]
            green_hex = convert_decimal_to_hex(green_decimal_str)
            blue_decimal_str = splits[2]
            blue_hex = convert_decimal_to_hex(blue_decimal_str)
            hex_str = "#" + red_hex + green_hex + blue_hex
            return hex_str
    	else:
    		return ""
    else:
        return ""

def conver_hex_to_rgb(hex_str):
	if hex_str.startswith("#"):
		hex_str = hex_str[1:]
        if len(hex_str) == 6:
            red_decimal_var = sint(hex_str[0:2],16)
            green_decimal_var = sint(hex_str[2:4],16)
            blue_decimal_var = sint(hex_str[4:6],16)
            return "(%d,%d,%d)" % (red_decimal_var, green_decimal_var, blue_decimal_var)
        elif len(hex_str) == 3:
            red_decimal_var = sint(hex_str[0:1] + hex_str[0:1],16)
            green_decimal_var = sint(hex_str[1:2] + hex_str[1:2],16)
            blue_decimal_var = sint(hex_str[2:3] + hex_str[2:3],16)
            return "(%d,%d,%d)" % (red_decimal_var, green_decimal_var, blue_decimal_var)
	else:
		return ""

if __name__ == u"__main__":
    wf = Workflow()
    sys.exit(wf.run(main))


然后我们就可以通过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

折腾完毕。

Unity3D插件之Behavior Designer Movement Pack For Apex Path的巨坑

在目前的项目中,我们有使用到一个叫Apex Path的插件来做自动寻路的事情,这个插件整体来说还是棒棒哒,完全对得起$65的价格,我在写这篇文章的时候,刚好这货又在做半价促销了,链接在此

今天先不展开聊这个Apex Path插件了,今天主要是要吐槽一下Behavior Designer插件的增强包Movement Pack,在初次接触到Behavior Designer插件之时,顿时觉得这货能帮忙解决AI行为树的大部分问题,立马买回来尝试了几把,发现真心不错,可以解决很多问题。然后在他们网站上发现还有一个额外的用于处理移动的增强包,果断再次入手。拿回来简单地测试了一番,感觉还不错。但是在后续的开发中,总是会发现一些奇怪的问题,大体的表现就是NPC完全在没有移动到应该移动到的攻击位置就开始攻击了以及类似的问题,而且这个问题是在有多个使用了Behavior Designer控制的NPC出现在场景中的时候才会出现。

这个真心奇了怪了,作为一个朝内程序猿,我一直都奔着崇洋媚外的态度来看待国外程序员大大们,一直认为他们好牛逼好牛逼,然后先入为主地认为是自己用得肯定有问题,多次检查了自己的代码,依然没能找到问题。最终我就只能搞了一个空场景,就放了两个NPC,让NPC做最简单的行为,把Log加上,尼玛最后知道真相的我,眼泪掉下来啊,有木有!

我们来看看Behavior Designer Movement Pack For Apex Path中Patrol脚本中关于Apex中UnitNavigationEventMessage回调的处理。

 // Add the waypoint back on the stack when the destination is reached
public override void Handle(UnitNavigationEventMessage message)
{
    switch (message.eventCode) {
        case UnitNavigationEventMessage.Event.WaypointReached:
            movableAgent.MoveTo(waypoints[waypointIndex].position, true);
            waypointIndex = (waypointIndex + 1) % waypoints.Length;
        break;
    }
}

然后我们对比一下Apex Path插件中自带的一个PatrolBehaviour脚本是如何处理这个UnitNavigationEventMessage的回调的。

        
void IHandleMessage.Handle(UnitNavigationEventMessage message)
{
    if (message.entity != this.gameObject || message.isHandled)
    {
        return;
    }

    if (message.eventCode == UnitNavigationEventMessage.Event.WaypointReached)
    {
        message.isHandled = true;
        MoveNext(true);
    } else if (message.eventCode == UnitNavigationEventMessage.Event.DestinationReached)
    {
        message.isHandled = true;
        StartCoroutine(DelayedMove());
    }
}

细心对比一下,我们就会发现,在后者的处理逻辑中,加入了一个关于这个消息的entity对象是否为当前脚本所绑定的GameObject对象的判断,如果当前接收到的消息不是当前这个GameObject发出的,是不做任何逻辑,直接return的。然后再回过头来看看Behavior Designer Movement Pack For Apex Path中的处理,你就知道问题出在哪儿了。

因为Apex Path的GameServices中的MessageBus实现机制是任何对象都可以通过GameServices.messageBus访问到这个全局静态的MessageBus对象,并且可以通过subscribe和unsubscribe方法来进行消息的订阅和取消订阅。也就因为这个任何MessageBus的全局机制,任意注册了的对象都会收到回调,所以我们需要在处理消息回调的时候自行判断这个消息是否是我们需要的,或者说需要判断这个消息是否是发给自己的。MessageBus的机制就是一个广播,只要有任何一个对象触发了事件,注册了消息的所有对象都会收到广播,所以需要收到广播消息的对象自行甄别这货是否是发给自己的。

Behavior Designer Movement Pack For Apex Path脚本中对于回调的处理显然是忽略了这个甄别的过程,所以就会出现一些完全不在意料之中的事情,一切都能解释得通了。

Opsive的这个团队在Behavior Designer的工作确实非常惊艳,让人竖大拇哥,但是在这种问题上犯错误只能让我觉得这是实习生做的,或者就是完全没有真正地使用过Apex Path,然后就直接推出了一个似是而非的Movement增强包,我是很不以为然的。今天看到这个Movement的插件还升级了,Update需要花费$10,鉴于这个问题,我想暂时还是不升级了,虽然我很想看看他们升级的版本中是否已经修复了这个低级错误,等我今天闲下来了,我得给他们发个邮件问问。


给Opsive团队发过邮件了,邮件回复说是新的版本中已经加入这个判断了,问题已经修复,回复内容如下:

Hello,

Thank you for letting me know. Have you imported the most recent version of the Apex Path integration off of the integrations page? This integration doesn’t include an ApexPathSteeringBase file. It only includes ApexPathMovement as the base class. Within that class there is a check for the GameObject:

        public virtual void Handle(UnitNavigationEventMessage message)

        {

            if (!message.entity.Equals(gameObject)) {

                return;

            }

            switch (message.eventCode) {

                case UnitNavigationEventMessage.Event.DestinationReached:

                    arrived = true;

                    break;

            }

        }

I am not setting message.isHandled to true because all I am doing when destination is reached is setting a flag so a new destination hasn’t been set yet.

Thank you,

Justin

Unity3D Mecanim动画AnimatorTransitionInfo和AnimatorStateInfo在角色移动和待机平滑切换中的应用

在之前的开发过程中一直仅仅使用到了AnimatorStateInfo这货,平时在做一些判断的时候还特意加入一个判断!Animator.IsInTransition(0)来确定当前这个Animator没有在进行动画过渡,可是这几天同事们总是反应游戏中主角移动起步很慢和停止移动的时候会出现滑步的情况,这个不能忍,听得我很是汗颜啊。

好吧,汗颜的事情就先不表了,我们来看看这个可能是神马问题吧。通常我们游戏当中,角色都会有一个待机的动作,跑步和行走都会有相应地动作,而不同的动作之间的切换,Unity3D会自行做动画融合,这样主角从待机动作切换到跑步的动作时,就不会出现一帧直接切换导致看起来非常机械和卡顿的问题,看起来整个过程会是非常平滑的,这个就是我们要谈到的重点了。

在之前的实现中,我在主角的移动控制脚本PlayerMovement中使用了一段这样的代码来判断当前女主角已经成功的从Idle状态切换到Locomotion状态了

AnimatorStateInfo animInfo = animator.GetCurrentAnimatorStateInfo (0);
if (animInfo.isName ("Locomotion")) {
    // 这里控制角色的位置移动
}

然后在控制角色位置移动的代码段中,我们根据需要进行计算获得女主角在不同坐标上的移动位置,然后将位移作用到角色上。

但是这样做会有什么问题呢?

  • 首先,当角色在从Idle切换到Locomotion动画的过渡中时,上面的这段代码会直接忽略动画过渡的这段时间,所以在女主角从静止起步到跑步的过程中,Animator实际上一直都会保持为Idle的状态,直到整个Transition完成了,Animator的State才会切换到Locomotion,这样的话女主角实际上是在原地播放了这个过渡的动画,而这段时间的动画中,女主角的脚步会从静止切换到小碎步,再到大步跑,而这个时候女主角的位置不会发生变化(没有在女主角跑动动画对应的方向上移动),最终的结果就是主角看起来反应非常慢,起步的时候会出现卡顿,要在原地跑一段时间之后才会移动,这显然是不能接受的。
  • 其次,当角色从Locomotion切换到Idle动画的过渡中时,依然会出现逻辑被忽略的情况,也就是角色实际上已经在从Locomotion切换到Idle了,角色的脚步动作越来越小了,但是因为Animator的设计是在切换过程中,State的名字不会改变,会保持为状态切换之前的状态名,也就是在动画完全切换到Idle之前,State的名字一直都是Locomotion,所以主角在这个时间里头,逻辑会让主角继续运动(因为它满足上面的逻辑,所以还会计算位移,并应用到角色对象上),最终的结果就是看起来主角的跑步动画已经停止但是身体还在动画方向上做位移,出现滑步了,尼玛啊。

既然已经找到问题了,那么我们就肯定有办法来解决它。既然我们知道了动画在切换的过程中可以通过AnimatorTransitionInfo来获取过渡的信息,那么就有办法了,首先我们能确定Idle到Locomotion和Locomotion到Idle的nameHash值,通过比对这两个值就能明确知道当前Animator是从哪个状态切换到哪个状态了,然后根据AnimatorTransitionInfo.normalizedTime可以获取到过渡的进度信息,这样一来我们就能准确的计算出来动画过渡的过程中,角色应有的运动速度了,例如从Idle切换到Locomotion是从速度0到速度4m/s,那么在Idle切换到Locomotion的过程中通过Mathf.Lerp (04normalizedTime)就可以获取实时速度了。最终代码如下:

int hashIdle2Locomotion = Animator.StringToHash ("Base Layer.Idle -> Base Layer.Locomotion");
int hashLocomotion2Idle = Animator.StringToHash ("Base Layer.Locomotion -> Base Layer.Idle");
float moveSpeed = 4f;
float speed = 0f;    
if (animator.IsInTransition (0)) {
    AnimatorTransitionInfo transitionInfo = animator.GetAnimatorTransitionInfo (0);
    float normalizedTime = transitionInfo.normalizedTime;
    if (transitionInfo.nameHash == hashIdle2Locomotion) {
        speed = Mathf.Lerp (0, moveSpeed, normalizedTime);
    } else if (transitionInfo.nameHash == hashLocomotion2Idle) {
        speed = Mathf.Lerp (moveSpeed, 0, normalizedTime);
    }
} else if (animInfo.IsName ("Locomotion")) {
    speed = moveSpeed;
} else if (animInfo.IsName ("Idle")) {
    speed = 0f;
}

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字段就可以了。