作者归档:贺 利华

关于贺 利华

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

[译文]使用Coroutine进行编程

原文链接在这里[Unity Patterns | Scripting with Coroutines]

序言

在Unity中,Coroutine是我最喜欢的特性之一。我几乎在所有的项目中都会用它来控制角色的运动,连续的行为以及对象的行为等等。在这个教程中,我将会详细介绍Coroutine的工作原理,同时还会通过几个实际的小例子来演示Coroutine能如何被应用到实际的项目中。

使用Coroutines进行编程

注意:这是关于Coroutine的教程中的第二篇,如果你还没有读过这个教程中的第一篇文章的话,建议你看一下,地址在这里

还是计时器

上一篇教程中,我们学习了如何利用Coroutine的特性来暂停和继续一个方法的执行,并且让其在执行的过程中完成我们所需的操作,最终我们完成了一个非常棒的计时器。Coroutine最棒的一个特性就是,通过这些通用的程序逻辑,我们可以很容易地进行抽象和代码重用。

Coroutine参数

抽象一个Coroutine的第一步就是给它定义几个参数。因为Coroutine就是方法,所以理所当然的,它们都可以有参数。下面就是一个栗子,这个栗子会根据我们传入的频率来输出消息:

using UnityEngine;
using System.Collections;

public class TimerExample : MonoBehaviour
{
    void Start()
    {
        //Log "Hello!" 5 times with 1 second between each log
        StartCoroutine(RepeatMessage(5, 1.0f, "Hello!"));
    }

    IEnumerator RepeatMessage(int count, float frequency, string message)
    {
        for (int i = 0; i < count; i++)
        {
            Debug.Log(message);
            for (float timer = 0; timer < frequency; timer += Time.deltaTime)
                yield return 0;

        }
    }
}

嵌套Coroutine

直到现在我们在Coroutine中总是在yield return 0 或者 null,这意味着Coroutine会等到下一帧再从此处开始继续执行Coroutine的方法体。然而Coroutine牛逼之处远不止于此,这货还有一个最牛掰的技能,那就是可以通过yield return语句实现Coroutine的嵌套。

我们来看看这个嵌套Coroutine在实际代码中是如何应用的,首先让我们创建一个简单的Coroutine:Wait(),这货实际上啥也不做,就只是消耗传入时长的时间,然后结束并返回。

IEnumerator Wait(float duration)
{
    for (float timer = 0; timer < duration; timer += Time.deltaTime)
        yield return 0;
}

接下来我们再写一个Coroutine:

using UnityEngine;
using System.Collections;

public class TimerExample : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(SaySomeThings());
    }

    //Say some messages separated by time
    IEnumerator SaySomeThings()
    {
        Debug.Log("The routine has started");
        yield return StartCoroutine(Wait(1.0f));
        Debug.Log("1 second has passed since the last message");
        yield return StartCoroutine(Wait(2.5f));
        Debug.Log("2.5 seconds have passed since the last message");
    }

    //Our wait function
    IEnumerator Wait(float duration)
    {
        for (float timer = 0; timer < duration; timer += Time.deltaTime)
            yield return 0;
    }
}

在这个新的方法中,我们没有再使用yield return 0或者null来返回,而是直接yield return Wait()方法,这个意思就是说在Wait()方法成功执行返回之前不继续执行SaySomeThings()方法中的代码,等到Wait()方法执行结束之后再继续执行。

接下来就是见证Coroutine真正牛逼之处的时刻。

控制对象的行为

接下来的这个栗子,我们会演示如何像创建计时器般,方便地通过Coroutine来控制目标对象的行为。Coroutine并非只能用于如此简单的计时器,结合嵌套Coroutine,我们能够随心所欲地控制游戏中各个对象的各种状态。

移动到指定位置

下面的这段代码中,我们有一个简单的脚本,脚本中有targetPosition和moveSpeed两个可以设置的变量。当我们运行游戏的时候,挂载这个脚本的对象会在场景中以我们设置的moveSpeed速度向targetPosition移动。

using UnityEngine;
using System.Collections;

public class MoveExample : MonoBehaviour
{
    public Vector3 targetPosition;
    public float moveSpeed;
    void Start()
    {
        StartCoroutine(MoveToPosition(targetPosition));
    }

    IEnumerator MoveToPosition(Vector3 target)
    {
        while (transform.position != target)
        {
            transform.position = Vector3.MoveTowards(transform.position, target, moveSpeed * Time.deltaTime);
            yield return 0;
        }
    }
}

这个Coroutine就不再是等待某个计时器结束或者死循环了,它会在目标对象移动到指定目标位置之后结束。

按路径移动

我们还可以再扩展一下上面的这个栗子,例如不只是移动到某个指定的位置,而是让目标对象按照指定的路径在场景中移动,我们可以通过一组点来设定对象移动的路径。我们可以顺序遍历路径中的每个坐标点,然后依次调用MoveToPosition方法,让目标对象依次通过这些路径点。

using UnityEngine;
using System.Collections;

public class MoveExample : MonoBehaviour
{
    public Vector3[] path;
    public float moveSpeed;

    void Start()
    {
        StartCoroutine(MoveOnPath(true));
    }

    IEnumerator MoveOnPath(bool loop)
    {
        do
        {
            foreach (var point in path)
                yield return StartCoroutine(MoveToPosition(point));
        }
        while (loop);
    }

    IEnumerator MoveToPosition(Vector3 target)
    {
        while (transform.position != target)
        {
            transform.position = Vector3.MoveTowards(transform.position, target, moveSpeed * Time.deltaTime);
            yield return 0;
        }
    }
}

另外,我还加了个bool变量loop用于控制是否需要循环按照这个路径移动,如果loop为true,那么目标对象就会在移动到路径的重点之后,再次回到路径起点,然后再按照路径走一遍。

当然我们还可以用Wait()方法来给这个移动的方法做些锦上添花(或者说添枝加叶?)的事情,例如在每次移动到路径关键点之后调用一下Wait()方法来让目标对象休息一会儿,这样是不是就很类似于我们游戏中常见的巡逻的AI捏?

注意事项

如果你是刚接触到Coroutine这货,希望这两篇教程能帮助你对Coroutine的运行机制以及它能做些啥有个初步的了解。下面是一些在使用Coroutine的过程中可能需要注意的事项:

  • 你只能通过字面常量的Coroutine方法名来调用StopCoroutine()方法结束一个指定的Coroutine(译者注:不知道作者写这边文章的时候使用的Unity3D是哪个版本,Unity3D 4.6中已经可以通过Coroutine方法变量来结束一个指定的Coroutine了);
  • 可以同时有多个Coroutine在运行,它们会按照启动的顺序依次执行;
  • Coroutine可以无限嵌套,想怎么折腾就怎么折腾(在栗子中我们只嵌套了1层);
  • 如果你想在多个脚本中访问到某个Coroutine,你只需要将Coroutine方法声明为静态的就可以了;
  • 尽管这货看起来很像多线程的玩意儿,但实际上Coroutine并不是多线程的,它们跟你的脚本运行在同一个线程中;
  • 如果你的方法需要进行大量的计算,那么你可以考虑使用Coroutine来分步完成它;
  • IEnumerator方法不能有ref或out的参数,但是可以通过引用传入;
  • 目前没有什么好用的方法可以用来检测当前有多少个Coroutine在一个对象上执行;

如果你知道一些不错的关于Unity Coroutine的教程或者相关的教程的话,希望你能在评论中留一下这些教程的链接。
另外,如果你发现这个教程中有哪些不对的地方或者不可用的链接,或者有什么其他的问题,欢迎你联系我们

欢迎打赏

网站主机租赁需要费用,写教程呢,又是个颇费时间的活,所以你懂的。欢迎通过PayPal打赏,以便Unity Patterns能持续运营下去。 Chevy(译者注:原文作者名称)是个穷小子,给多给少都是莫大的帮助哦。


以上翻译未对原文做任何删减,酌情增加了一丢丢内容,如果翻译有错误的地方请指正,更欢迎大家用美金支持原文作者的这种分享,使用上方的PayPal链接就可以给原文作者进行捐赠支持了。

[译文]Coroutine简介

原文链接在这里[Unity Patterns | Introduction to Coroutines]

序言

在Unity中,Coroutine是我最喜欢的特性之一。我几乎在所有的项目中都会用它来控制角色的运动,连续的行为以及对象的行为等等。在这个教程中,我将会详细介绍Coroutine的工作原理,同时还会通过几个实际的小例子来演示Coroutine能如何被应用到实际的项目中。

Coroutine简介

注意:这是关于Coroutine的教程中的第一篇,如果你有兴趣还可以读一下这个教程中的第二篇文章,地址在这里

Unity的Coroutine系统源自于C#的IEnumerator,IEnumerator接口的功能简单又强大,我们可以很容易的为我们自己的集合类型定义自己的enumarator。不过我们不用担心自定义Enumerator的实现是不是会太复杂,我们可以直接先来看一个例子,通过这个简单的例子我们来分析一下Coroutine的工作原理。首先,我们先看一段简单的代码。

计时器

这是一个非常简单的计时器脚本,计时器时间到0的时候,就输出一个Log信息,表明计时已经结束。

using UnityEngine;
using System.Collections;

public class Countdown : MonoBehaviour
{
    public float timer = 3;
    void Update()
    {
        timer -= Time.deltaTime;
        if (timer < = 0)
            Debug.Log("Timer has finished!");
    }
}

这段代码乍看上去还不错,代码简短同时也很好地实现了计时的需求。但是问题来了,如果我们有一些更加复杂的脚本(例如玩家角色或者怪物),这些脚本中都含有多个计时器呢?那么我们的代码最终就有可能会变成这个样子了。

using UnityEngine;
using System.Collections;

public class MultiTimer : MonoBehaviour
{
    public float firstTimer = 3;
    public float secondTimer = 2;
    public float thirdTimer = 1;

    void Update()
    {
        firstTimer -= Time.deltaTime;
        if (firstTimer < = 0)
            Debug.Log("First timer has finished!");

        secondTimer -= Time.deltaTime;
        if (secondTimer <= 0)
            Debug.Log("Second timer has finished!");

        thirdTimer -= Time.deltaTime;
        if (thirdTimer <= 0)
            Debug.Log("Third timer has finished!");
    }
}

其实如果能忍的话,这段代码也还算可以啦,并没有啥硬伤。但是就我个人而言,我实在不喜欢有这么多的Timer变量,搞得整个代码毫无美感。而且我当我想重启这些Timer的时候,我一定得重置这些Timer的值(而事实上我总忘),这简直不能忍啊,有木有。

那么如果我写个循环是不是能再优化一些呢?

for (float timer = 3; timer >= 0; timer -= Time.deltaTime)
{
    //Just do nothing...
}
Debug.Log("This happens after 3 seconds!");

嗯,看起来已经好多了,这回Timer变量成了循环内部的局部变量了,我也不需要再手动地设置每个循环中Timer的值了。

你可能已经知道接下来我要说的是啥了:对的,Coroutine干的就是这个。

初尝Coroutine

下面是一个跟前面我们写的计时器脚本功能一模一样的例子,只不过这个例子是使用了Coroutine来实现计时的功能。咱们最好实际动手敲一遍这个脚本,然后在Unity中运行一下,看看效果是不是一样的。

using UnityEngine;
using System.Collections;

public class CoroutineCountdown : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(Countdown());
    }

    IEnumerator Countdown()
    {
        for (float timer = 3; timer >= 0; timer -= Time.deltaTime)
            yield return 0;

        Debug.Log("This message appears after 3 seconds!");
    }
}

看上去跟之前的脚本还是有很大差别的呢,那么好吧,接下来我们就好好地来分析一下在这段代码里头都发生了些啥。

StartCoroutine(Countdown());

首先,这行代码启动了一个调用Countdown方法的Coroutine。请注意在这里我直接传入了一个方法调用作为参数(这样可以有效地将Countdown方法的返回值当做参数传入到StartCoroutine方法中),而没有传入Countdown函数的引用。

Yielding

这个Countdown方法除了以下两部分内容,其实已经很好地完成了自解释的过程:

  • IEnumerator类型的返回值
  • 循环中的yield return代码段

为了让Countdown方法能跨多帧执行(这个例子中,Countdown方法会在三分钟之内的每一帧都执行),Unity需要将整个函数执行的状态保存到内存的某个地方。而这个操作就是通过你使用yield return代码段返回的IEnumerator来实现的。当你“yield”一个方法的时候,实际上你就在告诉Unity“暂时先停止执行这个方法,等到下一帧的时候再从这儿开始执行这个方法!”。

注意:yield return代码段返回0或者null的意思就是告诉Unity等下一帧再从这里开始执行这个方法。但是你完全可以yield return其他的Coroutine,这个我们会在下一个教程中谈到。

举些栗子

在我们刚刚开始接触Corounines的时候,确实比较容易被搞得稀里糊涂的,我已经看过不少挫逼和牛逼的程序员对这个Coroutine的语法愁眉不展了。所以捏,偶觉得还是需要通过一些栗子来让我们更快更好地去理解Coroutine这货:

说几次“Hello”

首先我们要清楚一点,yield return这货的作用就是告诉程序“运行到这里先停一下,等到程序执行到下一帧的时候再继续往下执行”,这就意味着我们可以这么搞:

//This will say hello 5 times, once each frame for 5 frames
IEnumerator SayHelloFiveTimes()
{
    yield return 0;
    Debug.Log("Hello");
    yield return 0;
    Debug.Log("Hello");
    yield return 0;
    Debug.Log("Hello");
    yield return 0;
    Debug.Log("Hello");
    yield return 0;
    Debug.Log("Hello");
}

//This will do the exact same thing as the above function!
IEnumerator SayHello5Times()
{
    for (int i = 0; i < 5; i++)
    {
        Debug.Log("Hello");
        yield return 0;
    }
}

不停地说“Hello”

通过上面的代码,我们可以想象出在一个while循环中使用Coroutine的话,其实是可以达成死循环的效果滴。这个特性可以让我们很轻易的就模拟出Update方法的效果了,如下:

//Once started, this will run until manually stopped or the object is destroyed
IEnumerator SayHelloEveryFrame()
{
    while (true)
    {
        //1. Say hello
        Debug.Log("Hello");
        //2. Wait until next frame
        yield return 0;
    } //3. This is a forever-loop, goto 1
}

读个秒

其实我们不仅仅能模拟个Update方法的效果,我们还可以在Coroutine中做更牛逼的事情,例如:

IEnumerator CountSeconds()
{
    int seconds = 0;

    while (true)
    {
        for (float timer = 0; timer < 1; timer += Time.deltaTime)
            yield return 0;

        seconds++;
        Debug.Log(seconds + " seconds have passed since the Coroutine started.");
    }
}

这个方法向我们展示了Coroutine的一个非常骚比的特性,那就是这个方法执行体内部的所有状态都会被自动保存,包括方法体中定义的局部变量,都会在整个程序执行的过程中被自动保存起来,直到下一帧再次被调用。还记得文章开头的那些无聊的timer字段们吗?有了Coroutine之后,我们就不再需要那些货了,只需要把每个计时器需要的字段放到自己的计时方法内定义就OK了,是不是处女座程序猿童鞋的福音捏?

启动和结束Coroutine

至此,我们已经学习了通过StartCoroutine()方法来启动一个Coroutine:

StartCoroutine(Countdown());

如果我们想结束所有正在运行的Coroutine的话,我们可以通过StopAllCoroutines()这个方法来结束。但是需要注意的是,这个方法只会结束由调用StopCoroutines()方法的同一个MonoBehaviour组件启动的Coroutine们,对于通过其他MonoBehaviour组件启动的Coroutine就没有效果了。

如果我们通过下面的这种方式启动了两个Coroutine:

StartCoroutine(FirstTimer());
StartCoroutine(SecondTimer());

然后我们又想单独结束两个Timer中的某一个,上面提到的方法就无法胜任了。如果我们想单独结束某一个指定的Timer的话,我们可以通过使用Coroutine的名称来作为StartCoroutine()和StopCoroutine()方法的参数,如下:

//If you start a Coroutine by name...
StartCoroutine("FirstTimer");
StartCoroutine("SecondTimer");

//You can stop it anytime by name!
StopCoroutine("FirstTimer");

[译者自己加上的]又或者这样,不使用字面常量:

// Define the Timers
IEnumerator firstTimer = FirstTimer();
IEnumerator secondTimer = SecondTimer();

// Start the Coroutines
StartCoroutine(firstTimer);
StartCoroutine(secondTimer);

// Stop the first timer coroutine
StopCoroutine(firstTimer);

扩展阅读

扩展阅读链接

如果你知道一些不错的关于Unity Coroutine的教程或者相关的教程的话,希望你能在评论中留一下这些教程的链接。
另外,如果你发现这个教程中有哪些不对的地方或者不可用的链接,或者有什么其他的问题,欢迎你联系我们

欢迎打赏

网站主机租赁需要费用,写教程呢,又是个颇费时间的活,所以你懂的。欢迎通过PayPal打赏,以便Unity Patterns能持续运营下去。 Chevy(译者注:原文作者名称)是个穷小子,给多给少都是莫大的帮助哦。


以上翻译未对原文做任何删减,酌情增加了一丢丢内容,如果翻译有错误的地方请指正,更欢迎大家用美金支持原文作者的这种分享,使用上方的PayPal链接就可以给原文作者进行捐赠支持了。

如何在Unity3D Editor脚本中使用Coroutine进行异步操作

今天在做编辑器中一个功能时,碰到一个这样的需求,Unity3D Editor脚本中会通过System.Diagnostics.Process这个类来调用外部的系统命令来执行一个操作,其实就是调用系统的Python命令执行一个Python脚本,来将Excel文件转换成文本数据。

[MenuItem ("LightHonor/Config/ImportExcel")]
static void ConvertXlsxToTxtConfig ()
{
    string txt2xlsxPythonFilePath = Directory.GetParent (Application.dataPath) 
        + Path.DirectorySeparatorChar.ToString () + "Docs" 
        + Path.DirectorySeparatorChar.ToString () + "Tools" 
        + Path.DirectorySeparatorChar.ToString () + "convert.py";
    if (Application.platform == RuntimePlatform.OSXEditor) {
        System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo ("/usr/bin/python", txt2xlsxPythonFilePath);
        System.Diagnostics.Process convertProcess = new System.Diagnostics.Process ();
        convertProcess.StartInfo = startInfo;
        convertProcess.EnableRaisingEvents = true;
        convertProcess.Exited += new System.EventHandler (ConvertProcessExited);
        convertProcess.Start ();
    } else if (Application.platform == RuntimePlatform.WindowsEditor) {
        System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo (@"C:\Python27\python.exe", txt2xlsxPythonFilePath);
        System.Diagnostics.Process convertProcess = new System.Diagnostics.Process ();
        convertProcess.StartInfo = startInfo;
        convertProcess.EnableRaisingEvents = true;
        convertProcess.Exited += new System.EventHandler (ConvertProcessExited);
        convertProcess.Start ();
    }
}

private static void ConvertProcessExited (object sender, System.EventArgs e)
{
    Debug.Log ("Convert Excel Xlsx Config To Text File Done!");
}

但是这样执行完ImportExcel菜单项之后,可能会出现我们通过外部的脚本修改了Unity3D需要使用到的文本文件,但是Unity3D未能及时更新AssetDatabase,导致Unity3D中读取到的文本文件还是执行ImportConfig菜单项之前的数据。那么我们能否直接在这个外部程序执行完毕的回调中执行AssetDatabase.Refresh()方法呢?不行,因为这些函数都只能在主线程里头调用,而这个回调函数是在启动外部程序的线程中执行的,所以在ConvertProcessExited()方法中访问AssetDatabase对象会抛出异常。这个时候我们可以通过EditorApplication的update委托来配合Coroutine机制来完成我们想要的功能。

关于EditorApplication如何支持Coroutine,这个是在GitHub上找到了一个很棒的参考,地址在这儿,链接页面中是一个完整的独立的EditorCoroutine的实现,借助于这个漂亮的工具,我们修改了代码,然后就能达成我们的需求了,在执行完外部转化的Python程序之后,及时通过AssetDatabase刷新Unity3D中需要使用到的文本文件,代码如下:

private static bool sConvertDone = false;

[MenuItem ("LightHonor/Config/ImportExcel")]
static void ConvertXlsxToTxtConfig ()
{
    string txt2xlsxPythonFilePath = Directory.GetParent (Application.dataPath) 
        + Path.DirectorySeparatorChar.ToString () + "Docs" 
        + Path.DirectorySeparatorChar.ToString () + "Tools" 
        + Path.DirectorySeparatorChar.ToString () + "convert.py";
    if (Application.platform == RuntimePlatform.OSXEditor) {
        System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo ("/usr/bin/python", txt2xlsxPythonFilePath);
        System.Diagnostics.Process convertProcess = new System.Diagnostics.Process ();
        convertProcess.StartInfo = startInfo;
        convertProcess.EnableRaisingEvents = true;
        convertProcess.Exited += new System.EventHandler (ConvertProcessExited);
        convertProcess.Start ();
    } else if (Application.platform == RuntimePlatform.WindowsEditor) {
        System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo (@"C:\Python27\python.exe", txt2xlsxPythonFilePath);
        System.Diagnostics.Process convertProcess = new System.Diagnostics.Process ();
        convertProcess.StartInfo = startInfo;
        convertProcess.EnableRaisingEvents = true;
        convertProcess.Exited += new System.EventHandler (ConvertProcessExited);
        convertProcess.Start ();
    }
    sConvertDone = false;
    EditorCoroutine.Start (RefreshAssetDatabase ());
}

static IEnumerator RefreshAssetDatabase ()
{
    while (!sConvertDone) {
        yield return 0;
    }
    AssetDatabase.Refresh ();
    Debug.Log ("Refresh Asset Database Done");
}

private static void ConvertProcessExited (object sender, System.EventArgs e)
{
    Debug.Log ("Convert Excel Xlsx Config To Text File Done!");
    sConvertDone = true;
}

 

AnimatorStateInfo中nameHash值

AnimatorStateInfo.IsName(sting name)这个方法是用来判断当前Animator播放的否是某个动画,而这个方法不只是比对动画名称的nameHash值,通过测试我们会发现传入纯粹的动画状态名称或者传入状态所在层名称和状态名称的组合,都是可以成功匹配的。

AnimatorStateInfo.IsName(string name)方法不仅会比对这个FullPath Name,还会比对Simple Name,例如我们在Base Layer中有一个名为Idle的状态。

那么通过AnimatorStateInfo.IsName(“Idle”)和AnimatorStateInfo.IsName(“Base Layer.Idle”)都是True,但是如果我们要直接比对nameHash的值,那就必须使用Animator.StringToHash(“Base Layer.Idle”) 和AnimatorStateInfo的nameHash属性值进行比对,而不能使用简称的nameHash值Animator.StringToHash(“Idle”)来进行比对。

在Unity4.6中AnimatorStateInfo中得nameHash属性值是通过Animator.StringToHash(“Base Layer.StateName”)获得的,参数是全路径名,全路径由状态所在层名称和动画状态名称组成,格式为“[LayerName].[StateName]”,请自行使用对应的LayerName和StateName替换方括号中的内容。


升级到Unity5之后,AnimatorStateInfo中就木有nameHash这个属性了,不过新增了两个字面意思更明确的属性,shortNameHash和fullPathName,两者的区别就是fullPathHash传入的Name参数是带上State所在的Layer的名字的,例如:Base Layer.Idle,而shortNameHash就是不带Layer名字的,例如:Idle。这样更明确了显然是更好的,看到Unity3D的文档一天天往完善了发展,还是深感幸福啊。

用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

折腾完毕。